From 2da37e6cc66add9e961c429cc336ac4a762936a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 13:58:26 +0000 Subject: [PATCH 1/3] Initial plan From 40c7bd9d240a6fec5776af81538f159434612ca9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 14:06:30 +0000 Subject: [PATCH 2/3] fix(api-proxy): set GOOGLE_GEMINI_BASE_URL to route Gemini via sidecar Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/0c1c4c92-49b5-4ce0-9650-5fbb747ed7f8 --- docs/api-proxy-sidecar.md | 9 ++++-- docs/authentication-architecture.md | 6 ++-- src/docker-manager.test.ts | 49 ++++++++++++++++++++++++++++- src/docker-manager.ts | 14 +++++++-- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index c7e9bb571..c394de550 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -149,7 +149,8 @@ The agent container receives **redacted placeholders** and proxy URLs: | `COPILOT_OFFLINE` | `true` | `COPILOT_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake) | | `COPILOT_PROVIDER_BASE_URL` | `http://172.30.0.30:10002` | `COPILOT_API_KEY` provided to host | Points Copilot CLI BYOK provider at sidecar | | `COPILOT_PROVIDER_API_KEY` | `placeholder-token-for-credential-isolation` | `COPILOT_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) | -| `GEMINI_API_BASE_URL` | `http://172.30.0.30:10003` | `GEMINI_API_KEY` provided to host | Redirects Gemini CLI to proxy | +| `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) | +| `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) | | `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) | | `OPENAI_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) | | `ANTHROPIC_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) | @@ -160,7 +161,9 @@ The agent container receives **redacted placeholders** and proxy URLs: | `AWF_ONE_SHOT_TOKENS` | `COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,...` | Always | Tokens protected by one-shot-token library | :::note[Gemini setup is conditional] -`GEMINI_API_BASE_URL`, the `GEMINI_API_KEY` placeholder, the `~/.gemini` home directory mount, and the `AWF_GEMINI_ENABLED` signal are only configured when `GEMINI_API_KEY` is provided to the host AWF process. This avoids spurious log entries and unnecessary directory setup in non-Gemini runs (e.g. Copilot-only workflows). +`GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_BASE_URL`, the `GEMINI_API_KEY` placeholder, the `~/.gemini` home directory mount, and the `AWF_GEMINI_ENABLED` signal are only configured when `GEMINI_API_KEY` is provided to the host AWF process. This avoids spurious log entries and unnecessary directory setup in non-Gemini runs (e.g. Copilot-only workflows). + +`GOOGLE_GEMINI_BASE_URL` is the primary variable read by the Gemini CLI (`google-gemini/gemini-cli`). `GEMINI_API_BASE_URL` is kept for backward compatibility with older SDK versions. **Important**: `GEMINI_API_KEY` must be set as a **runner-level environment variable** (e.g. `env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}` in the workflow step), not only as a GitHub Actions secret. The AWF process running on the runner must be able to read it so it can pass the key to the api-proxy sidecar container. ::: @@ -429,7 +432,7 @@ Fields: ### Gemini proxy returns 503 -When `--enable-api-proxy` is active, `GEMINI_API_BASE_URL` and a placeholder `GEMINI_API_KEY` are always injected into the agent container. If the real `GEMINI_API_KEY` was not set in the AWF runner environment, the api-proxy Gemini listener (port 10003) responds with **503** to all requests. +When `--enable-api-proxy` is active, `GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_BASE_URL`, and a placeholder `GEMINI_API_KEY` are always injected into the agent container. If the real `GEMINI_API_KEY` was not set in the AWF runner environment, the api-proxy Gemini listener (port 10003) responds with **503** to all requests. **Solution**: Export `GEMINI_API_KEY` in the runner environment before invoking AWF. In GitHub Actions, add it to the step's `env:` block and use `sudo --preserve-env`: diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md index df86449c2..7eccce49a 100644 --- a/docs/authentication-architecture.md +++ b/docs/authentication-architecture.md @@ -125,6 +125,7 @@ agent: - ANTHROPIC_BASE_URL=http://172.30.0.30:10001 - OPENAI_BASE_URL=http://172.30.0.30:10000 - COPILOT_API_URL=http://172.30.0.30:10002 + - GOOGLE_GEMINI_BASE_URL=http://172.30.0.30:10003 - GEMINI_API_BASE_URL=http://172.30.0.30:10003 # GitHub token for MCP servers (protected separately) - GITHUB_TOKEN=ghp_... @@ -180,7 +181,7 @@ Handles requests from the agent using `COPILOT_API_URL`. Injects the resolved Co #### Port 10003: Google Gemini proxy -Handles requests from the agent using `GEMINI_API_BASE_URL`. Injects `x-goog-api-key` from `GEMINI_API_KEY`, forwarding to `generativelanguage.googleapis.com`. Returns `503` if `GEMINI_API_KEY` is not configured. +Handles requests from the agent using `GOOGLE_GEMINI_BASE_URL` (read by the Gemini CLI) and `GEMINI_API_BASE_URL` (read by older SDK versions). Injects `x-goog-api-key` from `GEMINI_API_KEY`, forwarding to `generativelanguage.googleapis.com`. Returns `503` if `GEMINI_API_KEY` is not configured. #### Port 10004: OpenCode proxy @@ -200,6 +201,7 @@ The agent container sees these environment variables: ANTHROPIC_BASE_URL=http://172.30.0.30:10001 OPENAI_BASE_URL=http://172.30.0.30:10000 COPILOT_API_URL=http://172.30.0.30:10002 +GOOGLE_GEMINI_BASE_URL=http://172.30.0.30:10003 GEMINI_API_BASE_URL=http://172.30.0.30:10003 ``` @@ -211,7 +213,7 @@ These are standard environment variables recognized by the official SDKs: - Claude Code CLI - Codex CLI - GitHub Copilot CLI (`gh copilot`) -- Google Gemini CLI +- Google Gemini CLI (reads `GOOGLE_GEMINI_BASE_URL`) When the agent code makes an API call: diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index f993a3e19..03e7da54e 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2999,6 +2999,24 @@ describe('docker-manager', () => { expect(env.GEMINI_API_BASE_URL).toBe('http://172.30.0.30:10003'); }); + it('should set GOOGLE_GEMINI_BASE_URL in agent when geminiApiKey is provided', () => { + // GOOGLE_GEMINI_BASE_URL is the env var read by the Gemini CLI (google-gemini/gemini-cli) + // to override the API endpoint. Without it, the CLI bypasses the proxy sidecar. + const configWithProxy = { ...mockConfig, enableApiProxy: true, geminiApiKey: 'AIza-test-gemini-key' }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + expect(env.GOOGLE_GEMINI_BASE_URL).toBe('http://172.30.0.30:10003'); + }); + + it('should set GOOGLE_GEMINI_BASE_URL and GEMINI_API_BASE_URL to the same proxy URL', () => { + // Both vars must point to the same proxy so CLI and SDK clients both route through sidecar. + const configWithProxy = { ...mockConfig, enableApiProxy: true, geminiApiKey: 'AIza-test-gemini-key' }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const env = result.services.agent.environment as Record; + expect(env.GOOGLE_GEMINI_BASE_URL).toBe(env.GEMINI_API_BASE_URL); + }); + it('should set GEMINI_API_KEY placeholder in agent when geminiApiKey is provided', () => { const configWithProxy = { ...mockConfig, enableApiProxy: true, geminiApiKey: 'AIza-test-gemini-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); @@ -3049,6 +3067,33 @@ describe('docker-manager', () => { expect(env.GEMINI_API_BASE_URL).toBeUndefined(); }); + it('should NOT set GOOGLE_GEMINI_BASE_URL in agent when api-proxy is enabled without geminiApiKey', () => { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + // Must not be set without a Gemini key to avoid polluting non-Gemini runs. + expect(env.GOOGLE_GEMINI_BASE_URL).toBeUndefined(); + }); + + it('should not inherit GOOGLE_GEMINI_BASE_URL from host env via envAll when geminiApiKey is absent', () => { + const origVal = process.env.GOOGLE_GEMINI_BASE_URL; + process.env.GOOGLE_GEMINI_BASE_URL = 'http://some-other-proxy'; + try { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key', envAll: true }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const env = result.services.agent.environment as Record; + // GOOGLE_GEMINI_BASE_URL is in EXCLUDED_ENV_VARS so it must not be inherited from host + expect(env.GOOGLE_GEMINI_BASE_URL).toBeUndefined(); + } finally { + if (origVal !== undefined) { + process.env.GOOGLE_GEMINI_BASE_URL = origVal; + } else { + delete process.env.GOOGLE_GEMINI_BASE_URL; + } + } + }); + it('should NOT set GEMINI_API_KEY placeholder in agent when api-proxy is enabled without geminiApiKey', () => { const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); @@ -3068,8 +3113,9 @@ describe('docker-manager', () => { const env = agent.environment as Record; // Agent should NOT have the real API key — only the sidecar gets it expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation'); - // Agent should have GEMINI_API_BASE_URL to proxy through sidecar + // Agent should have both base URL vars to proxy through sidecar expect(env.GEMINI_API_BASE_URL).toBe('http://172.30.0.30:10003'); + expect(env.GOOGLE_GEMINI_BASE_URL).toBe('http://172.30.0.30:10003'); } finally { if (origKey !== undefined) { process.env.GEMINI_API_KEY = origKey; @@ -3090,6 +3136,7 @@ describe('docker-manager', () => { // Even with envAll, agent should NOT have the real GEMINI_API_KEY expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation'); expect(env.GEMINI_API_BASE_URL).toBe('http://172.30.0.30:10003'); + expect(env.GOOGLE_GEMINI_BASE_URL).toBe('http://172.30.0.30:10003'); } finally { if (origKey !== undefined) { process.env.GEMINI_API_KEY = origKey; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 9efd4031c..20d4b94bf 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -772,6 +772,7 @@ export function generateDockerCompose( EXCLUDED_ENV_VARS.add('ANTHROPIC_API_KEY'); EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY'); EXCLUDED_ENV_VARS.add('GEMINI_API_KEY'); + EXCLUDED_ENV_VARS.add('GOOGLE_GEMINI_BASE_URL'); // COPILOT_GITHUB_TOKEN and COPILOT_API_KEY get placeholders (not excluded), protected by one-shot-token // GITHUB_API_URL is intentionally NOT excluded: the Copilot CLI needs it to know the // GitHub API base URL. Copilot-specific API calls (inference and token exchange) go @@ -1931,8 +1932,15 @@ export function generateDockerCompose( // directory and GEMINI_API_KEY placeholder to appear in non-Gemini runs (e.g. // Copilot-only runs), producing suspicious-looking log entries. if (config.geminiApiKey) { - environment.GEMINI_API_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`; - logger.debug(`Google Gemini API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`); + const geminiProxyUrl = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`; + // GOOGLE_GEMINI_BASE_URL is the env var read by the Gemini CLI (google-gemini/gemini-cli) + // when authType === USE_GEMINI. Setting it routes all Gemini CLI traffic through + // the api-proxy sidecar instead of calling generativelanguage.googleapis.com directly. + environment.GOOGLE_GEMINI_BASE_URL = geminiProxyUrl; + // GEMINI_API_BASE_URL is kept for backward compatibility with older SDK versions + // and other tools that may read it (e.g. @google/generative-ai npm package). + environment.GEMINI_API_BASE_URL = geminiProxyUrl; + logger.debug(`Google Gemini API will be proxied through sidecar at ${geminiProxyUrl}`); if (config.geminiApiTarget) { logger.debug(`Gemini API target overridden to: ${config.geminiApiTarget}`); } @@ -1941,7 +1949,7 @@ export function generateDockerCompose( } // Set placeholder key so Gemini CLI's startup auth check passes (exit code 41). - // Real authentication happens via GEMINI_API_BASE_URL pointing to api-proxy. + // Real authentication happens via GOOGLE_GEMINI_BASE_URL / GEMINI_API_BASE_URL pointing to api-proxy. environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation'; logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation'); } From 09cb7e1991a4a8a8aaef0ac14d287e58eba86644 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 14:43:05 +0000 Subject: [PATCH 3/3] fix(api-proxy): exclude GEMINI_API_BASE_URL from env-all passthrough Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/45da4d20-2dd1-4626-bdd7-cfe45a6c0037 --- docs/api-proxy-sidecar.md | 2 +- src/docker-manager.test.ts | 18 ++++++++++++++++++ src/docker-manager.ts | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index c394de550..cb0de5e22 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -432,7 +432,7 @@ Fields: ### Gemini proxy returns 503 -When `--enable-api-proxy` is active, `GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_BASE_URL`, and a placeholder `GEMINI_API_KEY` are always injected into the agent container. If the real `GEMINI_API_KEY` was not set in the AWF runner environment, the api-proxy Gemini listener (port 10003) responds with **503** to all requests. +When `--enable-api-proxy` is active **and `GEMINI_API_KEY` is provided to the AWF runner**, `GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_BASE_URL`, and a placeholder `GEMINI_API_KEY` are injected into the agent container. If the real `GEMINI_API_KEY` was not set in the AWF runner environment, the Gemini routing vars are never set and the api-proxy Gemini listener (port 10003) responds with **503** to any requests that do reach it. **Solution**: Export `GEMINI_API_KEY` in the runner environment before invoking AWF. In GitHub Actions, add it to the step's `env:` block and use `sudo --preserve-env`: diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 03e7da54e..88c607e24 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -3094,6 +3094,24 @@ describe('docker-manager', () => { } }); + it('should not inherit GEMINI_API_BASE_URL from host env via envAll when geminiApiKey is absent', () => { + const origVal = process.env.GEMINI_API_BASE_URL; + process.env.GEMINI_API_BASE_URL = 'http://some-other-proxy'; + try { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key', envAll: true }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const env = result.services.agent.environment as Record; + // GEMINI_API_BASE_URL is in EXCLUDED_ENV_VARS so it must not be inherited from host + expect(env.GEMINI_API_BASE_URL).toBeUndefined(); + } finally { + if (origVal !== undefined) { + process.env.GEMINI_API_BASE_URL = origVal; + } else { + delete process.env.GEMINI_API_BASE_URL; + } + } + }); + it('should NOT set GEMINI_API_KEY placeholder in agent when api-proxy is enabled without geminiApiKey', () => { const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 20d4b94bf..f397bc2bc 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -773,6 +773,7 @@ export function generateDockerCompose( EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY'); EXCLUDED_ENV_VARS.add('GEMINI_API_KEY'); EXCLUDED_ENV_VARS.add('GOOGLE_GEMINI_BASE_URL'); + EXCLUDED_ENV_VARS.add('GEMINI_API_BASE_URL'); // COPILOT_GITHUB_TOKEN and COPILOT_API_KEY get placeholders (not excluded), protected by one-shot-token // GITHUB_API_URL is intentionally NOT excluded: the Copilot CLI needs it to know the // GitHub API base URL. Copilot-specific API calls (inference and token exchange) go