Client or integration
Codex SDK
Area
Authentication and account pool
Summary
When hostname is set to 0.0.0.0, the data plane requires x-opencodex-api-key, but only callers that go through the generated codex shim receive OPENCODEX_API_AUTH_TOKEN. A codex app-server launched by a third-party host that resolves the Codex entrypoint directly (require.resolve('@openai/codex/bin/codex.js')) never gets the token, so every model call is rejected with 401 before an SSE stream opens.
Request: codex app-server should receive the key as transparently as a local codex invocation does, so that a non-loopback bind does not silently break app-server-driven clients. Today the shim already covers app-server when it is the process being launched; the gap is that the token is only available when the shim is on the execution path at all, and it is captured once at process start.
Reproduction
- Set
hostname to 0.0.0.0 in ~/.opencodex/config.json and run ocx start (proxy on 127.0.0.1:10100).
- Configure Codex with the injected provider block:
model = "kiro/claude-opus-5"
model_provider = "opencodex"
[model_providers.opencodex]
base_url = "http://127.0.0.1:10100/v1"
wire_api = "responses"
env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }
- Start an app-server from a host process that does not inherit
OPENCODEX_API_AUTH_TOKEN and does not go through the generated shim. Any harness that resolves the entrypoint directly reproduces this:
spawn(process.execPath, [require.resolve('@openai/codex/bin/codex.js'), 'app-server', ...])
- Send a turn from that client.
- Observed: the turn fails after 5 retries. The client surfaces a generic stream failure because the
401 happens at admission, before any SSE frame is emitted.
- Expected: the app-server authenticates against the local proxy transparently, exactly as
codex launched through the shim does.
A second, related trigger without any third-party host: restart ocx so the token file is rewritten. Every already-running codex app-server keeps the previous value and starts returning 401 until it is restarted.
Version
@bitkyc08/opencodex v2.10.1
Operating system
Ubuntu 22.04.5 LTS (aarch64), Node.js v22.12.0, Codex CLI v0.146.1
Provider and model
kiro / claude-opus-5 (also reproduced with gpt-5.6-sol; the failure is provider-independent)
Logs or error output
From Codex's structured log store, target codex_core::responses_retry. Four separate turns each exhausted all 5 retries:
13:14:55~13:15:01 unexpected status 401 Unauthorized: opencodex API key required,
url: http://127.0.0.1:10100/v1/responses model=gpt-5.6-sol retries=1..5
13:17:44~13:17:50 same 401 model=kiro/claude-opus-5
13:20:42~13:20:48 same 401 model=kiro/claude-opus-5
13:25:24~13:25:30 same 401 model=kiro/claude-opus-5
Reproduced directly against the proxy with no key. No SSE frames are produced:
POST http://127.0.0.1:10100/v1/responses {"model":"kiro/claude-opus-5","stream":true}
HTTP 401
{"error":{"message":"opencodex API key required","type":"authentication_error","code":"invalid_api_key"}}
The same request with x-opencodex-api-key set streams normally and ends with response.completed.
Screenshots and supporting files
N/A
Redacted configuration
{
"port": 10100,
"hostname": "0.0.0.0",
"defaultProvider": "openai",
"providers": {
"kiro": { "adapter": "kiro", "authMode": "oauth", "defaultModel": "kiro-auto" }
}
}
Current implementation facts
isApiAuthRequired() keys admission entirely off the bind hostname, so 0.0.0.0 makes the key mandatory:
// src/server/auth-cors.ts
export function isApiAuthRequired(config: OcxConfig): boolean {
return !isLoopbackHostname(config.hostname);
}
resolveApiAuth() returns { kind: "loopback" } and skips the token entirely on a loopback bind; otherwise it requires one of x-opencodex-api-key, Authorization: Bearer, or x-api-key, and requireApiAuth() answers 401 "opencodex API key required".
- The expected value is read from the environment only:
configuredApiAuthToken() returns process.env.OPENCODEX_API_AUTH_TOKEN?.trim().
shouldInjectApiAuthHeader() in src/codex/inject.ts uses the same loopback predicate, which is why the env_http_headers line is injected for a 0.0.0.0 bind.
buildUnixCodexShim() in src/codex/shim.ts exports the token before parsing the subcommand, and app-server is already listed in CODEX_INTERNAL_COMMANDS. So codex app-server through the shim is authenticated; the token is read from serviceApiTokenFilePath() once, at process start.
- Consequence, observed on this host: the app-server started via the shim has
OPENCODEX_API_AUTH_TOKEN set, while a host process that resolves @openai/codex/bin/codex.js directly has no such variable to pass down to the app-server it spawns.
writeServiceApiTokenFile() in src/service.ts rewrites the token file from the current environment on service start, so the file contents can change while older app-server processes keep a stale value in memory.
Problem statement
A non-loopback bind is a supported configuration, and the provider block is injected for it automatically. But authentication only works when the caller's environment happens to carry the token. Any Codex-app-server-based integration that launches the Codex entrypoint directly gets a 401 at admission, which reaches the user as an opaque stream failure rather than an auth error, because the rejection precedes the first SSE frame. Token rotation on ocx restart produces the same symptom for already-running app-servers.
Goal
Make the API key reach codex app-server transparently whenever opencodex has injected the x-opencodex-api-key header requirement, so that switching hostname to 0.0.0.0 does not break app-server clients or require callers to plumb OPENCODEX_API_AUTH_TOKEN themselves.
Scope
- Ensure the token is available to
codex app-server regardless of whether the process was launched through the generated shim.
- Handle token rotation so an app-server started before an
ocx restart does not keep failing with a stale value.
- Make the resulting failure diagnosable when the key genuinely cannot be supplied.
Non-goals
Suggested direction
Inferred, not verified as the maintainers' preferred design:
- Have the app-server auth path resolve the token at request time rather than only from the start-time environment, falling back to
serviceApiTokenFilePath() when OPENCODEX_API_AUTH_TOKEN is absent or no longer matches. This addresses the direct-spawn case and rotation together, and mirrors what the shim already does for the CLI.
- Alternatively, or in addition, accept a loopback-only admission for requests that arrive on
127.0.0.1 even when the bind is 0.0.0.0, keyed on the local socket address rather than the configured hostname. That keeps the network surface gated while making local app-server traffic work without a key. This is a security-relevant change and deserves an explicit decision.
- Likely files:
src/server/auth-cors.ts (configuredApiAuthToken, resolveApiAuth), src/codex/shim.ts, src/lib/service-secrets.ts.
- Consider making
requireApiAuth()'s message distinguish "no key presented" from "key did not match", so a stale token is not misread as a missing one.
Acceptance criteria
- With
hostname set to 0.0.0.0, a turn driven by a codex app-server that was launched without OPENCODEX_API_AUTH_TOKEN in its environment completes successfully instead of returning 401.
- After
ocx restarts and rewrites the token file, an app-server process that was already running either recovers or reports a specific, actionable error.
- Regression: a loopback bind (
127.0.0.1) still admits requests with no key, and resolveApiAuth() still reports { kind: "loopback" }.
- Regression: with a non-loopback bind, a request presenting a wrong key is still rejected with
401, and the network-facing surface remains gated.
- A test covers the direct-spawn path, that is, an app-server whose environment lacks
OPENCODEX_API_AUTH_TOKEN.
Suggested smoke test
- Set
hostname to 0.0.0.0, run ocx start.
- Launch
codex app-server from a shell where OPENCODEX_API_AUTH_TOKEN is explicitly unset and the generated shim is not on PATH.
- Drive one turn through that app-server and confirm it streams to completion.
- Restart
ocx, then drive another turn on the same app-server process and confirm the documented behavior (recovery or a specific error).
- Confirm
curl against /v1/responses with no key still returns 401.
Checks
Note: This issue was filed with AI assistance. The 401 bursts, the keyless 401 response, and the authenticated response.completed were reproduced on the reporter's host; the suggested direction is inferred from reading the source and is not a proven design.
Client or integration
Codex SDK
Area
Authentication and account pool
Summary
When
hostnameis set to0.0.0.0, the data plane requiresx-opencodex-api-key, but only callers that go through the generatedcodexshim receiveOPENCODEX_API_AUTH_TOKEN. Acodex app-serverlaunched by a third-party host that resolves the Codex entrypoint directly (require.resolve('@openai/codex/bin/codex.js')) never gets the token, so every model call is rejected with401before an SSE stream opens.Request:
codex app-servershould receive the key as transparently as a localcodexinvocation does, so that a non-loopback bind does not silently break app-server-driven clients. Today the shim already coversapp-serverwhen it is the process being launched; the gap is that the token is only available when the shim is on the execution path at all, and it is captured once at process start.Reproduction
hostnameto0.0.0.0in~/.opencodex/config.jsonand runocx start(proxy on127.0.0.1:10100).OPENCODEX_API_AUTH_TOKENand does not go through the generated shim. Any harness that resolves the entrypoint directly reproduces this:401happens at admission, before any SSE frame is emitted.codexlaunched through the shim does.A second, related trigger without any third-party host: restart
ocxso the token file is rewritten. Every already-runningcodex app-serverkeeps the previous value and starts returning401until it is restarted.Version
@bitkyc08/opencodexv2.10.1Operating system
Ubuntu 22.04.5 LTS (aarch64), Node.js v22.12.0, Codex CLI v0.146.1
Provider and model
kiro / claude-opus-5 (also reproduced with gpt-5.6-sol; the failure is provider-independent)
Logs or error output
From Codex's structured log store, target
codex_core::responses_retry. Four separate turns each exhausted all 5 retries:Reproduced directly against the proxy with no key. No SSE frames are produced:
The same request with
x-opencodex-api-keyset streams normally and ends withresponse.completed.Screenshots and supporting files
N/A
Redacted configuration
{ "port": 10100, "hostname": "0.0.0.0", "defaultProvider": "openai", "providers": { "kiro": { "adapter": "kiro", "authMode": "oauth", "defaultModel": "kiro-auto" } } }Current implementation facts
isApiAuthRequired()keys admission entirely off the bind hostname, so0.0.0.0makes the key mandatory:resolveApiAuth()returns{ kind: "loopback" }and skips the token entirely on a loopback bind; otherwise it requires one ofx-opencodex-api-key,Authorization: Bearer, orx-api-key, andrequireApiAuth()answers401 "opencodex API key required".configuredApiAuthToken()returnsprocess.env.OPENCODEX_API_AUTH_TOKEN?.trim().shouldInjectApiAuthHeader()insrc/codex/inject.tsuses the same loopback predicate, which is why theenv_http_headersline is injected for a0.0.0.0bind.buildUnixCodexShim()insrc/codex/shim.tsexports the token before parsing the subcommand, andapp-serveris already listed inCODEX_INTERNAL_COMMANDS. Socodex app-serverthrough the shim is authenticated; the token is read fromserviceApiTokenFilePath()once, at process start.OPENCODEX_API_AUTH_TOKENset, while a host process that resolves@openai/codex/bin/codex.jsdirectly has no such variable to pass down to the app-server it spawns.writeServiceApiTokenFile()insrc/service.tsrewrites the token file from the current environment on service start, so the file contents can change while older app-server processes keep a stale value in memory.Problem statement
A non-loopback bind is a supported configuration, and the provider block is injected for it automatically. But authentication only works when the caller's environment happens to carry the token. Any Codex-app-server-based integration that launches the Codex entrypoint directly gets a
401at admission, which reaches the user as an opaque stream failure rather than an auth error, because the rejection precedes the first SSE frame. Token rotation onocxrestart produces the same symptom for already-running app-servers.Goal
Make the API key reach
codex app-servertransparently whenever opencodex has injected thex-opencodex-api-keyheader requirement, so that switchinghostnameto0.0.0.0does not break app-server clients or require callers to plumbOPENCODEX_API_AUTH_TOKENthemselves.Scope
codex app-serverregardless of whether the process was launched through the generated shim.ocxrestart does not keep failing with a stale value.Non-goals
isApiAuthRequired()loopback predicate or weakening auth for a0.0.0.0bind.resolveApiAuth().Host-header admission problem in Port-remapped tunnels are rejected by the loopback Host check: dashboard loads, every API call 403s #570, which is a different gate.Suggested direction
Inferred, not verified as the maintainers' preferred design:
serviceApiTokenFilePath()whenOPENCODEX_API_AUTH_TOKENis absent or no longer matches. This addresses the direct-spawn case and rotation together, and mirrors what the shim already does for the CLI.127.0.0.1even when the bind is0.0.0.0, keyed on the local socket address rather than the configured hostname. That keeps the network surface gated while making local app-server traffic work without a key. This is a security-relevant change and deserves an explicit decision.src/server/auth-cors.ts(configuredApiAuthToken,resolveApiAuth),src/codex/shim.ts,src/lib/service-secrets.ts.requireApiAuth()'s message distinguish "no key presented" from "key did not match", so a stale token is not misread as a missing one.Acceptance criteria
hostnameset to0.0.0.0, a turn driven by acodex app-serverthat was launched withoutOPENCODEX_API_AUTH_TOKENin its environment completes successfully instead of returning401.ocxrestarts and rewrites the token file, an app-server process that was already running either recovers or reports a specific, actionable error.127.0.0.1) still admits requests with no key, andresolveApiAuth()still reports{ kind: "loopback" }.401, and the network-facing surface remains gated.OPENCODEX_API_AUTH_TOKEN.Suggested smoke test
hostnameto0.0.0.0, runocx start.codex app-serverfrom a shell whereOPENCODEX_API_AUTH_TOKENis explicitly unset and the generated shim is not onPATH.ocx, then drive another turn on the same app-server process and confirm the documented behavior (recovery or a specific error).curlagainst/v1/responseswith no key still returns401.Checks