99. Codex credential delivery
September 3, 2026 · View on GitHub
Date: 2026-09-02
Status
Accepted
Context
Adding openai/codex as a third agent runtime (ADR 0091) has to answer one question before anything else: how the agent reaches OpenAI without a credential entering the sandbox. The threat model treats keeping credentials out of the sandbox as the main limit on an injection's blast radius (security-threat-model.md), and the sandbox — not the runtime — is the containment boundary (agent-infrastructure.md).
ADR 0092 already settled the runner half: exchange the
job's OIDC token for a short-lived OpenAI token, put it in a run-scoped OpenShell provider, and let
the sandbox see only a gateway placeholder. Its in-sandbox half is written for pi specifically —
pi's auth.json, and a fullsend-openai egress profile admitting **/node. Codex is a native
binary that reads its credential differently, so that half needs generalising; this ADR records how,
and amends 0092 accordingly.
The constraint that decides the design: those tokens live minutes, and OpenShell pins each
placeholder to the credential generation it was issued for, so a running agent must be able to pick
up a new placeholder mid-run. Codex's built-in openai provider reads OPENAI_API_KEY once at
startup and built-in provider ids cannot be overridden, so it cannot.
Options
The built-in openai provider driven by the environment. Simplest, and wrong for the reason
above: the process reads the variable once and keeps using a placeholder that stops resolving at the
first refresh.
A host-side API server the sandbox calls instead of OpenAI. Would work, but it is tier 3 in ADR 0025's ordering, which prefers a provider whenever one is viable — and here one is.
Decision
Codex is configured with a custom model provider (fullsend-openai, wire_api = "responses")
whose auth.command is an absolute path to a runner-written script that prints the current
placeholder from a runner-owned token file under CODEX_HOME. Codex re-runs that command every
refresh_interval_ms, and the runner re-seeds the file after every credential refresh through the
runtime-neutral OpenAICredentialSeeder interface — the same follow-the-refresh shape pi gets from
re-reading auth.json, expressed in codex's own mechanism. A value that is not a gateway
placeholder fails the run rather than being forwarded.
This amends ADR 0092, whose in-sandbox half is pi-specific: the fullsend-openai egress profile
admits **/codex as well as **/node, and "seed pi's auth.json" is one implementation of a
runtime-neutral seeder interface rather than the mechanism itself.
The endpoint and the auth command are additionally pinned as -c SessionFlag overrides, which
outrank every config layer, and the runner-owned config.toml is guarded by a whole-file digest —
because a single projects."<repo>".trust_level = "trusted" line there would make codex load the
target repo's own .codex/ layer, and codex offers no -c override for project trust.
Consequences
- Codex has no Vertex path, so it gets no default behaviour-CI coverage until an OpenAI organization is mapped to the pool repositories; until then the evidence is unit tests, recorded fixtures and local smoke runs, one of which exercised the full rotate → re-seed → next-turn path.
- With a custom provider codex also issues
GET /v1/modelsat startup, which thefullsend-openaiprofile denies; it is logged, non-fatal and does not delay the first turn, but "onlyPOST /v1/responsesis attempted" is not true of codex the way it is of pi. - Codex reports no cost, so
total_cost_usdstays 0 for codex runs; token usage is reported. - The config guard compares against runner-held digests — digests the runner records outside the sandbox at Bootstrap and injects into the launch command at Run. The
agent-writable manifest was rejected as the anchor for them, since an agent that rewrote
config.tomlcould rewrite a digest recorded there to match; soRunrequiresBootstrapto have run in the same process and fails closed otherwise. - The pin must be re-checked on every
CODEX_VERSIONbump againstauth.commandsemantics and the provider config keys; the list is in runtime-implementation.md.
Sandbox tool hooks on codex are a separate decision — see ADR 0100.