The Codex CLI becomes the second AgentProvider (codex): full-access never-ask spawn, argv-first config with two verified global-state exceptions, device-code auth as a new generic seam kind, and rollout-JSONL read-through — every fragile coupling live-pinned against codex-cli 0.133.0¶
ADR-0026 promised a second provider is "one new implementation and zero refactor of the generic core"; the hardening wave carried that promise across every surface — attribution markers (ADR-0033), login sessions and host config (the two ADR-0034s), context-file names and skills indexing (ADR-0035), the two-tier conformance bar (ADR-0036). Issue #87 (superseding #2) cashes it: the Codex CLI adapter — provider id codex, package internal/provider/codex — built to ADR-0036's bar. The issue's body plus its two amendments carry the full Tier-2 spike evidence (2026-07-10, all eleven live-spike questions answered against the pinned binary, codex-cli 0.133.0); this ADR pins the decisions, the compat record at internal/compat/codex/compat.md pins the couplings.
The spikes are why this ADR exists. Codex is not claude with a different binary name: the research-pinned spec lost five couplings on contact with the binary — the documented trust pre-grant, the documented commit_attribution key, the rumored effort bracket syntax, the sandbox posture, the redirect auth flow — and where spec and evidence conflicted, the evidence won.
The pins, decided:
- Spawn posture: full access, never ask. Argv
codex --ask-for-approval never --sandbox danger-full-access, seed prompt as the trailing positional. Isolation is the host's job (service user + worktree), not the agent sandbox's — parity with claude-code's--permission-mode auto, and stronger evidence than parity: codex'sworkspace-writesandbox has three live-verified traps for lab sessions — network off by default (breakslabctlandgit push), writes confined to cwd (a linked worktree's commits write to the shared git dir outside cwd), and.gitmounted read-only (commits impossible even with.gitinside cwd) — a sandbox that cannot commit is a broken session, not a safety layer. - Argv-first per-session config, with two verified global-state exceptions. Per-session settings ride
-coverrides (project_doc_fallback_filenames,model_reasoning_effort); the global~/.codex/config.tomlis touched only where an override verifiably cannot carry the setting. Live-refuted:-c 'projects."<dir>".trust_level="trusted"'does NOT suppress the first-run trust prompt on 0.133 —SeedWorkspaceperforms a guarded, append-only[projects."<worktree>"] trust_level = "trusted"config append instead, tolerant of codex auto-writing its own trust entries. Dropped from the original spec:-c commit_attribution=""— an unknown config field on 0.133 (silently ignored normally, hard error under--strict-config); attribution is already absent at source, thecodex_git_commitfeature is off. - Context file:
AGENTS.local.md, double-pathed. ADR-0035's hard rule holds — lab-owned.localname, never the repo-trackedAGENTS.md; making codex read it is the adapter's own job, in two legs. Leg 1:-c project_doc_fallback_filenames=["AGENTS.local.md"]— fallback-only per directory level, so a repo-trackedAGENTS.mdsilently skips it (the trap ADR-0035 predicted, live-verified). Leg 2: a lab-managed, marker-guarded block in the global$CODEX_HOME/AGENTS.md— always concatenated regardless of repo files — instructing the agent to readAGENTS.local.mdat the workspace root. Both legs live-verified end-to-end, including against a repo with a trackedAGENTS.md. NativeSkillDiscovery: false. 0.133 has native skills ($CODEX_HOME/skills/, a/skillscommand) but discovery is global-only: planted markers in<repo>/.codex/skills/and<repo>/.agents/skills/were not discovered. No per-workspace seedable path means the ADR-0035 skills index appended to the context file stays.- Auth: device-code flow, a new generic seam kind. A maintainer amendment replaced the OAuth localhost redirect — an SSH port-forward on a server-first deployment — with
codex login --device-auth: browser anywhere plus a one-time code (15-minute expiry), URL and code on stdout, status shapes pinned live (exit 0 "Logged in using ChatGPT" / exit 1 "Not logged in"). The seam gains the genericAuthFlowDeviceCodekind; the optionalLoginCodeReportercapability (ADR-0017's type-assertion pattern) surfaces the code throughPOST /providers/{id}/auth/login/startasuser_code; the typed sentinelErrLoginCodeUnsupportedanswersLoginSubmitCode— the code travels operator→browser, never into lab; httpapi maps the sentinel to 409. Completion is adapter-driven: a background poller forcescodex login statusuntil the login lands, then publishesprovider.auth.changed— the oauth-code UX contract without a pasted code. Login runs in the ADR-0034 per-provider session (lab-login-codex). - Chat recipes: hazard-pinned keystrokes. Interrupt is a single Esc, legal only while a turn is working (lands in the rollout as
turn_abortedreasoninterrupted— a clean lifecycle marker). Ctrl-C is forbidden everywhere: it quits codex immediately, no confirmation — the worst case, confirmed live. Esc on an idle composer arms the backtrack overlay (qrecovers), so the recipe never emits it when idle. Replies are bracketed paste plus a separate paced Enter; slash-command text and its Enter are separate paced bursts — merged, the command text corrupts instead of executing. Mid-turn replies are legal queue-with-steer (queued for after the next tool call, delivered in the same turn, live) — no typed drop error exists or is needed. - Transcript: rollout JSONL, native rotation.
$CODEX_HOME/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl, located newest-first bysession_meta.cwdmatch — filename timestamps are local time, payload UTC./newrotates the file natively (fresh rollout, new session id), so the ADR-0026 clear/epoch obligation is satisfied for free — no synthesized epoch path.event_msgdrives prose and state (user_message/agent_message/task_started/task_complete/turn_aborted);response_itemdrives tool chips (function_call/custom_tool_callpaired with outputs bycall_id) and thinking (reasoning summaries only — the content is encrypted). Dialogs never render (never-ask posture, no structured question tool). - Optional capabilities omitted — honest degradation (ADR-0026/0017). No
DeepLinker: no per-session web surface exists — OpenAI's pairing requires the desktop app — so instance rows fall back to the copyable tmux-attach affordance. NoLiveSignalsin v1: hooks ARE stable in 0.133 (SessionStart/PreToolUse/PostToolUse/Stop/PermissionRequestet al.), recorded in the compat record as the follow-up substrate — omitted, never faked. NoConnectingReporter. - Model catalog pinned from
codex debug models— machine-readable JSON, not TUI scraping:gpt-5.5(default; effort default medium) andgpt-5.4-mini, efforts low/medium/high/xhigh, the internalcodex-auto-reviewentry filtered. Effort is always passed explicitly on spawn (-c model_reasoning_effort=) becausecodex execdefaults to none. The rumored-m model[effort]bracket syntax is refuted — the string passes verbatim as the slug and 400s. - Attribution and incogni: nothing to disable at source, defensive markers anyway. Ground truth from a real codex-authored commit on 0.133: no trailer, no attribution of any kind. The adapter still declares defensive
ScrubPatterns(co-authored-by:…codex,co-authored-by:.*<[^>]*@openai\.com>,generated with.*codex, BRE∩RE2 dialect) that join the ADR-0033 cross-provider union on registration and serve as the adapter's conformance marker samples.internal/provider/codexjoinsneutrality_test.go's exempt list exactly asclaudecodedoes — provider packages are where marker declarations live, not a leak.
Status¶
Accepted. Amended by ADR-0043 (issue #156): the pinned-catalog decision — "Model catalog pinned from codex debug models" — is superseded; the catalog is now probed from codex debug models at provider construction, and the pinned 0.133.0 list remains only as the probe-failure fallback. The first AgentProvider proved against ADR-0036's two-tier bar after claude-code: Tier 1 through the one registration table (internal/provider/conformance_test.go), Tier 2 evidenced at internal/compat/codex/compat.md (the four named spikes plus the wider 2026-07-10 record in issue #87). Extends ADR-0026's seam additively with AuthFlowDeviceCode, the optional LoginCodeReporter capability (ADR-0017's pattern), and the ErrLoginCodeUnsupported sentinel — generic kinds, not codex-shaped ones. Complies with ADR-0033 (declared ScrubPatterns join the union), both ADR-0034s (lab-login-codex derived from the provider id, zero exclusion-site changes; the generic host-config maps needed zero changes), and ADR-0035 (lab-owned AGENTS.local.md, the skills index inherited); honors ADR-0026's obligations — clear/epoch via native rotation, reply-while-working via queue-with-steer, hazard-checked interrupts, honest capability omission. Resolves #87.
Considered options¶
- OAuth localhost redirect (
oauth-redirect) with SSH port-forward instructions — the original spec. Rejected: lab runs server-first — the redirect demands an SSH forward of localhost:1455 before every login; the device flow needs a browser anywhere plus a one-time code. Superseded by maintainer amendment. - Widen
LoginStart's signature to return the one-time code. Rejected: it touches every existing adapter and fake for a value only device-code flows have; the optional-capability assertion (LoginCodeReporter) adds it additively — claude-code and the fakes compile untouched. - Embed the code in the returned URL, or carry it in static
AuthFlow.Instructions. Rejected: the device page has no URL-prefill contract, andInstructionsare static descriptor metadata while the code is per-attempt with a 15-minute expiry. - Run under codex's
workspace-writesandbox. Rejected on the three live-verified traps: network off by default, writes confined to cwd,.gitmounted read-only. The sandbox only breaks what lab needs; host isolation already covers the rest. - Synthesize a clear epoch for
/new. Rejected: live-verified that/newrotates the rollout file natively with a new session id, so the located-transcript identity changes on its own — synthesis is dead weight. - Ship
LiveSignalsnow on the 0.133 hooks substrate. Rejected for v1: the hook vocabulary is stable, but its payloads are unverified against lab's spool contract (ADR-0020) — a half-verified live channel is what honest degradation forbids. Deferred with the evidence recorded in the compat record.
Consequences¶
internal/provider/codexis the new package: the adapter, itsSeedMeta, the spawn argv, the trust-append and global-AGENTS.md-bridgeSeedWorkspace, the keystroke recipes, the rollout reader, and the device-code login driver.internal/provider/provider.gogains the genericAuthFlowDeviceCodekind, the optionalLoginCodeReportercapability, and theErrLoginCodeUnsupportedsentinel;internal/provider/providertestaccepts the new kind. No mandatory seam method changes.internal/httpapi/providers.gopassesuser_codethrough onPOST /providers/{id}/auth/login/startfor providers assertingLoginCodeReporter, and mapsErrLoginCodeUnsupportedto 409;web/src'sProviderAuthCardandapi.tsrender the device-code card from the descriptor — flows, never providers.internal/provider/conformance_test.gogains the codex registration-table entry (Tier 1, hermetic, in CI);internal/provider/neutrality_test.go's exempt list gainsinternal/provider/codex;cmd/lab/main.goregisters the adapter and addscodexto the known ids.internal/compat/codex/compat.mdis the committed Tier-2 record per ADR-0036: rollout excerpts, keystroke recipes, login transcript shapes, attribution ground truth, and the hooks findings that seed theLiveSignalsfollow-up.- The ADR-0034 generic host-config surface needed zero changes:
-provider-bin codex=…/-provider-config codex=…just work — provider N+1 as two map entries, kept on its first test.