Skip to content

Commit a1aa098

Browse files
authored
feat: support Copilot CLI offline mode + BYOK for api-proxy deployments
Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/dac02eb8-4902-453e-813b-887f091740f0
1 parent 4b8c74a commit a1aa098

4 files changed

Lines changed: 86 additions & 1 deletion

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ if [ -n "$COPILOT_API_URL" ]; then
127127
echo "[health-check] ✓ COPILOT_TOKEN is placeholder value (correct)"
128128
fi
129129

130+
# Verify COPILOT_PROVIDER_API_KEY (offline+BYOK) is placeholder when api-proxy is enabled (if present)
131+
if [ -n "$COPILOT_PROVIDER_API_KEY" ]; then
132+
if [ "$COPILOT_PROVIDER_API_KEY" != "placeholder-token-for-credential-isolation" ]; then
133+
echo "[health-check][ERROR] COPILOT_PROVIDER_API_KEY contains non-placeholder value!"
134+
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
135+
exit 1
136+
fi
137+
echo "[health-check] ✓ COPILOT_PROVIDER_API_KEY is placeholder value (correct)"
138+
fi
139+
140+
# Verify COPILOT_PROVIDER_BASE_URL matches the sidecar URL when set (offline+BYOK mode)
141+
if [ -n "$COPILOT_PROVIDER_BASE_URL" ]; then
142+
echo "[health-check] COPILOT_PROVIDER_BASE_URL=$COPILOT_PROVIDER_BASE_URL (offline+BYOK mode)"
143+
echo "[health-check] ✓ Copilot CLI offline+BYOK mode configured"
144+
fi
145+
130146
# Perform health check using API URL
131147
echo "[health-check] Testing connectivity to GitHub Copilot API proxy at $COPILOT_API_URL..."
132148

docs/api-proxy-sidecar.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ The agent container receives **redacted placeholders** and proxy URLs:
145145
| `COPILOT_TOKEN` | `placeholder-token-for-credential-isolation` | `COPILOT_GITHUB_TOKEN` or `COPILOT_API_KEY` provided to host | Placeholder token (real auth via API_URL) |
146146
| `COPILOT_GITHUB_TOKEN` | `placeholder-token-for-credential-isolation` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token protected by one-shot-token |
147147
| `COPILOT_API_KEY` | `placeholder-token-for-credential-isolation` | `COPILOT_API_KEY` provided to host | BYOK placeholder token protected by one-shot-token |
148+
| `COPILOT_OFFLINE` | `true` | `COPILOT_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake) |
149+
| `COPILOT_PROVIDER_BASE_URL` | `http://172.30.0.30:10002` | `COPILOT_API_KEY` provided to host | Points Copilot CLI BYOK provider at sidecar |
150+
| `COPILOT_PROVIDER_API_KEY` | `placeholder-token-for-credential-isolation` | `COPILOT_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) |
148151
| `OPENAI_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) |
149152
| `ANTHROPIC_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) |
150153
| `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid proxy |

src/docker-manager.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2633,6 +2633,54 @@ describe('docker-manager', () => {
26332633
expect(env.COPILOT_TOKEN).toBe('placeholder-token-for-credential-isolation');
26342634
});
26352635

2636+
it('should set COPILOT_OFFLINE=true in agent when copilotApiKey is provided (offline+BYOK mode)', () => {
2637+
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotApiKey: 'cpat_test_byok_key' };
2638+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
2639+
const agent = result.services.agent;
2640+
const env = agent.environment as Record<string, string>;
2641+
expect(env.COPILOT_OFFLINE).toBe('true');
2642+
});
2643+
2644+
it('should set COPILOT_PROVIDER_BASE_URL in agent when copilotApiKey is provided (offline+BYOK mode)', () => {
2645+
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotApiKey: 'cpat_test_byok_key' };
2646+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
2647+
const agent = result.services.agent;
2648+
const env = agent.environment as Record<string, string>;
2649+
expect(env.COPILOT_PROVIDER_BASE_URL).toBe('http://172.30.0.30:10002');
2650+
});
2651+
2652+
it('should set COPILOT_PROVIDER_API_KEY placeholder in agent when copilotApiKey is provided (offline+BYOK mode)', () => {
2653+
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotApiKey: 'cpat_test_byok_key' };
2654+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
2655+
const agent = result.services.agent;
2656+
const env = agent.environment as Record<string, string>;
2657+
expect(env.COPILOT_PROVIDER_API_KEY).toBe('placeholder-token-for-credential-isolation');
2658+
});
2659+
2660+
it('should not set COPILOT_OFFLINE when only copilotGithubToken is provided', () => {
2661+
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotGithubToken: 'ghu_test_token' };
2662+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
2663+
const agent = result.services.agent;
2664+
const env = agent.environment as Record<string, string>;
2665+
expect(env.COPILOT_OFFLINE).toBeUndefined();
2666+
});
2667+
2668+
it('should not set COPILOT_PROVIDER_BASE_URL when only copilotGithubToken is provided', () => {
2669+
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotGithubToken: 'ghu_test_token' };
2670+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
2671+
const agent = result.services.agent;
2672+
const env = agent.environment as Record<string, string>;
2673+
expect(env.COPILOT_PROVIDER_BASE_URL).toBeUndefined();
2674+
});
2675+
2676+
it('should include COPILOT_PROVIDER_API_KEY in AWF_ONE_SHOT_TOKENS', () => {
2677+
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotApiKey: 'cpat_test_byok_key' };
2678+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
2679+
const agent = result.services.agent;
2680+
const env = agent.environment as Record<string, string>;
2681+
expect(env.AWF_ONE_SHOT_TOKENS).toContain('COPILOT_PROVIDER_API_KEY');
2682+
});
2683+
26362684
it('should include api-proxy service when enableApiProxy is true with Gemini key', () => {
26372685
const configWithProxy = { ...mockConfig, enableApiProxy: true, geminiApiKey: 'AIza-test-gemini-key' };
26382686
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);

src/docker-manager.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ export function generateDockerCompose(
642642
}),
643643
// Configure one-shot-token library with sensitive tokens to protect
644644
// These tokens are cached on first access and unset from /proc/self/environ
645-
AWF_ONE_SHOT_TOKENS: 'COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN,GITHUB_API_TOKEN,GITHUB_PAT,GH_ACCESS_TOKEN,OPENAI_API_KEY,OPENAI_KEY,ANTHROPIC_API_KEY,CLAUDE_API_KEY,CODEX_API_KEY,COPILOT_API_KEY',
645+
AWF_ONE_SHOT_TOKENS: 'COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN,GITHUB_API_TOKEN,GITHUB_PAT,GH_ACCESS_TOKEN,OPENAI_API_KEY,OPENAI_KEY,ANTHROPIC_API_KEY,CLAUDE_API_KEY,CODEX_API_KEY,COPILOT_API_KEY,COPILOT_PROVIDER_API_KEY',
646646
};
647647

648648
// When api-proxy is enabled with Copilot, set placeholder tokens early
@@ -654,6 +654,8 @@ export function generateDockerCompose(
654654
if (config.enableApiProxy && config.copilotApiKey) {
655655
environment.COPILOT_API_KEY = 'placeholder-token-for-credential-isolation';
656656
logger.debug('COPILOT_API_KEY set to placeholder value (early) to prevent --env-all override');
657+
environment.COPILOT_PROVIDER_API_KEY = 'placeholder-token-for-credential-isolation';
658+
logger.debug('COPILOT_PROVIDER_API_KEY set to placeholder value (early) to prevent --env-all override');
657659
}
658660

659661
// Always set NO_PROXY to prevent HTTP clients from proxying localhost traffic through Squid.
@@ -1636,6 +1638,22 @@ export function generateDockerCompose(
16361638
// Note: COPILOT_GITHUB_TOKEN and COPILOT_API_KEY placeholders are set early (before --env-all)
16371639
// to prevent override by host environment variable
16381640
}
1641+
if (config.copilotApiKey) {
1642+
// Enable Copilot CLI offline + BYOK mode so it skips the GitHub OAuth handshake
1643+
// and talks directly to the sidecar without needing GitHub authentication for inference.
1644+
// Reference: https://github.blog/changelog/2026-04-07-copilot-cli-now-supports-byok-and-local-models/
1645+
environment.COPILOT_OFFLINE = 'true';
1646+
logger.debug('COPILOT_OFFLINE set to true for offline+BYOK mode');
1647+
1648+
// Point Copilot CLI's BYOK provider URL at the sidecar, which injects the real API key
1649+
// and forwards the request through Squid. This is the new canonical BYOK env var.
1650+
environment.COPILOT_PROVIDER_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.COPILOT}`;
1651+
logger.debug(`COPILOT_PROVIDER_BASE_URL set to sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.COPILOT}`);
1652+
1653+
// COPILOT_PROVIDER_API_KEY placeholder: real key is held by the sidecar, never exposed to agent.
1654+
// Set early placeholder (before this block) already handled above.
1655+
logger.debug('COPILOT_PROVIDER_API_KEY placeholder set for credential isolation');
1656+
}
16391657
if (config.geminiApiKey) {
16401658
environment.GEMINI_API_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`;
16411659
logger.debug(`Google Gemini API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`);

0 commit comments

Comments
 (0)