feat: Persistent "Always Allow" policies with granular shell & MCP support (#14737)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Allen Hutchison
2025-12-12 13:45:39 -08:00
committed by GitHub
parent d2a1a45646
commit 5f298c17d7
18 changed files with 431 additions and 21 deletions
+1
View File
@@ -313,6 +313,7 @@ class EditToolInvocation
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
}
await this.publishPolicyUpdate(outcome);
if (ideConfirmation) {
const result = await ideConfirmation;
+10
View File
@@ -16,6 +16,7 @@ import {
BaseToolInvocation,
Kind,
ToolConfirmationOutcome,
type PolicyUpdateOptions,
} from './tools.js';
import type { CallableTool, FunctionCall, Part } from '@google/genai';
import { ToolErrorType } from './tool-error.js';
@@ -87,6 +88,12 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
);
}
protected override getPolicyUpdateOptions(
_outcome: ToolConfirmationOutcome,
): PolicyUpdateOptions | undefined {
return { mcpName: this.serverName };
}
protected override async getConfirmationDetails(
_abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
@@ -115,6 +122,9 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
DiscoveredMCPToolInvocation.allowlist.add(serverAllowListKey);
} else if (outcome === ToolConfirmationOutcome.ProceedAlwaysTool) {
DiscoveredMCPToolInvocation.allowlist.add(toolAllowListKey);
} else if (outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave) {
DiscoveredMCPToolInvocation.allowlist.add(toolAllowListKey);
await this.publishPolicyUpdate(outcome);
}
},
};
+1
View File
@@ -226,6 +226,7 @@ class MemoryToolInvocation extends BaseToolInvocation<
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
MemoryToolInvocation.allowlist.add(allowlistKey);
}
await this.publishPolicyUpdate(outcome);
},
};
return confirmationDetails;
+11
View File
@@ -22,6 +22,7 @@ import {
BaseToolInvocation,
ToolConfirmationOutcome,
Kind,
type PolicyUpdateOptions,
} from './tools.js';
import { ApprovalMode } from '../policy/types.js';
@@ -83,6 +84,15 @@ export class ShellToolInvocation extends BaseToolInvocation<
return description;
}
protected override getPolicyUpdateOptions(
outcome: ToolConfirmationOutcome,
): PolicyUpdateOptions | undefined {
if (outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave) {
return { commandPrefix: this.params.command };
}
return undefined;
}
protected override async getConfirmationDetails(
_abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
@@ -124,6 +134,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
commandsToConfirm.forEach((command) => this.allowlist.add(command));
}
await this.publishPolicyUpdate(outcome);
},
};
return confirmationDetails;
+1
View File
@@ -683,6 +683,7 @@ class EditToolInvocation
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
}
await this.publishPolicyUpdate(outcome);
if (ideConfirmation) {
const result = await ideConfirmation;
+44 -9
View File
@@ -65,6 +65,14 @@ export interface ToolInvocation<
): Promise<TResult>;
}
/**
* Options for policy updates that can be customized by tool invocations.
*/
export interface PolicyUpdateOptions {
commandPrefix?: string;
mcpName?: string;
}
/**
* A convenience base class for ToolInvocation.
*/
@@ -112,6 +120,40 @@ export abstract class BaseToolInvocation<
return this.getConfirmationDetails(abortSignal);
}
/**
* Returns tool-specific options for policy updates.
* Subclasses can override this to provide additional options like
* commandPrefix (for shell) or mcpName (for MCP tools).
*/
protected getPolicyUpdateOptions(
_outcome: ToolConfirmationOutcome,
): PolicyUpdateOptions | undefined {
return undefined;
}
/**
* Helper method to publish a policy update when user selects
* ProceedAlways or ProceedAlwaysAndSave.
*/
protected async publishPolicyUpdate(
outcome: ToolConfirmationOutcome,
): Promise<void> {
if (
outcome === ToolConfirmationOutcome.ProceedAlways ||
outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave
) {
if (this.messageBus && this._toolName) {
const options = this.getPolicyUpdateOptions(outcome);
await this.messageBus.publish({
type: MessageBusType.UPDATE_POLICY,
toolName: this._toolName,
persist: outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave,
...options,
});
}
}
}
/**
* Subclasses should override this method to provide custom confirmation UI
* when the policy engine's decision is 'ASK_USER'.
@@ -129,15 +171,7 @@ export abstract class BaseToolInvocation<
title: `Confirm: ${this._toolDisplayName || this._toolName}`,
prompt: this.getDescription(),
onConfirm: async (outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
if (this.messageBus && this._toolName) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.messageBus.publish({
type: MessageBusType.UPDATE_POLICY,
toolName: this._toolName,
});
}
}
await this.publishPolicyUpdate(outcome);
},
};
return confirmationDetails;
@@ -686,6 +720,7 @@ export type ToolCallConfirmationDetails =
export enum ToolConfirmationOutcome {
ProceedOnce = 'proceed_once',
ProceedAlways = 'proceed_always',
ProceedAlwaysAndSave = 'proceed_always_and_save',
ProceedAlwaysServer = 'proceed_always_server',
ProceedAlwaysTool = 'proceed_always_tool',
ModifyWithEditor = 'modify_with_editor',
+1
View File
@@ -244,6 +244,7 @@ ${textContent}
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
}
await this.publishPolicyUpdate(outcome);
},
};
return confirmationDetails;
+1
View File
@@ -224,6 +224,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
}
await this.publishPolicyUpdate(outcome);
if (ideConfirmation) {
const result = await ideConfirmation;