| author | |
| committer | |
| log | c908cea52510ed29199c3cc477204da425601301 |
| tree | 7811c27148721d755a8c52dc235c4cf7d7ff5189 |
| parent | 3b6af0bd1600f034b0466e1b8ff017fbabeba910 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 35 insertions(+), 6 deletions(-)
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+16-3| ... | ... | @@ -478,11 +478,14 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { |
| 478 | 478 | type: "http", |
| 479 | 479 | url: "http://127.0.0.1:43123/mcp", |
| 480 | 480 | headers: { |
| 481 | Authorization: "Bearer secret-claude-token", | |
| 481 | Authorization: "${T3_CODE_MCP_AUTHORIZATION}", | |
| 482 | 482 | }, |
| 483 | 483 | timeout: ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, |
| 484 | 484 | }, |
| 485 | 485 | } as const; |
| 486 | const T3_MCP_ENVIRONMENT = { | |
| 487 | T3_CODE_MCP_AUTHORIZATION: "Bearer secret-claude-token", | |
| 488 | } as const; | |
| 486 | 489 | |
| 487 | 490 | const withMcpSession = (threadId: ThreadId, run: () => void) => { |
| 488 | 491 | McpProviderSession.setMcpProviderSession({ |
| ... | ... | @@ -531,6 +534,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { |
| 531 | 534 | assert.deepEqual(overrides, { |
| 532 | 535 | allowedTools: [ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_WILDCARD], |
| 533 | 536 | mcpServers: T3_MCP_SERVERS, |
| 537 | mcpEnvironment: T3_MCP_ENVIRONMENT, | |
| 534 | 538 | }); |
| 535 | 539 | }); |
| 536 | 540 | }); |
| ... | ... | @@ -547,6 +551,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { |
| 547 | 551 | assert.deepEqual(overrides, { |
| 548 | 552 | allowedTools: ["Read", "mcp__t3-code__*"], |
| 549 | 553 | mcpServers: T3_MCP_SERVERS, |
| 554 | mcpEnvironment: T3_MCP_ENVIRONMENT, | |
| 550 | 555 | }); |
| 551 | 556 | }); |
| 552 | 557 | }); |
| ... | ... | @@ -566,6 +571,7 @@ describe("ClaudeAdapterV2 MCP query overrides", () => { |
| 566 | 571 | ...ClaudeAdapterV2.CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS, |
| 567 | 572 | ], |
| 568 | 573 | mcpServers: T3_MCP_SERVERS, |
| 574 | mcpEnvironment: T3_MCP_ENVIRONMENT, | |
| 569 | 575 | }); |
| 570 | 576 | assert.isFalse(overrides.allowedTools?.includes(ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_WILDCARD)); |
| 571 | 577 | }); |
| ... | ... | @@ -697,11 +703,12 @@ describe("ClaudeAdapterV2 native protocol logging", () => { |
| 697 | 703 | type: "http", |
| 698 | 704 | url: "http://127.0.0.1:43123/mcp", |
| 699 | 705 | headers: { |
| 700 | Authorization: "Bearer secret-claude-token", | |
| 706 | Authorization: "${T3_CODE_MCP_AUTHORIZATION}", | |
| 701 | 707 | }, |
| 702 | 708 | timeout: ClaudeAdapterV2.CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, |
| 703 | 709 | }, |
| 704 | 710 | }, |
| 711 | mcpEnvironment: { T3_CODE_MCP_AUTHORIZATION: "Bearer secret-claude-token" }, | |
| 705 | 712 | }); |
| 706 | 713 | |
| 707 | 714 | const options = ClaudeAdapterV2.makeClaudeQueryOptions({ |
| ... | ... | @@ -712,8 +719,14 @@ describe("ClaudeAdapterV2 native protocol logging", () => { |
| 712 | 719 | nativeThreadId: "native-thread-claude-mcp", |
| 713 | 720 | resume: false, |
| 714 | 721 | cwd: "/workspace", |
| 715 | ...overrides, | |
| 722 | allowedTools: overrides.allowedTools ?? [], | |
| 723 | mcpServers: overrides.mcpServers ?? {}, | |
| 724 | environment: { ...overrides.mcpEnvironment }, | |
| 716 | 725 | }); |
| 726 | // mcpServers becomes a CLI argument, readable by every local user; the | |
| 727 | // credential may only travel in the child's environment. | |
| 728 | assert.notInclude(JSON.stringify(options.mcpServers), "secret-claude-token"); | |
| 729 | assert.equal(options.env?.T3_CODE_MCP_AUTHORIZATION, "Bearer secret-claude-token"); | |
| 717 | 730 | assert.isObject(options.systemPrompt); |
| 718 | 731 | const systemPrompt = options.systemPrompt as { |
| 719 | 732 | readonly type: string; |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+19-3| ... | ... | @@ -966,6 +966,13 @@ export const CLAUDE_T3_MCP_TOOL_TIMEOUT_MS = 65 * 60 * 1_000; |
| 966 | 966 | // not pre-approved), but read-only sandboxes pre-approve only the annotated |
| 967 | 967 | // read-only orchestrator tools so a read-only session cannot silently spawn |
| 968 | 968 | // threads or scheduled tasks. |
| 969 | // | |
| 970 | // The SDK passes `mcpServers` to the CLI as an inline `--mcp-config` argument, | |
| 971 | // and process arguments are readable by every local user. The credential | |
| 972 | // therefore travels in the child's environment, which only its owner can read, | |
| 973 | // and the CLI expands the `${VAR}` reference when it connects. | |
| 974 | const CLAUDE_T3_MCP_AUTHORIZATION_ENV = "T3_CODE_MCP_AUTHORIZATION"; | |
| 975 | ||
| 969 | 976 | export function claudeMcpQueryOverrides(input: { |
| 970 | 977 | readonly threadId: ThreadId; |
| 971 | 978 | readonly readOnlySandbox: boolean; |
| ... | ... | @@ -973,6 +980,7 @@ export function claudeMcpQueryOverrides(input: { |
| 973 | 980 | }): { |
| 974 | 981 | readonly allowedTools?: ReadonlyArray<string>; |
| 975 | 982 | readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; |
| 983 | readonly mcpEnvironment?: Readonly<Record<string, string>>; | |
| 976 | 984 | } { |
| 977 | 985 | const session = McpProviderSession.readMcpProviderSession(input.threadId); |
| 978 | 986 | if (session === undefined) { |
| ... | ... | @@ -988,11 +996,12 @@ export function claudeMcpQueryOverrides(input: { |
| 988 | 996 | type: "http", |
| 989 | 997 | url: session.endpoint, |
| 990 | 998 | headers: { |
| 991 | Authorization: session.authorizationHeader, | |
| 999 | Authorization: `\${${CLAUDE_T3_MCP_AUTHORIZATION_ENV}}`, | |
| 992 | 1000 | }, |
| 993 | 1001 | timeout: CLAUDE_T3_MCP_TOOL_TIMEOUT_MS, |
| 994 | 1002 | }, |
| 995 | 1003 | }, |
| 1004 | mcpEnvironment: { [CLAUDE_T3_MCP_AUTHORIZATION_ENV]: session.authorizationHeader }, | |
| 996 | 1005 | }; |
| 997 | 1006 | } |
| 998 | 1007 | |
| ... | ... | @@ -1623,6 +1632,7 @@ export function claudeEffectiveQueryPolicyKey( |
| 1623 | 1632 | mcpOverrides: { |
| 1624 | 1633 | readonly allowedTools?: ReadonlyArray<string>; |
| 1625 | 1634 | readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; |
| 1635 | readonly mcpEnvironment?: Readonly<Record<string, string>>; | |
| 1626 | 1636 | }, |
| 1627 | 1637 | ): string { |
| 1628 | 1638 | return JSON.stringify({ |
| ... | ... | @@ -1633,6 +1643,7 @@ export function claudeEffectiveQueryPolicyKey( |
| 1633 | 1643 | : { allowedTools: mcpOverrides.allowedTools }), |
| 1634 | 1644 | }), |
| 1635 | 1645 | mcpServers: mcpOverrides.mcpServers, |
| 1646 | mcpEnvironment: mcpOverrides.mcpEnvironment, | |
| 1636 | 1647 | }); |
| 1637 | 1648 | } |
| 1638 | 1649 | |
| ... | ... | @@ -7338,9 +7349,14 @@ export function makeClaudeAdapterV2( |
| 7338 | 7349 | cwd: turnInput.runtimePolicy.cwd, |
| 7339 | 7350 | attachmentsDir, |
| 7340 | 7351 | settings: adapterOptions.settings, |
| 7341 | environment: adapterOptions.environment, | |
| 7352 | environment: { ...adapterOptions.environment, ...mcpOverrides.mcpEnvironment }, | |
| 7342 | 7353 | tools: queryPolicy.tools ?? CLAUDE_CODE_PRESET_TOOLS, |
| 7343 | ...mcpOverrides, | |
| 7354 | ...(mcpOverrides.allowedTools === undefined | |
| 7355 | ? {} | |
| 7356 | : { allowedTools: mcpOverrides.allowedTools }), | |
| 7357 | ...(mcpOverrides.mcpServers === undefined | |
| 7358 | ? {} | |
| 7359 | : { mcpServers: mcpOverrides.mcpServers }), | |
| 7344 | 7360 | permissionMode: queryPolicy.permissionMode, |
| 7345 | 7361 | ...(queryPolicy.allowDangerouslySkipPermissions === undefined |
| 7346 | 7362 | ? {} |