diff --git a/containers/agent/api-proxy-health-check.sh b/containers/agent/api-proxy-health-check.sh index 3f76777f..691617fe 100755 --- a/containers/agent/api-proxy-health-check.sh +++ b/containers/agent/api-proxy-health-check.sh @@ -33,9 +33,9 @@ if [ -n "$ANTHROPIC_BASE_URL" ]; then # Verify ANTHROPIC_AUTH_TOKEN is placeholder (if present) if [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then - if [ "$ANTHROPIC_AUTH_TOKEN" != "placeholder-token-for-credential-isolation" ]; then + if [ "$ANTHROPIC_AUTH_TOKEN" != "sk-ant-placeholder-key-for-credential-isolation" ]; then echo "[health-check][ERROR] ANTHROPIC_AUTH_TOKEN contains non-placeholder value!" - echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'" + echo "[health-check][ERROR] Token should be 'sk-ant-placeholder-key-for-credential-isolation'" exit 1 fi echo "[health-check] ✓ ANTHROPIC_AUTH_TOKEN is placeholder value (correct)" diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js index 35ddd194..8b663baf 100644 --- a/containers/api-proxy/server.js +++ b/containers/api-proxy/server.js @@ -43,9 +43,11 @@ function shouldStripHeader(name) { } // Read API keys from environment (set by docker-compose) -const OPENAI_API_KEY = process.env.OPENAI_API_KEY; -const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; -const COPILOT_GITHUB_TOKEN = process.env.COPILOT_GITHUB_TOKEN; +// Trim whitespace/newlines to prevent malformed HTTP headers — env vars from +// CI secrets or docker-compose YAML may include trailing whitespace. +const OPENAI_API_KEY = (process.env.OPENAI_API_KEY || '').trim() || undefined; +const ANTHROPIC_API_KEY = (process.env.ANTHROPIC_API_KEY || '').trim() || undefined; +const COPILOT_GITHUB_TOKEN = (process.env.COPILOT_GITHUB_TOKEN || '').trim() || undefined; // Configurable API target hosts (supports custom endpoints / internal LLM routers) const OPENAI_API_TARGET = process.env.OPENAI_API_TARGET || 'api.openai.com'; @@ -332,6 +334,21 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath = headers['x-request-id'] = requestId; Object.assign(headers, injectHeaders); + // Log auth header injection for debugging credential-isolation issues + const injectedKey = injectHeaders['x-api-key'] || injectHeaders['authorization']; + if (injectedKey) { + const keyPreview = injectedKey.length > 8 + ? `${injectedKey.substring(0, 8)}...${injectedKey.substring(injectedKey.length - 4)}` + : '(short)'; + logRequest('debug', 'auth_inject', { + request_id: requestId, + provider, + key_length: injectedKey.length, + key_preview: keyPreview, + has_anthropic_version: !!headers['anthropic-version'], + }); + } + const options = { hostname: targetHost, port: 443, @@ -391,6 +408,19 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath = // Copy response headers and add X-Request-ID const resHeaders = { ...proxyRes.headers, 'x-request-id': requestId }; + + // Log upstream auth failures prominently for debugging + if (proxyRes.statusCode === 401 || proxyRes.statusCode === 403) { + logRequest('warn', 'upstream_auth_error', { + request_id: requestId, + provider, + status: proxyRes.statusCode, + upstream_host: targetHost, + path: sanitizeForLog(req.url), + message: `Upstream returned ${proxyRes.statusCode} — check that the API key is valid and has not expired`, + }); + } + res.writeHead(proxyRes.statusCode, resHeaders); proxyRes.pipe(res); }); diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 7a748e17..42be910f 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2048,7 +2048,7 @@ describe('docker-manager', () => { const agent = result.services.agent; const env = agent.environment as Record; expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); - expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); @@ -2059,7 +2059,7 @@ describe('docker-manager', () => { const env = agent.environment as Record; expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000/v1'); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); - expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); @@ -2070,7 +2070,7 @@ describe('docker-manager', () => { const env = agent.environment as Record; expect(env.OPENAI_BASE_URL).toBeUndefined(); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); - expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); @@ -2130,7 +2130,7 @@ describe('docker-manager', () => { // 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 - expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); } finally { if (origKey !== undefined) { process.env.ANTHROPIC_API_KEY = origKey; @@ -2219,7 +2219,7 @@ describe('docker-manager', () => { 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('placeholder-token-for-credential-isolation'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation'); } finally { if (origKey !== undefined) { process.env.ANTHROPIC_API_KEY = origKey; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 286a781e..a5a3ef89 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1447,7 +1447,8 @@ export function generateDockerCompose( // Set placeholder token for Claude Code CLI compatibility // Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy - environment.ANTHROPIC_AUTH_TOKEN = 'placeholder-token-for-credential-isolation'; + // Use sk-ant- prefix so Claude Code's key-format validation passes + environment.ANTHROPIC_AUTH_TOKEN = 'sk-ant-placeholder-key-for-credential-isolation'; logger.debug('ANTHROPIC_AUTH_TOKEN set to placeholder value for credential isolation'); // Set API key helper for Claude Code CLI to use credential isolation diff --git a/tests/integration/api-proxy.test.ts b/tests/integration/api-proxy.test.ts index d58b72d2..e786ca8c 100644 --- a/tests/integration/api-proxy.test.ts +++ b/tests/integration/api-proxy.test.ts @@ -101,7 +101,7 @@ describe('API Proxy Sidecar', () => { ); expect(result).toSucceed(); - expect(result.stdout).toContain('ANTHROPIC_AUTH_TOKEN=placeholder-token-for-credential-isolation'); + expect(result.stdout).toContain('ANTHROPIC_AUTH_TOKEN=sk-ant-placeholder-key-for-credential-isolation'); }, 180000); test('should set OPENAI_BASE_URL in agent when OpenAI key is provided', async () => {