Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
57 changes: 52 additions & 5 deletions docs/api-proxy-sidecar.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ The API proxy sidecar receives **real credentials** and routing configuration:
| `ANTHROPIC_API_KEY` | Real API key | `--enable-api-proxy` and env set | Anthropic API key (injected into requests) |
| `COPILOT_GITHUB_TOKEN` | Real token | `--enable-api-proxy` and env set | GitHub Copilot token (injected into requests) |
| `COPILOT_API_KEY` | Real API key | `--enable-api-proxy` and env set | GitHub Copilot BYOK key (injected into requests) |
| `GEMINI_API_KEY` | Real API key | `--enable-api-proxy` and env set | Google Gemini API key (injected into requests) |
| `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |
| `HTTPS_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |

Expand All @@ -148,6 +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` | `--enable-api-proxy` always | Redirects Gemini CLI to proxy (set unconditionally — see note below) |
| `GEMINI_API_KEY` | `gemini-api-key-placeholder-for-credential-isolation` | `--enable-api-proxy` always | 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) |
| `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid proxy |
Expand All @@ -156,6 +159,14 @@ The agent container receives **redacted placeholders** and proxy URLs:
| `AWF_API_PROXY_IP` | `172.30.0.30` | `--enable-api-proxy` | Used by iptables setup script |
| `AWF_ONE_SHOT_TOKENS` | `COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,...` | Always | Tokens protected by one-shot-token library |

:::note[Gemini always redirected to proxy]
Unlike OpenAI, Anthropic, and Copilot, `GEMINI_API_BASE_URL` and the `GEMINI_API_KEY` placeholder are **always** set in the agent when `--enable-api-proxy` is active, regardless of whether `GEMINI_API_KEY` is present in the runner environment.

This prevents the Gemini CLI from failing with exit code 41 ("no auth method") when the real API key is only available as a GitHub Actions secret (not as a runner-level environment variable). In that case the api-proxy sidecar will return `503` for Gemini requests — a clear, actionable failure rather than a confusing missing-auth error.

**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.
:::

:::tip[Placeholder tokens]
Token variables in the agent are set to `placeholder-token-for-credential-isolation` instead of real values. This ensures:
- Agent code cannot exfiltrate credentials
Expand Down Expand Up @@ -262,6 +273,24 @@ sudo awf --enable-api-proxy [OPTIONS] -- COMMAND
**Required environment variables** (at least one):
- `OPENAI_API_KEY` — OpenAI API key
- `ANTHROPIC_API_KEY` — Anthropic API key
- `GEMINI_API_KEY` — Google Gemini API key
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

The "Required environment variables" list appears incomplete: the CLI warning path treats COPILOT_GITHUB_TOKEN / COPILOT_API_KEY as valid ways to enable the api-proxy as well, but they are not listed here. Consider updating this list so it matches the actual supported credentials for --enable-api-proxy.

This issue also appears on line 346 of the same file.

Suggested change
- `GEMINI_API_KEY` — Google Gemini API key
- `GEMINI_API_KEY` — Google Gemini API key
- `COPILOT_GITHUB_TOKEN` — GitHub Copilot access token
- `COPILOT_API_KEY` — GitHub Copilot API key

Copilot uses AI. Check for mistakes.
- `COPILOT_GITHUB_TOKEN` — GitHub Copilot access token
- `COPILOT_API_KEY` — GitHub Copilot API key (BYOK)

:::caution[GitHub Actions: expose keys as runner env vars]
When running AWF in a GitHub Actions workflow, API keys must be available as **runner-level environment variables** — not just as GitHub Actions secrets. AWF reads the key from the environment at startup to pass it to the api-proxy sidecar container. Use `env:` in the workflow step and `sudo --preserve-env` to ensure keys pass through:

```yaml
- name: Run agent
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: sudo --preserve-env=GEMINI_API_KEY awf --enable-api-proxy ...
```
Comment on lines +280 to +288
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

In GitHub Actions, setting GEMINI_API_KEY under a step's env: does not automatically make it visible to the awf process if awf is executed via sudo (sudo typically drops non-whitelisted env vars). Consider updating this caution/example to show using sudo -E/--preserve-env=GEMINI_API_KEY (or otherwise ensure GEMINI_API_KEY is available to the privileged awf process), so users don't still hit the "key not set" warning unexpectedly.

Copilot uses AI. Check for mistakes.

> **Note:** `sudo` strips most environment variables by default. Use `--preserve-env=VAR` (or `sudo -E` to preserve all) to ensure API keys are visible to the AWF process.

If the key is present only in `secrets.*` but not exported into the step's `env:`, AWF will warn that no Gemini key was found and the api-proxy Gemini listener will return `503`.
:::

**Recommended domain whitelist**:
- `api.openai.com` — for OpenAI/Codex
Expand All @@ -283,7 +312,7 @@ The sidecar container:
- **Image**: `ghcr.io/github/gh-aw-firewall/api-proxy:latest`
- **Base**: `node:22-alpine`
- **Network**: `awf-net` at `172.30.0.30`
- **Ports**: 10000 (OpenAI), 10001 (Anthropic), 10002 (GitHub Copilot)
- **Ports**: 10000 (OpenAI), 10001 (Anthropic), 10002 (GitHub Copilot), 10003 (Google Gemini)
- **Proxy**: Routes via Squid at `http://172.30.0.10:3128`

### Health check
Expand All @@ -296,14 +325,33 @@ Docker healthcheck on the `/health` endpoint (port 10000):

## Troubleshooting

### 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.

**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`:

```yaml
- name: Run Gemini agent
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: |
sudo --preserve-env=GEMINI_API_KEY \
awf --enable-api-proxy \
--allow-domains generativelanguage.googleapis.com \
-- gemini ...
```

> **Note:** Exit code 41 ("no auth method") should no longer occur with `--enable-api-proxy` since the placeholder key satisfies the CLI's pre-flight check. If you see exit 41, ensure `--enable-api-proxy` is active.

### API keys not detected

```
⚠️ API proxy enabled but no API keys found in environment
Set OPENAI_API_KEY or ANTHROPIC_API_KEY to use the proxy
Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, COPILOT_GITHUB_TOKEN, or COPILOT_API_KEY to use the proxy
```

**Solution**: Export API keys before running awf:
**Solution**: Export API keys before running awf (use `sudo --preserve-env` in CI):

```bash
export OPENAI_API_KEY="sk-..."
Expand Down Expand Up @@ -343,9 +391,8 @@ docker exec awf-squid cat /var/log/squid/access.log | grep DENIED

## Limitations

- Only supports OpenAI and Anthropic APIs
- Keys must be set as environment variables (not file-based)
- No support for Azure OpenAI endpoints
- No support for Azure OpenAI endpoints (use `--openai-api-target` for custom endpoints)
- No request/response logging (by design, for security)

## Related documentation
Expand Down
16 changes: 14 additions & 2 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2713,12 +2713,24 @@ describe('docker-manager', () => {
expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation');
});

it('should not set GEMINI_API_BASE_URL in agent when geminiApiKey is not provided', () => {
it('should always set GEMINI_API_BASE_URL in agent when api-proxy is enabled (regardless of 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>;
expect(env.GEMINI_API_BASE_URL).toBeUndefined();
// GEMINI_API_BASE_URL must be set even without a geminiApiKey so that the
// Gemini CLI does not fail with exit code 41 ("no auth method") when the
// GEMINI_API_KEY is only available as a GitHub Actions secret.
expect(env.GEMINI_API_BASE_URL).toBe('http://172.30.0.30:10003');
});

it('should 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);
const agent = result.services.agent;
const env = agent.environment as Record<string, string>;
// Placeholder is required so Gemini CLI's startup auth check passes (exit code 41).
expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation');
});

it('should not leak GEMINI_API_KEY to agent when api-proxy is enabled', () => {
Expand Down
33 changes: 19 additions & 14 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1654,21 +1654,26 @@ export function generateDockerCompose(
// Set early placeholder (before this block) already handled above.
logger.debug('COPILOT_PROVIDER_API_KEY placeholder set for credential isolation');
}
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}`);
if (config.geminiApiTarget) {
logger.debug(`Gemini API target overridden to: ${config.geminiApiTarget}`);
}
if (config.geminiApiBasePath) {
logger.debug(`Gemini API base path set to: ${config.geminiApiBasePath}`);
}
// Always point the agent at the Gemini sidecar whenever --enable-api-proxy is active,
// regardless of whether GEMINI_API_KEY is present in the AWF runner environment.
// This prevents the Gemini CLI from failing with "no auth method" (exit code 41)
// when the key is only available as a GitHub Actions secret (not an env var visible
// to the AWF process itself). The sidecar returns 503 when the key is absent —
// a clear, actionable failure rather than a confusing missing-auth error.
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}`);
if (config.geminiApiTarget) {
logger.debug(`Gemini API target overridden to: ${config.geminiApiTarget}`);
}
if (config.geminiApiBasePath) {
logger.debug(`Gemini API base path set to: ${config.geminiApiBasePath}`);
}

// 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.
environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation';
logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation');
} else {
// 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.
environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation';
logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation');
if (!config.geminiApiKey) {
logger.warn('--enable-api-proxy is active but GEMINI_API_KEY is not set.');
logger.warn(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will start in fallback mode and return 503 responses until GEMINI_API_KEY is set.`);
logger.warn(' Set GEMINI_API_KEY in the AWF runner environment to enable Gemini credential isolation.');
Comment on lines 1677 to 1679
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

Now that GEMINI_API_BASE_URL / GEMINI_API_KEY placeholder are always set when --enable-api-proxy is active, the agent's pre-flight health check should also validate Gemini proxy configuration/connectivity (similar to OpenAI/Anthropic/Copilot). Currently the health check script only keys off OPENAI_BASE_URL, ANTHROPIC_BASE_URL, and COPILOT_API_URL, so a Gemini-only setup can skip checks entirely and proceed to runtime failures. Consider extending the pre-flight health check to include GEMINI_API_BASE_URL and placeholder validation.

Suggested change
logger.warn('--enable-api-proxy is active but GEMINI_API_KEY is not set.');
logger.warn(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will start in fallback mode and return 503 responses until GEMINI_API_KEY is set.`);
logger.warn(' Set GEMINI_API_KEY in the AWF runner environment to enable Gemini credential isolation.');
throw new Error(
`--enable-api-proxy is active but GEMINI_API_KEY is not set. ` +
`Refusing to continue because Gemini proxy traffic would be configured with a placeholder key only, ` +
`and the api-proxy Gemini listener on port ${API_PROXY_PORTS.GEMINI} would otherwise fail later with 503 responses.`
);

Copilot uses AI. Check for mistakes.
Expand Down
Loading