Skip to content

Commit 737a5e9

Browse files
lpcoxCopilotCopilot
authored
fix: inject OPENAI_API_KEY/CODEX_API_KEY placeholders for Codex api-proxy routing (#2136)
* fix: inject OPENAI_API_KEY/CODEX_API_KEY placeholders for api-proxy credential isolation Codex v0.121.0 introduced a CODEX_API_KEY-based WebSocket auth flow. When neither CODEX_API_KEY nor OPENAI_API_KEY is present in the agent environment, Codex bypasses OPENAI_BASE_URL and connects directly to wss://api.openai.com/v1/responses for OAuth authentication, getting a 401 because AWF holds the real keys in the api-proxy sidecar. Fix: inject placeholder values for OPENAI_API_KEY and CODEX_API_KEY when api-proxy is active (same pattern as ANTHROPIC_AUTH_TOKEN for credential isolation). With a key present, Codex routes API calls through OPENAI_BASE_URL=http://172.30.0.30:10000. The api-proxy sidecar then replaces the placeholder Authorization header with the real key via Object.assign(forwardHeaders, injectHeaders) before forwarding to api.openai.com. The real keys are never present in the agent container; the placeholders are intercepted by the api-proxy WebSocket upgrade handler (proxyWebSocket() in server.js:689) and replaced with the real credentials. The one-shot-token LD_PRELOAD library caches and clears placeholders from /proc/self/environ as normal. Update tests to assert placeholder is set and real key is not leaked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update src/docker-manager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: update api-proxy health check to allow OpenAI/Codex placeholder keys The health check was blocking startup when OPENAI_API_KEY or CODEX_API_KEY contained the placeholder value 'sk-placeholder-for-api-proxy', treating it as a credential isolation failure. Update the check to allow the known placeholder value while still rejecting any real (non-placeholder) keys. This mirrors the Anthropic pattern where ANTHROPIC_AUTH_TOKEN is allowed to hold its placeholder value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 92f9578 commit 737a5e9

3 files changed

Lines changed: 52 additions & 12 deletions

File tree

containers/agent/api-proxy-health-check.sh

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,36 @@ if [ -n "$OPENAI_BASE_URL" ]; then
6363
API_PROXY_CONFIGURED=true
6464
echo "[health-check] Checking OpenAI API proxy configuration..."
6565

66-
# Verify credentials are NOT in agent environment
67-
if [ -n "$OPENAI_API_KEY" ] || [ -n "$CODEX_API_KEY" ] || [ -n "$OPENAI_KEY" ]; then
66+
# Verify credentials are NOT in agent environment (real keys must stay in api-proxy sidecar).
67+
# A placeholder value is intentionally injected so clients like Codex v0.121+ (which bypass
68+
# OPENAI_BASE_URL when no key is present) still route through the sidecar. The placeholder
69+
# is never sent upstream — the api-proxy replaces it with the real key before forwarding.
70+
AWF_PLACEHOLDER="sk-placeholder-for-api-proxy"
71+
REAL_KEY_PRESENT=false
72+
if [ -n "$OPENAI_API_KEY" ] && [ "$OPENAI_API_KEY" != "$AWF_PLACEHOLDER" ]; then
73+
REAL_KEY_PRESENT=true
74+
fi
75+
if [ -n "$CODEX_API_KEY" ] && [ "$CODEX_API_KEY" != "$AWF_PLACEHOLDER" ]; then
76+
REAL_KEY_PRESENT=true
77+
fi
78+
if [ -n "$OPENAI_KEY" ] && [ "$OPENAI_KEY" != "$AWF_PLACEHOLDER" ]; then
79+
REAL_KEY_PRESENT=true
80+
fi
81+
82+
if [ "$REAL_KEY_PRESENT" = "true" ]; then
6883
echo "[health-check][ERROR] OpenAI/Codex API key found in agent environment!"
6984
echo "[health-check][ERROR] Credential isolation failed - keys should only be in api-proxy container"
7085
echo "[health-check][ERROR] OPENAI_API_KEY=${OPENAI_API_KEY:+<present>}"
7186
echo "[health-check][ERROR] CODEX_API_KEY=${CODEX_API_KEY:+<present>}"
7287
echo "[health-check][ERROR] OPENAI_KEY=${OPENAI_KEY:+<present>}"
7388
exit 1
7489
fi
75-
echo "[health-check] ✓ OpenAI/Codex credentials NOT in agent environment (correct)"
90+
91+
if [ -n "$OPENAI_API_KEY" ] || [ -n "$CODEX_API_KEY" ]; then
92+
echo "[health-check] ✓ OpenAI/Codex placeholder key in agent environment (credential isolation active)"
93+
else
94+
echo "[health-check] ✓ OpenAI/Codex credentials NOT in agent environment (correct)"
95+
fi
7696

7797
# Perform health check using BASE_URL
7898
echo "[health-check] Testing connectivity to OpenAI API proxy at $OPENAI_BASE_URL..."

src/docker-manager.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,8 +2526,11 @@ describe('docker-manager', () => {
25262526
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
25272527
const agent = result.services.agent;
25282528
const env = agent.environment as Record<string, string>;
2529-
// Agent should NOT have the raw API key — only the sidecar gets it
2530-
expect(env.OPENAI_API_KEY).toBeUndefined();
2529+
// Agent should NOT have the real API key — only the sidecar holds it.
2530+
// A placeholder is injected so Codex/OpenAI clients route through OPENAI_BASE_URL
2531+
// (Codex v0.121+ bypasses OPENAI_BASE_URL when no key is present in the env).
2532+
expect(env.OPENAI_API_KEY).toBe('sk-placeholder-for-api-proxy');
2533+
expect(env.OPENAI_API_KEY).not.toBe('sk-secret-key');
25312534
// Agent should have OPENAI_BASE_URL to proxy through sidecar
25322535
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
25332536
} finally {
@@ -2540,17 +2543,19 @@ describe('docker-manager', () => {
25402543
});
25412544

25422545
it('should not leak CODEX_API_KEY to agent when api-proxy is enabled with envAll', () => {
2543-
// Simulate the key being in process.env AND envAll enabled
2544-
// CODEX_API_KEY is now excluded when api-proxy is enabled for credential isolation
2546+
// Simulate the key being in process.env AND envAll enabled.
2547+
// The host's real CODEX_API_KEY must not reach the agent; a placeholder is
2548+
// injected instead so Codex routes through OPENAI_BASE_URL (api-proxy).
25452549
const origKey = process.env.CODEX_API_KEY;
25462550
process.env.CODEX_API_KEY = 'sk-codex-secret';
25472551
try {
25482552
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test', envAll: true };
25492553
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
25502554
const agent = result.services.agent;
25512555
const env = agent.environment as Record<string, string>;
2552-
// CODEX_API_KEY should NOT be passed to agent when api-proxy is enabled
2553-
expect(env.CODEX_API_KEY).toBeUndefined();
2556+
// CODEX_API_KEY placeholder is set; the real host key must not be present
2557+
expect(env.CODEX_API_KEY).toBe('sk-placeholder-for-api-proxy');
2558+
expect(env.CODEX_API_KEY).not.toBe('sk-codex-secret');
25542559
// OPENAI_BASE_URL should be set when api-proxy is enabled with openaiApiKey
25552560
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
25562561
} finally {
@@ -2563,16 +2568,18 @@ describe('docker-manager', () => {
25632568
});
25642569

25652570
it('should not leak OPENAI_API_KEY to agent when api-proxy is enabled with envAll', () => {
2566-
// Simulate envAll scenario (smoke-codex uses --env-all)
2571+
// Simulate envAll scenario (smoke-codex uses --env-all).
2572+
// Even with envAll, the real key must not reach the agent; a placeholder is used instead.
25672573
const origKey = process.env.OPENAI_API_KEY;
25682574
process.env.OPENAI_API_KEY = 'sk-openai-secret';
25692575
try {
25702576
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-openai-secret', envAll: true };
25712577
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
25722578
const agent = result.services.agent;
25732579
const env = agent.environment as Record<string, string>;
2574-
// Even with envAll, agent should NOT have OPENAI_API_KEY when api-proxy is enabled
2575-
expect(env.OPENAI_API_KEY).toBeUndefined();
2580+
// Placeholder is set; real key must not be passed to agent
2581+
expect(env.OPENAI_API_KEY).toBe('sk-placeholder-for-api-proxy');
2582+
expect(env.OPENAI_API_KEY).not.toBe('sk-openai-secret');
25762583
// Agent should have OPENAI_BASE_URL to proxy through sidecar
25772584
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
25782585
} finally {

src/docker-manager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,6 +1821,19 @@ export function generateDockerCompose(
18211821
if (config.openaiApiBasePath) {
18221822
logger.debug(`OpenAI API base path set to: ${config.openaiApiBasePath}`);
18231823
}
1824+
1825+
// Inject placeholder API keys for OpenAI/Codex credential isolation.
1826+
// Codex v0.121+ introduced a CODEX_API_KEY-based WebSocket auth flow: when no
1827+
// API key is found in the agent env, Codex bypasses OPENAI_BASE_URL and connects
1828+
// directly to api.openai.com for OAuth, getting a 401. With a placeholder key
1829+
// present, Codex routes API calls through OPENAI_BASE_URL (the api-proxy sidecar),
1830+
// which replaces the Authorization header with the real key before forwarding.
1831+
// The real keys are held securely in the sidecar; when requests are routed
1832+
// through api-proxy, these placeholders are expected to be overwritten by the
1833+
// api-proxy's injectHeaders before forwarding upstream.
1834+
environment.OPENAI_API_KEY = 'sk-placeholder-for-api-proxy';
1835+
environment.CODEX_API_KEY = 'sk-placeholder-for-api-proxy';
1836+
logger.debug('OPENAI_API_KEY and CODEX_API_KEY set to placeholder values for credential isolation');
18241837
}
18251838
if (config.anthropicApiKey) {
18261839
environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.ANTHROPIC}`;

0 commit comments

Comments
 (0)