src/vs/platform/agentHost/node/claude/claudeCanUseTool.ts

277 LOC · 268 covered · 9 uncovered · 38 ranges · 357 concepts · 17 introducers · 200 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- claudeAgent.ts ×91
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
7 > import { ClaudePermissionMode, ClaudeSessionConfigKey } from '../../common/claudeSessionConfigKeys.js';
8 > import { ChatInputResponseKind, ToolCallPendingConfirmationState, ToolCallStatus } from '../../common/state/protocol/state.js';
9 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
10 > import { ClaudeAgentSession } from './claudeAgentSession.js';
11 > import { buildAskUserSessionInputQuestions, buildExitPlanModeConfirmationState, flattenAskUserAnswers, parseAskUserQuestionInput } from './claudeInteractiveTools.js';
12 > import { CLAUDE_PLAN_DECLINED_MESSAGE, CLAUDE_QUESTION_CANCELLED_MESSAGE, CLAUDE_USER_DECLINED_MESSAGE } from './claudeToolDenial.js';
13 > import { getClaudeConfirmationTitle, getClaudeInvocationMessage, getClaudePermissionKind, getClaudeToolDisplayName, getClaudeToolInputString, getClaudeToolPath, INTERACTIVE_CLAUDE_TOOLS, buildClaudeToolMeta } from './claudeToolDisplay.js';
14 >
15 > /**
16 > * Dependencies for {@link handleCanUseTool}. Kept narrow: a session
17 > * lookup callback (so the agent's `_sessions` map stays private) and
18 > * the configuration service for the one mutation point
19 > * (`ExitPlanMode` Approve persists `permissionMode = 'acceptEdits'`).
20 > * Subagent correlation reads from `session.subagents` (the per-session
21 > * {@link import('./claudeSubagentRegistry.js').SubagentRegistry}); the
22 > * bridge no longer takes a host-singleton resolver dep.
23 > */
24 > export interface IClaudeCanUseToolDeps {
25 > readonly getSession: (sessionId: string) => ClaudeAgentSession | undefined;
26 > readonly configurationService: IAgentConfigurationService;
27 > }
28 >
29 > /**
30 > * SDK `canUseTool` `options` shape. Re-stated here to keep this module
31 > * decoupled from the agent's import wall.
32 > */
33 > export interface IClaudeCanUseToolOptions {
34 > readonly suggestions?: PermissionUpdate[];
35 > readonly signal: AbortSignal;
36 > readonly blockedPath?: string;
37 > readonly toolUseID: string;
38 > /**
39 > * Phase 12 step 5 — SDK-supplied subagent id for inner-tool
40 > * confirmations. When set, the bridge resolves the parent
41 > * `tool_use_id` via the mapper state and tags the resulting
42 > * `pending_confirmation` so the host can route it to the subagent
43 > * session and feed the resolver cache.
44 > */
45 > readonly agentID?: string;
46 > }
47 >
48 > /**
49 > * SDK `canUseTool` callback. Fires `pending_confirmation` and parks
50 > * on {@link ClaudeAgentSession.requestPermission} (or
51 > * {@link ClaudeAgentSession.requestUserInput} for `AskUserQuestion`)
52 > * until the workbench dispatches a response.
53 > *
54 > * **Pure UI bridge.** No permission judgement of its own — the SDK
55 > * owns auto-approval / auto-denial via `permissionMode`
56 > * ([sdk.d.ts:1558](../../../../../../extensions/copilot/node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts#L1558))
57 > * and only invokes `canUseTool` for tools it has decided the host
58 > * needs to surface. The interactive built-ins (`AskUserQuestion`,
59 > * `ExitPlanMode`) are exempt from auto-approval and always reach
60 > * `canUseTool` regardless of mode — their "permission" is itself the
61 > * user-facing question.
62 > *
63 > * Note: protocol-level auto-approve for write tools lives in
64 > * `agentSideEffects.ts:_handleToolReady`, which subscribes to the
65 > * `pending_confirmation` signal and calls
66 > * `respondToPermissionRequest`. The atomic register-then-fire
67 > * invariant lives inside {@link ClaudeAgentSession.requestPermission}
68 > * (via `PendingRequestRegistry.registerAndFire`).
69 > */
70 > export async function handleCanUseTool( claudeCanUseTool.ts ×3
71 > deps: IClaudeCanUseToolDeps,
72 > sessionId: string,
73 > toolName: string,
74 > input: Record<string, unknown>,
75 > options: IClaudeCanUseToolOptions,
76 > ): Promise<PermissionResult> {
77 > const session = deps.getSession(sessionId);
78 > if (!session) {
79 return { behavior: 'deny', message: 'Session is no longer active' };
80 }
82 > // Observe the SDK's per-request abort signal so a host parked on
83 > // `requestPermission` / `requestUserInput` unwinds promptly when
84 > // the SDK cancels the canUseTool call (subprocess teardown,
85 > // upstream abort). Both `respondTo*` methods are no-ops if the
86 > // id is not pending, so it is safe to fire both regardless of
87 > // which channel this tool happens to use.
88 > if (options.signal.aborted) {
89 > return { behavior: 'deny', message: 'SDK aborted the tool request' }; claudeCanUseTool.ts ×1
90 > }
91 > const abortHandler = () => { claudeCanUseTool.ts ×6
92 > session.respondToPermissionRequest(options.toolUseID, false); claudeCanUseTool.ts ×1
93 > session.respondToUserInputRequest(options.toolUseID, ChatInputResponseKind.Cancel);
94 > };
95 > options.signal.addEventListener('abort', abortHandler); claudeCanUseTool.ts ×6
96 > try {
97 > return await dispatchCanUseTool(deps, session, toolName, input, options);
98 > } finally {
99 > options.signal.removeEventListener('abort', abortHandler);
100 > }
103 > async function dispatchCanUseTool( claudeCanUseTool.ts ×6
104 > deps: IClaudeCanUseToolDeps,
105 > session: ClaudeAgentSession,
106 > toolName: string,
107 > input: Record<string, unknown>,
108 > options: IClaudeCanUseToolOptions,
109 > ): Promise<PermissionResult> {
110 > // Interactive tools (`AskUserQuestion`, `ExitPlanMode`) are
111 > // exempt from SDK `permissionMode` auto-approval, so they reach
112 > // `canUseTool` even under `bypassPermissions`. Routing then
113 > // splits by tool semantics rather than by the
114 > // `INTERACTIVE_CLAUDE_TOOLS` flag itself: `ExitPlanMode` is a
115 > // permission gate (Approve/Deny on whether to leave plan mode)
116 > // so it uses the standard `pending_confirmation` channel with
117 > // custom button labels; `AskUserQuestion` is structured user
118 > // input (a question carousel) so it routes through
119 > // `requestUserInput` / `ChatInputRequested`.
120 > if (INTERACTIVE_CLAUDE_TOOLS.has(toolName)) {
121 > return handleInteractiveTool(deps, session, toolName, input, options); claudeCanUseTool.ts ×5
122 > }
124 > const permissionKind = getClaudePermissionKind(toolName);
125 > const displayName = getClaudeToolDisplayName(toolName);
126 > const permissionPath = options.blockedPath ?? getClaudeToolPath(toolName, input);
127 > const toolInputString = getClaudeToolInputString(toolName, input); claudeCanUseTool.ts ×6
128 > const meta = buildClaudeToolMeta(toolName);
129 > const state: ToolCallPendingConfirmationState = {
130 > status: ToolCallStatus.PendingConfirmation,
131 > toolCallId: options.toolUseID,
132 > toolName,
133 > displayName,
134 > invocationMessage: getClaudeInvocationMessage(toolName, displayName, input),
135 > toolInput: toolInputString,
136 > confirmationTitle: getClaudeConfirmationTitle(toolName),
137 > ...(meta ? { _meta: meta } : {}),
138 > };
139 >
140 > const parentToolCallId = resolveSubagentParent(session, options);
141 >
142 > const approved = await session.requestPermission({
143 > toolUseID: options.toolUseID,
144 > state,
145 > permissionKind,
146 > ...(permissionPath !== undefined ? { permissionPath } : {}),
147 > ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
148 > });
149 > return approved claudeCanUseTool.ts ×2
150 > ? { behavior: 'allow', updatedInput: input } claudeCanUseTool.ts ×1
151 > : { behavior: 'deny', message: CLAUDE_USER_DECLINED_MESSAGE }; claudeCanUseTool.ts ×1
154 > /**
155 > * Phase 12 step 5 — shared subagent-context resolution for every
156 > * `pending_confirmation` and `ChatInputRequested` emission. When the
157 > * SDK delivers `options.agentID`, look up the parent spawn via the
158 > * session's registry and write the agentId back to it. The write is
159 > * **first-writer-wins** (a mismatched late agentID is silently dropped
160 > * — see {@link SubagentSpawn.setAgentId}); all writers converge on the
161 > * SDK's single identity for a given Task, so conflict is not expected.
162 > * Returns the parent `tool_use_id` for top-level callers to spread
163 > * onto the request payload, or `undefined` when this isn't an inner
164 > * tool call (no subagent context).
165 > */
166 > function resolveSubagentParent( claudeCanUseTool.ts ×6
167 > session: ClaudeAgentSession,
168 > options: IClaudeCanUseToolOptions,
169 > ): string | undefined {
170 > if (!options.agentID) {
171 > return undefined; claudeCanUseTool.ts ×1
172 > }
173 > const parentSpawn = session.subagents.getParentSpawn(options.toolUseID); claudeCanUseTool.ts ×1
174 > if (parentSpawn) {
175 > parentSpawn.setAgentId(options.agentID);
176 > return parentSpawn.toolUseId;
177 > }
178 return undefined;
179 }
181 > /**
182 > * Dispatch the two interactive built-in tools (S3.5). They share a
183 > * dispatcher only because both are exempt from SDK
184 > * `permissionMode` auto-approval; routing then splits by tool
185 > * semantics. Caller must guard with {@link INTERACTIVE_CLAUDE_TOOLS} —
186 > * the `default` branch is defensive and should never fire.
187 > */
188 > function handleInteractiveTool( claudeCanUseTool.ts ×5
189 > deps: IClaudeCanUseToolDeps,
190 > session: ClaudeAgentSession,
191 > toolName: string,
192 > input: Record<string, unknown>,
193 > options: IClaudeCanUseToolOptions,
194 > ): Promise<PermissionResult> {
195 > switch (toolName) {
196 > case 'ExitPlanMode':
197 > return handleExitPlanMode(deps, session, input, options); claudeCanUseTool.ts ×2
198 > case 'AskUserQuestion': claudeCanUseTool.ts ×5
199 > return handleAskUserQuestion(deps, session, input, options); claudeCanUseTool.ts ×3
200 > default: claudeCanUseTool.ts ×5
201 return Promise.resolve({ behavior: 'deny', message: `Unsupported interactive tool: ${toolName}` });
203 > }
205 > /**
206 > * `ExitPlanMode` (S3.5b): render the plan body inside the standard
207 > * tool-confirmation card (`pending_confirmation` channel — same path
208 > * normal write tools take), persist `permissionMode = 'acceptEdits'`
209 > * on Approve (next `sendMessage` forwards via `Query.setPermissionMode`),
210 > * deny with production-mirrored wording on cancel.
211 > *
212 > * NOTE: we MUST NOT call `session.setPermissionMode` here. That issues
213 > * a live SDK control request on the same channel the SDK is using to
214 > * deliver the canUseTool request — interleaving a second control
215 > * request before returning the canUseTool response collides with the
216 > * SDK's loop and the turn never resumes. Production updates state
217 > * post-tool-result (`claudeMessageDispatch.ts:328` →
218 > * `setPermissionModeForSession`); we mirror by writing
219 > * `IAgentConfigurationService` and letting `sendMessage`'s
220 > * `entry.setPermissionMode(...)` (between turns) do the live forward.
221 > */
222 > async function handleExitPlanMode( claudeCanUseTool.ts ×2
223 > deps: IClaudeCanUseToolDeps,
224 > session: ClaudeAgentSession,
225 > input: Record<string, unknown>,
226 > options: IClaudeCanUseToolOptions,
227 > ): Promise<PermissionResult> {
228 > const toolUseID = options.toolUseID;
229 > const parentToolCallId = resolveSubagentParent(session, options);
230 > const approved = await session.requestPermission({
231 > toolUseID,
232 > state: buildExitPlanModeConfirmationState(input, toolUseID),
233 > permissionKind: getClaudePermissionKind('ExitPlanMode'),
234 > ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
235 > });
236 > if (approved) {
237 > deps.configurationService.updateSessionConfig(session.sessionUri.toString(), { claudeCanUseTool.ts ×1
238 > [ClaudeSessionConfigKey.PermissionMode]: 'acceptEdits' satisfies ClaudePermissionMode,
239 > });
240 > return { behavior: 'allow', updatedInput: input };
241 > }
242 > return { behavior: 'deny', message: CLAUDE_PLAN_DECLINED_MESSAGE }; claudeCanUseTool.ts ×1
243 > }
245 > /**
246 > * `AskUserQuestion` (S3.5a): translate the SDK's question carousel
247 > * into a {@link ChatInputRequest}, await the workbench answer,
248 > * and re-key answers by question text (matching the production
249 > * extension's `Record<question, value>` contract).
250 > */
251 > async function handleAskUserQuestion( claudeCanUseTool.ts ×3
252 > deps: IClaudeCanUseToolDeps,
253 > session: ClaudeAgentSession,
254 > input: Record<string, unknown>,
255 > options: IClaudeCanUseToolOptions,
256 > ): Promise<PermissionResult> {
257 > const toolUseID = options.toolUseID;
258 > const askInput = parseAskUserQuestionInput(input);
259 > if (!askInput) {
260 return { behavior: 'deny', message: 'AskUserQuestion called without questions' };
261 }
263 > const parentToolCallId = resolveSubagentParent(session, options);
264 > const answer = await session.requestUserInput({
265 > id: toolUseID,
266 > questions: buildAskUserSessionInputQuestions(askInput),
267 > }, parentToolCallId);
268 > if (answer.response !== ChatInputResponseKind.Accept || !answer.answers) {
269 > return { behavior: 'deny', message: CLAUDE_QUESTION_CANCELLED_MESSAGE }; claudeCanUseTool.ts ×1
270 > }
272 > const answers = flattenAskUserAnswers(askInput, answer.answers);
273 > if (Object.keys(answers).length === 0) {
274 return { behavior: 'deny', message: CLAUDE_QUESTION_CANCELLED_MESSAGE };
275 }
276 > return { behavior: 'allow', updatedInput: { ...input, answers } }; claudeCanUseTool.ts ×2
277 > }