diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 4c829c17..7e52d161 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -710,10 +710,11 @@ jobs: timeout-minutes: 5 run: | set -o pipefail - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --build-local \ + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --build-local --enable-api-proxy \ -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' \ 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: + AWF_ONE_SHOT_TOKEN_DEBUG: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json @@ -831,6 +832,7 @@ jobs: # Fix permissions on firewall logs so they can be uploaded as artifacts # AWF runs with sudo, creating files owned by root sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/api-proxy-logs 2>/dev/null || true awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - name: Upload cache-memory data as artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 @@ -852,6 +854,7 @@ jobs: /tmp/gh-aw/aw_info.json /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/api-proxy-logs/ /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ if-no-files-found: ignore diff --git a/containers/agent/api-proxy-health-check.sh b/containers/agent/api-proxy-health-check.sh index e83b723f..3f76777f 100755 --- a/containers/agent/api-proxy-health-check.sh +++ b/containers/agent/api-proxy-health-check.sh @@ -91,6 +91,49 @@ if [ -n "$OPENAI_BASE_URL" ]; then fi fi +# Check GitHub Copilot configuration +if [ -n "$COPILOT_API_URL" ]; then + API_PROXY_CONFIGURED=true + echo "[health-check] Checking GitHub Copilot API proxy configuration..." + echo "[health-check] COPILOT_API_URL=$COPILOT_API_URL" + + # Verify COPILOT_GITHUB_TOKEN is placeholder (protected by one-shot-token) + if [ -n "$COPILOT_GITHUB_TOKEN" ]; then + if [ "$COPILOT_GITHUB_TOKEN" != "placeholder-token-for-credential-isolation" ]; then + echo "[health-check][ERROR] COPILOT_GITHUB_TOKEN contains non-placeholder value!" + echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'" + exit 1 + fi + echo "[health-check] ✓ COPILOT_GITHUB_TOKEN is placeholder value (correct)" + fi + + # Verify COPILOT_TOKEN is placeholder (if present) + if [ -n "$COPILOT_TOKEN" ]; then + if [ "$COPILOT_TOKEN" != "placeholder-token-for-credential-isolation" ]; then + echo "[health-check][ERROR] COPILOT_TOKEN contains non-placeholder value!" + echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'" + exit 1 + fi + echo "[health-check] ✓ COPILOT_TOKEN is placeholder value (correct)" + fi + + # Perform health check using API URL + echo "[health-check] Testing connectivity to GitHub Copilot API proxy at $COPILOT_API_URL..." + + # Extract host and port from API URL (format: http://IP:PORT) + PROXY_HOST=$(echo "$COPILOT_API_URL" | sed -E 's|^https?://([^:]+):.*|\1|') + PROXY_PORT=$(echo "$COPILOT_API_URL" | sed -E 's|^https?://[^:]+:([0-9]+).*|\1|') + + # Test TCP connectivity with timeout + if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$PROXY_HOST/$PROXY_PORT" 2>/dev/null; then + echo "[health-check] ✓ GitHub Copilot API proxy is reachable at $COPILOT_API_URL" + else + echo "[health-check][ERROR] Cannot connect to GitHub Copilot API proxy at $COPILOT_API_URL" + echo "[health-check][ERROR] Proxy may not be running or network is blocked" + exit 1 + fi +fi + # Summary if [ "$API_PROXY_CONFIGURED" = "true" ]; then echo "[health-check] ==========================================" @@ -99,7 +142,7 @@ if [ "$API_PROXY_CONFIGURED" = "true" ]; then echo "[health-check] ✓ Connectivity established" echo "[health-check] ==========================================" else - echo "[health-check] No API proxy configured (ANTHROPIC_BASE_URL and OPENAI_BASE_URL not set)" + echo "[health-check] No API proxy configured (ANTHROPIC_BASE_URL, OPENAI_BASE_URL, and COPILOT_API_URL not set)" echo "[health-check] Skipping health checks" fi diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js index 8f381224..aec9ab00 100644 --- a/containers/api-proxy/server.js +++ b/containers/api-proxy/server.js @@ -45,6 +45,7 @@ function sanitizeForLog(str) { // 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; // Squid proxy configuration (set via HTTP_PROXY/HTTPS_PROXY in docker-compose) const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; @@ -57,6 +58,9 @@ if (OPENAI_API_KEY) { if (ANTHROPIC_API_KEY) { console.log('[API Proxy] Anthropic API key configured'); } +if (COPILOT_GITHUB_TOKEN) { + console.log('[API Proxy] GitHub Copilot token configured'); +} // Create proxy agent for routing through Squid const proxyAgent = HTTPS_PROXY ? new HttpsProxyAgent(HTTPS_PROXY) : undefined; @@ -169,7 +173,7 @@ if (OPENAI_API_KEY) { status: 'healthy', service: 'awf-api-proxy', squid_proxy: HTTPS_PROXY || 'not configured', - providers: { openai: true, anthropic: !!ANTHROPIC_API_KEY }, + providers: { openai: true, anthropic: !!ANTHROPIC_API_KEY, copilot: !!COPILOT_GITHUB_TOKEN }, })); return; } @@ -193,7 +197,7 @@ if (OPENAI_API_KEY) { status: 'healthy', service: 'awf-api-proxy', squid_proxy: HTTPS_PROXY || 'not configured', - providers: { openai: false, anthropic: !!ANTHROPIC_API_KEY }, + providers: { openai: false, anthropic: !!ANTHROPIC_API_KEY, copilot: !!COPILOT_GITHUB_TOKEN }, })); return; } @@ -231,6 +235,29 @@ if (ANTHROPIC_API_KEY) { }); } + +// GitHub Copilot API proxy (port 10002) +if (COPILOT_GITHUB_TOKEN) { + const copilotServer = http.createServer((req, res) => { + // Health check endpoint + if (req.url === '/health' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'healthy', service: 'copilot-proxy' })); + return; + } + + // Log and proxy the request + console.log(`[Copilot Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`); + console.log(`[Copilot Proxy] Injecting Authorization header with COPILOT_GITHUB_TOKEN`); + proxyRequest(req, res, 'api.githubcopilot.com', { + 'Authorization': `Bearer ${COPILOT_GITHUB_TOKEN}`, + }); + }); + + copilotServer.listen(10002, '0.0.0.0', () => { + console.log('[API Proxy] GitHub Copilot proxy listening on port 10002'); + }); +} // Graceful shutdown process.on('SIGTERM', () => { console.log('[API Proxy] Received SIGTERM, shutting down gracefully...'); diff --git a/examples/github-copilot.sh b/examples/github-copilot.sh index 8a28cc72..157b254c 100644 --- a/examples/github-copilot.sh +++ b/examples/github-copilot.sh @@ -1,20 +1,28 @@ #!/bin/bash -# Example: Using GitHub Copilot CLI with the firewall +# Example: Using GitHub Copilot CLI with the firewall and API proxy # -# This example shows how to run GitHub Copilot CLI through the firewall. -# Copilot requires access to several GitHub domains. +# This example shows how to run GitHub Copilot CLI through the firewall +# with credential isolation via the API proxy sidecar. # # Prerequisites: # - GitHub Copilot CLI installed: npm install -g @github/copilot -# - GITHUB_TOKEN environment variable set +# - COPILOT_API_KEY environment variable set (for API proxy) +# - GITHUB_TOKEN environment variable set (for GitHub API access) # # Usage: sudo -E ./examples/github-copilot.sh set -e -echo "=== AWF GitHub Copilot CLI Example ===" +echo "=== AWF GitHub Copilot CLI Example (with API Proxy) ===" echo "" +# Check for COPILOT_API_KEY +if [ -z "$COPILOT_API_KEY" ]; then + echo "Error: COPILOT_API_KEY environment variable is not set" + echo "Set it with: export COPILOT_API_KEY='your_copilot_api_key'" + exit 1 +fi + # Check for GITHUB_TOKEN if [ -z "$GITHUB_TOKEN" ]; then echo "Error: GITHUB_TOKEN environment variable is not set" @@ -22,18 +30,23 @@ if [ -z "$GITHUB_TOKEN" ]; then exit 1 fi -echo "Running GitHub Copilot CLI through the firewall..." +# Enable one-shot-token debug logging +export AWF_ONE_SHOT_TOKEN_DEBUG=1 + +echo "Running GitHub Copilot CLI with API proxy and debug logging enabled..." echo "" -# Run Copilot CLI with required domains -# Use sudo -E to preserve environment variables (especially GITHUB_TOKEN) +# Run Copilot CLI with API proxy enabled +# Use sudo -E to preserve environment variables (COPILOT_GITHUB_TOKEN, GITHUB_TOKEN, AWF_ONE_SHOT_TOKEN_DEBUG) # Required domains: +# - api.githubcopilot.com: Copilot API endpoint (proxied via api-proxy) # - github.com: GitHub API access # - api.github.com: GitHub REST API -# - api.enterprise.githubcopilot.com: Copilot API endpoint # - registry.npmjs.org: NPM package registry (for npx) sudo -E awf \ - --allow-domains github.com,api.github.com,api.enterprise.githubcopilot.com,registry.npmjs.org \ + --enable-api-proxy \ + --allow-domains api.githubcopilot.com,github.com,api.github.com,registry.npmjs.org \ + --log-level debug \ -- 'npx @github/copilot --prompt "What is 2+2?" --no-mcp' echo "" diff --git a/src/cli.ts b/src/cli.ts index 60b6b8a8..3805b796 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -262,12 +262,14 @@ export interface ApiProxyValidationResult { * @param enableApiProxy - Whether --enable-api-proxy flag was provided * @param hasOpenaiKey - Whether an OpenAI API key is present * @param hasAnthropicKey - Whether an Anthropic API key is present + * @param hasCopilotKey - Whether a GitHub Copilot API key is present * @returns ApiProxyValidationResult with warnings and debug messages */ export function validateApiProxyConfig( enableApiProxy: boolean, hasOpenaiKey?: boolean, - hasAnthropicKey?: boolean + hasAnthropicKey?: boolean, + hasCopilotKey?: boolean ): ApiProxyValidationResult { if (!enableApiProxy) { return { enabled: false, warnings: [], debugMessages: [] }; @@ -276,9 +278,9 @@ export function validateApiProxyConfig( const warnings: string[] = []; const debugMessages: string[] = []; - if (!hasOpenaiKey && !hasAnthropicKey) { + if (!hasOpenaiKey && !hasAnthropicKey && !hasCopilotKey) { warnings.push('⚠️ API proxy enabled but no API keys found in environment'); - warnings.push(' Set OPENAI_API_KEY or ANTHROPIC_API_KEY to use the proxy'); + warnings.push(' Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or COPILOT_GITHUB_TOKEN to use the proxy'); } if (hasOpenaiKey) { debugMessages.push('OpenAI API key detected - will be held securely in sidecar'); @@ -286,6 +288,9 @@ export function validateApiProxyConfig( if (hasAnthropicKey) { debugMessages.push('Anthropic API key detected - will be held securely in sidecar'); } + if (hasCopilotKey) { + debugMessages.push('GitHub Copilot API key detected - will be held securely in sidecar'); + } return { enabled: true, warnings, debugMessages }; } @@ -987,6 +992,7 @@ program enableApiProxy: options.enableApiProxy, openaiApiKey: process.env.OPENAI_API_KEY, anthropicApiKey: process.env.ANTHROPIC_API_KEY, + copilotGithubToken: process.env.COPILOT_GITHUB_TOKEN, }; // Warn if --env-all is used @@ -1025,8 +1031,15 @@ program const apiProxyValidation = validateApiProxyConfig( config.enableApiProxy || false, !!config.openaiApiKey, - !!config.anthropicApiKey + !!config.anthropicApiKey, + !!config.copilotGithubToken ); + + // Log API proxy status at info level for visibility + if (config.enableApiProxy) { + logger.info(`API proxy enabled: OpenAI=${!!config.openaiApiKey}, Anthropic=${!!config.anthropicApiKey}, Copilot=${!!config.copilotGithubToken}`); + } + for (const warning of apiProxyValidation.warnings) { logger.warn(warning); } @@ -1038,7 +1051,7 @@ program // to prevent sensitive data from flowing to logger (CodeQL sensitive data logging) const redactedConfig: Record = {}; for (const [key, value] of Object.entries(config)) { - if (key === 'openaiApiKey' || key === 'anthropicApiKey') continue; + if (key === 'openaiApiKey' || key === 'anthropicApiKey' || key === 'copilotGithubToken') continue; redactedConfig[key] = key === 'agentCommand' ? redactSecrets(value as string) : value; } logger.debug('Configuration:', JSON.stringify(redactedConfig, null, 2)); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index ce6f9a14..8705e707 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -334,6 +334,7 @@ export function generateDockerCompose( EXCLUDED_ENV_VARS.add('CODEX_API_KEY'); EXCLUDED_ENV_VARS.add('ANTHROPIC_API_KEY'); EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY'); + // COPILOT_GITHUB_TOKEN gets a placeholder (not excluded), protected by one-shot-token } // Start with required/overridden environment variables @@ -346,8 +347,18 @@ export function generateDockerCompose( SQUID_PROXY_PORT: SQUID_PORT.toString(), HOME: homeDir, PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + // Configure one-shot-token library with sensitive tokens to protect + // These tokens are cached on first access and unset from /proc/self/environ + 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', }; + // When api-proxy is enabled with Copilot, set placeholder tokens early + // so --env-all won't override them with real values from host environment + if (config.enableApiProxy && config.copilotGithubToken) { + environment.COPILOT_GITHUB_TOKEN = 'placeholder-token-for-credential-isolation'; + logger.debug('COPILOT_GITHUB_TOKEN set to placeholder value (early) to prevent --env-all override'); + } + // When host access is enabled, bypass the proxy for the host gateway IPs. // MCP Streamable HTTP (SSE) traffic through Squid crashes it (comm.cc:1583), // so MCP gateway traffic must go directly to the host, not through Squid. @@ -421,6 +432,7 @@ export function generateDockerCompose( if (process.env.OPENAI_API_KEY && !config.enableApiProxy) environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY; if (process.env.CODEX_API_KEY && !config.enableApiProxy) environment.CODEX_API_KEY = process.env.CODEX_API_KEY; if (process.env.ANTHROPIC_API_KEY && !config.enableApiProxy) environment.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; + // COPILOT_GITHUB_TOKEN is handled separately - gets placeholder when api-proxy enabled if (process.env.USER) environment.USER = process.env.USER; if (process.env.TERM) environment.TERM = process.env.TERM; if (process.env.XDG_CONFIG_HOME) environment.XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME; @@ -950,6 +962,7 @@ export function generateDockerCompose( // Pass API keys securely to sidecar (not visible to agent) ...(config.openaiApiKey && { OPENAI_API_KEY: config.openaiApiKey }), ...(config.anthropicApiKey && { ANTHROPIC_API_KEY: config.anthropicApiKey }), + ...(config.copilotGithubToken && { COPILOT_GITHUB_TOKEN: config.copilotGithubToken }), // Route through Squid to respect domain whitelisting HTTP_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`, HTTPS_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`, @@ -1013,6 +1026,18 @@ export function generateDockerCompose( environment.CLAUDE_CODE_API_KEY_HELPER = '/usr/local/bin/get-claude-key.sh'; logger.debug('Claude Code API key helper configured: /usr/local/bin/get-claude-key.sh'); } + if (config.copilotGithubToken) { + environment.COPILOT_API_URL = `http://${networkConfig.proxyIp}:10002`; + logger.debug(`GitHub Copilot API will be proxied through sidecar at http://${networkConfig.proxyIp}:10002`); + + // Set placeholder token for GitHub Copilot CLI compatibility + // Real authentication happens via COPILOT_API_URL pointing to api-proxy + environment.COPILOT_TOKEN = 'placeholder-token-for-credential-isolation'; + logger.debug('COPILOT_TOKEN set to placeholder value for credential isolation'); + + // Note: COPILOT_GITHUB_TOKEN placeholder is set early (before --env-all) + // to prevent override by host environment variable + } logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container'); logger.info('API proxy will route through Squid to respect domain whitelisting'); diff --git a/src/types.ts b/src/types.ts index ece8415d..affd0a42 100644 --- a/src/types.ts +++ b/src/types.ts @@ -385,24 +385,27 @@ export interface WrapperConfig { * Enable API proxy sidecar for holding authentication credentials * * When true, deploys a Node.js proxy sidecar container that: - * - Holds OpenAI and Anthropic API keys securely + * - Holds OpenAI, Anthropic, and GitHub Copilot API keys securely * - Automatically injects authentication headers * - Routes all traffic through Squid to respect domain whitelisting * - Proxies requests to LLM providers * - * The sidecar exposes two endpoints accessible from the agent container: + * The sidecar exposes three endpoints accessible from the agent container: * - http://api-proxy:10000 - OpenAI API proxy (for Codex) * - http://api-proxy:10001 - Anthropic API proxy (for Claude) + * - http://api-proxy:10002 - GitHub Copilot API proxy * * When the corresponding API key is provided, the following environment * variables are set in the agent container: * - OPENAI_BASE_URL=http://api-proxy:10000/v1 (set when OPENAI_API_KEY is provided) * - ANTHROPIC_BASE_URL=http://api-proxy:10001 (set when ANTHROPIC_API_KEY is provided) + * - COPILOT_API_URL=http://api-proxy:10002 (set when COPILOT_GITHUB_TOKEN is provided) * - CLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh (set when ANTHROPIC_API_KEY is provided) * * API keys are passed via environment variables: * - OPENAI_API_KEY - Optional OpenAI API key for Codex * - ANTHROPIC_API_KEY - Optional Anthropic API key for Claude + * - COPILOT_GITHUB_TOKEN - Optional GitHub token for Copilot * * @default false * @example @@ -410,7 +413,8 @@ export interface WrapperConfig { * # Enable API proxy with keys from environment * export OPENAI_API_KEY="sk-..." * export ANTHROPIC_API_KEY="sk-ant-..." - * awf --enable-api-proxy --allow-domains api.openai.com,api.anthropic.com -- command + * export COPILOT_GITHUB_TOKEN="ghp_..." + * awf --enable-api-proxy --allow-domains api.openai.com,api.anthropic.com,api.githubcopilot.com -- command * ``` */ enableApiProxy?: boolean; @@ -438,6 +442,19 @@ export interface WrapperConfig { * @default undefined */ anthropicApiKey?: string; + + /** + * GitHub token for Copilot (used by API proxy sidecar) + * + * When enableApiProxy is true, this token is injected into the Node.js sidecar + * container and used to authenticate requests to api.githubcopilot.com. + * + * The token is NOT exposed to the agent container - only the proxy URL is provided. + * The agent receives a placeholder value that is protected by the one-shot-token library. + * + * @default undefined + */ + copilotGithubToken?: string; } /** diff --git a/tests/integration/api-proxy.test.ts b/tests/integration/api-proxy.test.ts index 5fc24164..a231a7f5 100644 --- a/tests/integration/api-proxy.test.ts +++ b/tests/integration/api-proxy.test.ts @@ -172,4 +172,82 @@ describe('API Proxy Sidecar', () => { // Port 10001 should also be healthy expect(result.stdout).toContain('anthropic-proxy'); }, 180000); + + test('should start api-proxy sidecar with Copilot key and pass healthcheck', async () => { + const result = await runner.runWithSudo( + `curl -s http://${API_PROXY_IP}:10002/health`, + { + allowDomains: ['api.githubcopilot.com'], + enableApiProxy: true, + buildLocal: true, + logLevel: 'debug', + timeout: 120000, + env: { + COPILOT_GITHUB_TOKEN: 'ghp_fake-test-token-12345', + }, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('"status":"healthy"'); + expect(result.stdout).toContain('copilot-proxy'); + }, 180000); + + test('should set COPILOT_API_URL in agent when Copilot token is provided', async () => { + const result = await runner.runWithSudo( + 'bash -c "echo COPILOT_API_URL=$COPILOT_API_URL"', + { + allowDomains: ['api.githubcopilot.com'], + enableApiProxy: true, + buildLocal: true, + logLevel: 'debug', + timeout: 120000, + env: { + COPILOT_GITHUB_TOKEN: 'ghp_fake-test-token-12345', + }, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain(`COPILOT_API_URL=http://${API_PROXY_IP}:10002`); + }, 180000); + + test('should set COPILOT_TOKEN to placeholder in agent when Copilot token is provided', async () => { + const result = await runner.runWithSudo( + 'bash -c "echo COPILOT_TOKEN=$COPILOT_TOKEN"', + { + allowDomains: ['api.githubcopilot.com'], + enableApiProxy: true, + buildLocal: true, + logLevel: 'debug', + timeout: 120000, + env: { + COPILOT_GITHUB_TOKEN: 'ghp_fake-test-token-12345', + }, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('COPILOT_TOKEN=placeholder-token-for-credential-isolation'); + }, 180000); + + test('should report copilot in health providers when Copilot token is provided', async () => { + // When Copilot token is provided, the main health endpoint should report copilot: true + const result = await runner.runWithSudo( + `curl -s http://${API_PROXY_IP}:10000/health`, + { + allowDomains: ['api.githubcopilot.com'], + enableApiProxy: true, + buildLocal: true, + logLevel: 'debug', + timeout: 120000, + env: { + COPILOT_GITHUB_TOKEN: 'ghp_fake-test-token-12345', + }, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('"copilot":true'); + }, 180000); });