Skip to content

Commit 7a116c6

Browse files
authored
fix: use GitHub-token-shaped Copilot auth placeholder for CLI compatibility
Change the placeholder token from "placeholder-token-for-credential-isolation" to "ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" (ghu_ prefix + 36 chars). The Copilot CLI validates token format before making API calls — the old placeholder fails this check, causing "No authentication information found" errors before the request reaches the API proxy where real credentials are injected. Centralizes the constant in src/constants/placeholders.ts with a matching shell variable in the health-check script to prevent mismatches. Fixes github/gh-aw#31701
1 parent 1db4638 commit 7a116c6

10 files changed

Lines changed: 48 additions & 25 deletions

File tree

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
set -e
1212

13+
# Must match src/constants/placeholders.ts (COPILOT_PLACEHOLDER_TOKEN)
14+
COPILOT_PLACEHOLDER_TOKEN="ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
15+
1316
echo "[health-check] API Proxy Pre-flight Check"
1417
echo "[health-check] =========================================="
1518

@@ -119,39 +122,39 @@ if [ -n "$COPILOT_API_URL" ]; then
119122

120123
# Verify COPILOT_GITHUB_TOKEN is placeholder (protected by one-shot-token)
121124
if [ -n "$COPILOT_GITHUB_TOKEN" ]; then
122-
if [ "$COPILOT_GITHUB_TOKEN" != "placeholder-token-for-credential-isolation" ]; then
125+
if [ "$COPILOT_GITHUB_TOKEN" != "$COPILOT_PLACEHOLDER_TOKEN" ]; then
123126
echo "[health-check][ERROR] COPILOT_GITHUB_TOKEN contains non-placeholder value!"
124-
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
127+
echo "[health-check][ERROR] Token should be '$COPILOT_PLACEHOLDER_TOKEN'"
125128
exit 1
126129
fi
127130
echo "[health-check] ✓ COPILOT_GITHUB_TOKEN is placeholder value (correct)"
128131
fi
129132

130133
# Verify COPILOT_API_KEY (BYOK) is placeholder when api-proxy is enabled (if present)
131134
if [ -n "$COPILOT_API_KEY" ]; then
132-
if [ "$COPILOT_API_KEY" != "placeholder-token-for-credential-isolation" ]; then
135+
if [ "$COPILOT_API_KEY" != "$COPILOT_PLACEHOLDER_TOKEN" ]; then
133136
echo "[health-check][ERROR] COPILOT_API_KEY contains non-placeholder value!"
134-
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
137+
echo "[health-check][ERROR] Token should be '$COPILOT_PLACEHOLDER_TOKEN'"
135138
exit 1
136139
fi
137140
echo "[health-check] ✓ COPILOT_API_KEY is placeholder value (correct)"
138141
fi
139142

140143
# Verify COPILOT_TOKEN is placeholder (if present)
141144
if [ -n "$COPILOT_TOKEN" ]; then
142-
if [ "$COPILOT_TOKEN" != "placeholder-token-for-credential-isolation" ]; then
145+
if [ "$COPILOT_TOKEN" != "$COPILOT_PLACEHOLDER_TOKEN" ]; then
143146
echo "[health-check][ERROR] COPILOT_TOKEN contains non-placeholder value!"
144-
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
147+
echo "[health-check][ERROR] Token should be '$COPILOT_PLACEHOLDER_TOKEN'"
145148
exit 1
146149
fi
147150
echo "[health-check] ✓ COPILOT_TOKEN is placeholder value (correct)"
148151
fi
149152

150153
# Verify COPILOT_PROVIDER_API_KEY (offline+BYOK) is placeholder when api-proxy is enabled (if present)
151154
if [ -n "$COPILOT_PROVIDER_API_KEY" ]; then
152-
if [ "$COPILOT_PROVIDER_API_KEY" != "placeholder-token-for-credential-isolation" ]; then
155+
if [ "$COPILOT_PROVIDER_API_KEY" != "$COPILOT_PLACEHOLDER_TOKEN" ]; then
153156
echo "[health-check][ERROR] COPILOT_PROVIDER_API_KEY contains non-placeholder value!"
154-
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
157+
echo "[health-check][ERROR] Token should be '$COPILOT_PLACEHOLDER_TOKEN'"
155158
exit 1
156159
fi
157160
echo "[health-check] ✓ COPILOT_PROVIDER_API_KEY is placeholder value (correct)"

docs/api-proxy-sidecar.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,12 @@ The agent container receives **redacted placeholders** and proxy URLs:
143143
| `ANTHROPIC_AUTH_TOKEN` | `placeholder-token-for-credential-isolation` | `ANTHROPIC_API_KEY` provided to host | Placeholder token (real auth via BASE_URL) |
144144
| `CLAUDE_CODE_API_KEY_HELPER` | `/usr/local/bin/get-claude-key.sh` | `ANTHROPIC_API_KEY` provided to host | Helper script for Claude Code CLI |
145145
| `COPILOT_API_URL` | `http://172.30.0.30:10002` | `COPILOT_GITHUB_TOKEN` or `COPILOT_API_KEY` provided to host | Redirects Copilot CLI to proxy |
146-
| `COPILOT_TOKEN` | `placeholder-token-for-credential-isolation` | `COPILOT_GITHUB_TOKEN` or `COPILOT_API_KEY` provided to host | Placeholder token (real auth via API_URL) |
147-
| `COPILOT_GITHUB_TOKEN` | `placeholder-token-for-credential-isolation` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token protected by one-shot-token |
148-
| `COPILOT_API_KEY` | `placeholder-token-for-credential-isolation` | `COPILOT_API_KEY` provided to host | BYOK placeholder token protected by one-shot-token |
146+
| `COPILOT_TOKEN` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` or `COPILOT_API_KEY` provided to host | Placeholder token (real auth via API_URL) |
147+
| `COPILOT_GITHUB_TOKEN` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token protected by one-shot-token |
148+
| `COPILOT_API_KEY` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_API_KEY` provided to host | BYOK placeholder token protected by one-shot-token |
149149
| `COPILOT_OFFLINE` | `true` | `COPILOT_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake) |
150150
| `COPILOT_PROVIDER_BASE_URL` | `http://172.30.0.30:10002` | `COPILOT_API_KEY` provided to host | Points Copilot CLI BYOK provider at sidecar |
151-
| `COPILOT_PROVIDER_API_KEY` | `placeholder-token-for-credential-isolation` | `COPILOT_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) |
151+
| `COPILOT_PROVIDER_API_KEY` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) |
152152
| `GOOGLE_GEMINI_BASE_URL` | `http://172.30.0.30:10003` | `GEMINI_API_KEY` provided to host | Redirects Gemini CLI to proxy (primary var read by Gemini CLI) |
153153
| `GEMINI_API_BASE_URL` | `http://172.30.0.30:10003` | `GEMINI_API_KEY` provided to host | Redirects Gemini SDK to proxy (kept for backward compatibility) |
154154
| `GEMINI_API_KEY` | `gemini-api-key-placeholder-for-credential-isolation` | `GEMINI_API_KEY` provided to host | Placeholder so Gemini CLI auth check passes (real key in sidecar) |
@@ -169,7 +169,7 @@ The agent container receives **redacted placeholders** and proxy URLs:
169169
:::
170170

171171
:::tip[Placeholder tokens]
172-
Token variables in the agent are set to `placeholder-token-for-credential-isolation` instead of real values. This ensures:
172+
Token variables in the agent are set to placeholder values (for Copilot, `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`) instead of real values. This ensures:
173173
- Agent code cannot exfiltrate credentials
174174
- CLI tools that check for token presence still work
175175
- Real authentication happens via the `*_BASE_URL` or `*_API_URL` environment variables

docs/test-analysis/protocol-security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ GitHub Actions runners may have IPv6 enabled. The firewall must handle IPv6 DNS
129129
| **Health endpoint with Anthropic-only** | Verifies port 10000 health shows `openai:false, anthropic:true` when only Anthropic key is provided. |
130130
| **Copilot healthcheck** | Starts with `COPILOT_GITHUB_TOKEN` on port 10002. |
131131
| **COPILOT_API_URL set** | Verifies `COPILOT_API_URL=http://172.30.0.30:10002`. |
132-
| **COPILOT_TOKEN placeholder** | Verifies `COPILOT_TOKEN=placeholder-token-for-credential-isolation`. |
132+
| **COPILOT_TOKEN placeholder** | Verifies `COPILOT_TOKEN=ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`. |
133133
| **Copilot in health providers** | Verifies health endpoint reports `copilot:true`. |
134134

135135
### Real-World Mapping

src/constants/placeholders.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { COPILOT_PLACEHOLDER_TOKEN } from './placeholders';
4+
5+
describe('COPILOT_PLACEHOLDER_TOKEN', () => {
6+
it('matches the health-check shell placeholder value', () => {
7+
const healthCheckScriptPath = path.resolve(__dirname, '../../containers/agent/api-proxy-health-check.sh');
8+
const scriptContent = fs.readFileSync(healthCheckScriptPath, 'utf8');
9+
const match = scriptContent.match(/COPILOT_PLACEHOLDER_TOKEN="([^"]+)"/);
10+
11+
expect(match?.[1]).toBe(COPILOT_PLACEHOLDER_TOKEN);
12+
});
13+
});

src/constants/placeholders.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* GitHub-token-shaped Copilot placeholder used when api-proxy credential isolation is enabled.
3+
* The `ghu_` prefix plus 36 alphanumeric characters allows Copilot CLI auth prechecks to pass,
4+
* while real credentials remain isolated in the api-proxy sidecar and are injected only upstream.
5+
*/
6+
export const COPILOT_PLACEHOLDER_TOKEN = 'ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';

src/services/agent-environment-credentials.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe('agent environment: credentials', () => {
5959
const result = generateDockerCompose(configWithProxy, proxyNetworkConfig);
6060
const env = result.services.agent.environment as Record<string, string>;
6161
// Placeholder is set to prevent --env-all from leaking the real key
62-
expect(env.COPILOT_API_KEY).toBe('placeholder-token-for-credential-isolation');
62+
expect(env.COPILOT_API_KEY).toBe('ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
6363
} finally {
6464
if (original !== undefined) process.env.COPILOT_API_KEY = original;
6565
else delete process.env.COPILOT_API_KEY;
@@ -90,7 +90,7 @@ describe('agent environment: credentials', () => {
9090
const proxyNetworkConfig = { ...mockNetworkConfig, proxyIp: '172.30.0.30' };
9191
const result = generateDockerCompose(configWithProxy, proxyNetworkConfig);
9292
const env = result.services.agent.environment as Record<string, string>;
93-
expect(env.COPILOT_PROVIDER_API_KEY).toBe('placeholder-token-for-credential-isolation');
93+
expect(env.COPILOT_PROVIDER_API_KEY).toBe('ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
9494
} finally {
9595
if (original !== undefined) process.env.COPILOT_PROVIDER_API_KEY = original;
9696
else delete process.env.COPILOT_PROVIDER_API_KEY;
@@ -105,7 +105,7 @@ describe('agent environment: credentials', () => {
105105
const proxyNetworkConfig = { ...mockNetworkConfig, proxyIp: '172.30.0.30' };
106106
const result = generateDockerCompose(configWithProxy, proxyNetworkConfig);
107107
const env = result.services.agent.environment as Record<string, string>;
108-
expect(env.COPILOT_API_KEY).toBe('placeholder-token-for-credential-isolation');
108+
expect(env.COPILOT_API_KEY).toBe('ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
109109
} finally {
110110
if (original !== undefined) process.env.COPILOT_API_KEY = original;
111111
else delete process.env.COPILOT_API_KEY;

src/services/agent-environment.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { logger } from '../logger';
1818
import { PROXY_ENV_VARS } from '../upstream-proxy';
1919
import { WrapperConfig } from '../types';
20+
import { COPILOT_PLACEHOLDER_TOKEN } from '../constants/placeholders';
2021
import { NetworkConfig } from './squid-service';
2122

2223
// ─── Agent Environment ────────────────────────────────────────────────────────
@@ -152,13 +153,13 @@ export function buildAgentEnvironment(params: AgentEnvironmentParams): Record<st
152153
// When api-proxy is enabled with Copilot, set placeholder tokens early
153154
// so --env-all won't override them with real values from host environment
154155
if (config.enableApiProxy && config.copilotGithubToken) {
155-
environment.COPILOT_GITHUB_TOKEN = 'placeholder-token-for-credential-isolation';
156+
environment.COPILOT_GITHUB_TOKEN = COPILOT_PLACEHOLDER_TOKEN;
156157
logger.debug('COPILOT_GITHUB_TOKEN set to placeholder value (early) to prevent --env-all override');
157158
}
158159
if (config.enableApiProxy && config.copilotApiKey) {
159-
environment.COPILOT_API_KEY = 'placeholder-token-for-credential-isolation';
160+
environment.COPILOT_API_KEY = COPILOT_PLACEHOLDER_TOKEN;
160161
logger.debug('COPILOT_API_KEY set to placeholder value (early) to prevent --env-all override');
161-
environment.COPILOT_PROVIDER_API_KEY = 'placeholder-token-for-credential-isolation';
162+
environment.COPILOT_PROVIDER_API_KEY = COPILOT_PLACEHOLDER_TOKEN;
162163
logger.debug('COPILOT_PROVIDER_API_KEY set to placeholder value (early) to prevent --env-all override');
163164
}
164165

src/services/api-proxy-service.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ describe('API proxy sidecar', () => {
773773
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
774774
const agent = result.services.agent;
775775
const env = agent.environment as Record<string, string>;
776-
expect(env.COPILOT_TOKEN).toBe('placeholder-token-for-credential-isolation');
776+
expect(env.COPILOT_TOKEN).toBe('ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
777777
});
778778

779779
it('should set COPILOT_OFFLINE=true in agent when copilotApiKey is provided (offline+BYOK mode)', () => {
@@ -797,7 +797,7 @@ describe('API proxy sidecar', () => {
797797
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
798798
const agent = result.services.agent;
799799
const env = agent.environment as Record<string, string>;
800-
expect(env.COPILOT_PROVIDER_API_KEY).toBe('placeholder-token-for-credential-isolation');
800+
expect(env.COPILOT_PROVIDER_API_KEY).toBe('ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
801801
});
802802

803803
it.each(['gpt-5', 'openai/o3-mini', 'provider:gpt-5_preview', 'GPT-5', 'O3'])('should set COPILOT_PROVIDER_WIRE_API=responses in BYOK mode when COPILOT_MODEL is %s', (copilotModel) => {

src/services/api-proxy-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { buildRuntimeImageRef } from '../image-tag';
99
import { logger } from '../logger';
1010
import { WrapperConfig, API_PROXY_PORTS, API_PROXY_HEALTH_PORT } from '../types';
1111
import { pickEnvVars } from '../env-utils';
12+
import { COPILOT_PLACEHOLDER_TOKEN } from '../constants/placeholders';
1213
import { NetworkConfig, ImageBuildConfig } from './squid-service';
1314

1415
interface ApiProxyBuildResult {
@@ -33,7 +34,6 @@ interface ApiProxyServiceParams {
3334
// "copilot/o3-mini"). Prefix is intentionally broad because model providers/prefixes
3435
// are runtime-configurable and not limited to a fixed allowlist.
3536
const RESPONSES_WIRE_API_MODEL_PATTERN = /(^|[/:])(gpt-5|o3)([-_.]|$)/i;
36-
3737
function getCopilotModel(config: WrapperConfig): string | undefined {
3838
const envFileModel = config.envFile
3939
? readEnvFile(config.envFile).COPILOT_MODEL
@@ -267,7 +267,7 @@ export function buildApiProxyService(params: ApiProxyServiceParams): ApiProxyBui
267267

268268
// Set placeholder token for GitHub Copilot CLI compatibility
269269
// Real authentication happens via COPILOT_API_URL pointing to api-proxy
270-
agentEnvAdditions.COPILOT_TOKEN = 'placeholder-token-for-credential-isolation';
270+
agentEnvAdditions.COPILOT_TOKEN = COPILOT_PLACEHOLDER_TOKEN;
271271
logger.debug('COPILOT_TOKEN set to placeholder value for credential isolation');
272272

273273
// Note: COPILOT_GITHUB_TOKEN and COPILOT_API_KEY placeholders are set early (before --env-all)

tests/integration/api-proxy.test.ts

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

233233
expect(result).toSucceed();
234-
expect(result.stdout).toContain('COPILOT_TOKEN=placeholder-token-for-credential-isolation');
234+
expect(result.stdout).toContain('COPILOT_TOKEN=ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
235235
}, 180000);
236236

237237
test('should report copilot in health providers when Copilot token is provided', async () => {

0 commit comments

Comments
 (0)