mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-06 07:51:53 +00:00
fix(xai-oauth): quarantine terminal refresh errors so dead tokens are not replayed across sessions
When refresh_xai_oauth_pure raises a terminal error (HTTP 400/401/403, i.e. revoked or reused refresh token), _refresh_entry's existing race- recovery path re-syncs from auth.json and returns if another process has already rotated the tokens. If auth.json still holds the same stale token pair, the function fell through to _mark_exhausted — leaving the dead credentials in auth.json. On the next Hermes startup _seed_from_singletons re-seeded the pool from those stale tokens, causing the same failure loop on every session. Fix: after the auth.json re-sync check in the xAI-oauth error handler, detect terminal errors with the new _is_terminal_xai_oauth_refresh_error helper and apply a quarantine: - Clear access_token and refresh_token from providers["xai-oauth"]["tokens"] in auth.json so they are not re-seeded. - Write a last_auth_error entry for hermes doctor / auth status diagnostics. - Remove all loopback_pkce entries from the in-memory pool so the current session stops retrying with the dead credentials. Mirrors the identical quarantine already in place for Nous OAuth (c90556262). Closes the parity gap introduced whenc90556262added Nous-only terminal error handling without a corresponding xAI-oauth path.
This commit is contained in:
parent
226680500d
commit
5e40f83cb7
3 changed files with 200 additions and 1 deletions
|
|
@ -10,7 +10,7 @@ import time
|
|||
import uuid
|
||||
import re
|
||||
from dataclasses import dataclass, fields, replace
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from hermes_constants import OPENROUTER_BASE_URL
|
||||
|
|
@ -907,6 +907,50 @@ class CredentialPool:
|
|||
self._replace_entry(synced, updated)
|
||||
self._persist()
|
||||
return updated
|
||||
# Terminal error: auth.json has no newer tokens — the stored
|
||||
# refresh_token is dead. Clear it from auth.json so the next
|
||||
# session does not re-seed the same revoked credentials, and
|
||||
# remove all singleton-seeded (loopback_pkce) entries from the
|
||||
# in-memory pool. Mirrors the Nous quarantine path above.
|
||||
if auth_mod._is_terminal_xai_oauth_refresh_error(exc):
|
||||
logger.debug(
|
||||
"xAI OAuth refresh token is terminally invalid; clearing local token state"
|
||||
)
|
||||
try:
|
||||
with _auth_store_lock():
|
||||
auth_store = _load_auth_store()
|
||||
state = _load_provider_state(auth_store, "xai-oauth") or {}
|
||||
if isinstance(state, dict):
|
||||
tokens = state.get("tokens") or {}
|
||||
if isinstance(tokens, dict):
|
||||
store_refresh = str(tokens.get("refresh_token") or "").strip()
|
||||
entry_refresh = str(entry.refresh_token or "").strip()
|
||||
if not store_refresh or store_refresh == entry_refresh:
|
||||
tokens.pop("access_token", None)
|
||||
tokens.pop("refresh_token", None)
|
||||
state["tokens"] = tokens
|
||||
state["last_auth_error"] = {
|
||||
"provider": "xai-oauth",
|
||||
"code": getattr(exc, "code", "unknown"),
|
||||
"message": str(exc),
|
||||
"reason": "credential_pool_refresh_failure",
|
||||
"relogin_required": True,
|
||||
"at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
_save_provider_state(auth_store, "xai-oauth", state)
|
||||
_save_auth_store(auth_store)
|
||||
except Exception as clear_exc:
|
||||
logger.debug(
|
||||
"Failed to clear terminal xAI OAuth state: %s", clear_exc
|
||||
)
|
||||
self._entries = [
|
||||
item for item in self._entries
|
||||
if item.source != "loopback_pkce"
|
||||
]
|
||||
if self._current_id == entry.id:
|
||||
self._current_id = None
|
||||
self._persist()
|
||||
return None
|
||||
# For nous: another process may have consumed the refresh token
|
||||
# between our proactive sync and the HTTP call. Re-sync from
|
||||
# auth.json and adopt the fresh tokens if available.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue