Repository navigation
feat(zarvis): codex-oauth provider — ChatGPT subscription via OAuth - #46
Merged
Merged
Conversation
Adds the `codex-oauth:` model-spec prefix so users can opt into the
subscription-billed Codex backend (`chatgpt.com/backend-api/codex/responses`)
distinct from the platform `openai:` provider. No bare-name fallback —
opting into a different billing path requires the explicit prefix,
even for codex-specific model strings like `gpt-5-codex`.
This commit is scaffolding only:
* `provider::routing::Provider::CodexOauth` + `codex-oauth:` parser
* New `provider::codex_oauth` module exposing a `CodexOauth` struct
that satisfies the `LlmProvider` trait and returns a clear
`not yet implemented` error from `complete()`
* Match-arm coverage in `ResolvedModel::provider_name`,
`resolve_model_from_spec`, and `title_mode::provider_for`
(title-gen explicitly does not support codex-oauth — it picks
from openai/anthropic/ollama defaults)
* Routing tests for both directions: explicit `codex-oauth:` works,
bare `gpt-5-codex` does NOT silently route here
Subsequent commits will fill in:
- `~/.codex/auth.json` loader with refresh-token rotation
persistence (refresh tokens rotate per call; failing to persist
triggers `refresh_token_reused` and logs the user out)
- Request builder for the Responses-API-shaped body
(`instructions` + `input`, distinct from chat completions)
- SSE event parser for `response.output_text.delta` etc.
- Required-headers helper: `Authorization`, `ChatGPT-Account-ID`,
`OpenAI-Beta`, `originator: codex_cli_rs`, non-default User-Agent
(the default `reqwest/*` UA gets blocked by Cloudflare; verified
against `openai/codex` CLI hitting the same wall on Linux)
- Tool-use mapping (Responses tool shape differs from Chat Completions)
- Context window for codex-oauth models
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires up the credential half of the provider. The `complete()` call
still returns "implementation pending" but now does so AFTER:
- locating `auth.json` (`$CODEX_HOME` → `~/.codex/auth.json`),
- parsing the OAuth `tokens` block with actionable errors when the
user is in API-key mode or hasn't run `codex login`,
- refreshing against `https://auth.openai.com/oauth/token` when
`last_refresh` is missing or >24h old (modulo a 5-min leeway),
- persisting the rotated `refresh_token` atomically (tmp file
+ fsync + rename, matching codex CLI's
`codex-rs/login/src/auth/manager.rs` dance) so a crash during
rotation can't strand the user with `refresh_token_reused` on
the next start,
- and surfacing a clear "re-run `codex login`" diagnostic when the
server says `refresh_token_reused` / `refresh_token_invalidated`
instead of letting it look like a transient network failure.
reqwest client uses a non-default `codex_cli_rs/<ver>` User-Agent.
The default `reqwest/*` UA gets blocked at Cloudflare on the
`chatgpt.com/backend-api/...` path (verified against openai/codex
sources — affects even their own CLI on Linux). The cookie store
isn't wired yet because workspace reqwest doesn't have the `cookies`
feature enabled; documented in the constructor for if Cloudflare
challenges start firing.
In-process refresh is serialized via a tokio Mutex on the auth
state. Cross-process (multiple zarvis adapters refreshing at the
same instant) is still a possible race; documented as a known
limitation since the refresh window is narrow and file-locking has
its own concurrency edges.
Six new unit tests, all on the pure path-logic / file I/O surface:
load round-trip, missing-file error, missing-tokens hint, blank
tokens, atomic save, and needs_refresh decisioning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ming Implements the live half of the `codex-oauth` provider: builds the `POST https://chatgpt.com/backend-api/codex/responses` request body in Responses-API shape, sends it with the required Cloudflare-/ CLI-mimicking headers (`OpenAI-Beta: responses=experimental`, `originator: codex_cli_rs`, `ChatGPT-Account-ID`, custom UA), and streams the SSE response back into `ProviderTurn` (text deltas through the sink, accumulated tool calls in stream order, usage + stop reason from the terminal event). Notable design choices: * `instructions` is NOT auto-populated from the agent's `system` arg or from a bundled prompt. Operator MUST set `AGENTD_ZARVIS_CODEX_INSTRUCTIONS`; unset/empty produces a clear error that names the env var and points to where to copy a canonical prompt from (the upstream codex CLI's `gpt_5_codex_prompt.md`). Per the design decision: avoid shipping zarvis's prompt into the codex prompt-cache key and avoid silently mirroring CLI behavior the operator didn't opt into. * `Message` → Responses `input` items mapping is the long pole. AssistantToolCalls fans out into one `message` item (only when the pre-tool prose is non-empty) plus one `function_call` item per call. ToolResult becomes a `function_call_output` item. Tool spec uses the flatter Responses shape (no `function` wrapper, top-level `name`/`description`/`parameters`). * SSE event handling covers `response.output_text.delta` for streamed prose, `response.output_item.added` + `response.function_call_arguments.{delta,done}` for tool calls accumulated by `item_id`, and the `response.{completed,incomplete,failed}` terminal events for usage + stop reason. Other event kinds (reasoning summary deltas, raw items) are observed but not surfaced — follow-up. * Auth: the existing in-process Mutex around `AuthState` is held only across the optional refresh + token snapshot; the HTTP call streams without holding the lock so concurrent turns don't serialize. * Error mapping: 400 with overflow-shaped body becomes the typed `ContextOverflow` sentinel (the existing `parse_overflow` matcher already covers the wording variants we'd expect from this backend). 401 surfaces as "re-run `codex login`" so the agent loop doesn't retry forever. * `context_window_tokens` gets a `codex-oauth` arm at 400K (matches the gpt-5 family); the runtime overflow-learn path in `model_limits.rs` tightens it if the subscription tier enforces something lower. Four new unit tests on the request-body shape (pure JSON, no network): plain message turn, tool-spec emission, full tool-call/result round trip, and the no-empty-prose edge case. Total tests in adapter-zarvis: 81 → 93. Live integration smoke test is still pending — needs a real `~/.codex/auth.json` to exercise the full path; the test surface today pins the request/response JSON shape independently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…prompt by default
Two follow-ups from live testing against a real ChatGPT-account
subscription:
1) The ChatGPT-account backend rejects requests with `store: true`
(the public Responses API default) with
`400 "Store must be set to false"`. Codex CLI sets it explicitly
in `codex-rs/core/src/client.rs`; we now do too. Locked in by an
assertion in `build_body_emits_responses_message_shape`.
2) The `AGENTD_ZARVIS_CODEX_INSTRUCTIONS` env var was overcautious.
When live-testing, the obvious move was to set it to `"hi"` just
to satisfy the "non-empty" server check — friction without value.
Resolution order is now:
a) `AGENTD_ZARVIS_CODEX_INSTRUCTIONS` (operator explicit
override — useful for mirroring Codex CLI exactly by pasting
the upstream `gpt_5_codex_prompt.md`),
b) the agent's `system` arg (zarvis system prompt — same slot
every other provider already uses; this is the default
out-of-box experience),
c) error only when both are empty.
This makes `codex-oauth:` symmetric with `openai:` /
`anthropic:` / `ollama:` from the operator's perspective — no
special env-var setup unless they want CLI-exact behavior.
Also fixes a minor doc lie: the `_system` argument is no longer
intentionally ignored — it's the default `instructions` value.
New test `resolve_instructions_prefers_env_then_system` covers all
four branches (env wins, env-blank-falls-back, env-unset-uses-system,
both-empty-errors).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 19, 2026
3 tasks done
edwin-zvs
added a commit
that referenced
this pull request
May 19, 2026
Adds an optional `env` table to `[adapters.<name>]` so operators can set per-harness env defaults from `~/.config/agentd/config.toml` without needing to export them in the shell that launches the daemon. Closes the gap users hit after #46 — wanting to pin the zarvis default model without `export AGENTD_ZARVIS_MODEL=…` in their shell config. [adapters.zarvis] env = { AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5" } Precedence (lowest → highest): 1. `[adapters.<name>].env` — operator default in config.toml 2. `params.env` — per-session env from `agent new --env KEY=VAL` 3. daemon meta — AGENTD_SESSION_DATA_DIR / AGENTD_SESSION_KIND (always wins so adapters can rely on them being right) The merge is applied in both `SessionManager::create` and `SessionManager::respawn`. Persisted `start_params.env` still wins on respawn because it gets layered on top of `adapter_cfg.env` — the per-session CLI flag stays authoritative across daemon restarts. Two new tests pin the TOML parse + the absent-env-is-empty default. The inline TOML example comment in `config.rs` also got a `zarvis.env` snippet so users discover the field when they look at the bundled default config. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
edwin-zvs
added a commit
that referenced
this pull request
May 19, 2026
Resolved conflicts:
- crates/cli/src/ui.rs: took upstream's reveal-pin-on-real-drops algorithm
(#47, #49) and reapplied the SessionEvent::ContextCompacted arms in
format_event_body + short_event_label. Conflicts were only there
because cargo fmt churn from this branch touched lines near the
upstream rewrite.
- crates/adapter-zarvis/src/provider/codex_oauth.rs: new provider
introduced by #46 needed a Content::Summary arm matching the other
providers — emits the SUMMARY_WIRE_PREFIX'd text as a user input
message in the Responses-API shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
codex-oauth:provider so zarvis can run against the user's ChatGPT subscription, billed via the OAuth-backed Codex backend (the same endpoint Codex CLI uses). Distinct fromopenai:which stays on platform pay-as-you-go.Reads
~/.codex/auth.json(the filecodex loginwrites). Refresh tokens rotate; this code persists rotations atomically with the same tmp+fsync+rename dance Codex CLI uses, and surfacesrefresh_token_reused/refresh_token_invalidatedas actionable errors instead of silent retries.Why a separate provider?
Different host + path (
chatgpt.com/backend-api/codex/responses, notapi.openai.com/v1/responses), different auth (Bearer +ChatGPT-Account-ID, notx-api-key), different request body (Responses-API shape,instructions+inputitems, not Chat Completions), different model strings (gpt-5,gpt-5-codex,gpt-5-codex-mini), different cookie/UA fingerprint expectations (Cloudflare blocks defaultreqwest/*UA). Forcing all that throughopenai:would silently swap billing paths; explicit prefix keeps the choice visible.Three commits
22c0ac8codex-oauth:prefix in routing, stub provider that returns "not yet implemented", match-arm coverage inprovider_name,resolve_model_from_spec,title_mode.b166d6aauth.jsonloader, refresh againstauth.openai.com/oauth/token, atomic rotation persistence,needs_refreshheuristic.363b4f9build_responses_bodyfor the Responses-API JSON, SSE event streaming through theTextSink, tool-call accumulation byitem_id, terminal-event usage + stop reason,context_window_tokensentry.Operator-supplied
instructionsThe Codex backend rejects empty
instructions. We don't bundle the upstreamgpt_5_codex_prompt.mdby default (would silently mirror Codex CLI's behavior into zarvis sessions). Operator must exportAGENTD_ZARVIS_CODEX_INSTRUCTIONSwith either:Unset/empty → hard error at first
complete()call, with the env-var name + a pointer to where to copy a canonical prompt from.Known limitations / follow-ups
~/.codex/auth.json→ refresh → streaming response →ProviderTurn) hasn't been exercised against a real subscription account yet. Operator-tested in this PR.reqwestlacks thecookiesfeature). If Cloudflare challenges start firing we'll need to enable it.account_id: only thetokens.account_idfield is read today. Newer codex builds populate it directly; older builds left it blank and required parsing it from theid_tokenJWT. Add when we see it bite.Test plan
cargo test --workspace --test-threads=1— all green (zarvis: 81 → 93 tests).codex-oauth:parses; baregpt-5-codexstays onopenai:.needs_refreshdecisioning.~/.codex/auth.json— operator.🤖 Generated with Claude Code