Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/api-proxy-sidecar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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.
:::
Expand Down Expand Up @@ -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.

Comment on lines 433 to 436

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This troubleshooting text says the Gemini base URL vars and placeholder GEMINI_API_KEY are “always injected” when --enable-api-proxy is active, but generateDockerCompose only configures the Gemini env vars/placeholder when geminiApiKey is provided. Update this section to reflect the conditional behavior (or, if the doc is correct, the code needs to be updated to match).

Copilot uses AI. Check for mistakes.
**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`:

Expand Down
6 changes: 4 additions & 2 deletions docs/authentication-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_...
Expand Down Expand Up @@ -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

Expand All @@ -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
```

Expand All @@ -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:

Expand Down
49 changes: 48 additions & 1 deletion src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
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<string, string>;
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);
Expand Down Expand Up @@ -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<string, string>;
// 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<string, string>;
// 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);
Expand All @@ -3068,8 +3113,9 @@ describe('docker-manager', () => {
const env = agent.environment as Record<string, string>;
// 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;
Expand All @@ -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;
Expand Down
14 changes: 11 additions & 3 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOOGLE_GEMINI_BASE_URL is excluded from --env-all passthrough, but GEMINI_API_BASE_URL is not. In non-Gemini runs with --env-all, a host-defined GEMINI_API_BASE_URL will still be inherited because the Gemini routing block doesn’t run, which can accidentally re-enable Gemini SDK traffic (or at least cause confusing/proxy-bypass behavior). Consider adding GEMINI_API_BASE_URL to EXCLUDED_ENV_VARS (and/or explicitly deleting/overriding it when config.geminiApiKey is absent) for consistency with the “Gemini setup is conditional” intent.

Suggested change
EXCLUDED_ENV_VARS.add('GOOGLE_GEMINI_BASE_URL');
EXCLUDED_ENV_VARS.add('GOOGLE_GEMINI_BASE_URL');
EXCLUDED_ENV_VARS.add('GEMINI_API_BASE_URL');

Copilot uses AI. Check for mistakes.
// 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
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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');
}
Expand Down
Loading