mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
feat(policy): support auto-add to policy by default and scoped persistence (#20361)
This commit is contained in:
@@ -16,8 +16,12 @@ import {
|
||||
import { checkPolicy, updatePolicy, getPolicyDenialError } from './policy.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { MessageBusType } from '../confirmation-bus/types.js';
|
||||
import {
|
||||
MessageBusType,
|
||||
type SerializableConfirmationDetails,
|
||||
} from '../confirmation-bus/types.js';
|
||||
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
|
||||
import { escapeRegex } from '../policy/utils.js';
|
||||
import {
|
||||
ToolConfirmationOutcome,
|
||||
type AnyDeclarativeTool,
|
||||
@@ -219,6 +223,8 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle standard policy updates with persistence', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -453,6 +459,8 @@ describe('policy.ts', () => {
|
||||
|
||||
it('should handle MCP ProceedAlwaysAndSave (persist: true)', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
@@ -487,6 +495,96 @@ describe('policy.ts', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should determine persistScope: workspace in trusted folders', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
getWorkspacePoliciesDir: vi
|
||||
.fn()
|
||||
.mockReturnValue('/mock/project/policies'),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
const mockMessageBus = {
|
||||
publish: vi.fn(),
|
||||
} as unknown as Mocked<MessageBus>;
|
||||
const tool = { name: 'test-tool' } as AnyDeclarativeTool;
|
||||
|
||||
await updatePolicy(
|
||||
tool,
|
||||
ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
undefined,
|
||||
{ config: mockConfig, messageBus: mockMessageBus },
|
||||
);
|
||||
|
||||
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
persistScope: 'workspace',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should determine persistScope: user in untrusted folders', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi
|
||||
.fn()
|
||||
.mockReturnValue('/mock/project/policies'),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
const mockMessageBus = {
|
||||
publish: vi.fn(),
|
||||
} as unknown as Mocked<MessageBus>;
|
||||
const tool = { name: 'test-tool' } as AnyDeclarativeTool;
|
||||
|
||||
await updatePolicy(
|
||||
tool,
|
||||
ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
undefined,
|
||||
{ config: mockConfig, messageBus: mockMessageBus },
|
||||
);
|
||||
|
||||
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
persistScope: 'user',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should narrow edit tools with argsPattern', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
|
||||
getTargetDir: vi.fn().mockReturnValue('/mock/dir'),
|
||||
setApprovalMode: vi.fn(),
|
||||
} as unknown as Mocked<Config>;
|
||||
const mockMessageBus = {
|
||||
publish: vi.fn(),
|
||||
} as unknown as Mocked<MessageBus>;
|
||||
const tool = { name: 'write_file' } as AnyDeclarativeTool;
|
||||
const details: SerializableConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: 'Edit',
|
||||
filePath: 'src/foo.ts',
|
||||
fileName: 'foo.ts',
|
||||
fileDiff: '--- foo.ts\n+++ foo.ts\n@@ -1 +1 @@\n-old\n+new',
|
||||
originalContent: 'old',
|
||||
newContent: 'new',
|
||||
};
|
||||
|
||||
await updatePolicy(
|
||||
tool,
|
||||
ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
details,
|
||||
{ config: mockConfig, messageBus: mockMessageBus },
|
||||
);
|
||||
|
||||
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
toolName: 'write_file',
|
||||
argsPattern: escapeRegex('"file_path":"src/foo.ts"'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPolicyDenialError', () => {
|
||||
|
||||
@@ -20,8 +20,11 @@ import {
|
||||
import {
|
||||
ToolConfirmationOutcome,
|
||||
type AnyDeclarativeTool,
|
||||
type AnyToolInvocation,
|
||||
type PolicyUpdateOptions,
|
||||
} from '../tools/tools.js';
|
||||
import { buildFilePathArgsPattern } from '../policy/utils.js';
|
||||
import { makeRelative } from '../utils/paths.js';
|
||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||
import { EDIT_TOOL_NAMES } from '../tools/tool-names.js';
|
||||
import type { ValidatingToolCall } from './types.js';
|
||||
@@ -94,7 +97,11 @@ export async function updatePolicy(
|
||||
tool: AnyDeclarativeTool,
|
||||
outcome: ToolConfirmationOutcome,
|
||||
confirmationDetails: SerializableConfirmationDetails | undefined,
|
||||
deps: { config: Config; messageBus: MessageBus },
|
||||
deps: {
|
||||
config: Config;
|
||||
messageBus: MessageBus;
|
||||
toolInvocation?: AnyToolInvocation;
|
||||
},
|
||||
): Promise<void> {
|
||||
// Mode Transitions (AUTO_EDIT)
|
||||
if (isAutoEditTransition(tool, outcome)) {
|
||||
@@ -102,6 +109,20 @@ export async function updatePolicy(
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine persist scope if we are persisting.
|
||||
let persistScope: 'workspace' | 'user' | undefined;
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave) {
|
||||
// If folder is trusted and workspace policies are enabled, we prefer workspace scope.
|
||||
if (
|
||||
deps.config.isTrustedFolder() &&
|
||||
deps.config.getWorkspacePoliciesDir() !== undefined
|
||||
) {
|
||||
persistScope = 'workspace';
|
||||
} else {
|
||||
persistScope = 'user';
|
||||
}
|
||||
}
|
||||
|
||||
// Specialized Tools (MCP)
|
||||
if (confirmationDetails?.type === 'mcp') {
|
||||
await handleMcpPolicyUpdate(
|
||||
@@ -109,6 +130,7 @@ export async function updatePolicy(
|
||||
outcome,
|
||||
confirmationDetails,
|
||||
deps.messageBus,
|
||||
persistScope,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -119,6 +141,9 @@ export async function updatePolicy(
|
||||
outcome,
|
||||
confirmationDetails,
|
||||
deps.messageBus,
|
||||
persistScope,
|
||||
deps.toolInvocation,
|
||||
deps.config,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,21 +173,31 @@ async function handleStandardPolicyUpdate(
|
||||
outcome: ToolConfirmationOutcome,
|
||||
confirmationDetails: SerializableConfirmationDetails | undefined,
|
||||
messageBus: MessageBus,
|
||||
persistScope?: 'workspace' | 'user',
|
||||
toolInvocation?: AnyToolInvocation,
|
||||
config?: Config,
|
||||
): Promise<void> {
|
||||
if (
|
||||
outcome === ToolConfirmationOutcome.ProceedAlways ||
|
||||
outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave
|
||||
) {
|
||||
const options: PolicyUpdateOptions = {};
|
||||
const options: PolicyUpdateOptions =
|
||||
toolInvocation?.getPolicyUpdateOptions?.(outcome) || {};
|
||||
|
||||
if (confirmationDetails?.type === 'exec') {
|
||||
if (!options.commandPrefix && confirmationDetails?.type === 'exec') {
|
||||
options.commandPrefix = confirmationDetails.rootCommands;
|
||||
} else if (!options.argsPattern && confirmationDetails?.type === 'edit') {
|
||||
const filePath = config
|
||||
? makeRelative(confirmationDetails.filePath, config.getTargetDir())
|
||||
: confirmationDetails.filePath;
|
||||
options.argsPattern = buildFilePathArgsPattern(filePath);
|
||||
}
|
||||
|
||||
await messageBus.publish({
|
||||
type: MessageBusType.UPDATE_POLICY,
|
||||
toolName: tool.name,
|
||||
persist: outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
persistScope,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -180,6 +215,7 @@ async function handleMcpPolicyUpdate(
|
||||
{ type: 'mcp' }
|
||||
>,
|
||||
messageBus: MessageBus,
|
||||
persistScope?: 'workspace' | 'user',
|
||||
): Promise<void> {
|
||||
const isMcpAlways =
|
||||
outcome === ToolConfirmationOutcome.ProceedAlways ||
|
||||
@@ -204,5 +240,6 @@ async function handleMcpPolicyUpdate(
|
||||
toolName,
|
||||
mcpName: confirmationDetails.serverName,
|
||||
persist,
|
||||
persistScope,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -608,6 +608,7 @@ export class Scheduler {
|
||||
await updatePolicy(toolCall.tool, outcome, lastDetails, {
|
||||
config: this.config,
|
||||
messageBus: this.messageBus,
|
||||
toolInvocation: toolCall.invocation,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user