mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(policy): repurpose "Always Allow" persistence to workspace level (#19707)
This commit is contained in:
@@ -41,8 +41,9 @@ export async function createPolicyEngineConfig(
|
|||||||
export function createPolicyUpdater(
|
export function createPolicyUpdater(
|
||||||
policyEngine: PolicyEngine,
|
policyEngine: PolicyEngine,
|
||||||
messageBus: MessageBus,
|
messageBus: MessageBus,
|
||||||
|
storage: Storage,
|
||||||
) {
|
) {
|
||||||
return createCorePolicyUpdater(policyEngine, messageBus);
|
return createCorePolicyUpdater(policyEngine, messageBus, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspacePolicyState {
|
export interface WorkspacePolicyState {
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ export async function main() {
|
|||||||
|
|
||||||
const policyEngine = config.getPolicyEngine();
|
const policyEngine = config.getPolicyEngine();
|
||||||
const messageBus = config.getMessageBus();
|
const messageBus = config.getMessageBus();
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, config.storage);
|
||||||
|
|
||||||
// Register SessionEnd hook to fire on graceful exit
|
// Register SessionEnd hook to fire on graceful exit
|
||||||
// This runs before telemetry shutdown in runExitCleanup()
|
// This runs before telemetry shutdown in runExitCleanup()
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const TMP_DIR_NAME = 'tmp';
|
|||||||
const BIN_DIR_NAME = 'bin';
|
const BIN_DIR_NAME = 'bin';
|
||||||
const AGENTS_DIR_NAME = '.agents';
|
const AGENTS_DIR_NAME = '.agents';
|
||||||
|
|
||||||
|
export const AUTO_SAVED_POLICY_FILENAME = 'auto-saved.toml';
|
||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
private readonly targetDir: string;
|
private readonly targetDir: string;
|
||||||
private readonly sessionId: string | undefined;
|
private readonly sessionId: string | undefined;
|
||||||
@@ -154,6 +156,13 @@ export class Storage {
|
|||||||
return path.join(this.getGeminiDir(), 'policies');
|
return path.join(this.getGeminiDir(), 'policies');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAutoSavedPolicyPath(): string {
|
||||||
|
return path.join(
|
||||||
|
this.getWorkspacePoliciesDir(),
|
||||||
|
AUTO_SAVED_POLICY_FILENAME,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ensureProjectTempDirExists(): void {
|
ensureProjectTempDirExists(): void {
|
||||||
fs.mkdirSync(this.getProjectTempDir(), { recursive: true });
|
fs.mkdirSync(this.getProjectTempDir(), { recursive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ export const WORKSPACE_POLICY_TIER = 2;
|
|||||||
export const USER_POLICY_TIER = 3;
|
export const USER_POLICY_TIER = 3;
|
||||||
export const ADMIN_POLICY_TIER = 4;
|
export const ADMIN_POLICY_TIER = 4;
|
||||||
|
|
||||||
|
// Specific priority offsets and derived priorities for dynamic/settings rules.
|
||||||
|
// These are added to the tier base (e.g., USER_POLICY_TIER).
|
||||||
|
|
||||||
|
// Workspace tier (2) + high priority (950/1000) = ALWAYS_ALLOW_PRIORITY
|
||||||
|
// This ensures user "always allow" selections are high priority
|
||||||
|
// within the workspace tier but still lose to user/admin policies.
|
||||||
|
export const ALWAYS_ALLOW_PRIORITY = WORKSPACE_POLICY_TIER + 0.95;
|
||||||
|
|
||||||
|
export const MCP_EXCLUDED_PRIORITY = USER_POLICY_TIER + 0.9;
|
||||||
|
export const EXCLUDE_TOOLS_FLAG_PRIORITY = USER_POLICY_TIER + 0.4;
|
||||||
|
export const ALLOWED_TOOLS_FLAG_PRIORITY = USER_POLICY_TIER + 0.3;
|
||||||
|
export const TRUSTED_MCP_SERVER_PRIORITY = USER_POLICY_TIER + 0.2;
|
||||||
|
export const ALLOWED_MCP_SERVER_PRIORITY = USER_POLICY_TIER + 0.1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the list of directories to search for policy files, in order of increasing priority
|
* Gets the list of directories to search for policy files, in order of increasing priority
|
||||||
* (Default -> User -> Project -> Admin).
|
* (Default -> User -> Project -> Admin).
|
||||||
@@ -233,13 +247,14 @@ export async function createPolicyEngineConfig(
|
|||||||
// This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
// This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
||||||
// while allowing user-specified priorities to work within each tier.
|
// while allowing user-specified priorities to work within each tier.
|
||||||
//
|
//
|
||||||
// Settings-based and dynamic rules (all in user tier 3.x):
|
// Settings-based and dynamic rules (mixed tiers):
|
||||||
// 3.95: Tools that the user has selected as "Always Allow" in the interactive UI
|
// MCP_EXCLUDED_PRIORITY: MCP servers excluded list (security: persistent server blocks)
|
||||||
// 3.9: MCP servers excluded list (security: persistent server blocks)
|
// EXCLUDE_TOOLS_FLAG_PRIORITY: Command line flag --exclude-tools (explicit temporary blocks)
|
||||||
// 3.4: Command line flag --exclude-tools (explicit temporary blocks)
|
// ALLOWED_TOOLS_FLAG_PRIORITY: Command line flag --allowed-tools (explicit temporary allows)
|
||||||
// 3.3: Command line flag --allowed-tools (explicit temporary allows)
|
// TRUSTED_MCP_SERVER_PRIORITY: MCP servers with trust=true (persistent trusted servers)
|
||||||
// 3.2: MCP servers with trust=true (persistent trusted servers)
|
// ALLOWED_MCP_SERVER_PRIORITY: MCP servers allowed list (persistent general server allows)
|
||||||
// 3.1: MCP servers allowed list (persistent general server allows)
|
// ALWAYS_ALLOW_PRIORITY: Tools that the user has selected as "Always Allow" in the interactive UI
|
||||||
|
// (Workspace tier 2.x - scoped to the project)
|
||||||
//
|
//
|
||||||
// TOML policy priorities (before transformation):
|
// TOML policy priorities (before transformation):
|
||||||
// 10: Write tools default to ASK_USER (becomes 1.010 in default tier)
|
// 10: Write tools default to ASK_USER (becomes 1.010 in default tier)
|
||||||
@@ -250,33 +265,33 @@ export async function createPolicyEngineConfig(
|
|||||||
// 999: YOLO mode allow-all (becomes 1.999 in default tier)
|
// 999: YOLO mode allow-all (becomes 1.999 in default tier)
|
||||||
|
|
||||||
// MCP servers that are explicitly excluded in settings.mcp.excluded
|
// MCP servers that are explicitly excluded in settings.mcp.excluded
|
||||||
// Priority: 3.9 (highest in user tier for security - persistent server blocks)
|
// Priority: MCP_EXCLUDED_PRIORITY (highest in user tier for security - persistent server blocks)
|
||||||
if (settings.mcp?.excluded) {
|
if (settings.mcp?.excluded) {
|
||||||
for (const serverName of settings.mcp.excluded) {
|
for (const serverName of settings.mcp.excluded) {
|
||||||
rules.push({
|
rules.push({
|
||||||
toolName: `${serverName}__*`,
|
toolName: `${serverName}__*`,
|
||||||
decision: PolicyDecision.DENY,
|
decision: PolicyDecision.DENY,
|
||||||
priority: 3.9,
|
priority: MCP_EXCLUDED_PRIORITY,
|
||||||
source: 'Settings (MCP Excluded)',
|
source: 'Settings (MCP Excluded)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tools that are explicitly excluded in the settings.
|
// Tools that are explicitly excluded in the settings.
|
||||||
// Priority: 3.4 (user tier - explicit temporary blocks)
|
// Priority: EXCLUDE_TOOLS_FLAG_PRIORITY (user tier - explicit temporary blocks)
|
||||||
if (settings.tools?.exclude) {
|
if (settings.tools?.exclude) {
|
||||||
for (const tool of settings.tools.exclude) {
|
for (const tool of settings.tools.exclude) {
|
||||||
rules.push({
|
rules.push({
|
||||||
toolName: tool,
|
toolName: tool,
|
||||||
decision: PolicyDecision.DENY,
|
decision: PolicyDecision.DENY,
|
||||||
priority: 3.4,
|
priority: EXCLUDE_TOOLS_FLAG_PRIORITY,
|
||||||
source: 'Settings (Tools Excluded)',
|
source: 'Settings (Tools Excluded)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tools that are explicitly allowed in the settings.
|
// Tools that are explicitly allowed in the settings.
|
||||||
// Priority: 3.3 (user tier - explicit temporary allows)
|
// Priority: ALLOWED_TOOLS_FLAG_PRIORITY (user tier - explicit temporary allows)
|
||||||
if (settings.tools?.allowed) {
|
if (settings.tools?.allowed) {
|
||||||
for (const tool of settings.tools.allowed) {
|
for (const tool of settings.tools.allowed) {
|
||||||
// Check for legacy format: toolName(args)
|
// Check for legacy format: toolName(args)
|
||||||
@@ -296,7 +311,7 @@ export async function createPolicyEngineConfig(
|
|||||||
rules.push({
|
rules.push({
|
||||||
toolName,
|
toolName,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
priority: 3.3,
|
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||||
argsPattern: new RegExp(pattern),
|
argsPattern: new RegExp(pattern),
|
||||||
source: 'Settings (Tools Allowed)',
|
source: 'Settings (Tools Allowed)',
|
||||||
});
|
});
|
||||||
@@ -308,7 +323,7 @@ export async function createPolicyEngineConfig(
|
|||||||
rules.push({
|
rules.push({
|
||||||
toolName,
|
toolName,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
priority: 3.3,
|
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||||
source: 'Settings (Tools Allowed)',
|
source: 'Settings (Tools Allowed)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -320,7 +335,7 @@ export async function createPolicyEngineConfig(
|
|||||||
rules.push({
|
rules.push({
|
||||||
toolName,
|
toolName,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
priority: 3.3,
|
priority: ALLOWED_TOOLS_FLAG_PRIORITY,
|
||||||
source: 'Settings (Tools Allowed)',
|
source: 'Settings (Tools Allowed)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -328,7 +343,7 @@ export async function createPolicyEngineConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MCP servers that are trusted in the settings.
|
// MCP servers that are trusted in the settings.
|
||||||
// Priority: 3.2 (user tier - persistent trusted servers)
|
// Priority: TRUSTED_MCP_SERVER_PRIORITY (user tier - persistent trusted servers)
|
||||||
if (settings.mcpServers) {
|
if (settings.mcpServers) {
|
||||||
for (const [serverName, serverConfig] of Object.entries(
|
for (const [serverName, serverConfig] of Object.entries(
|
||||||
settings.mcpServers,
|
settings.mcpServers,
|
||||||
@@ -339,7 +354,7 @@ export async function createPolicyEngineConfig(
|
|||||||
rules.push({
|
rules.push({
|
||||||
toolName: `${serverName}__*`,
|
toolName: `${serverName}__*`,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
priority: 3.2,
|
priority: TRUSTED_MCP_SERVER_PRIORITY,
|
||||||
source: 'Settings (MCP Trusted)',
|
source: 'Settings (MCP Trusted)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -347,13 +362,13 @@ export async function createPolicyEngineConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MCP servers that are explicitly allowed in settings.mcp.allowed
|
// MCP servers that are explicitly allowed in settings.mcp.allowed
|
||||||
// Priority: 3.1 (user tier - persistent general server allows)
|
// Priority: ALLOWED_MCP_SERVER_PRIORITY (user tier - persistent general server allows)
|
||||||
if (settings.mcp?.allowed) {
|
if (settings.mcp?.allowed) {
|
||||||
for (const serverName of settings.mcp.allowed) {
|
for (const serverName of settings.mcp.allowed) {
|
||||||
rules.push({
|
rules.push({
|
||||||
toolName: `${serverName}__*`,
|
toolName: `${serverName}__*`,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
priority: 3.1,
|
priority: ALLOWED_MCP_SERVER_PRIORITY,
|
||||||
source: 'Settings (MCP Allowed)',
|
source: 'Settings (MCP Allowed)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -381,6 +396,7 @@ interface TomlRule {
|
|||||||
export function createPolicyUpdater(
|
export function createPolicyUpdater(
|
||||||
policyEngine: PolicyEngine,
|
policyEngine: PolicyEngine,
|
||||||
messageBus: MessageBus,
|
messageBus: MessageBus,
|
||||||
|
storage: Storage,
|
||||||
) {
|
) {
|
||||||
// Use a sequential queue for persistence to avoid lost updates from concurrent events.
|
// Use a sequential queue for persistence to avoid lost updates from concurrent events.
|
||||||
let persistenceQueue = Promise.resolve();
|
let persistenceQueue = Promise.resolve();
|
||||||
@@ -400,10 +416,7 @@ export function createPolicyUpdater(
|
|||||||
policyEngine.addRule({
|
policyEngine.addRule({
|
||||||
toolName,
|
toolName,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
// User tier (3) + high priority (950/1000) = 3.95
|
priority: ALWAYS_ALLOW_PRIORITY,
|
||||||
// This ensures user "always allow" selections are high priority
|
|
||||||
// but still lose to admin policies (4.xxx) and settings excludes (300)
|
|
||||||
priority: 3.95,
|
|
||||||
argsPattern: new RegExp(pattern),
|
argsPattern: new RegExp(pattern),
|
||||||
source: 'Dynamic (Confirmed)',
|
source: 'Dynamic (Confirmed)',
|
||||||
});
|
});
|
||||||
@@ -425,10 +438,7 @@ export function createPolicyUpdater(
|
|||||||
policyEngine.addRule({
|
policyEngine.addRule({
|
||||||
toolName,
|
toolName,
|
||||||
decision: PolicyDecision.ALLOW,
|
decision: PolicyDecision.ALLOW,
|
||||||
// User tier (3) + high priority (950/1000) = 3.95
|
priority: ALWAYS_ALLOW_PRIORITY,
|
||||||
// This ensures user "always allow" selections are high priority
|
|
||||||
// but still lose to admin policies (4.xxx) and settings excludes (300)
|
|
||||||
priority: 3.95,
|
|
||||||
argsPattern,
|
argsPattern,
|
||||||
source: 'Dynamic (Confirmed)',
|
source: 'Dynamic (Confirmed)',
|
||||||
});
|
});
|
||||||
@@ -437,9 +447,9 @@ export function createPolicyUpdater(
|
|||||||
if (message.persist) {
|
if (message.persist) {
|
||||||
persistenceQueue = persistenceQueue.then(async () => {
|
persistenceQueue = persistenceQueue.then(async () => {
|
||||||
try {
|
try {
|
||||||
const userPoliciesDir = Storage.getUserPoliciesDir();
|
const workspacePoliciesDir = storage.getWorkspacePoliciesDir();
|
||||||
await fs.mkdir(userPoliciesDir, { recursive: true });
|
await fs.mkdir(workspacePoliciesDir, { recursive: true });
|
||||||
const policyFile = path.join(userPoliciesDir, 'auto-saved.toml');
|
const policyFile = storage.getAutoSavedPolicyPath();
|
||||||
|
|
||||||
// Read existing file
|
// Read existing file
|
||||||
let existingData: { rule?: TomlRule[] } = {};
|
let existingData: { rule?: TomlRule[] } = {};
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import {
|
|||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import * as fs from 'node:fs/promises';
|
import * as fs from 'node:fs/promises';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { createPolicyUpdater } from './config.js';
|
import { createPolicyUpdater, ALWAYS_ALLOW_PRIORITY } from './config.js';
|
||||||
import { PolicyEngine } from './policy-engine.js';
|
import { PolicyEngine } from './policy-engine.js';
|
||||||
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
import { MessageBusType } from '../confirmation-bus/types.js';
|
import { MessageBusType } from '../confirmation-bus/types.js';
|
||||||
import { Storage } from '../config/storage.js';
|
import { Storage, AUTO_SAVED_POLICY_FILENAME } from '../config/storage.js';
|
||||||
import { ApprovalMode } from './types.js';
|
import { ApprovalMode } from './types.js';
|
||||||
|
|
||||||
vi.mock('node:fs/promises');
|
vi.mock('node:fs/promises');
|
||||||
@@ -28,6 +28,7 @@ vi.mock('../config/storage.js');
|
|||||||
describe('createPolicyUpdater', () => {
|
describe('createPolicyUpdater', () => {
|
||||||
let policyEngine: PolicyEngine;
|
let policyEngine: PolicyEngine;
|
||||||
let messageBus: MessageBus;
|
let messageBus: MessageBus;
|
||||||
|
let mockStorage: Storage;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
policyEngine = new PolicyEngine({
|
policyEngine = new PolicyEngine({
|
||||||
@@ -36,6 +37,7 @@ describe('createPolicyUpdater', () => {
|
|||||||
approvalMode: ApprovalMode.DEFAULT,
|
approvalMode: ApprovalMode.DEFAULT,
|
||||||
});
|
});
|
||||||
messageBus = new MessageBus(policyEngine);
|
messageBus = new MessageBus(policyEngine);
|
||||||
|
mockStorage = new Storage('/mock/project');
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,10 +46,17 @@ describe('createPolicyUpdater', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should persist policy when persist flag is true', async () => {
|
it('should persist policy when persist flag is true', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
const userPoliciesDir = '/mock/user/policies';
|
const workspacePoliciesDir = '/mock/project/.gemini/policies';
|
||||||
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(userPoliciesDir);
|
const policyFile = path.join(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
AUTO_SAVED_POLICY_FILENAME,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getWorkspacePoliciesDir').mockReturnValue(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||||
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
||||||
(fs.readFile as unknown as Mock).mockRejectedValue(
|
(fs.readFile as unknown as Mock).mockRejectedValue(
|
||||||
new Error('File not found'),
|
new Error('File not found'),
|
||||||
@@ -70,8 +79,8 @@ describe('createPolicyUpdater', () => {
|
|||||||
// Wait for async operations (microtasks)
|
// Wait for async operations (microtasks)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
expect(Storage.getUserPoliciesDir).toHaveBeenCalled();
|
expect(mockStorage.getWorkspacePoliciesDir).toHaveBeenCalled();
|
||||||
expect(fs.mkdir).toHaveBeenCalledWith(userPoliciesDir, {
|
expect(fs.mkdir).toHaveBeenCalledWith(workspacePoliciesDir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,12 +94,12 @@ describe('createPolicyUpdater', () => {
|
|||||||
);
|
);
|
||||||
expect(fs.rename).toHaveBeenCalledWith(
|
expect(fs.rename).toHaveBeenCalledWith(
|
||||||
expect.stringMatching(/\.tmp$/),
|
expect.stringMatching(/\.tmp$/),
|
||||||
path.join(userPoliciesDir, 'auto-saved.toml'),
|
policyFile,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not persist policy when persist flag is false or undefined', async () => {
|
it('should not persist policy when persist flag is false or undefined', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
await messageBus.publish({
|
await messageBus.publish({
|
||||||
type: MessageBusType.UPDATE_POLICY,
|
type: MessageBusType.UPDATE_POLICY,
|
||||||
@@ -104,10 +113,17 @@ describe('createPolicyUpdater', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should persist policy with commandPrefix when provided', async () => {
|
it('should persist policy with commandPrefix when provided', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
const userPoliciesDir = '/mock/user/policies';
|
const workspacePoliciesDir = '/mock/project/.gemini/policies';
|
||||||
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(userPoliciesDir);
|
const policyFile = path.join(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
AUTO_SAVED_POLICY_FILENAME,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getWorkspacePoliciesDir').mockReturnValue(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||||
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
||||||
(fs.readFile as unknown as Mock).mockRejectedValue(
|
(fs.readFile as unknown as Mock).mockRejectedValue(
|
||||||
new Error('File not found'),
|
new Error('File not found'),
|
||||||
@@ -136,7 +152,7 @@ describe('createPolicyUpdater', () => {
|
|||||||
const rules = policyEngine.getRules();
|
const rules = policyEngine.getRules();
|
||||||
const addedRule = rules.find((r) => r.toolName === toolName);
|
const addedRule = rules.find((r) => r.toolName === toolName);
|
||||||
expect(addedRule).toBeDefined();
|
expect(addedRule).toBeDefined();
|
||||||
expect(addedRule?.priority).toBe(3.95);
|
expect(addedRule?.priority).toBe(ALWAYS_ALLOW_PRIORITY);
|
||||||
expect(addedRule?.argsPattern).toEqual(
|
expect(addedRule?.argsPattern).toEqual(
|
||||||
new RegExp(`"command":"git\\ status(?:[\\s"]|\\\\")`),
|
new RegExp(`"command":"git\\ status(?:[\\s"]|\\\\")`),
|
||||||
);
|
);
|
||||||
@@ -150,10 +166,17 @@ describe('createPolicyUpdater', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should persist policy with mcpName and toolName when provided', async () => {
|
it('should persist policy with mcpName and toolName when provided', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
const userPoliciesDir = '/mock/user/policies';
|
const workspacePoliciesDir = '/mock/project/.gemini/policies';
|
||||||
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(userPoliciesDir);
|
const policyFile = path.join(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
AUTO_SAVED_POLICY_FILENAME,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getWorkspacePoliciesDir').mockReturnValue(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||||
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
||||||
(fs.readFile as unknown as Mock).mockRejectedValue(
|
(fs.readFile as unknown as Mock).mockRejectedValue(
|
||||||
new Error('File not found'),
|
new Error('File not found'),
|
||||||
@@ -189,10 +212,17 @@ describe('createPolicyUpdater', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should escape special characters in toolName and mcpName', async () => {
|
it('should escape special characters in toolName and mcpName', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
const userPoliciesDir = '/mock/user/policies';
|
const workspacePoliciesDir = '/mock/project/.gemini/policies';
|
||||||
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(userPoliciesDir);
|
const policyFile = path.join(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
AUTO_SAVED_POLICY_FILENAME,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getWorkspacePoliciesDir').mockReturnValue(
|
||||||
|
workspacePoliciesDir,
|
||||||
|
);
|
||||||
|
vi.spyOn(mockStorage, 'getAutoSavedPolicyPath').mockReturnValue(policyFile);
|
||||||
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
(fs.mkdir as unknown as Mock).mockResolvedValue(undefined);
|
||||||
(fs.readFile as unknown as Mock).mockRejectedValue(
|
(fs.readFile as unknown as Mock).mockRejectedValue(
|
||||||
new Error('File not found'),
|
new Error('File not found'),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import * as fs from 'node:fs/promises';
|
import * as fs from 'node:fs/promises';
|
||||||
import { createPolicyUpdater } from './config.js';
|
import { createPolicyUpdater, ALWAYS_ALLOW_PRIORITY } from './config.js';
|
||||||
import { PolicyEngine } from './policy-engine.js';
|
import { PolicyEngine } from './policy-engine.js';
|
||||||
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
import { MessageBusType } from '../confirmation-bus/types.js';
|
import { MessageBusType } from '../confirmation-bus/types.js';
|
||||||
@@ -41,6 +41,7 @@ interface TestableShellToolInvocation {
|
|||||||
describe('createPolicyUpdater', () => {
|
describe('createPolicyUpdater', () => {
|
||||||
let policyEngine: PolicyEngine;
|
let policyEngine: PolicyEngine;
|
||||||
let messageBus: MessageBus;
|
let messageBus: MessageBus;
|
||||||
|
let mockStorage: Storage;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
@@ -48,8 +49,9 @@ describe('createPolicyUpdater', () => {
|
|||||||
vi.spyOn(policyEngine, 'addRule');
|
vi.spyOn(policyEngine, 'addRule');
|
||||||
|
|
||||||
messageBus = new MessageBus(policyEngine);
|
messageBus = new MessageBus(policyEngine);
|
||||||
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(
|
mockStorage = new Storage('/mock/project');
|
||||||
'/mock/user/policies',
|
vi.spyOn(mockStorage, 'getWorkspacePoliciesDir').mockReturnValue(
|
||||||
|
'/mock/project/.gemini/policies',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ describe('createPolicyUpdater', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add multiple rules when commandPrefix is an array', async () => {
|
it('should add multiple rules when commandPrefix is an array', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
await messageBus.publish({
|
await messageBus.publish({
|
||||||
type: MessageBusType.UPDATE_POLICY,
|
type: MessageBusType.UPDATE_POLICY,
|
||||||
@@ -72,6 +74,7 @@ describe('createPolicyUpdater', () => {
|
|||||||
1,
|
1,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
toolName: 'run_shell_command',
|
toolName: 'run_shell_command',
|
||||||
|
priority: ALWAYS_ALLOW_PRIORITY,
|
||||||
argsPattern: new RegExp('"command":"echo(?:[\\s"]|\\\\")'),
|
argsPattern: new RegExp('"command":"echo(?:[\\s"]|\\\\")'),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -79,13 +82,14 @@ describe('createPolicyUpdater', () => {
|
|||||||
2,
|
2,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
toolName: 'run_shell_command',
|
toolName: 'run_shell_command',
|
||||||
|
priority: ALWAYS_ALLOW_PRIORITY,
|
||||||
argsPattern: new RegExp('"command":"ls(?:[\\s"]|\\\\")'),
|
argsPattern: new RegExp('"command":"ls(?:[\\s"]|\\\\")'),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a single rule when commandPrefix is a string', async () => {
|
it('should add a single rule when commandPrefix is a string', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
await messageBus.publish({
|
await messageBus.publish({
|
||||||
type: MessageBusType.UPDATE_POLICY,
|
type: MessageBusType.UPDATE_POLICY,
|
||||||
@@ -98,13 +102,14 @@ describe('createPolicyUpdater', () => {
|
|||||||
expect(policyEngine.addRule).toHaveBeenCalledWith(
|
expect(policyEngine.addRule).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
toolName: 'run_shell_command',
|
toolName: 'run_shell_command',
|
||||||
|
priority: ALWAYS_ALLOW_PRIORITY,
|
||||||
argsPattern: new RegExp('"command":"git(?:[\\s"]|\\\\")'),
|
argsPattern: new RegExp('"command":"git(?:[\\s"]|\\\\")'),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should persist multiple rules correctly to TOML', async () => {
|
it('should persist multiple rules correctly to TOML', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
|
vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
|
||||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||||
|
|
||||||
@@ -139,7 +144,7 @@ describe('createPolicyUpdater', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject unsafe regex patterns', async () => {
|
it('should reject unsafe regex patterns', async () => {
|
||||||
createPolicyUpdater(policyEngine, messageBus);
|
createPolicyUpdater(policyEngine, messageBus, mockStorage);
|
||||||
|
|
||||||
await messageBus.publish({
|
await messageBus.publish({
|
||||||
type: MessageBusType.UPDATE_POLICY,
|
type: MessageBusType.UPDATE_POLICY,
|
||||||
|
|||||||
Reference in New Issue
Block a user