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

255 LOC · 255 covered · 0 uncovered · 24 ranges · 374 concepts · 16 introducers · 211 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 > /*--------------------------------------------------------------------------------------------- claudeSdkOptions.ts ×4
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 { McpSdkServerConfigWithInstance, OnElicitation, Options } from '@anthropic-ai/claude-agent-sdk';
7 > import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8 > import { tmpdir } from 'os';
9 > import { delimiter, dirname } from '../../../../base/common/path.js';
10 > import { URI } from '../../../../base/common/uri.js';
11 > import { rgDiskPath } from '../../../../base/node/ripgrep.js';
12 > import { AiAgentEnvValue, AiAgentEnvVar } from '../../../chat/common/aiAgentEnv.js';
13 > import { ClaudePermissionMode } from '../../common/claudeSessionConfigKeys.js';
14 > import { resolveClaudeEffort } from '../../common/claudeModelConfig.js';
15 > import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js';
16 > import type { ModelSelection } from '../../common/state/protocol/state.js';
17 > import { IClaudeAgentSdkService } from './claudeAgentSdkService.js';
18 > import { buildClientToolMcpServer } from './clientTools/claudeClientToolMcpServer.js';
19 > import { toSdkModelId } from './claudeModelId.js';
20 > import type { ClaudeTransport } from './claudeProxyService.js';
21 > import { SessionClientToolsDiff } from './clientTools/claudeSessionClientToolsModel.js';
22 >
23 > /**
24 > * Inputs to {@link buildOptions} that vary per startup. Pure-data: no
25 > * services, no live event subscribers. The function is a deterministic
26 > * projection from this bag plus a {@link IClaudeProxyHandle} onto the
27 > * SDK's {@link Options} discriminated union.
28 > */
29 > export interface IBuildOptionsInput {
30 > readonly sessionId: string;
31 > readonly workingDirectory: URI;
32 > readonly model: ModelSelection | undefined;
33 > readonly abortController: AbortController;
34 > readonly permissionMode: ClaudePermissionMode;
35 > readonly canUseTool: NonNullable<Options['canUseTool']>;
36 > readonly onElicitation: OnElicitation;
37 > readonly isResume: boolean;
38 > /**
39 > * One-shot SDK assistant-message uuid to resume *up to and including*
40 > * (the SDK's `Options.resumeSessionAt`). Only meaningful with
41 > * {@link isResume}; truncates the loaded transcript to this anchor so
42 > * the next turn continues from the restored point on the same session
43 > * id. Omitted in the non-resume (`sessionId`) branch and on ordinary
44 > * resumes. Set by `truncateSession` for the rebuild that immediately
45 > * precedes the post-restore turn.
46 > */
47 > readonly resumeSessionAt?: string;
48 > readonly mcpServers: Record<string, McpSdkServerConfigWithInstance> | undefined;
49 > /**
50 > * SDK-prefixed tool names to auto-approve without prompting (projected
51 > * onto `Options.allowedTools`). Used for the agent host's feedback server
52 > * tools, which only touch the session's annotations channel and are always
53 > * safe. Omitted from the returned options when empty so the SDK keeps its
54 > * default.
55 > */
56 > readonly allowedTools?: readonly string[];
57 > /**
58 > * Local plugin directories to load at SDK startup. Projected onto
59 > * `Options.plugins` as `{ type: 'local', path }`. Omitted from the
60 > * returned options entirely when empty so the SDK keeps its default
61 > * (no plugins). Built per-session from
62 > * {@link SessionClientCustomizationsDiff.consume}.
63 > */
64 > readonly plugins?: readonly URI[];
65 > /**
66 > * Resolved SDK agent name (matches a key in `Options.agents`, or an
67 > * agent loaded from `~/.claude/agents/**`). Projected onto
68 > * `Options.agent` — the SDK's `--agent` flag. The plugin URI captured
69 > * at startup is the only path the SDK consults, so any `changeAgent`
70 > * after materialize triggers a yield-restart through the rematerializer.
71 > * Omit when no custom agent is selected (SDK default behavior).
72 > */
73 > readonly agent?: string;
74 > }
75 >
76 > /**
77 > * Build the SDK {@link Options} bag for a Claude session startup.
78 > * Deterministic over its declared inputs plus three ambient reads:
79 > * 1. `process.env.PATH` (composed into `Options.settings.env.PATH`
80 > * so ripgrep wins over any system install),
81 > * 2. `process.env` keys via {@link buildSubprocessEnv} (used to
82 > * strip `VSCODE_*` / `ELECTRON_*` / `NODE_OPTIONS` /
83 > * `ANTHROPIC_API_KEY` from the spawn env),
84 > * 3. the memoized `rgDiskPath()` lookup.
85 > * The returned options carry the caller-supplied `abortController` so a
86 > * racing dispose unwinds `sdk.startup()` cleanly.
87 > *
88 > * Used by both the initial materialize and the yield-restart rematerialize
89 > * — both call sites pass a freshly-built `mcpServers` snapshot consumed
90 > * from the session's {@link SessionClientToolsDiff}.
91 > */
92 > export async function buildOptions( claudeSdkOptions.ts ×4
93 > input: IBuildOptionsInput,
94 > transport: ClaudeTransport,
95 > logStderr: (data: string) => void,
96 > ): Promise<Options> {
97 > const isProxy = transport.kind === 'proxy';
98 > const subprocessEnv = buildSubprocessEnv(isProxy);
99 > const resolvedRgDiskPath = await rgDiskPath();
100 > const settingsEnv: Record<string, string> = {
101 > // Proxied (Copilot-routed) mode points the SDK at the local proxy on a
102 > // per-session bearer. Native (BYO-Anthropic) mode omits both so the SDK
103 > // uses its own credential resolution from the subprocess env
104 > // (`ANTHROPIC_API_KEY`, or `CLAUDE_CODE_OAUTH_TOKEN` from `claude
105 > // setup-token` — both forwarded by `buildSubprocessEnv`).
106 > ...(transport.kind === 'proxy'
108 > ANTHROPIC_BASE_URL: transport.handle.baseUrl,
109 > ANTHROPIC_AUTH_TOKEN: `${transport.handle.nonce}.${input.sessionId}`,
110 > }
112 > CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', claudeSdkOptions.ts ×4
113 > USE_BUILTIN_RIPGREP: '0',
114 > // Attribute the CLI's tool subprocesses (`gh`, …) to VS Code.
115 > // `settings.env` is what the CLI layers onto the commands it runs, so it
116 > // needs the marker in addition to the spawn env below. Note the CLI
117 > // re-stamps `AI_AGENT` as `claude-code_<version>_agent` for its own Bash
118 > // tool, so commands from that tool are not attributed to VS Code.
119 > [AiAgentEnvVar]: AiAgentEnvValue,
120 > PATH: `${dirname(resolvedRgDiskPath)}${delimiter}${process.env.PATH ?? ''}`,
121 > };
122 >
123 > return {
124 > cwd: input.workingDirectory.fsPath,
125 > executable: process.execPath as 'node',
126 > env: subprocessEnv,
127 > abortController: input.abortController,
128 > allowDangerouslySkipPermissions: true,
129 > canUseTool: input.canUseTool,
130 > onElicitation: input.onElicitation,
131 > disallowedTools: ['WebSearch'],
132 > includePartialMessages: true,
133 > forwardSubagentText: true,
134 > enableFileCheckpointing: true,
135 > model: toSdkModelId(input.model?.id),
136 > effort: resolveClaudeEffort(input.model),
137 > permissionMode: input.permissionMode,
138 > ...(input.isResume
139 > ? { resume: input.sessionId, ...(input.resumeSessionAt ? { resumeSessionAt: input.resumeSessionAt } : {}) } claudeSdkOptions.ts ×1
140 > : { sessionId: input.sessionId }), claudeSdkOptions.ts ×1
141 > ...(input.mcpServers ? { mcpServers: input.mcpServers } : {}), claudeSdkOptions.ts ×4
142 > ...(input.allowedTools && input.allowedTools.length > 0 ? { allowedTools: [...input.allowedTools] } : {}),
143 > ...(input.plugins && input.plugins.length > 0
144 > ? { plugins: input.plugins.map(p => ({ type: 'local' as const, path: p.fsPath })) } claudeSdkOptions.ts ×1
146 > ...(input.agent ? { agent: input.agent } : {}), claudeSdkOptions.ts ×4
147 > settingSources: ['user', 'project', 'local'],
148 > settings: { env: settingsEnv },
149 > systemPrompt: { type: 'preset', preset: 'claude_code' },
150 > stderr: logStderr,
151 > };
152 > }
154 > /**
155 > * Consume the diff (clears its dirty bit) and build the in-process MCP
156 > * server config from the resulting tool snapshot. Resolves to
157 > * `undefined` when the snapshot is empty so `Options.mcpServers` is
158 > * omitted entirely and the SDK keeps its default.
159 > *
160 > * On builder throw the caller is responsible for re-marking the diff
161 > * dirty (the diff has already been consumed). See
162 > * {@link SessionClientToolsDiff.markDirty}.
163 > */
164 > export async function buildClientMcpServers( claudeAgentSession.ts ×14
165 > toolDiff: SessionClientToolsDiff,
166 > registry: PendingRequestRegistry<CallToolResult>,
167 > sdkService: IClaudeAgentSdkService,
168 > ): Promise<Record<string, McpSdkServerConfigWithInstance> | undefined> {
169 > const tools = toolDiff.consume();
170 > if (tools.length === 0) {
171 > return undefined; claudeAgentSession.ts ×1
172 > }
173 > const server = await buildClientToolMcpServer(tools, id => registry.register(id), sdkService); claudeAgentSession.ts ×1
174 > return { client: server };
175 > }
177 > /**
178 > * Build a minimal {@link Options} bag for an ephemeral model-enumeration
179 > * query (Phase 19, native transport). No workspace (`cwd = os.tmpdir()`), no
180 > * proxy env, and the user's `ANTHROPIC_API_KEY` preserved so the SDK can
181 > * authenticate. Reads the user's real `~/.claude` config so subscription
182 > * models (e.g. Opus) surface; verified not to write any session transcript
183 > * because the enumeration never iterates a turn. The caller (`_fetchNativeModels`)
184 > * aborts the returned `abortController` during teardown, alongside `query.close()`.
185 > */
186 > export function buildModelEnumerationOptions(): Options {
187 > return { claudeAgent.ts ×2
188 > cwd: tmpdir(),
189 > executable: process.execPath as 'node',
190 > env: buildSubprocessEnv(false),
191 > abortController: new AbortController(),
192 > systemPrompt: { type: 'preset', preset: 'claude_code' },
193 > settings: {
194 > env: {
195 > CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
196 > },
197 > },
198 > };
199 > }
201 > /**
202 > * Build the {@link Options.env} payload for the Claude subprocess.
203 > *
204 > * SDK >= 0.3 **replaces** the subprocess environment with `Options.env` — it is
205 > * NOT merged with `process.env` (sdk.d.ts:1402-1405: "this value REPLACES the
206 > * subprocess environment entirely … Spread `process.env` yourself"). Keys whose
207 > * value is `undefined` are dropped from the spawned env.
208 > *
209 > * Two modes, gated by `proxied`:
210 > *
211 > * - **Proxied (Copilot-routed), `true` (default):** a *sparse* env. Credentials
212 > * reach the CLI via `settings.env` (the per-session proxy bearer), so the
213 > * subprocess env stays minimal and the user's personal `ANTHROPIC_API_KEY`
214 > * must not leak to the Copilot proxy (stripped). `PATH` for ripgrep is
215 > * supplied through `settings.env`, not here.
216 > *
217 > * - **Native (BYO-Anthropic), `false`:** inherit the real `process.env` so the
218 > * user's own credentials (`CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`,
219 > * or `ANTHROPIC_API_KEY`) and `PATH` actually reach the `claude` subprocess.
220 > * Without this spread, replace semantics wipe the inherited token and the CLI
221 > * reports "Not logged in".
222 > *
223 > * In both modes the agent host's own `NODE_OPTIONS`, `ELECTRON_*`, and
224 > * `VSCODE_*` variables are stripped (they break the Electron-node subprocess),
225 > * `ELECTRON_RUN_AS_NODE=1` is set, and `AI_AGENT` is pinned so the sparse
226 > * proxied env still announces the originating VS Code surface. Mirror of the
227 > * strip pattern in `CopilotAgent._ensureClient()`.
228 > *
229 > * Exported for unit testing as a pure function over `process.env`.
230 > */
231 > export function buildSubprocessEnv(proxied: boolean = true): Record<string, string | undefined> {
232 > // Proxy mode: a sparse env (creds arrive via settings.env), and the user's claudeSdkOptions.ts ×3
233 > // personal ANTHROPIC_API_KEY must not leak to the Copilot proxy.
234 > // Native mode: inherit the real env so the user's own credentials + PATH
235 > // reach the subprocess (replace semantics wipe anything not present here).
236 > const env: Record<string, string | undefined> = proxied
238 > ELECTRON_RUN_AS_NODE: '1',
239 > NODE_OPTIONS: undefined,
240 > ANTHROPIC_API_KEY: undefined,
241 > HOME: process.env['HOME'],
242 > USERPROFILE: process.env['USERPROFILE'],
243 > }
244 > : { ...process.env, ELECTRON_RUN_AS_NODE: '1', NODE_OPTIONS: undefined }; claudeSdkOptions.ts ×1
245 > // Replace semantics mean the sparse (proxied) env would otherwise drop the claudeSdkOptions.ts ×3
246 > // agent host's own marker, so set it in both modes. See `AiAgentEnvVar`.
247 > env[AiAgentEnvVar] = AiAgentEnvValue;
248 > for (const key of Object.keys(process.env)) {
249 > if (key === 'ELECTRON_RUN_AS_NODE') { continue; }
250 > if (key.startsWith('VSCODE_') || key.startsWith('ELECTRON_')) {
251 > env[key] = undefined; claudeSdkOptions.ts ×1
252 > }
254 > return env;
255 > }