fix(codex): write rotated OAuth grant through to global root (multi-profile rotation race) - #48416
Conversation
…rofile rotation race) When a profile resolves an openai-codex grant from the global-root fallback (the profile has no own providers.openai-codex block) and the credential pool rotates that grant, the rotated refresh_token was written only to the PROFILE auth store. Because openai-codex rotates the refresh_token on every refresh, root was left holding a now-revoked refresh token — and every other profile reading the stale root grant later died with refresh_token_reused once its access token expired. Hermes already fixes the identical race for xAI OAuth (NousResearch#43589) via a write-through to the global root. This adds the equivalent for openai-codex: - _profile_has_own_codex_oauth_state / _write_through_codex_oauth_to_global_root in hermes_cli/auth.py, faithfully mirroring the xAI helpers (same best-effort error swallowing, same pytest seat belt that refuses to write the real ~/.hermes/auth.json under PYTEST_CURRENT_TEST). - In CredentialPool._sync_device_code_entry_to_auth_store, the openai-codex branch now detects the read-from-root case (profile-mode AND no own block) and, after the profile save, writes the rotated chain through to root too. The write-through is strictly best-effort — a failed root write never breaks the profile's own successful save. A profile that genuinely shadows root (has its own block) is left untouched; classic mode is a no-op. Adds tests/agent/test_codex_oauth_writethrough.py mirroring the xAI write-through test: write-through fires when the profile lacks its own block, is a no-op in classic mode, does not touch root when the profile shadows it, and swallows root-write errors without breaking the profile save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Maps the contributor email to the GitHub username so the contributor-attribution check (.github/workflows/contributor-check.yml) passes for this PR's Python changes.
|
merge conflicts This PR does not merge cleanly with the base branch. Please rebase or merge current Signed: GPT-5.5-medium in Codex |
|
Closing as moot. This deployment moved to native per-gateway independent Codex pools — each profile gets its own manual:device_code grants via separate device-code logins, with fill_first for primary->backup failover. That makes refresh-token write-through to global root unnecessary, and counterproductive: it would propagate one grant's rotated refresh token across stores, which is the shared-refresh-token-family stranding we were avoiding. Root cause was grants being copied across auth stores, not a missing write-through. Native multi-account support covers the multi-profile case cleanly, so this patch is retired. Thanks! |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Bug Fix — Looks Good
- Root cause addressed: When a profile uses the global-root fallback for openai-codex OAuth (no own providers.openai-codex block) and the grant rotates at runtime, the rotated state was only saved to the profile's auth store, not back to global root. Other profiles reading the stale global root grant would hit
refresh_token_reusederrors. - Correct fix: After saving the profile's auth store, write through the rotated state to global root when the profile was using the fallback (no own codex state)
- Mirrors xAI fix (#43589): Same write-through pattern applied for xai-oauth is now applied for openai-codex
- Best-effort write-through: Wrapped in try/except so a failed root write never breaks the profile's successful save
- New helpers:
_write_through_codex_oauth_to_global_root()and_profile_has_own_codex_oauth_state()keep the logic clean - No debug artifacts or secrets
Reviewed by Hermes Agent
What does this PR do?
Fixes a multi-profile OAuth rotation race for
openai-codex— the exact analog of the xAI OAuth race that #43589 documented and that was fixed by the write-through in #46614 (commit497352bc4).The race:
openai-codexrotates therefresh_tokenon every refresh. When a profile resolves a Codex grant from the global-rootauth.jsonfallback (it has no ownproviders.openai-codexblock) and the credential pool then rotates that grant, the rotated chain is written only to the profile's store — never back to root. Root is left holding a now-revoked refresh token, and every other profile reading the stale root grant dies withrefresh_token_reusedonce its access token expires.This PR adds the write-through to global root for
openai-codex, mirroring the accepted xAI fix 1:1.Related Issue
Fixes #48415
Direct analog of #43589 (xAI OAuth), whose write-through fix landed in #46614 /
497352bc4.Type of Change
Changes Made
hermes_cli/auth.py— two new helpers placed adjacent to and mirroring the xAI pair (_profile_has_own_xai_oauth_state/_write_through_xai_oauth_to_global_root):_profile_has_own_codex_oauth_state(auth_store)— distinguishes a profile that genuinely shadows the root grant (has its ownproviders.openai-codexblock) from one that only reads root via fallback._write_through_codex_oauth_to_global_root(state)— best-effort, TOCTOU-safe write-through that reuses_save_auth_storewith an explicit target path. Carries the same pytest seat belt that refuses to write the real~/.hermes/auth.jsonunderPYTEST_CURRENT_TEST. Classic mode (profile == root) is a no-op.agent/credential_pool.py— inCredentialPool._sync_device_code_entry_to_auth_store, theopenai-codexbranch now detects the read-from-root case (profile-mode and no own block) and, after the profile save, writes the rotated chain through to root. Strictly best-effort: a failed root write is swallowed and never breaks the profile's own successful save. Profiles that genuinely shadow root are untouched.scripts/release.py— adds the AUTHOR_MAP entry for the contributor email so the attribution check passes (same housekeeping fix(auth): resolve xAI OAuth credentials across profiles + write rotated tokens back to root #46614 did for its salvaged author).Why it's safe
providers.openai-codexblock (i.e. it genuinely read root via fallback). A profile that deliberately shadows root is never clobbered._global_auth_file_path()isNone), the profile save already hit root and the write-through returns early._save_auth_store(..., target_path), and the call-site structure are a faithful 1:1 mirror of the xAI fix already merged in fix(auth): resolve xAI OAuth credentials across profiles + write rotated tokens back to root #46614.How to Test
scripts/run_tests.sh tests/agent/test_codex_oauth_writethrough.py— the new suite (4 tests).providers.openai-codexblock (both read root via fallback), let one refresh its Codex grant, then let the other's access token expire — before this fix the second dies withrefresh_token_reused; after it, root carries the rotated chain and the second profile keeps working.Tested on macOS (Python 3.11).
Checklist
Code
fix(codex):,chore:)tests/agent/test_codex_oauth_writethrough.py4 passed;tests/agent/test_credential_pool.py+tests/hermes_cli/test_xai_oauth_writethrough.py82 passed)Documentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A_save_auth_storewriter, no new platform primitives)Tests
tests/agent/test_codex_oauth_writethrough.pymirrorstests/hermes_cli/test_xai_oauth_writethrough.pyand drives the real on-disk save path:providers.openai-codexblock;4 passed.tests/agent/test_credential_pool.py+tests/hermes_cli/test_xai_oauth_writethrough.pystay green (82 passed).🤖 Generated with Claude Code