Skip to content

fix(auth): self-heal Codex refresh_token rotation by reimporting from ~/.codex - #45261

Closed
Kenmege wants to merge 2 commits into
NousResearch:mainfrom
Kenmege:fix/codex-refresh-token-self-heal
Closed

fix(auth): self-heal Codex refresh_token rotation by reimporting from ~/.codex#45261
Kenmege wants to merge 2 commits into
NousResearch:mainfrom
Kenmege:fix/codex-refresh-token-self-heal

Conversation

@Kenmege

@Kenmege Kenmege commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

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_oauth_pure → 401 invalid_grant / refresh_token_reused

_refresh_codex_auth_tokens propagates that as a hard AuthError, so the turn 401s with token_expired — even though ~/.codex/auth.json holds a perfectly fresh token. In practice this hits idle desktop profiles after a few days (conversation_loop logs Non-retryable client error … token_expired), and only a manual hermes 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 interactive hermes auth flow — never the refresh/401 path.

Fix

_refresh_codex_auth_tokens now 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.

  • Scoped to genuine staleness: gated on exc.relogin_required. Transient failures (e.g. 429 quota, where relogin_required=False and the stored token is still valid) re-raise unchanged — no spurious reimport.
  • Happy path untouched: a normal successful refresh is unchanged; ~/.codex is never consulted on success.
  • Complements fix: force relogin on 401/403 Codex token refresh failures #6525 (force relogin on 401/403): this attempts automatic recovery before surfacing a relogin prompt.

Tests

tests/hermes_cli/test_auth_codex_self_heal.py:

  • self-heal on invalid_grant
  • no self-heal on 429 quota (relogin_required=False)
  • re-raise when ~/.codex is absent/expired
  • happy-path-unchanged (no ~/.codex consult)

All green; existing tests/hermes_cli/test_auth_codex_provider.py suite unaffected (33 passed locally, 132 in a broader auth/credential sweep).

… ~/.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>
Copilot AI review requested due to automatic review settings June 12, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_tokens to import and persist Codex CLI tokens when a refresh fails with relogin_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.

Comment thread hermes_cli/auth.py
Comment on lines +3684 to +3686
imported = _import_codex_cli_tokens()
if not (imported and str(imported.get("access_token", "") or "").strip()):
raise
Comment thread hermes_cli/auth.py
Comment on lines +3691 to +3692
_save_codex_tokens(imported)
return dict(imported)
Comment thread hermes_cli/auth.py
Comment on lines +3687 to +3690
logger.info(
"Codex refresh_token rejected (%s); recovered from ~/.codex/auth.json.",
getattr(exc, "code", None) or "auth_error",
)
@liuhao1024

Copy link
Copy Markdown
Contributor

Verified — the Codex refresh_token self-heal logic is correct and well-gated.

Checked:

  1. relogin_required gate — only self-heals on permanent failures (invalid_grant, refresh_token_reused), NOT on transient ones (429 quota). The getattr(exc, "relogin_required", False) check with default False is safe for exceptions that don't set the attribute.
  2. Recovery path_import_codex_cli_tokens()_save_codex_tokens(imported)return dict(imported). Falls back to re-raising the original error if import returns None/empty. Three-state handling is correct: recover → raise → propagate.
  3. Test coverage — 4 tests cover: stale refresh_token → self-heal, rate-limit → no self-heal, relogin-required but no codex CLI token → re-raise, happy path → no import. All scenarios pass.
  4. Diff shows *** in test mocks — confirmed this is GitHub secret scanning redaction, not actual test values. CI checks pass (6 shards all green; check-attribution is CLA, not code).

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>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists area/auth Authentication, OAuth, credential pools provider/openai OpenAI / Codex Responses API labels Jun 13, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the contribution — this has already landed on main with the original contributor credited. This is an automated hermes-sweeper review.

  • bd66e7e3fbbc8f18d29fd800762a0264e8a64bbd (fix(auth): self-heal Codex refresh_token rotation by reimporting from ~/.codex) implements the relogin-required recovery path.
  • Current hermes_cli/auth.py:3568-3601 retries through _recover_codex_tokens_from_cli(...), preserves transient errors, and persists valid recovered credentials.
  • Current tests/hermes_cli/test_auth_codex_self_heal.py:23-208 contains the PR's regression scenarios and additional malformed-singleton recovery coverage.
  • The fix is contained in release tag v2026.6.19.

@teknium1 teknium1 closed this Jul 14, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists provider/openai OpenAI / Codex Responses API sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants