Skip to content

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's workspace-write sandbox has three live-verified traps for lab sessions — network off by default (breaks labctl and git push), writes confined to cwd (a linked worktree's commits write to the shared git dir outside cwd), and .git mounted read-only (commits impossible even with .git inside 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 -c overrides (project_doc_fallback_filenames, model_reasoning_effort); the global ~/.codex/config.toml is 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 — SeedWorkspace performs 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, the codex_git_commit feature is off.
  • Context file: AGENTS.local.md, double-pathed. ADR-0035's hard rule holds — lab-owned .local name, never the repo-tracked AGENTS.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-tracked AGENTS.md silently 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 read AGENTS.local.md at the workspace root. Both legs live-verified end-to-end, including against a repo with a tracked AGENTS.md.
  • NativeSkillDiscovery: false. 0.133 has native skills ($CODEX_HOME/skills/, a /skills command) 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 generic AuthFlowDeviceCode kind; the optional LoginCodeReporter capability (ADR-0017's type-assertion pattern) surfaces the code through POST /providers/{id}/auth/login/start as user_code; the typed sentinel ErrLoginCodeUnsupported answers LoginSubmitCode — the code travels operator→browser, never into lab; httpapi maps the sentinel to 409. Completion is adapter-driven: a background poller forces codex login status until the login lands, then publishes provider.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_aborted reason interrupted — 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 (q recovers), 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 by session_meta.cwd match — filename timestamps are local time, payload UTC. /new rotates the file natively (fresh rollout, new session id), so the ADR-0026 clear/epoch obligation is satisfied for free — no synthesized epoch path. event_msg drives prose and state (user_message/agent_message/task_started/task_complete/turn_aborted); response_item drives tool chips (function_call/custom_tool_call paired with outputs by call_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. No LiveSignals in v1: hooks ARE stable in 0.133 (SessionStart/PreToolUse/PostToolUse/Stop/PermissionRequest et al.), recorded in the compat record as the follow-up substrate — omitted, never faked. No ConnectingReporter.
  • Model catalog pinned from codex debug models — machine-readable JSON, not TUI scraping: gpt-5.5 (default; effort default medium) and gpt-5.4-mini, efforts low/medium/high/xhigh, the internal codex-auto-review entry filtered. Effort is always passed explicitly on spawn (-c model_reasoning_effort=) because codex exec defaults 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/codex joins neutrality_test.go's exempt list exactly as claudecode does — 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, and Instructions are static descriptor metadata while the code is per-attempt with a 15-minute expiry.
  • Run under codex's workspace-write sandbox. Rejected on the three live-verified traps: network off by default, writes confined to cwd, .git mounted 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 /new rotates the rollout file natively with a new session id, so the located-transcript identity changes on its own — synthesis is dead weight.
  • Ship LiveSignals now 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/codex is the new package: the adapter, its SeedMeta, the spawn argv, the trust-append and global-AGENTS.md-bridge SeedWorkspace, the keystroke recipes, the rollout reader, and the device-code login driver.
  • internal/provider/provider.go gains the generic AuthFlowDeviceCode kind, the optional LoginCodeReporter capability, and the ErrLoginCodeUnsupported sentinel; internal/provider/providertest accepts the new kind. No mandatory seam method changes.
  • internal/httpapi/providers.go passes user_code through on POST /providers/{id}/auth/login/start for providers asserting LoginCodeReporter, and maps ErrLoginCodeUnsupported to 409; web/src's ProviderAuthCard and api.ts render the device-code card from the descriptor — flows, never providers.
  • internal/provider/conformance_test.go gains the codex registration-table entry (Tier 1, hermetic, in CI); internal/provider/neutrality_test.go's exempt list gains internal/provider/codex; cmd/lab/main.go registers the adapter and adds codex to the known ids.
  • internal/compat/codex/compat.md is the committed Tier-2 record per ADR-0036: rollout excerpts, keystroke recipes, login transcript shapes, attribution ground truth, and the hooks findings that seed the LiveSignals follow-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.