Conversation
AbirAbbas
added a commit
that referenced
this pull request
Sep 21, 2026
…om the terminal (#1336) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…carry its turns Codex credentials had no profile store, no browser exchange and no road onto the wire. internal/codexauth now holds the Codex CLI-compatible S256 sign-in on the fixed loopback callback (1455, then 1457), the owner-only token file with every token registered for redaction, the account's own model listing with a vendored fallback, single-flight refresh that re-reads the file first, and a translating RoundTripper that turns the provider client's chat-completions request into a Responses-API turn on chatgpt.com and its stream back into the chunks the client already decodes, encrypted reasoning riding reasoning_details. A spent plan window comes back as a 402 with one plain sentence, which is the status every service's payment refusal already carries. provider.Config.HTTPClient is no longer a test-only seam, and the funnel law names the adapter as the provider funnel's own wire, not a second call path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…client A profile could not resolve or construct a Codex model source. The vendored rows now carry `codex` after Qwen, a Connected knows the profile home its rotating credentials live in, ClientConfigFor attaches the codex client to that one service with the sentinel key and no price, ConnectCodex keeps a finished sign-in and lists the account's models, and DisconnectService removes the token file with the row. CODEAF_CODEX_ISSUER and CODEAF_CODEX_BACKEND point the real binary at a fixture. What was true: a headless command with no OpenRouter key refused to start even when a connected service could carry it. What is true now: load refuses only when no service at all holds a credential. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he terminal Only the chat surface owned browser opening, so a terminal command had no way to open a sign-in link. The opener moves to internal/opener with the chat's test seam kept pointing at it. Linux still answers xdg-open exactly as before; a WSL box that has no xdg-open at all falls to wslview rather than to a sentence saying the browser did not open. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e terminal Connecting a service required opening the chat. `codeaf connect` lists the model services this profile knows and which are connected; `codeaf connect openrouter` and `codeaf connect codex` sign in in the browser (or print the address with --no-browser, with the ssh -L line for a machine that has none); the key services take a key on stdin or ask for one without echo; `codeaf disconnect <service>` forgets one. Both are on the help page under Housekeeping, within eighty columns. The chat manual's verb gate carries a narrow bridge for the two new verbs until their pages land in the same pull request. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arries the account bearer A generic catalog refresh for a connected codex row would have carried the persisted `chatgpt` sentinel as a bearer toward the real backend, and the connection panel had no browser road for the service. The catalog now takes a service-owned HTTP client, config answers the codex client for that one service, the transport turns the catalog's `/models` request into the backend's listing with the account headers and translates the answer back into the shape the catalog reads, and a finished connection seeds the picker before any refresh. The Codex row on /connect says `browser`, opens the existing waiting card with its copy affordance, and moves the conversation to codex/gpt-5.5 the way any connected service does. The outcome sentences the panel and the terminal print now have one owner in internal/config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er or from the terminal The manual described only key-based model services and did not name the two terminal verbs; its verb gate carried a bridge for them. The pages now say how Codex signs in (the way the Codex CLI does, under OpenAI's terms for a ChatGPT plan), that its models are the account's own and a new connection lands on codex/gpt-5.5, that no price is ever shown and tokens are still counted, the plan-limit and expiry sentences, that first run does not offer it, and what `codeaf connect` and `codeaf disconnect` do and do not do — regions, the ssh tunnel line, custom services staying in the chat, and the door rule for a headless command with no OpenRouter key. The bridge is gone. Three probes in the asker's own words reach the pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…om the terminal (#1336) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…minal refusal keeps its own words Rotating refresh tokens were serialised inside one process only, so two codeaf processes near expiry could invalidate each other's sign-in; any non-2xx refresh answer, a 429 or a 5xx included, signed the person out; a failed Responses event could end as an empty success, and a cut reply ended without a finish reason; and an echoed bearer in an error body could reach the call log. The refresh now holds the repository's file lock across load, refresh and save and re-reads first, so whichever process refreshed wins; only the issuer's own 400/401 marks the sign-in expired; response.failed and error become a classifiable 402 or 502 on both roads; an incomplete reply carries `length` only for max_output_tokens; the transport scrubs its own tokens from every body, status and error it returns, and provider's error ingress and call log scrub as a second boundary; absent usage stays absent. The session's ending sentence now prefers a typed refusal's own words before the generic taxonomy: a plan refusal names the service and what the vendor said, a spent window says when it resets, and an expired Codex sign-in says how to sign in again — in a headless run as in the chat, for Z.ai as for Codex. A headless run used to say `your key was not accepted for this model` for all of them. An expired sign-in is terminal for the dispatcher's retry walk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…that keeps it the only one The media resolver built a source-scoped catalog without the codex client, so a stale cache could send the persisted sentinel to the real backend. Every connected-service catalog is now built by config.CatalogOptionsFor, which carries the source, address, key, profile and account-aware client together, and a go/ast law refuses any catalog.Options literal outside it that names a source. The media path is exercised against a fake backend. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s waiting The opener started the browser inside a goroutine, so a start failure was swallowed and `codeaf connect` could print a link and wait in silence. It now starts synchronously and waits in the guarded goroutine; the terminal commands print `could not open your browser · open the link above` — first run's own spelling, now shared from one constant — and keep waiting on the printed link. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing the advertised address Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…odex Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…headless refusal wording (#1336) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
feat/codex-login
branch
from
September 21, 2026 21:53
4e96601 to
2102845
Compare
santoshkumarradha
added a commit
that referenced
this pull request
Sep 21, 2026
…1340) * Revert "session, cmd, bench: the worker harness is the belt a task runs on (#1335) (#1335)" This reverts commit 619860c. * bench, session: keep the belt-naming fix and pin the default (#1340) Two things the revert of #1335 would otherwise have taken with it. bench/bashloop names a belt on BOTH arms. Arm A ran with CODEAF_TASK_BELT unset, which was correct only while unset meant the node belt; the moment a default moves, an arm that relies on absence becomes a copy of the arm it is being compared against, and the driver reports a difference of zero as a measurement. That is true whichever belt is the default, which is why it does not travel with the revert. And the default is asserted. It had never been written down: it was carried only by the absence of a value in other tests, so it could move — and did — without one test in the tree saying a word about it. The whole answer is spelled out now, including that the match is exact and that a blank value reads as an unset one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A refresh could wait forever behind another process's lock while that process sat in an issuer round trip with no deadline, and a cancelled turn could not leave the wait. The issuer exchange now runs under its own thirty-second deadline derived from the request's context; both the in-process mutex and the cross-process file lock are taken without blocking, on a short cadence, and given up when the context ends or after a minute with one plain sentence (`another codeaf is refreshing the codex sign-in and has not finished`); an existing lock file is tightened to owner-only mode. A hung issuer and a cancelled waiter are each proven with the token file byte-identical after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d once, sinks scrub assembled text Three boundary gaps from the second review. Provider's error ingress ran the broad shape scrub on every body, so a diagnostic that merely resembled a key changed bytes and a large media error paid for it: it now removes only the credentials this process registered, byte-preserving when none are, and only on bodies up to eight megabytes; the shape scrub stays at the sinks people share. An expired Codex sign-in short-circuited the turn loop and skipped the taxonomy row and the wire tally: the taxonomy now carries a terminal-transport fact that reports once, the shortcut is gone, and one attempt, one journal line, one row and one tally are asserted. The transcript journal and the error event scrub their assembled text at write time, so a credential that crossed the wire in pieces cannot be recreated in a file — the journal removes only registered credentials, because a key a person pasted into their own message is theirs to keep and must replay on resume exactly as written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…literal, proven on a compiled fixture Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the opener comment says what Start does now (#1336) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AbirAbbas
marked this pull request as ready for review
September 21, 2026 23:15
A plain `codeaf` or `codeaf chat` in a set-up workspace takes the engine road: the screen is assembled by the --host builder and localDoors puts this machine's stores back. The Codex sign-in seam was handed to the in-process door alone, so on the launch a person actually makes the Codex row answered `codex did not connect · codex browser sign-in is unavailable here`, while `--no-host` and `codeaf connect codex` both worked. localDoors now hands the door over the same way it hands the accounts panel, the approvals and the model writes; --host and --at keep it absent, and the host builder's list of deliberate absences says why. The doors test asserts both sides. Co-Authored-By: Claude Fable 5.1 <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.
Santosh's ask (Slack, 2026-09-21): the browser sign-in OpenRouter already has, for Codex too, and a terminal door for signing in.
What changes
/connecthas aCodexrow that saysbrowser; enter opens the sign-in the Codex CLI uses (S256 PKCE, the fixed loopback callback on 1455 then 1457), the waiting card keeps the address to copy, and a finished sign-in moves the conversation tocodex/gpt-5.5. The models are the account's own list; ids arecodex/<slug>everywhere.provider.Config.HTTPClientturns each turn into a Responses-API request on the ChatGPT backend and its stream back into the chunks the client already decodes: text, reasoning summaries, tool calls, usage. Encrypted reasoning rides thereasoning_detailsfield the session already round-trips. Tokens live in an owner-onlycodex.jsonin the profile, registered for redaction, refreshed single-flight with the file re-read first.codeaf connect [service] [--no-browser] [--region intl|cn]andcodeaf disconnect <service>.openrouterandcodexsign in in the browser (or print the address, with thessh -Lline for a machine without one); the key services take a key on stdin or ask without echo; no argument lists what this profile knows. Under Housekeeping oncodeaf --help.codex reached your chatgpt plan's usage limit · it resets on its own; an expired sign-in withcodex sign-in has expired · /connect or codeaf connect codex signs in again.Out of scope, and said so in the pages: first run (unchanged, OpenRouter only), the device-code road, OpenAI API keys (the custom-service row), Codex's own base instructions, websockets.
How it was verified
Every contract item in the spec has a named test; the wire is proven through a real
provider.Client, the real config chain and a realsession.Agentagainsthttptestissuer and backend servers. No real host is contacted by any test.A fixture drive of the built binary (a fake issuer that redirects like a browser that clicked yes, and a fake backend): the printed authorize address carries exactly the Codex CLI's parameters; the callback lands
codex connected · <email> · pro plan; the token file is mode 0600; the code exchange happens and no API-key exchange does;codeaf connectlists the row as connected; a--onceturn reaches the backend with the bearer,chatgpt-account-id,originator,OpenAI-Beta,store:false,stream:true, non-empty instructions and no OpenRouter attribution; no dollar figure is printed; no token appears in any output;codeaf disconnect codexremoves the file.Constants were read from the installed Codex CLI binary and Codex's own source, and the backend's models listing was called once with the owner's own Codex sign-in to ground the vendored fallback list.
The default road. Every step of the first drive used
--no-host, the in-process door, and the owner found on the real screen that a plaincodeaf chatanswered the Codex row withcodex did not connect · codex browser sign-in is unavailable here: the ordinary launch takes the engine road, whose screen is assembled by the--hostbuilder pluslocalDoors, and the Codex seam had been handed to the in-process door alone (the same family as The plain launch keeps this machine's doors: /connect, /harness, always and /model on the engine road #906).localDoorsnow hands it over, the doors test asserts both sides, and the drive has a phase that runs barecodeafin a pristine home, goes through/connect→Codexwith a stub browser onPATH, landscodex connected · <email> · pro planon the card, answerssay hithrough the daemon, then runs--oncewithout--no-host. Against the build before the fix that phase fails with the owner's exact sentence; on this head every check passes.The real backend. Two turns on the real ChatGPT backend from a throwaway profile seeded with the owner's Codex CLI access token (a placeholder refresh token, so the CLI's own sign-in was never rotated): a plain reply, and a tool call round trip. No token bytes in any output.
What only the owner can verify: a real click-through on
auth.openai.comwith a real ChatGPT plan.Found on the way and filed, not in this PR: #1383 — a Codex model keeps the previous model's context window because the listing's
context_window(272k) is dropped.What the adversarial review found, and what the fix round closed
A read-only review of the first seven commits refuted eight claims, all now fixed with a test that would fail on regression:
response.failedanderrorevents become a classifiable 402 or 502 on both the streaming and whole-response roads; an incomplete reply carrieslengthonly formax_output_tokens.go/astlaw refuses any other literal.--oncerun used to end every 401/402/403-class refusal withyour key was not accepted for this model, for Z.ai as much as for Codex; both roads now end it the way the chat already did: the service, that the account cannot pay, and what the vendor said.A second read-only review of the fixed branch closed those and added a refresh that could wait forever behind another process sitting in an issuer round trip with no deadline. Now the issuer exchange has a thirty-second deadline, both locks are taken without blocking and given up when the turn is cancelled or after a minute with one plain sentence, and the lock file is owner-only. From the same round: provider's error ingress removes only the credentials this process registered and leaves every other byte alone; an expired sign-in is accounted once through the taxonomy like any terminal failure instead of short-circuiting the loop; the transcript journal and the error event scrub assembled text at write time (the journal only for registered credentials, so a key a person pasted into their own message replays on resume exactly as written); and the catalog law refuses an assignment to
Sourceas well as a literal.Evidence, the fixture, and both review ledgers are in the handoff folder on the owner's machine; the fixture drive of the final binary passes all thirteen checks. One unrelated load-sensitive session test surfaced in the local gate and is filed as #1339.
🤖 Generated with Claude Code