fix(auth): self-heal Codex refresh_token rotation by reimporting from ~/.codex - #45261
fix(auth): self-heal Codex refresh_token rotation by reimporting from ~/.codex#45261Kenmege wants to merge 2 commits into
Conversation
… ~/.codex Hermes keeps its own copy of the Codex OAuth token per profile and at the top level, separate from the Codex CLI's ~/.codex/auth.json. OAuth refresh_tokens are single-use, so when the Codex CLI (or another Hermes process) rotates the shared token, the frozen copy's refresh_token goes stale and refresh_codex_oauth_pure fails with a relogin-required error (invalid_grant / refresh_token_reused / 401). Today that surfaces as a hard 401 on the turn — idle profiles and desktop sessions 401 "token_expired" until a manual re-auth — even though ~/.codex/auth.json holds a fresh token. _refresh_codex_auth_tokens now falls back to _import_codex_cli_tokens() (the canonical Codex CLI store) when the stored refresh_token is rejected, adopts and persists the fresh token, and lets the in-flight retry succeed. This complements PR NousResearch#6525 (force relogin on 401/403): we attempt automatic recovery before surfacing a relogin prompt. Transient failures (e.g. 429 quota, relogin_required=False) are never self-healed — the stored token is still valid there — so they re-raise unchanged, and the happy path is untouched. Adds tests/hermes_cli/test_auth_codex_self_heal.py covering: self-heal on invalid_grant, no self-heal on 429 quota, re-raise when ~/.codex is absent, and happy-path-unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a “self-heal” path for Codex OAuth refresh failures where Hermes can recover from cross-store refresh token rotation by re-importing the canonical token from the Codex CLI auth file, plus regression tests to prevent accidental retries/imports on transient failures.
Changes:
- Add fallback logic in
_refresh_codex_auth_tokensto import and persist Codex CLI tokens when a refresh fails withrelogin_required=True. - Add regression tests covering: stale refresh token recovery, no recovery on rate limit, re-raise when CLI token missing, and unchanged happy path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| hermes_cli/auth.py | Implements self-heal fallback on relogin-required refresh errors by importing Codex CLI tokens and persisting them to Hermes. |
| tests/hermes_cli/test_auth_codex_self_heal.py | Adds test coverage for the new self-heal behavior and key non-heal scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| imported = _import_codex_cli_tokens() | ||
| if not (imported and str(imported.get("access_token", "") or "").strip()): | ||
| raise |
| _save_codex_tokens(imported) | ||
| return dict(imported) |
| logger.info( | ||
| "Codex refresh_token rejected (%s); recovered from ~/.codex/auth.json.", | ||
| getattr(exc, "code", None) or "auth_error", | ||
| ) |
|
Verified — the Codex refresh_token self-heal logic is correct and well-gated. Checked:
CI: code checks passing. LGTM. |
…r email
Addresses PR review feedback:
- Validate refresh_token (not only access_token) before persisting the
re-imported Codex token, so a half-token payload can't silently break the
next refresh cycle.
- Make the recovery log path-agnostic ("Codex CLI auth.json") since
_import_codex_cli_tokens can read $CODEX_HOME, not only ~/.codex.
- Add regression test: relogin-required + imported token missing refresh_token
-> re-raise and persist nothing.
- Map kenmege@yahoo.com -> Kenmege in scripts/release.py AUTHOR_MAP
(fixes the check-attribution job).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the contribution — this has already landed on
|
Problem
Hermes keeps its own copy of the Codex OAuth token — per profile (
~/.hermes/profiles/<name>/auth.json) and at the top level (~/.hermes/auth.json) — separate from the Codex CLI's~/.codex/auth.json, deliberately, to avoid clobbering the CLI/VS Code session on refresh.OAuth refresh_tokens are single-use. When the Codex CLI (or another Hermes process) rotates the shared token, every idle Hermes copy keeps a now-consumed refresh_token. The next refresh on that copy fails:
_refresh_codex_auth_tokenspropagates that as a hardAuthError, so the turn 401s withtoken_expired— even though~/.codex/auth.jsonholds a perfectly fresh token. In practice this hits idle desktop profiles after a few days (conversation_looplogsNon-retryable client error … token_expired), and only a manualhermes auth/ periodic resync recovers it._import_codex_cli_tokens()already knows how to read the canonical fresh token from~/.codex/auth.json, but it's only wired into the interactivehermes authflow — never the refresh/401 path.Fix
_refresh_codex_auth_tokensnow self-heals: when the stored refresh_token is rejected with a relogin-required error, it reimports the canonical token from~/.codex/auth.json, persists it, and returns it so the in-flight retry succeeds.exc.relogin_required. Transient failures (e.g. 429 quota, whererelogin_required=Falseand the stored token is still valid) re-raise unchanged — no spurious reimport.~/.codexis never consulted on success.Tests
tests/hermes_cli/test_auth_codex_self_heal.py:invalid_grantrelogin_required=False)~/.codexis absent/expired~/.codexconsult)All green; existing
tests/hermes_cli/test_auth_codex_provider.pysuite unaffected (33 passed locally, 132 in a broader auth/credential sweep).