fix(codex): the pane re-authenticates itself when the token is dead - #191
Open
defangdevs wants to merge 3 commits into
Open
fix(codex): the pane re-authenticates itself when the token is dead#191defangdevs wants to merge 3 commits into
defangdevs wants to merge 3 commits into
Conversation
A codex Remote Control pane on a box whose ChatGPT token had been invalidated printed the raw pairing error — HTTP status, cf-ray, JSON body — and offered "Press Enter to try again", which could not work: `codex login status` is a LOCAL check, so it kept reporting "Logged in using ChatGPT", the sign-in guard kept (correctly) declining, and every Enter reprinted the same 401. Only typing the undocumented `login` word got out. Pairing is where the rejection surfaces, so classify its failure instead of retrying blind: on an auth-shaped error (`token_invalidated`, `HTTP 401`, `invalid_grant`, "unauthorized", "sign in again") pair() returns 2 at once rather than burning three attempts over six seconds, and onboard() drops the dead credentials and re-runs the device flow by itself, printing the server's own message and not the transport guts. Details worth knowing: - The automatic sign-in happens once per cycle, guarded by a flag: `logout` is destructive, and a 401 a fresh token cannot cure (wrong account, revoked access) would otherwise spin the pane through logout/device-auth forever. The second rejection says so and stops; the flag clears on a successful pairing, so a token that expires hours later still gets one automatic recovery. - Failures that are NOT about auth keep the old behaviour exactly — the cold-start enrollment race and a dead network are retried three times, reported raw, and never cost the box its working credentials. - Shell globs, not grep: the agent unit's PATH carries coreutils but no gnugrep, and `grep: command not found` would have read as "not an auth failure" and silently restored the dead end. Same reason the reason-string extraction is parameter expansion. - `login` stays as a typed word: signing in as the wrong ACCOUNT produces no error to detect, and only the user knows about it. - relogin() now clears was_signed_in when the box ends up signed out (a device flow the user walked away from), so the health loop still notices sign-in whenever it does complete. The transition check keeps its short-circuit, so a signed-in pane spawns no `login status` every 5s. tests/sessions.nix drives the supervisor wrapper against a stub codex — a real server-side rejection is not producible in the sandbox — and asserts the three outcomes: invalidated token recovers with no keystroke and no HTTP status on screen, a still-rejected fresh sign-in reports once without looping, and a network failure retries without logging out. Fixes #187 (first item; the remotable-vs-local question there is separate). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FY7YhQDw21A83rdQtkmtAi
`as_agent(f"{wrapper} '' /tmp/stub-codex ...")` passed the empty host label
the way shell wants it — but a bare pair of single quotes CLOSES a Nix
indented string, so tests/sessions.nix stopped parsing and every VM test in
the run died at evaluation. Double quotes are an equally empty argument and
are inert in Nix. (The first attempt to explain that in a comment used the
same two characters, and broke it again.)
Caught by `nix-instantiate --parse tests/*.nix modules/*.nix flake.nix` plus
`nix eval .#checks.x86_64-linux.sessions.drvPath` — the test derivation now
EVALUATES natively on aarch64 even though running its VM needs x86, which is
the cheap check that was missing here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FY7YhQDw21A83rdQtkmtAi
`re.search(...).group(0)` is a `Match[str] | None`, and the nixos test driver runs `ty check` (plus `ruff check --select F`) over testScript as part of BUILDING the driver — so this failed the whole sessions test before any VM booted, with "Attribute `group` is not defined on `None`". Assign the match, assert it, then read it. Both gates now run natively here against the extracted script, with the driver-supplied names (machine, client, start_all, subtest) stubbed: `ty` and `ruff` are aarch64-available from the pinned nixpkgs even though the x86 driver is not buildable. Reintroducing the bare `.group(0)` reproduces CI's diagnostic exactly, and the committed file passes both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FY7YhQDw21A83rdQtkmtAi
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.
Closes the first item of #187.
The dead end
On a box whose ChatGPT token had been invalidated, the codex Remote Control pane showed this and could not get out of it:
codex login statusis a local check — it reports how~/.codex/auth.jsonwas minted, not whether the backend still honours it — so it kept saying "Logged in using ChatGPT",device_loginkept correctly declining (it deletesauth.jsonas it starts, so the guard has to stay), and pairing was the only thing that noticed. Enter re-ran exactly that. The single escape was typinglogin, a word the pane's footer mentions but nothing explains.The change
pair()now classifies its failure andonboard()acts on it:loginand at account access; no logout/device-auth spinThe automatic retry is behind a flag rather than unconditional (
logoutis destructive), and the flag clears on a successful pairing, so a token that expires hours into a pane's life still gets one automatic recovery.loginstays a typed word because the wrong-account case produces no error string to detect — only the user knows.Two things worth calling out:
grep. The agent unit's PATH carries coreutils but no gnugrep, sogrep: command not foundwould have read as "not an auth failure" and silently restored the dead end. Matching is shell globs; the reason string is extracted with parameter expansion.relogin()clearswas_signed_inwhen the box ends up signed out (a device flow the user walked away from), so the health loop still notices sign-in whenever it completes. The transition check keeps its short-circuit — a signed-in pane spawns nologin statusevery 5s.Testing
tests/sessions.nixdrives the supervisor wrapper directly against a stub codex (a real server-side rejection is not producible in the sandbox, anddaemon versionfails in the stub so the health loop ends) and asserts all three rows of that table, including call counts: one failedpairthen one that succeeds for the recovering case, exactly onelogoutfor the hopeless case, threepairs and zerologouts for the network case.Verified locally on this aarch64 box by extracting the generated wrapper and running the same four scenarios (plus logged-out, unchanged) under a PATH containing only coreutils — proof the grep-free matching works:
module-generated-up-to-date,multi-userandmodule-single-filepass natively; thesessionsVM test is x86_64-only, so CI runs it.Not in scope: the remotable-vs-"local" codex question in #187, which is a distinct change (the CLI and the settings form both hardcode
remoteControl: true) and is waiting on a clarification in the issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01FY7YhQDw21A83rdQtkmtAi