feat(provider): Codex CLI adapter behind AgentProvider — device-code auth, rollout chat surface, conformance to the #80 bar (ADR-0037) #91

Merged
dominik.polakovics merged 1 commit from afk/87 into main 2026-07-10 11:39:41 +02:00

The second AgentProvider (id codex, internal/provider/codex), implementing issue #87's research-pinned spec — the issue body plus both amendments — to ADR-0036's two-tier conformance bar. Every fragile coupling is live-pinned against codex-cli 0.133.0: the Tier-2 spike sweep in the issue's Amendment 2 plus fresh probes from this branch (device-auth stdout, codex debug models, login status shapes, a live TUI slash-popup scrape, and a live validation that the trust config-append suppresses the first-run prompt).

The adapter

  • Spawncodex --ask-for-approval never --sandbox danger-full-access -c project_doc_fallback_filenames=["AGENTS.local.md"] [--model M] -c model_reasoning_effort=E [prompt]. Effort is always explicit (codex exec defaults to none); no -c commit_attribution="" (unknown key on 0.133, hard error under --strict-config; attribution is absent at source). Full access is evidence-based: workspace-write's three live-verified traps (network off, cwd-confined writes, .git mounted read-only) each break a lab session.
  • Auth — device-code flow per the maintainer amendment: LoginStart drives codex login --device-auth in the lab-login-codex session (ADR-0034), scrapes the verification URL + one-time code from the pane, and a single background poller forces codex login status until the login lands, then publishes provider.auth.changed. LoginSubmitCode returns the new typed ErrLoginCodeUnsupported; logout is status-decided with an auth.json removal escalation.
  • SeedingSeedWorkspace writes nothing in-tree: a guarded, append-only [projects."<worktree>"] trust_level = "trusted" table in config.toml (the argv trust route is dead on 0.133 — live-refuted; the append validated live on a real spawn) and a marker-guarded AGENTS.local.md bridge block in the global $CODEX_HOME/AGENTS.md (a repo-tracked AGENTS.md silently skips the fallback-filenames leg — the trap ADR-0035 predicted). NativeSkillDiscovery: false (0.133 discovers skills globally only) so the ADR-0035 skills index applies. Incogni is a no-op by ground truth; defensive ScrubPatterns join the ADR-0033 union.
  • Chat surface — rollout JSONL under $CODEX_HOME/sessions/YYYY/MM/DD/, located newest-first by session_meta.cwd (local-time filenames, UTC payloads; /new rotates natively so clear/epoch identity is free). event_msg records drive prose and state (turn_aborted(reason=interrupted) → interrupted lifecycle); tool chips resolve by call_id; encrypted reasoning renders from summaries only. Reply = bracketed paste + paced Enter (queue-with-steer mid-turn); interrupt = single Esc, never Ctrl-C (immediate quit — worst case confirmed live). A curated 40-command catalog scraped verbatim from the live TUI popup.
  • Honest omissions (ADR-0026/0017) — no DeepLinker (no per-session web surface; rows fall back to tmux attach), no LiveSignals in v1 (0.133's stable hooks vocabulary is recorded in the compat record as the follow-up substrate), no ConnectingReporter.

Seam + SPA (generic, additive)

New AuthFlowDeviceCode kind, optional LoginCodeReporter capability, and ErrLoginCodeUnsupported sentinel in internal/provider; httpapi returns user_code from login/start for providers asserting the capability and maps the sentinel to 409; the SPA auth card gains a descriptor-driven device-code branch (verification link + prominent one-time code + SSE completion). Existing adapters and fakes compile untouched.

Conformance evidence

  • Tier 1: TestConformance/codex — all ten obligations pass via the #80 registration table.
  • Tier 2: committed compat record at internal/compat/codex/compat.md (§1–§10, provenance-tagged, version-tagged fixtures under testdata/), 13 hermetic pin tests plus LAB_COMPAT_LIVE=1 probes — both live probes pass against the real authenticated binary on this host.
  • Core neutrality: neutrality_test.go extended with the codex package exactly as claudecode; no codex coupling outside the adapter and the cmd/lab/main.go composition root.
  • Full verification: go build ./... (+ -tags ui), go test ./... green (the only failures are internal/tmuxx live-integration tests, untouched by this branch and environmental — the sandbox cannot start real tmux servers; CI provides them), golangci-lint 2.12.2 (CI's version): 0 issues, SPA eslint/prettier/vitest: 563/563.

ADR-0037 records the decisions and the rejected alternatives.

Closes #87

🤖 Generated with Claude Code

https://claude.ai/code/session_016NZQDuabyKgLnhqZWPQJWK

The second `AgentProvider` (id `codex`, `internal/provider/codex`), implementing issue #87's research-pinned spec — the issue body plus both amendments — to ADR-0036's two-tier conformance bar. Every fragile coupling is live-pinned against codex-cli 0.133.0: the Tier-2 spike sweep in the issue's Amendment 2 plus fresh probes from this branch (device-auth stdout, `codex debug models`, `login status` shapes, a live TUI slash-popup scrape, and a live validation that the trust config-append suppresses the first-run prompt). ### The adapter - **Spawn** — `codex --ask-for-approval never --sandbox danger-full-access -c project_doc_fallback_filenames=["AGENTS.local.md"] [--model M] -c model_reasoning_effort=E [prompt]`. Effort is always explicit (`codex exec` defaults to none); no `-c commit_attribution=""` (unknown key on 0.133, hard error under `--strict-config`; attribution is absent at source). Full access is evidence-based: workspace-write's three live-verified traps (network off, cwd-confined writes, `.git` mounted read-only) each break a lab session. - **Auth** — device-code flow per the maintainer amendment: `LoginStart` drives `codex login --device-auth` in the `lab-login-codex` session (ADR-0034), scrapes the verification URL + one-time code from the pane, and a single background poller forces `codex login status` until the login lands, then publishes `provider.auth.changed`. `LoginSubmitCode` returns the new typed `ErrLoginCodeUnsupported`; logout is status-decided with an `auth.json` removal escalation. - **Seeding** — `SeedWorkspace` writes nothing in-tree: a guarded, append-only `[projects."<worktree>"] trust_level = "trusted"` table in `config.toml` (the argv trust route is dead on 0.133 — live-refuted; the append validated live on a real spawn) and a marker-guarded `AGENTS.local.md` bridge block in the global `$CODEX_HOME/AGENTS.md` (a repo-tracked `AGENTS.md` silently skips the fallback-filenames leg — the trap ADR-0035 predicted). `NativeSkillDiscovery: false` (0.133 discovers skills globally only) so the ADR-0035 skills index applies. Incogni is a no-op by ground truth; defensive `ScrubPatterns` join the ADR-0033 union. - **Chat surface** — rollout JSONL under `$CODEX_HOME/sessions/YYYY/MM/DD/`, located newest-first by `session_meta.cwd` (local-time filenames, UTC payloads; `/new` rotates natively so clear/epoch identity is free). `event_msg` records drive prose and state (`turn_aborted(reason=interrupted)` → interrupted lifecycle); tool chips resolve by `call_id`; encrypted reasoning renders from summaries only. Reply = bracketed paste + paced Enter (queue-with-steer mid-turn); interrupt = single Esc, never Ctrl-C (immediate quit — worst case confirmed live). A curated 40-command catalog scraped verbatim from the live TUI popup. - **Honest omissions** (ADR-0026/0017) — no `DeepLinker` (no per-session web surface; rows fall back to tmux attach), no `LiveSignals` in v1 (0.133's stable hooks vocabulary is recorded in the compat record as the follow-up substrate), no `ConnectingReporter`. ### Seam + SPA (generic, additive) New `AuthFlowDeviceCode` kind, optional `LoginCodeReporter` capability, and `ErrLoginCodeUnsupported` sentinel in `internal/provider`; httpapi returns `user_code` from login/start for providers asserting the capability and maps the sentinel to 409; the SPA auth card gains a descriptor-driven device-code branch (verification link + prominent one-time code + SSE completion). Existing adapters and fakes compile untouched. ### Conformance evidence - Tier 1: `TestConformance/codex` — all ten obligations pass via the #80 registration table. - Tier 2: committed compat record at `internal/compat/codex/compat.md` (§1–§10, provenance-tagged, version-tagged fixtures under `testdata/`), 13 hermetic pin tests plus `LAB_COMPAT_LIVE=1` probes — both live probes pass against the real authenticated binary on this host. - Core neutrality: `neutrality_test.go` extended with the codex package exactly as claudecode; no codex coupling outside the adapter and the `cmd/lab/main.go` composition root. - Full verification: `go build ./...` (+ `-tags ui`), `go test ./...` green (the only failures are `internal/tmuxx` live-integration tests, untouched by this branch and environmental — the sandbox cannot start real tmux servers; CI provides them), golangci-lint 2.12.2 (CI's version): 0 issues, SPA eslint/prettier/vitest: 563/563. ADR-0037 records the decisions and the rejected alternatives. Closes #87 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016NZQDuabyKgLnhqZWPQJWK
The second AgentProvider (id codex, internal/provider/codex), built to
ADR-0036's two-tier bar with every fragile coupling live-pinned against
codex-cli 0.133.0 (issue #87's spike sweep plus this branch's re-probes):

- Spawn: --ask-for-approval never --sandbox danger-full-access (three
  live-verified workspace-write traps), argv-first -c config, effort always
  explicit, seed prompt as the trailing positional; no commit_attribution
  (unknown key on 0.133; attribution is absent at source).
- Seam (generic, additive): AuthFlowDeviceCode kind + LoginCodeReporter
  capability + ErrLoginCodeUnsupported sentinel; httpapi surfaces user_code
  on login/start and maps the sentinel to 409; the SPA auth card renders the
  device-code flow descriptor-driven (verification link + one-time code,
  completion via provider.auth.changed).
- Login: codex login --device-auth in the lab-login-codex session (ADR-0034),
  URL + code scraped from the pane, a single background status poller
  publishes provider.auth.changed; logout is status-decided with an
  auth.json removal escalation.
- Seeding: guarded [projects."<worktree>"] trust append to config.toml (the
  argv trust route is dead on 0.133, validated live) + a marker-guarded
  global AGENTS.md bridge; context file AGENTS.local.md double-pathed via
  project_doc_fallback_filenames; NativeSkillDiscovery=false (skills are
  global-only) so the ADR-0035 index applies; SeedWorkspace writes nothing
  in-tree; incogni is a no-op by ground truth, with defensive ScrubPatterns
  joining the ADR-0033 union.
- Chat: rollout JSONL located newest-first by session_meta.cwd (local-time
  filenames, UTC payloads; /new rotates natively), event_msg-driven prose
  and state incl. turn_aborted -> interrupted, call_id-resolved tool chips,
  encrypted reasoning rendered from summaries only; reply = bracketed paste
  + paced Enter; interrupt = single Esc, never Ctrl-C; a curated 40-command
  catalog scraped verbatim from the live TUI.
- Conformance: codex entry in the Tier-1 registration table (all ten
  obligations green); the Tier-2 compat record at internal/compat/codex/
  with version-tagged fixtures and LAB_COMPAT_LIVE probes (both pass against
  the real binary); core stays adapter-agnostic (the neutrality guard's
  exempt list gains the codex package).

ADR-0037 records the decisions.

Closes #87

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NZQDuabyKgLnhqZWPQJWK
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!91
No description provided.