Skip to content

Commit 3c695f7

Browse files
lpcoxCopilot
andauthored
fix: api-proxy auth chain — trim keys, align placeholder format, add diagnostics (#1528)
* fix: api-proxy auth chain — trim keys, align placeholder format, add diagnostics Three fixes for the Anthropic API proxy auth chain failure (#1527): 1. Trim API keys read from env vars in api-proxy (server.js) CI secrets and docker-compose YAML may include trailing whitespace or newlines. Untrimmed keys produce malformed HTTP headers (the newline terminates the header prematurely), causing silent auth failures with error_status: null. 2. Align ANTHROPIC_AUTH_TOKEN placeholder with sk-ant- key format Claude Code validates key format before making API calls. The old placeholder 'placeholder-token-for-credential-isolation' lacks the sk-ant- prefix and may fail format validation. Now uses the same value as apiKeyHelper: 'sk-ant-placeholder-key-for-credential-isolation'. 3. Add auth header injection + upstream error logging in api-proxy - 'auth_inject' debug log confirms key injection (length, preview) - 'upstream_auth_error' warning on 401/403 from upstream API Closes #1527 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update health check to match new ANTHROPIC_AUTH_TOKEN placeholder The api-proxy-health-check.sh validates that ANTHROPIC_AUTH_TOKEN contains a placeholder value. Update the expected value to match the new sk-ant- prefixed placeholder set in docker-manager.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2aff0f3 commit 3c695f7

5 files changed

Lines changed: 43 additions & 12 deletions

File tree

containers/agent/api-proxy-health-check.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ if [ -n "$ANTHROPIC_BASE_URL" ]; then
3333

3434
# Verify ANTHROPIC_AUTH_TOKEN is placeholder (if present)
3535
if [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then
36-
if [ "$ANTHROPIC_AUTH_TOKEN" != "placeholder-token-for-credential-isolation" ]; then
36+
if [ "$ANTHROPIC_AUTH_TOKEN" != "sk-ant-placeholder-key-for-credential-isolation" ]; then
3737
echo "[health-check][ERROR] ANTHROPIC_AUTH_TOKEN contains non-placeholder value!"
38-
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
38+
echo "[health-check][ERROR] Token should be 'sk-ant-placeholder-key-for-credential-isolation'"
3939
exit 1
4040
fi
4141
echo "[health-check] ✓ ANTHROPIC_AUTH_TOKEN is placeholder value (correct)"

containers/api-proxy/server.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ function shouldStripHeader(name) {
4343
}
4444

4545
// Read API keys from environment (set by docker-compose)
46-
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
47-
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
48-
const COPILOT_GITHUB_TOKEN = process.env.COPILOT_GITHUB_TOKEN;
46+
// Trim whitespace/newlines to prevent malformed HTTP headers — env vars from
47+
// CI secrets or docker-compose YAML may include trailing whitespace.
48+
const OPENAI_API_KEY = (process.env.OPENAI_API_KEY || '').trim() || undefined;
49+
const ANTHROPIC_API_KEY = (process.env.ANTHROPIC_API_KEY || '').trim() || undefined;
50+
const COPILOT_GITHUB_TOKEN = (process.env.COPILOT_GITHUB_TOKEN || '').trim() || undefined;
4951

5052
// Configurable API target hosts (supports custom endpoints / internal LLM routers)
5153
const OPENAI_API_TARGET = process.env.OPENAI_API_TARGET || 'api.openai.com';
@@ -332,6 +334,21 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath =
332334
headers['x-request-id'] = requestId;
333335
Object.assign(headers, injectHeaders);
334336

337+
// Log auth header injection for debugging credential-isolation issues
338+
const injectedKey = injectHeaders['x-api-key'] || injectHeaders['authorization'];
339+
if (injectedKey) {
340+
const keyPreview = injectedKey.length > 8
341+
? `${injectedKey.substring(0, 8)}...${injectedKey.substring(injectedKey.length - 4)}`
342+
: '(short)';
343+
logRequest('debug', 'auth_inject', {
344+
request_id: requestId,
345+
provider,
346+
key_length: injectedKey.length,
347+
key_preview: keyPreview,
348+
has_anthropic_version: !!headers['anthropic-version'],
349+
});
350+
}
351+
335352
const options = {
336353
hostname: targetHost,
337354
port: 443,
@@ -391,6 +408,19 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath =
391408

392409
// Copy response headers and add X-Request-ID
393410
const resHeaders = { ...proxyRes.headers, 'x-request-id': requestId };
411+
412+
// Log upstream auth failures prominently for debugging
413+
if (proxyRes.statusCode === 401 || proxyRes.statusCode === 403) {
414+
logRequest('warn', 'upstream_auth_error', {
415+
request_id: requestId,
416+
provider,
417+
status: proxyRes.statusCode,
418+
upstream_host: targetHost,
419+
path: sanitizeForLog(req.url),
420+
message: `Upstream returned ${proxyRes.statusCode} — check that the API key is valid and has not expired`,
421+
});
422+
}
423+
394424
res.writeHead(proxyRes.statusCode, resHeaders);
395425
proxyRes.pipe(res);
396426
});

src/docker-manager.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,7 +2048,7 @@ describe('docker-manager', () => {
20482048
const agent = result.services.agent;
20492049
const env = agent.environment as Record<string, string>;
20502050
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
2051-
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation');
2051+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
20522052
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
20532053
});
20542054

@@ -2059,7 +2059,7 @@ describe('docker-manager', () => {
20592059
const env = agent.environment as Record<string, string>;
20602060
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000/v1');
20612061
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
2062-
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation');
2062+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
20632063
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
20642064
});
20652065

@@ -2070,7 +2070,7 @@ describe('docker-manager', () => {
20702070
const env = agent.environment as Record<string, string>;
20712071
expect(env.OPENAI_BASE_URL).toBeUndefined();
20722072
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
2073-
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation');
2073+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
20742074
expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh');
20752075
});
20762076

@@ -2130,7 +2130,7 @@ describe('docker-manager', () => {
21302130
// Agent should have the BASE_URL to reach the sidecar instead
21312131
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
21322132
// Agent should have placeholder token for Claude Code compatibility
2133-
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation');
2133+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
21342134
} finally {
21352135
if (origKey !== undefined) {
21362136
process.env.ANTHROPIC_API_KEY = origKey;
@@ -2219,7 +2219,7 @@ describe('docker-manager', () => {
22192219
expect(env.ANTHROPIC_API_KEY).toBeUndefined();
22202220
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
22212221
// But should have placeholder token for Claude Code compatibility
2222-
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation');
2222+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-ant-placeholder-key-for-credential-isolation');
22232223
} finally {
22242224
if (origKey !== undefined) {
22252225
process.env.ANTHROPIC_API_KEY = origKey;

src/docker-manager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1447,7 +1447,8 @@ export function generateDockerCompose(
14471447

14481448
// Set placeholder token for Claude Code CLI compatibility
14491449
// Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy
1450-
environment.ANTHROPIC_AUTH_TOKEN = 'placeholder-token-for-credential-isolation';
1450+
// Use sk-ant- prefix so Claude Code's key-format validation passes
1451+
environment.ANTHROPIC_AUTH_TOKEN = 'sk-ant-placeholder-key-for-credential-isolation';
14511452
logger.debug('ANTHROPIC_AUTH_TOKEN set to placeholder value for credential isolation');
14521453

14531454
// Set API key helper for Claude Code CLI to use credential isolation

tests/integration/api-proxy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('API Proxy Sidecar', () => {
101101
);
102102

103103
expect(result).toSucceed();
104-
expect(result.stdout).toContain('ANTHROPIC_AUTH_TOKEN=placeholder-token-for-credential-isolation');
104+
expect(result.stdout).toContain('ANTHROPIC_AUTH_TOKEN=sk-ant-placeholder-key-for-credential-isolation');
105105
}, 180000);
106106

107107
test('should set OPENAI_BASE_URL in agent when OpenAI key is provided', async () => {

0 commit comments

Comments
 (0)