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
26 changes: 23 additions & 3 deletions containers/agent/api-proxy-health-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,36 @@ 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
# Verify credentials are NOT in agent environment (real keys must stay in api-proxy sidecar).
# A placeholder value is intentionally injected so clients like Codex v0.121+ (which bypass
# OPENAI_BASE_URL when no key is present) still route through the sidecar. The placeholder
# is never sent upstream — the api-proxy replaces it with the real key before forwarding.
AWF_PLACEHOLDER="sk-placeholder-for-api-proxy"
REAL_KEY_PRESENT=false
if [ -n "$OPENAI_API_KEY" ] && [ "$OPENAI_API_KEY" != "$AWF_PLACEHOLDER" ]; then
REAL_KEY_PRESENT=true
fi
if [ -n "$CODEX_API_KEY" ] && [ "$CODEX_API_KEY" != "$AWF_PLACEHOLDER" ]; then
REAL_KEY_PRESENT=true
fi
if [ -n "$OPENAI_KEY" ] && [ "$OPENAI_KEY" != "$AWF_PLACEHOLDER" ]; then
REAL_KEY_PRESENT=true
fi

if [ "$REAL_KEY_PRESENT" = "true" ]; then
echo "[health-check][ERROR] OpenAI/Codex 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:+<present>}"
echo "[health-check][ERROR] CODEX_API_KEY=${CODEX_API_KEY:+<present>}"
echo "[health-check][ERROR] OPENAI_KEY=${OPENAI_KEY:+<present>}"
exit 1
fi
echo "[health-check] ✓ OpenAI/Codex credentials NOT in agent environment (correct)"

if [ -n "$OPENAI_API_KEY" ] || [ -n "$CODEX_API_KEY" ]; then
echo "[health-check] ✓ OpenAI/Codex placeholder key in agent environment (credential isolation active)"
else
echo "[health-check] ✓ OpenAI/Codex credentials NOT in agent environment (correct)"
fi

# Perform health check using BASE_URL
echo "[health-check] Testing connectivity to OpenAI API proxy at $OPENAI_BASE_URL..."
Expand Down
25 changes: 16 additions & 9 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2526,8 +2526,11 @@ describe('docker-manager', () => {
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
const agent = result.services.agent;
const env = agent.environment as Record<string, string>;
// Agent should NOT have the raw API key — only the sidecar gets it
expect(env.OPENAI_API_KEY).toBeUndefined();
// Agent should NOT have the real API key — only the sidecar holds it.
// A placeholder is injected so Codex/OpenAI clients route through OPENAI_BASE_URL
// (Codex v0.121+ bypasses OPENAI_BASE_URL when no key is present in the env).
expect(env.OPENAI_API_KEY).toBe('sk-placeholder-for-api-proxy');
expect(env.OPENAI_API_KEY).not.toBe('sk-secret-key');
// Agent should have OPENAI_BASE_URL to proxy through sidecar
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
} finally {
Expand All @@ -2540,17 +2543,19 @@ describe('docker-manager', () => {
});

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
// CODEX_API_KEY is now excluded when api-proxy is enabled for credential isolation
// Simulate the key being in process.env AND envAll enabled.
// The host's real CODEX_API_KEY must not reach the agent; a placeholder is
// injected instead so Codex routes through OPENAI_BASE_URL (api-proxy).
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<string, string>;
// CODEX_API_KEY should NOT be passed to agent when api-proxy is enabled
expect(env.CODEX_API_KEY).toBeUndefined();
// CODEX_API_KEY placeholder is set; the real host key must not be present
expect(env.CODEX_API_KEY).toBe('sk-placeholder-for-api-proxy');
expect(env.CODEX_API_KEY).not.toBe('sk-codex-secret');
// OPENAI_BASE_URL should be set when api-proxy is enabled with openaiApiKey
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
} finally {
Expand All @@ -2563,16 +2568,18 @@ describe('docker-manager', () => {
});

it('should not leak OPENAI_API_KEY to agent when api-proxy is enabled with envAll', () => {
// Simulate envAll scenario (smoke-codex uses --env-all)
// Simulate envAll scenario (smoke-codex uses --env-all).
// Even with envAll, the real key must not reach the agent; a placeholder is used instead.
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<string, string>;
// Even with envAll, agent should NOT have OPENAI_API_KEY when api-proxy is enabled
expect(env.OPENAI_API_KEY).toBeUndefined();
// Placeholder is set; real key must not be passed to agent
expect(env.OPENAI_API_KEY).toBe('sk-placeholder-for-api-proxy');
expect(env.OPENAI_API_KEY).not.toBe('sk-openai-secret');
// Agent should have OPENAI_BASE_URL to proxy through sidecar
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
} finally {
Expand Down
13 changes: 13 additions & 0 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,19 @@ export function generateDockerCompose(
if (config.openaiApiBasePath) {
logger.debug(`OpenAI API base path set to: ${config.openaiApiBasePath}`);
}

// Inject placeholder API keys for OpenAI/Codex credential isolation.
// Codex v0.121+ introduced a CODEX_API_KEY-based WebSocket auth flow: when no
// API key is found in the agent env, Codex bypasses OPENAI_BASE_URL and connects
// directly to api.openai.com for OAuth, getting a 401. With a placeholder key
// present, Codex routes API calls through OPENAI_BASE_URL (the api-proxy sidecar),
// which replaces the Authorization header with the real key before forwarding.
// The real keys are held securely in the sidecar; when requests are routed
// through api-proxy, these placeholders are expected to be overwritten by the
// api-proxy's injectHeaders before forwarding upstream.
environment.OPENAI_API_KEY = 'sk-placeholder-for-api-proxy';
environment.CODEX_API_KEY = 'sk-placeholder-for-api-proxy';
logger.debug('OPENAI_API_KEY and CODEX_API_KEY set to placeholder values for credential isolation');
}
if (config.anthropicApiKey) {
environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.ANTHROPIC}`;
Expand Down
Loading