fix(core): prevent duplicate tool approval entries in auto-saved.toml (#19487)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Abhijit Balaji
2026-02-19 12:03:52 -08:00
committed by GitHub
parent c276d0c7b6
commit 3408542a66
11 changed files with 39 additions and 62 deletions
@@ -117,8 +117,8 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
type: 'info', type: 'info',
title: `Call Remote Agent: ${this.definition.displayName ?? this.definition.name}`, title: `Call Remote Agent: ${this.definition.displayName ?? this.definition.name}`,
prompt: `Calling remote agent: "${this.params.query}"`, prompt: `Calling remote agent: "${this.params.query}"`,
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (_outcome: ToolConfirmationOutcome) => {
await this.publishPolicyUpdate(outcome); // Policy updates are now handled centrally by the scheduler
}, },
}; };
} }
@@ -136,17 +136,17 @@ describe('Tool Confirmation Policy Updates', () => {
it.each([ it.each([
{ {
outcome: ToolConfirmationOutcome.ProceedAlways, outcome: ToolConfirmationOutcome.ProceedAlways,
shouldPublish: false, _shouldPublish: false,
expectedApprovalMode: ApprovalMode.AUTO_EDIT, expectedApprovalMode: ApprovalMode.AUTO_EDIT,
}, },
{ {
outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave, outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave,
shouldPublish: true, _shouldPublish: true,
persist: true, _persist: true,
}, },
])( ])(
'should handle $outcome correctly', 'should handle $outcome correctly',
async ({ outcome, shouldPublish, persist, expectedApprovalMode }) => { async ({ outcome, expectedApprovalMode }) => {
const tool = create(mockConfig, mockMessageBus); const tool = create(mockConfig, mockMessageBus);
// For file-based tools, ensure the file exists if needed // For file-based tools, ensure the file exists if needed
@@ -172,26 +172,19 @@ describe('Tool Confirmation Policy Updates', () => {
if (confirmation) { if (confirmation) {
await confirmation.onConfirm(outcome); await confirmation.onConfirm(outcome);
if (shouldPublish) { // Policy updates are no longer published by onConfirm; they are
expect(mockMessageBus.publish).toHaveBeenCalledWith( // handled centrally by the schedulers.
expect.objectContaining({ const publishCalls = (mockMessageBus.publish as any).mock.calls;
type: MessageBusType.UPDATE_POLICY, const hasUpdatePolicy = publishCalls.some(
persist, (call: any) => call[0].type === MessageBusType.UPDATE_POLICY,
}), );
); expect(hasUpdatePolicy).toBe(false);
} else {
// Should not publish UPDATE_POLICY message for ProceedAlways
const publishCalls = (mockMessageBus.publish as any).mock.calls;
const hasUpdatePolicy = publishCalls.some(
(call: any) => call[0].type === MessageBusType.UPDATE_POLICY,
);
expect(hasUpdatePolicy).toBe(false);
}
if (expectedApprovalMode !== undefined) { if (expectedApprovalMode !== undefined) {
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith( // expectedApprovalMode in this test (AUTO_EDIT) is now handled
expectedApprovalMode, // by updatePolicy in the scheduler, so it should not be called
); // here either.
expect(mockConfig.setApprovalMode).not.toHaveBeenCalled();
} }
} }
}, },
+4 -9
View File
@@ -14,7 +14,7 @@ import {
BaseToolInvocation, BaseToolInvocation,
Kind, Kind,
type ToolCallConfirmationDetails, type ToolCallConfirmationDetails,
ToolConfirmationOutcome, type ToolConfirmationOutcome,
type ToolEditConfirmationDetails, type ToolEditConfirmationDetails,
type ToolInvocation, type ToolInvocation,
type ToolLocation, type ToolLocation,
@@ -725,14 +725,9 @@ class EditToolInvocation
fileDiff, fileDiff,
originalContent: editData.currentContent, originalContent: editData.currentContent,
newContent: editData.newContent, newContent: editData.newContent,
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (_outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) { // Mode transitions (e.g. AUTO_EDIT) and policy updates are now
// No need to publish a policy update as the default policy for // handled centrally by the scheduler.
// AUTO_EDIT already reflects always approving edit.
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
} else {
await this.publishPolicyUpdate(outcome);
}
if (ideConfirmation) { if (ideConfirmation) {
const result = await ideConfirmation; const result = await ideConfirmation;
+1 -1
View File
@@ -105,7 +105,7 @@ export class EnterPlanModeInvocation extends BaseToolInvocation<
'This will restrict the agent to read-only tools to allow for safe planning.', 'This will restrict the agent to read-only tools to allow for safe planning.',
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (outcome: ToolConfirmationOutcome) => {
this.confirmationOutcome = outcome; this.confirmationOutcome = outcome;
await this.publishPolicyUpdate(outcome); // Policy updates are now handled centrally by the scheduler
}, },
}; };
} }
+1 -1
View File
@@ -130,7 +130,7 @@ export class DiscoveredMCPToolInvocation extends BaseToolInvocation<
DiscoveredMCPToolInvocation.allowlist.add(toolAllowListKey); DiscoveredMCPToolInvocation.allowlist.add(toolAllowListKey);
} else if (outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave) { } else if (outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave) {
DiscoveredMCPToolInvocation.allowlist.add(toolAllowListKey); DiscoveredMCPToolInvocation.allowlist.add(toolAllowListKey);
await this.publishPolicyUpdate(outcome); // Persistent policy updates are now handled centrally by the scheduler
} }
}, },
}; };
+1 -1
View File
@@ -204,7 +204,7 @@ class MemoryToolInvocation extends BaseToolInvocation<
if (outcome === ToolConfirmationOutcome.ProceedAlways) { if (outcome === ToolConfirmationOutcome.ProceedAlways) {
MemoryToolInvocation.allowlist.add(allowlistKey); MemoryToolInvocation.allowlist.add(allowlistKey);
} }
await this.publishPolicyUpdate(outcome); // Policy updates are now handled centrally by the scheduler
}, },
}; };
return confirmationDetails; return confirmationDetails;
+2 -2
View File
@@ -140,8 +140,8 @@ export class ShellToolInvocation extends BaseToolInvocation<
command: this.params.command, command: this.params.command,
rootCommand: rootCommandDisplay, rootCommand: rootCommandDisplay,
rootCommands, rootCommands,
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (_outcome: ToolConfirmationOutcome) => {
await this.publishPolicyUpdate(outcome); // Policy updates are now handled centrally by the scheduler
}, },
}; };
return confirmationDetails; return confirmationDetails;
+2 -2
View File
@@ -173,8 +173,8 @@ export abstract class BaseToolInvocation<
type: 'info', type: 'info',
title: `Confirm: ${this._toolDisplayName || this._toolName}`, title: `Confirm: ${this._toolDisplayName || this._toolName}`,
prompt: this.getDescription(), prompt: this.getDescription(),
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (_outcome: ToolConfirmationOutcome) => {
await this.publishPolicyUpdate(outcome); // Policy updates are now handled centrally by the scheduler
}, },
}; };
return confirmationDetails; return confirmationDetails;
+3 -4
View File
@@ -390,7 +390,7 @@ describe('WebFetchTool', () => {
expect(confirmationDetails).toBe(false); expect(confirmationDetails).toBe(false);
}); });
it('should call setApprovalMode when onConfirm is called with ProceedAlways', async () => { it('should NOT call setApprovalMode when onConfirm is called with ProceedAlways (now handled by scheduler)', async () => {
const tool = new WebFetchTool(mockConfig, bus); const tool = new WebFetchTool(mockConfig, bus);
const params = { prompt: 'fetch https://example.com' }; const params = { prompt: 'fetch https://example.com' };
const invocation = tool.build(params); const invocation = tool.build(params);
@@ -408,9 +408,8 @@ describe('WebFetchTool', () => {
); );
} }
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith( // Schedulers are now responsible for mode transitions via updatePolicy
ApprovalMode.AUTO_EDIT, expect(mockConfig.setApprovalMode).not.toHaveBeenCalled();
);
}); });
}); });
+4 -9
View File
@@ -13,7 +13,7 @@ import {
BaseDeclarativeTool, BaseDeclarativeTool,
BaseToolInvocation, BaseToolInvocation,
Kind, Kind,
ToolConfirmationOutcome, type ToolConfirmationOutcome,
} from './tools.js'; } from './tools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
@@ -246,14 +246,9 @@ ${textContent}
title: `Confirm Web Fetch`, title: `Confirm Web Fetch`,
prompt: this.params.prompt, prompt: this.params.prompt,
urls, urls,
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (_outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) { // Mode transitions (e.g. AUTO_EDIT) and policy updates are now
// No need to publish a policy update as the default policy for // handled centrally by the scheduler.
// AUTO_EDIT already reflects always approving web-fetch.
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
} else {
await this.publishPolicyUpdate(outcome);
}
}, },
}; };
return confirmationDetails; return confirmationDetails;
+4 -9
View File
@@ -25,7 +25,7 @@ import {
BaseDeclarativeTool, BaseDeclarativeTool,
BaseToolInvocation, BaseToolInvocation,
Kind, Kind,
ToolConfirmationOutcome, type ToolConfirmationOutcome,
} from './tools.js'; } from './tools.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
@@ -228,14 +228,9 @@ class WriteFileToolInvocation extends BaseToolInvocation<
fileDiff, fileDiff,
originalContent, originalContent,
newContent: correctedContent, newContent: correctedContent,
onConfirm: async (outcome: ToolConfirmationOutcome) => { onConfirm: async (_outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) { // Mode transitions (e.g. AUTO_EDIT) and policy updates are now
// No need to publish a policy update as the default policy for // handled centrally by the scheduler.
// AUTO_EDIT already reflects always approving write-file.
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
} else {
await this.publishPolicyUpdate(outcome);
}
if (ideConfirmation) { if (ideConfirmation) {
const result = await ideConfirmation; const result = await ideConfirmation;