From dcc78ae318745368129a2ce1e021502ab91f621e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:46:41 +0000 Subject: [PATCH 1/4] Initial plan From 800b342168deaca1ac8fd3d94115a9faf20784bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:15:26 +0000 Subject: [PATCH 2/4] fix: WIF auth - api-proxy Squid bypass and credential isolation --- src/api-proxy-config-validation.test.ts | 17 ++++++++ src/api-proxy-config.ts | 9 +++- src/commands/validators/config-assembly.ts | 14 +++++- src/services/api-proxy-credential-env.ts | 14 ++++-- .../api-proxy-service-key-isolation.test.ts | 10 +++-- src/services/api-proxy-service-split.test.ts | 43 +++++++++++++++++++ src/squid-config.test.ts | 17 ++++++++ src/squid/config-sections.ts | 7 +++ 8 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/api-proxy-config-validation.test.ts b/src/api-proxy-config-validation.test.ts index 7f6d68e0d..8723459ac 100644 --- a/src/api-proxy-config-validation.test.ts +++ b/src/api-proxy-config-validation.test.ts @@ -74,6 +74,23 @@ describe('validateApiProxyConfig', () => { expect(result.debugMessages[3]).toContain('Gemini'); }); + it('should not warn when Anthropic WIF auth is configured (no static key)', () => { + const result = validateApiProxyConfig(true, false, false, false, false, true); + expect(result.enabled).toBe(true); + expect(result.warnings).toEqual([]); + expect(result.debugMessages).toHaveLength(1); + expect(result.debugMessages[0]).toContain('WIF'); + }); + + it('should emit WIF debug message alongside static key debug message when both present', () => { + const result = validateApiProxyConfig(true, false, true, false, false, true); + expect(result.enabled).toBe(true); + expect(result.warnings).toEqual([]); + // Both Anthropic key and WIF debug messages appear + expect(result.debugMessages.some(m => m.includes('Anthropic API key'))).toBe(true); + expect(result.debugMessages.some(m => m.includes('WIF'))).toBe(true); + }); + it('should not warn when disabled even with keys', () => { const result = validateApiProxyConfig(false, true, true); expect(result.enabled).toBe(false); diff --git a/src/api-proxy-config.ts b/src/api-proxy-config.ts index 34f8d67f3..d67e6302c 100644 --- a/src/api-proxy-config.ts +++ b/src/api-proxy-config.ts @@ -26,6 +26,7 @@ interface ApiProxyValidationResult { * @param hasAnthropicKey - Whether an Anthropic API key is present * @param hasCopilotKey - Whether a GitHub Copilot API key is present * @param hasGeminiKey - Whether a Google Gemini API key is present + * @param hasAnthropicWif - Whether Anthropic WIF (GitHub OIDC) auth is configured * @returns ApiProxyValidationResult with warnings and debug messages */ export function validateApiProxyConfig( @@ -33,7 +34,8 @@ export function validateApiProxyConfig( hasOpenaiKey?: boolean, hasAnthropicKey?: boolean, hasCopilotKey?: boolean, - hasGeminiKey?: boolean + hasGeminiKey?: boolean, + hasAnthropicWif?: boolean, ): ApiProxyValidationResult { if (!enableApiProxy) { return { enabled: false, warnings: [], debugMessages: [] }; @@ -42,7 +44,7 @@ export function validateApiProxyConfig( const warnings: string[] = []; const debugMessages: string[] = []; - if (!hasOpenaiKey && !hasAnthropicKey && !hasCopilotKey && !hasGeminiKey) { + if (!hasOpenaiKey && !hasAnthropicKey && !hasCopilotKey && !hasGeminiKey && !hasAnthropicWif) { warnings.push('⚠️ API proxy enabled but no API keys found in environment'); warnings.push(' Set OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, COPILOT_PROVIDER_API_KEY, or GEMINI_API_KEY to use the proxy'); } @@ -52,6 +54,9 @@ export function validateApiProxyConfig( if (hasAnthropicKey) { debugMessages.push('Anthropic API key detected - will be held securely in sidecar'); } + if (hasAnthropicWif) { + debugMessages.push('Anthropic WIF (GitHub OIDC) auth configured - OIDC token exchange will be used in sidecar'); + } if (hasCopilotKey) { debugMessages.push('GitHub Copilot API key detected - will be held securely in sidecar'); } diff --git a/src/commands/validators/config-assembly.ts b/src/commands/validators/config-assembly.ts index d29f86167..3d0a0f0d4 100644 --- a/src/commands/validators/config-assembly.ts +++ b/src/commands/validators/config-assembly.ts @@ -18,6 +18,7 @@ import { applyHostServicePortsConfig, applyAgentTimeout, } from '../../option-parsers'; +import { getLowerCaseProcessEnvValue } from '../../env-utils'; import { buildConfig } from '../build-config'; import { LogAndLimitsResult } from './log-and-limits'; import { NetworkOptionsResult } from './network-options'; @@ -231,12 +232,18 @@ export function assembleAndValidateConfig( // mode (Azure Foundry, OpenRouter, etc.) without a GitHub token; the sidecar still // routes through it, so for "is there a Copilot path?" purposes either signal counts. const copilotByokDirect = !!config.copilotProviderApiKey; + // Detect Anthropic WIF (GitHub OIDC) auth: the sidecar performs the OIDC token + // exchange so no static ANTHROPIC_API_KEY is required. + const hasAnthropicWif = + getLowerCaseProcessEnvValue('AWF_AUTH_TYPE') === 'github-oidc' && + getLowerCaseProcessEnvValue('AWF_AUTH_PROVIDER') === 'anthropic'; const apiProxyValidation = validateApiProxyConfig( config.enableApiProxy || false, !!config.openaiApiKey, !!config.anthropicApiKey, !!config.copilotGithubToken || copilotByokDirect, !!config.geminiApiKey, + hasAnthropicWif, ); // Log API proxy status at info level for visibility @@ -246,8 +253,13 @@ export function assembleAndValidateConfig( : copilotByokDirect ? 'true (byok-direct)' : 'false'; + const anthropicStatus = config.anthropicApiKey + ? 'true' + : hasAnthropicWif + ? 'true (wif)' + : 'false'; logger.info( - `API proxy enabled: OpenAI=${!!config.openaiApiKey}, Anthropic=${!!config.anthropicApiKey}, Copilot=${copilotStatus}, Gemini=${!!config.geminiApiKey}`, + `API proxy enabled: OpenAI=${!!config.openaiApiKey}, Anthropic=${anthropicStatus}, Copilot=${copilotStatus}, Gemini=${!!config.geminiApiKey}`, ); } diff --git a/src/services/api-proxy-credential-env.ts b/src/services/api-proxy-credential-env.ts index e924c8fb9..dd0253f41 100644 --- a/src/services/api-proxy-credential-env.ts +++ b/src/services/api-proxy-credential-env.ts @@ -67,9 +67,17 @@ export function buildAgentCredentialEnv(params: ApiProxyCredentialEnvParams): Re logger.debug(`Anthropic API base path set to: ${config.anthropicApiBasePath}`); } - // Set placeholder token for Claude Code CLI compatibility - // Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy - // Use sk-ant- prefix so Claude Code's key-format validation passes + // Set placeholder credentials for Claude Code CLI credential isolation. + // Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy. + // Use sk-ant- prefix so Claude Code's key-format validation passes. + // + // ANTHROPIC_API_KEY must be set here (in agentEnvAdditions, applied last in + // compose-generator) so it overrides any value that leaked into the environment + // via config.additionalEnv (e.g. a --env ANTHROPIC_API_KEY=... workaround). + // Without this, the real or placeholder key would reach the agent, causing it + // to bypass ANTHROPIC_BASE_URL and go directly to api.anthropic.com (→ 401). + agentEnvAdditions.ANTHROPIC_API_KEY = 'sk-ant-placeholder-key-for-credential-isolation'; + logger.debug('ANTHROPIC_API_KEY set to placeholder value for credential isolation'); agentEnvAdditions.ANTHROPIC_AUTH_TOKEN = 'sk-ant-placeholder-key-for-credential-isolation'; logger.debug('ANTHROPIC_AUTH_TOKEN set to placeholder value for credential isolation'); diff --git a/src/services/api-proxy-service-key-isolation.test.ts b/src/services/api-proxy-service-key-isolation.test.ts index dfcb123ee..23124cf00 100644 --- a/src/services/api-proxy-service-key-isolation.test.ts +++ b/src/services/api-proxy-service-key-isolation.test.ts @@ -27,8 +27,9 @@ describe('API proxy sidecar: API key isolation', () => { const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - // Agent should NOT have the raw API key — only the sidecar gets it - expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + // Agent should NOT have the real API key — it must be replaced with the placeholder + // so it never reaches the agent regardless of how it entered the environment + expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); // Agent should have the BASE_URL to reach the sidecar instead expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); // Agent should have placeholder token for Claude Code compatibility @@ -124,8 +125,9 @@ describe('API proxy sidecar: API key isolation', () => { const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - // Even with envAll, agent should NOT have ANTHROPIC_API_KEY when api-proxy is enabled - expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + // Even with envAll, agent should NOT have the real ANTHROPIC_API_KEY — it is replaced + // with the placeholder so the real key never reaches the agent + expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); // But should have placeholder token for Claude Code compatibility expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); diff --git a/src/services/api-proxy-service-split.test.ts b/src/services/api-proxy-service-split.test.ts index 885ebb481..9aca79ae7 100644 --- a/src/services/api-proxy-service-split.test.ts +++ b/src/services/api-proxy-service-split.test.ts @@ -95,4 +95,47 @@ describe('API proxy split builders', () => { networkConfig: networkConfigWithoutProxyIp, })).toThrow('buildAgentCredentialEnv: networkConfig.proxyIp is required'); }); + + it('buildAgentCredentialEnv sets ANTHROPIC_API_KEY placeholder when anthropicApiKey is present', () => { + const agentEnvAdditions = buildAgentCredentialEnv({ + config: { + ...baseConfig, + workDir: '/tmp/awf-test', + enableApiProxy: true, + anthropicApiKey: 'sk-ant-real-key', + }, + networkConfig, + }); + + expect(agentEnvAdditions.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(agentEnvAdditions.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); + expect(agentEnvAdditions.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); + expect(agentEnvAdditions.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); + }); + + it('buildAgentCredentialEnv sets ANTHROPIC_API_KEY placeholder for WIF auth (no static key)', () => { + const originalEnv = process.env; + process.env = { + ...originalEnv, + AWF_AUTH_TYPE: 'github-oidc', + AWF_AUTH_PROVIDER: 'anthropic', + }; + try { + const agentEnvAdditions = buildAgentCredentialEnv({ + config: { + ...baseConfig, + workDir: '/tmp/awf-test', + enableApiProxy: true, + // No anthropicApiKey — WIF-only path + }, + networkConfig, + }); + + expect(agentEnvAdditions.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(agentEnvAdditions.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); + expect(agentEnvAdditions.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); + } finally { + process.env = originalEnv; + } + }); }); diff --git a/src/squid-config.test.ts b/src/squid-config.test.ts index 4f6d32918..8772a1235 100644 --- a/src/squid-config.test.ts +++ b/src/squid-config.test.ts @@ -413,6 +413,22 @@ describe('generatePolicyManifest', () => { expect(allowPos).toBeLessThan(denyIpv4Pos); }); + it('should insert from_api_proxy src rule before domain denyRule', () => { + const config: SquidConfig = { + domains: ['example.com'], + port: defaultPort, + apiProxyIp, + apiProxyPorts, + }; + const result = generateSquidConfig(config); + expect(result).toContain(`acl from_api_proxy src ${apiProxyIp}/32`); + expect(result).toContain('http_access allow from_api_proxy'); + // from_api_proxy allow rule must fire before the domain denyRule + const fromApiProxyPos = result.indexOf('http_access allow from_api_proxy'); + const denyRulePos = result.indexOf('http_access deny !allowed_domains'); + expect(fromApiProxyPos).toBeLessThan(denyRulePos); + }); + it('should not emit api-proxy rules when apiProxyIp is not set', () => { const config: SquidConfig = { domains: ['example.com'], @@ -420,6 +436,7 @@ describe('generatePolicyManifest', () => { }; const result = generateSquidConfig(config); expect(result).not.toContain('allow_api_proxy_ip'); + expect(result).not.toContain('from_api_proxy'); }); it('should reject non-integer apiProxyPorts values', () => { diff --git a/src/squid/config-sections.ts b/src/squid/config-sections.ts index c6fff88d7..8bcbbed68 100644 --- a/src/squid/config-sections.ts +++ b/src/squid/config-sections.ts @@ -129,6 +129,13 @@ function generateApiProxySection(apiProxyIp?: string): string { # This allow rule fires first for the known api-proxy IP. acl allow_api_proxy_ip dst ${apiProxyIp} http_access allow allow_api_proxy_ip + +# Allow the api-proxy sidecar unrestricted outbound through Squid. +# The sidecar must reach upstream API endpoints (e.g. api.anthropic.com for +# WIF/OIDC token exchange) that may not be in the agent's allow-list. +# The api-proxy is a trusted AWF component (not the agent threat model). +acl from_api_proxy src ${apiProxyIp}/32 +http_access allow from_api_proxy ` : ''; } From 316b8ecdf2e0d581067f85de7fc4075a5fbd247c Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 11 Jun 2026 08:08:17 -0700 Subject: [PATCH 3/4] fix: add allow-from-api-proxy rule to policy manifest The from_api_proxy Squid rule grants the api-proxy sidecar unrestricted outbound access (bypassing the agent's domain ACL). The policy manifest must reflect this so audit artifacts accurately describe the effective policy. Adds 'allow-from-api-proxy' rule with domains: ['*'] to indicate the sidecar is not subject to the agent domain whitelist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/squid-config.test.ts | 31 +++++++++++++++++++++++++++++++ src/squid/policy-manifest.ts | 9 +++++++++ 2 files changed, 40 insertions(+) diff --git a/src/squid-config.test.ts b/src/squid-config.test.ts index 8772a1235..45d0b4982 100644 --- a/src/squid-config.test.ts +++ b/src/squid-config.test.ts @@ -534,4 +534,35 @@ describe('generatePolicyManifest - Api-Proxy Rules', () => { const apiProxyRule = manifest.rules.find(r => r.id === 'allow-api-proxy-ip'); expect(apiProxyRule).toBeUndefined(); }); + + it('should include allow-from-api-proxy rule when apiProxyIp is set', () => { + const manifest = generatePolicyManifest({ + domains: ['example.com'], + port: defaultPort, + apiProxyIp: '172.30.0.30', + }); + + const fromProxyRule = manifest.rules.find(r => r.id === 'allow-from-api-proxy'); + expect(fromProxyRule).toBeDefined(); + expect(fromProxyRule!.action).toBe('allow'); + expect(fromProxyRule!.aclName).toBe('from_api_proxy'); + expect(fromProxyRule!.domains).toContain('*'); + expect(fromProxyRule!.description).toContain('unrestricted outbound from api-proxy'); + + // Must come after allow-api-proxy-ip and before deny-raw-ipv4 + const apiProxyRule = manifest.rules.find(r => r.id === 'allow-api-proxy-ip'); + const denyIpv4Rule = manifest.rules.find(r => r.id === 'deny-raw-ipv4'); + expect(fromProxyRule!.order).toBeGreaterThan(apiProxyRule!.order); + expect(fromProxyRule!.order).toBeLessThan(denyIpv4Rule!.order); + }); + + it('should not include allow-from-api-proxy rule when apiProxyIp is not set', () => { + const manifest = generatePolicyManifest({ + domains: ['example.com'], + port: defaultPort, + }); + + const fromProxyRule = manifest.rules.find(r => r.id === 'allow-from-api-proxy'); + expect(fromProxyRule).toBeUndefined(); + }); }); diff --git a/src/squid/policy-manifest.ts b/src/squid/policy-manifest.ts index 21320cf19..19746ed7d 100644 --- a/src/squid/policy-manifest.ts +++ b/src/squid/policy-manifest.ts @@ -83,6 +83,15 @@ export function generatePolicyManifest(config: SquidConfig): PolicyManifest { domains: [apiProxyIp], description: 'Allow connections to the AWF api-proxy sidecar IP before raw-IP deny rules', }); + rules.push({ + id: 'allow-from-api-proxy', + order: ++order, + action: 'allow', + aclName: 'from_api_proxy', + protocol: 'both', + domains: ['*'], + description: 'Allow unrestricted outbound from api-proxy sidecar (trusted AWF component, not subject to agent domain ACL)', + }); } // --- Raw IP blocking --- From abf9c154af7653d416723b77676f101f7b96e418 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Thu, 11 Jun 2026 08:35:06 -0700 Subject: [PATCH 4/4] fix: remove ANTHROPIC_API_KEY placeholder from agentEnvAdditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The placeholder was causing integration test failures because the health check flags ANY presence of ANTHROPIC_API_KEY in the agent environment as a credential leak. The correct approach is to rely on excluded-vars.ts (which already removes ANTHROPIC_API_KEY from the agent env when api-proxy is enabled) and only set ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN in agentEnvAdditions. Claude Code uses ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL to route through the sidecar — it does not require ANTHROPIC_API_KEY. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/services/api-proxy-credential-env.ts | 11 ++++------- src/services/api-proxy-service-key-isolation.test.ts | 10 ++++------ src/services/api-proxy-service-split.test.ts | 10 ++++++---- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/services/api-proxy-credential-env.ts b/src/services/api-proxy-credential-env.ts index dd0253f41..18720cc38 100644 --- a/src/services/api-proxy-credential-env.ts +++ b/src/services/api-proxy-credential-env.ts @@ -71,13 +71,10 @@ export function buildAgentCredentialEnv(params: ApiProxyCredentialEnvParams): Re // Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy. // Use sk-ant- prefix so Claude Code's key-format validation passes. // - // ANTHROPIC_API_KEY must be set here (in agentEnvAdditions, applied last in - // compose-generator) so it overrides any value that leaked into the environment - // via config.additionalEnv (e.g. a --env ANTHROPIC_API_KEY=... workaround). - // Without this, the real or placeholder key would reach the agent, causing it - // to bypass ANTHROPIC_BASE_URL and go directly to api.anthropic.com (→ 401). - agentEnvAdditions.ANTHROPIC_API_KEY = 'sk-ant-placeholder-key-for-credential-isolation'; - logger.debug('ANTHROPIC_API_KEY set to placeholder value for credential isolation'); + // NOTE: ANTHROPIC_API_KEY is NOT set here — it is excluded from the agent env + // via excluded-vars.ts when enableApiProxy is active. Setting it (even as a + // placeholder) would cause Claude Code to attempt direct auth with it instead + // of routing through ANTHROPIC_BASE_URL. agentEnvAdditions.ANTHROPIC_AUTH_TOKEN = 'sk-ant-placeholder-key-for-credential-isolation'; logger.debug('ANTHROPIC_AUTH_TOKEN set to placeholder value for credential isolation'); diff --git a/src/services/api-proxy-service-key-isolation.test.ts b/src/services/api-proxy-service-key-isolation.test.ts index 23124cf00..dfcb123ee 100644 --- a/src/services/api-proxy-service-key-isolation.test.ts +++ b/src/services/api-proxy-service-key-isolation.test.ts @@ -27,9 +27,8 @@ describe('API proxy sidecar: API key isolation', () => { const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - // Agent should NOT have the real API key — it must be replaced with the placeholder - // so it never reaches the agent regardless of how it entered the environment - expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); + // Agent should NOT have the raw API key — only the sidecar gets it + expect(env.ANTHROPIC_API_KEY).toBeUndefined(); // Agent should have the BASE_URL to reach the sidecar instead expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); // Agent should have placeholder token for Claude Code compatibility @@ -125,9 +124,8 @@ describe('API proxy sidecar: API key isolation', () => { const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - // Even with envAll, agent should NOT have the real ANTHROPIC_API_KEY — it is replaced - // with the placeholder so the real key never reaches the agent - expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); + // Even with envAll, agent should NOT have ANTHROPIC_API_KEY when api-proxy is enabled + expect(env.ANTHROPIC_API_KEY).toBeUndefined(); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); // But should have placeholder token for Claude Code compatibility expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); diff --git a/src/services/api-proxy-service-split.test.ts b/src/services/api-proxy-service-split.test.ts index 9aca79ae7..ff56ce85f 100644 --- a/src/services/api-proxy-service-split.test.ts +++ b/src/services/api-proxy-service-split.test.ts @@ -96,7 +96,7 @@ describe('API proxy split builders', () => { })).toThrow('buildAgentCredentialEnv: networkConfig.proxyIp is required'); }); - it('buildAgentCredentialEnv sets ANTHROPIC_API_KEY placeholder when anthropicApiKey is present', () => { + it('buildAgentCredentialEnv sets ANTHROPIC_AUTH_TOKEN placeholder when anthropicApiKey is present', () => { const agentEnvAdditions = buildAgentCredentialEnv({ config: { ...baseConfig, @@ -108,12 +108,13 @@ describe('API proxy split builders', () => { }); expect(agentEnvAdditions.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); - expect(agentEnvAdditions.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); + // ANTHROPIC_API_KEY must NOT be in agentEnvAdditions — it is excluded via excluded-vars.ts + expect(agentEnvAdditions.ANTHROPIC_API_KEY).toBeUndefined(); expect(agentEnvAdditions.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); expect(agentEnvAdditions.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); - it('buildAgentCredentialEnv sets ANTHROPIC_API_KEY placeholder for WIF auth (no static key)', () => { + it('buildAgentCredentialEnv sets ANTHROPIC_AUTH_TOKEN placeholder for WIF auth (no static key)', () => { const originalEnv = process.env; process.env = { ...originalEnv, @@ -132,7 +133,8 @@ describe('API proxy split builders', () => { }); expect(agentEnvAdditions.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); - expect(agentEnvAdditions.ANTHROPIC_API_KEY).toBe('sk-ant-placeholder-key-for-credential-isolation'); + // ANTHROPIC_API_KEY must NOT be in agentEnvAdditions — excluded-vars.ts handles removal + expect(agentEnvAdditions.ANTHROPIC_API_KEY).toBeUndefined(); expect(agentEnvAdditions.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); } finally { process.env = originalEnv;