From e2184e1acf8b43b7bbff815caff67470fa6c8ff7 Mon Sep 17 00:00:00 2001 From: justschen Date: Tue, 23 Jun 2026 10:59:48 -0700 Subject: [PATCH] fix client side tools not getting auto approved --- .../agentHost/node/agentSideEffects.ts | 33 ++++++++- .../test/node/agentSideEffects.test.ts | 70 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 0cb3f7fa50f59d..c5300895cee8d8 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -381,13 +381,41 @@ export class AgentSideEffects extends Disposable { return; } if (signal.kind === 'action') { - this._stateManager.dispatchServerAction(sessionKey, signal.action); - if (signal.action.type === ActionType.ChatTurnComplete) { + const action = this._stampClientToolAutoApprove(signal.action, sessionKey); + this._stateManager.dispatchServerAction(sessionKey, action); + if (action.type === ActionType.ChatTurnComplete) { this._runTurnCompleteSideEffects(sessionKey, undefined); } } } + /** + * Stamps `autoApproveBySetting` onto a client-contributed + * {@link ActionType.ChatToolCallStart} when the session is in Bypass + * Approvals mode (`autoApprove`). + * + * Client tools execute in the workbench and confirm via their own + * `prepareToolInvocation` card rather than the server-driven + * `pending_confirmation` flow that {@link _handleToolReady} auto-approves. + * Without this stamp the workbench client gate + * (`shouldAutoApproveClientToolCall`) never trips and the tool prompts + * even though the session bypasses approvals. Stamping at the start of the + * tool call carries the flag through the reducer onto the tool-call state, + * where the client reads it. Other actions are returned unchanged. + */ + private _stampClientToolAutoApprove(action: T, sessionKey: ProtocolURI): T { + if (action.type !== ActionType.ChatToolCallStart + || action.contributor?.kind !== ToolCallContributorKind.Client + || !this._permissionManager.isSessionAutoApproveEnabled(sessionKey) + ) { + return action; + } + return { + ...action, + _meta: { ...action._meta, ...toToolCallMeta({ autoApproveBySetting: true }) }, + }; + } + /** * Dispatches a signal against a resolved session+turn. Performs the * subagent-content merge for tool_complete and the related side effects. @@ -416,6 +444,7 @@ export class AgentSideEffects extends Disposable { if (hasKey(action, { turnId: true }) && action.turnId !== turnId) { action = { ...action, turnId }; } + action = this._stampClientToolAutoApprove(action, sessionKey); // When a parent tool call has an associated subagent session, // preserve the subagent content metadata in the completion result. diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 01a10fbcedb89b..886131c1dc27a8 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -678,6 +678,76 @@ suite('AgentSideEffects', () => { }); }); + // ---- client tool auto-approval under Bypass ----------------------------- + + suite('client tool auto-approval under Bypass', () => { + + function setAutoApprove(level: string): void { + stateManager.setSessionConfig(sessionUri.toString(), { + schema: { + type: 'object', + properties: { + autoApprove: { type: 'string', title: 'Approvals', enum: ['default', 'autoApprove', 'autopilot'], default: 'default' }, + }, + }, + values: { autoApprove: level }, + }); + } + + function fireClientToolStart(contributorKind: ToolCallContributorKind | undefined): ActionEnvelope[] { + const envelopes: ActionEnvelope[] = []; + disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); + disposables.add(sideEffects.registerProgressListener(agent)); + agent.fireProgress({ + kind: 'action', session: sessionUri, + action: { + type: ActionType.ChatToolCallStart, + turnId: 'turn-1', + toolCallId: 'tc-1', + toolName: 'runPlaywrightCode', + displayName: 'Run Playwright Code', + contributor: contributorKind === ToolCallContributorKind.Client + ? { kind: ToolCallContributorKind.Client, clientId: 'client-1' } + : undefined, + }, + }); + return envelopes; + } + + function startActionMeta(envelopes: ActionEnvelope[]): Record | undefined { + const start = envelopes.find(e => e.action.type === ActionType.ChatToolCallStart); + assert.ok(start, 'expected a ChatToolCallStart envelope'); + return (start.action as { _meta?: Record })._meta; + } + + test('stamps autoApproveBySetting on a client tool start when the session bypasses approvals', () => { + setupSession(); + startTurn('turn-1'); + setAutoApprove('autoApprove'); + + const meta = startActionMeta(fireClientToolStart(ToolCallContributorKind.Client)); + assert.strictEqual(meta?.autoApproveBySetting, true); + }); + + test('does not stamp a client tool start when the session does not bypass approvals', () => { + setupSession(); + startTurn('turn-1'); + setAutoApprove('default'); + + const meta = startActionMeta(fireClientToolStart(ToolCallContributorKind.Client)); + assert.strictEqual(meta?.autoApproveBySetting, undefined); + }); + + test('does not stamp a server (non-client) tool start under Bypass', () => { + setupSession(); + startTurn('turn-1'); + setAutoApprove('autoApprove'); + + const meta = startActionMeta(fireClientToolStart(undefined)); + assert.strictEqual(meta?.autoApproveBySetting, undefined); + }); + }); + // ---- agents observable -------------------------------------------------- suite('agents observable', () => {