diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index ebabc11d69ee93737b774c5f55292ef5974e1b11..bfef7f9e9a10475f389431145916700b6cd359de 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -478,11 +478,14 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { type: "http", url: "http://127.0.0.1:43123/mcp", headers: { - Authorization: "Bearer secret-claude-token", + Authorization: "${T3_CODE_MCP_AUTHORIZATION}", }, timeout: ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, }, } as const; + const T3_MCP_ENVIRONMENT = { + T3_CODE_MCP_AUTHORIZATION: "Bearer secret-claude-token", + } as const; const withMcpSession = (threadId: ThreadId, run: () => void) => { McpProviderSession.setMcpProviderSession({ @@ -531,6 +534,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { assert.deepEqual(overrides, { allowedTools: [ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_WILDCARD], mcpServers: T3_MCP_SERVERS, + mcpEnvironment: T3_MCP_ENVIRONMENT, }); }); }); @@ -547,6 +551,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { assert.deepEqual(overrides, { allowedTools: ["Read", "mcp__t3-code__*"], mcpServers: T3_MCP_SERVERS, + mcpEnvironment: T3_MCP_ENVIRONMENT, }); }); }); @@ -566,6 +571,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { ...ClaudeAdapterV2.CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS, ], mcpServers: T3_MCP_SERVERS, + mcpEnvironment: T3_MCP_ENVIRONMENT, }); assert.isFalse(overrides.allowedTools?.includes(ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_WILDCARD)); }); @@ -697,11 +703,12 @@ describe("ClaudeAdapterV2 native protocol logging", () => { type: "http", url: "http://127.0.0.1:43123/mcp", headers: { - Authorization: "Bearer secret-claude-token", + Authorization: "${T3_CODE_MCP_AUTHORIZATION}", }, timeout: ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, }, }, + mcpEnvironment: { T3_CODE_MCP_AUTHORIZATION: "Bearer secret-claude-token" }, }); const options = ClaudeAdapterV2.makeClaudeQueryOptions({ @@ -712,8 +719,14 @@ describe("ClaudeAdapterV2 native protocol logging", () => { nativeThreadId: "native-thread-claude-mcp", resume: false, cwd: "/workspace", - ...overrides, + allowedTools: overrides.allowedTools ?? [], + mcpServers: overrides.mcpServers ?? {}, + environment: { ...overrides.mcpEnvironment }, }); + // mcpServers becomes a CLI argument, readable by every local user; the + // credential may only travel in the child's environment. + assert.notInclude(JSON.stringify(options.mcpServers), "secret-claude-token"); + assert.equal(options.env?.T3_CODE_MCP_AUTHORIZATION, "Bearer secret-claude-token"); assert.isObject(options.systemPrompt); const systemPrompt = options.systemPrompt as { readonly type: string; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 4cd45b86df7f9e6ca747782ab901915959d92ad3..de747802d3490e702cf9b772154ddfab1d1f2efc 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -966,6 +966,13 @@ export const CLAUDE_T3_MCP_TOOL_TIMEOUT_MS = 65 * 60 * 1_000; // not pre-approved), but read-only sandboxes pre-approve only the annotated // read-only orchestrator tools so a read-only session cannot silently spawn // threads or scheduled tasks. +// +// The SDK passes `mcpServers` to the CLI as an inline `--mcp-config` argument, +// and process arguments are readable by every local user. The credential +// therefore travels in the child's environment, which only its owner can read, +// and the CLI expands the `${VAR}` reference when it connects. +const CLAUDE_T3_MCP_AUTHORIZATION_ENV = "T3_CODE_MCP_AUTHORIZATION"; + export function claudeMcpQueryOverrides(input: { readonly threadId: ThreadId; readonly readOnlySandbox: boolean; @@ -973,6 +980,7 @@ export function claudeMcpQueryOverrides(input: { }): { readonly allowedTools?: ReadonlyArray; readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; + readonly mcpEnvironment?: Readonly>; } { const session = McpProviderSession.readMcpProviderSession(input.threadId); if (session === undefined) { @@ -988,11 +996,12 @@ export function claudeMcpQueryOverrides(input: { type: "http", url: session.endpoint, headers: { - Authorization: session.authorizationHeader, + Authorization: `\${${CLAUDE_T3_MCP_AUTHORIZATION_ENV}}`, }, timeout: CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, }, }, + mcpEnvironment: { [CLAUDE_T3_MCP_AUTHORIZATION_ENV]: session.authorizationHeader }, }; } @@ -1623,6 +1632,7 @@ export function claudeEffectiveQueryPolicyKey( mcpOverrides: { readonly allowedTools?: ReadonlyArray; readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; + readonly mcpEnvironment?: Readonly>; }, ): string { return JSON.stringify({ @@ -1633,6 +1643,7 @@ export function claudeEffectiveQueryPolicyKey( : { allowedTools: mcpOverrides.allowedTools }), }), mcpServers: mcpOverrides.mcpServers, + mcpEnvironment: mcpOverrides.mcpEnvironment, }); } @@ -7338,9 +7349,14 @@ export function makeClaudeAdapterV2( cwd: turnInput.runtimePolicy.cwd, attachmentsDir, settings: adapterOptions.settings, - environment: adapterOptions.environment, + environment: { ...adapterOptions.environment, ...mcpOverrides.mcpEnvironment }, tools: queryPolicy.tools ?? CLAUDE_CODE_PRESET_TOOLS, - ...mcpOverrides, + ...(mcpOverrides.allowedTools === undefined + ? {} + : { allowedTools: mcpOverrides.allowedTools }), + ...(mcpOverrides.mcpServers === undefined + ? {} + : { mcpServers: mcpOverrides.mcpServers }), permissionMode: queryPolicy.permissionMode, ...(queryPolicy.allowDangerouslySkipPermissions === undefined ? {}