Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends StateAction>(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.
Expand Down Expand Up @@ -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.
Expand Down
70 changes: 70 additions & 0 deletions src/vs/platform/agentHost/test/node/agentSideEffects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined {
const start = envelopes.find(e => e.action.type === ActionType.ChatToolCallStart);
assert.ok(start, 'expected a ChatToolCallStart envelope');
return (start.action as { _meta?: Record<string, unknown> })._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', () => {
Expand Down
Loading