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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 63 additions & 31 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,46 +147,78 @@ echo "[entrypoint] iptables initialization complete"
/usr/local/bin/api-proxy-health-check.sh || exit 1

# 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
# This ensures the apiKeyHelper is properly configured in the config files
# The config files must exist before Claude Code starts for authentication to work
# We write to BOTH paths for compatibility:
# - ~/.claude.json (legacy path, used by older Claude Code versions)
# - ~/.claude/settings.json (used by Claude Code v2.1.81+)
# In chroot mode, we write to /host$HOME/... so files are accessible after chroot
if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then
echo "[entrypoint] Claude Code API key helper configured: $CLAUDE_CODE_API_KEY_HELPER"

# In chroot mode, write to /host path so file is accessible after chroot transition
# In chroot mode, write to /host path so files are accessible after chroot transition
if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
CONFIG_FILE="/host$HOME/.claude.json"
LEGACY_CONFIG_FILE="/host$HOME/.claude.json"
SETTINGS_DIR="/host$HOME/.claude"
else
CONFIG_FILE="$HOME/.claude.json"
LEGACY_CONFIG_FILE="$HOME/.claude.json"
SETTINGS_DIR="$HOME/.claude"
fi

if [ -f "$CONFIG_FILE" ]; then
# 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
SETTINGS_FILE="$SETTINGS_DIR/settings.json"

# Helper: write or validate apiKeyHelper in a config file
write_api_key_helper() {
local config_file="$1"
local label="$2"

if [ -f "$config_file" ]; then
if grep -q '"apiKeyHelper"' "$config_file"; then
CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$config_file" | cut -d'"' -f4)
if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then
echo "[entrypoint][ERROR] apiKeyHelper mismatch in $label:"
echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER"
echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER"
exit 1
fi
Comment on lines +174 to +182

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

The grep -q '"apiKeyHelper"' check is looser than the subsequent extraction regex grep -o '"apiKeyHelper":"[^"]*"'. If the JSON is pretty-printed (e.g., whitespace around :) or uses escaped characters, the extraction can fail and, with set -e, may terminate the entrypoint unexpectedly. Consider using a JSON parser for both validation and update, or make the extraction robust to whitespace and handle parse failures by rewriting/updating the key instead of exiting due to a failed grep in a command substitution.

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

Copilot uses AI. Check for mistakes.
echo "[entrypoint] ✓ $label apiKeyHelper validated"
else
echo "[entrypoint] $label exists but missing apiKeyHelper, merging..."
# Use node to safely add apiKeyHelper to existing JSON without losing other fields
# (e.g. hasCompletedOnboarding, session tokens, user preferences)
if AWF_CONFIG_FILE="$config_file" AWF_KEY_HELPER="$CLAUDE_CODE_API_KEY_HELPER" \
node -e "
const fs = require('fs');
const file = process.env.AWF_CONFIG_FILE;
const helper = process.env.AWF_KEY_HELPER;
let obj = {};
try { obj = JSON.parse(fs.readFileSync(file, 'utf8')); } catch(e) {}
obj.apiKeyHelper = helper;
fs.writeFileSync(file, JSON.stringify(obj) + '\n');
" 2>/dev/null; then
chmod 666 "$config_file"
echo "[entrypoint] ✓ Merged apiKeyHelper into $label"
else
# Fallback if node is unavailable or the file is not valid JSON
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$config_file"
chmod 666 "$config_file"
echo "[entrypoint] ✓ Wrote apiKeyHelper to $label (overwrite fallback)"
fi
fi
echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER"
else
# File exists but no apiKeyHelper - write it
echo "[entrypoint] Claude Code config file exists but missing apiKeyHelper, writing..."
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE"
chmod 666 "$CONFIG_FILE"
echo "[entrypoint] ✓ Wrote apiKeyHelper to $CONFIG_FILE"
echo "[entrypoint] Creating $label with apiKeyHelper..."
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$config_file"
chmod 666 "$config_file"
echo "[entrypoint] ✓ Created $label with apiKeyHelper: $CLAUDE_CODE_API_KEY_HELPER"
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 666 "$CONFIG_FILE"
echo "[entrypoint] ✓ Created $CONFIG_FILE with apiKeyHelper: $CLAUDE_CODE_API_KEY_HELPER"
fi
}

# Write to legacy path (~/.claude.json)
write_api_key_helper "$LEGACY_CONFIG_FILE" "$LEGACY_CONFIG_FILE"

# Write to settings path (~/.claude/settings.json) for Claude Code v2.1.81+
mkdir -p "$SETTINGS_DIR"
chmod 777 "$SETTINGS_DIR" 2>/dev/null || true
write_api_key_helper "$SETTINGS_FILE" "$SETTINGS_FILE"
fi

# Pre-seed JVM build tool proxy configuration
Expand Down
44 changes: 44 additions & 0 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2025,6 +2025,50 @@ describe('docker-manager', () => {
}
});

it('should pass GITHUB_API_URL to agent when api-proxy is enabled with envAll', () => {
// GITHUB_API_URL must remain in the agent environment even when api-proxy is enabled.
// The Copilot CLI needs it to locate the GitHub API (token exchange, user info, etc.).
// Copilot-specific calls route through COPILOT_API_URL → api-proxy regardless.
// See: github/gh-aw#20875
const origUrl = process.env.GITHUB_API_URL;
process.env.GITHUB_API_URL = 'https://api.github.com';
try {
const configWithProxy = { ...mockConfig, enableApiProxy: true, copilotGithubToken: 'ghp_test_token', envAll: true };
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
const agent = result.services.agent;
const env = agent.environment as Record<string, string>;
// GITHUB_API_URL should be passed to agent even when api-proxy is enabled
expect(env.GITHUB_API_URL).toBe('https://api.github.com');
// COPILOT_API_URL should also be set to route Copilot calls through the api-proxy
expect(env.COPILOT_API_URL).toBe('http://172.30.0.30:10002');
} finally {
if (origUrl !== undefined) {
process.env.GITHUB_API_URL = origUrl;
} else {
delete process.env.GITHUB_API_URL;
}
}
});

it('should pass GITHUB_API_URL to agent when api-proxy is NOT enabled with envAll', () => {
const origUrl = process.env.GITHUB_API_URL;
process.env.GITHUB_API_URL = 'https://api.github.com';
try {
const configNoProxy = { ...mockConfig, enableApiProxy: false, envAll: true };
const result = generateDockerCompose(configNoProxy, mockNetworkConfig);
const agent = result.services.agent;
const env = agent.environment as Record<string, string>;
// When api-proxy is NOT enabled, GITHUB_API_URL should be passed through
expect(env.GITHUB_API_URL).toBe('https://api.github.com');
} finally {
if (origUrl !== undefined) {
process.env.GITHUB_API_URL = origUrl;
} else {
delete process.env.GITHUB_API_URL;
}
}
});

it('should set AWF_RATE_LIMIT env vars when rateLimitConfig is provided', () => {
const configWithRateLimit = {
...mockConfig,
Expand Down
19 changes: 11 additions & 8 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,10 @@ export function generateDockerCompose(
EXCLUDED_ENV_VARS.add('ANTHROPIC_API_KEY');
EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY');
// COPILOT_GITHUB_TOKEN gets a placeholder (not excluded), protected by one-shot-token
// GITHUB_API_URL is intentionally NOT excluded: the Copilot CLI needs it to know the
// GitHub API base URL. Copilot-specific API calls (inference and token exchange) go
// through COPILOT_API_URL → api-proxy regardless of GITHUB_API_URL being set.
// See: github/gh-aw#20875
}

// Start with required/overridden environment variables
Expand Down Expand Up @@ -584,6 +588,11 @@ export function generateDockerCompose(
if (process.env.XDG_CONFIG_HOME) environment.XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
// Enterprise environment variables — needed for GHEC/GHES Copilot authentication
if (process.env.GITHUB_SERVER_URL) environment.GITHUB_SERVER_URL = process.env.GITHUB_SERVER_URL;
// GITHUB_API_URL — always pass when set. The Copilot CLI needs it to locate the GitHub API
// (especially on GHES/GHEC where the URL differs from api.github.com).
// Copilot-specific API calls (inference and token exchange) always route through
// COPILOT_API_URL → api-proxy when api-proxy is enabled, so GITHUB_API_URL does not
// interfere with credential isolation.
if (process.env.GITHUB_API_URL) environment.GITHUB_API_URL = process.env.GITHUB_API_URL;

// Auto-inject GH_HOST when GITHUB_SERVER_URL points to a GHES/GHEC instance
Expand All @@ -594,13 +603,6 @@ export function generateDockerCompose(
environment.GH_HOST = ghHost;
logger.debug(`Auto-injected GH_HOST=${ghHost} from GITHUB_SERVER_URL`);
}
// GITHUB_API_URL — only pass when api-proxy is NOT enabled.
// On GHES, workflows set GITHUB_API_URL to the GHES API endpoint (e.g., https://api.ghes-host).
// When api-proxy is enabled, Copilot CLI must use COPILOT_API_URL (pointing to the proxy)
// instead of GITHUB_API_URL, because the proxy correctly routes Copilot API requests to
// api.enterprise.githubcopilot.com (not the GHES API which lacks Copilot endpoints).
// See: github/gh-aw#20875
if (process.env.GITHUB_API_URL && !config.enableApiProxy) environment.GITHUB_API_URL = process.env.GITHUB_API_URL;
}

// Forward one-shot-token debug flag if set (used for testing/debugging)
Expand Down Expand Up @@ -762,7 +764,8 @@ export function generateDockerCompose(
// NOTE: ~/.claude.json is NOT bind-mounted as a file. File bind mounts on Linux
// prevent atomic writes (temp file + rename), which Claude Code requires.
// The writable home volume provides a writable $HOME, and entrypoint.sh
// creates ~/.claude.json with apiKeyHelper content from CLAUDE_CODE_API_KEY_HELPER.
// creates both ~/.claude.json (legacy) and ~/.claude/settings.json (v2.1.81+)
// with apiKeyHelper content from CLAUDE_CODE_API_KEY_HELPER.

// Mount ~/.cargo and ~/.rustup for Rust toolchain access
// On GitHub Actions runners, Rust is installed via rustup at $HOME/.cargo and $HOME/.rustup
Expand Down
Loading