mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(auth): codex chat path falls back to credential_pool when singleton is empty
Closes #32992. The chat path resolves Codex credentials via `resolve_codex_runtime_credentials` which only reads `providers.openai-codex.tokens` (the singleton). The auxiliary path uses `_read_codex_access_token` which checks the credential_pool first. For users whose tokens live only in the pool — manual seed, partial re-auth, restore from backup, or any state where the singleton is empty but the pool is healthy — the chat path raised AuthError or (worse, since OpenAI(api_key='') silently attaches no header) the wire saw HTTP 401 "Missing Authentication header" while the auxiliary path worked fine. This adds a pool fallback to `resolve_codex_runtime_credentials`: when the singleton has no usable access_token, scan `credential_pool.openai-codex` for the first entry that has a non-empty access_token and isn't in an exhaustion cooldown window (`last_error_reset_at` in the future). If found, return that token with `source="credential_pool"`. If no usable entry exists, the original AuthError propagates as before. Regression tests cover: - Empty singleton + healthy pool entry → pool token returned - Pool fallback skips entries currently in cooldown - Empty singleton + empty/wedged pool → AuthError propagates (existing contract preserved)
This commit is contained in:
parent
3e33e14335
commit
69dfcdcc15
2 changed files with 162 additions and 2 deletions
|
|
@ -3516,8 +3516,36 @@ def resolve_codex_runtime_credentials(
|
|||
refresh_if_expiring: bool = True,
|
||||
refresh_skew_seconds: int = CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
|
||||
) -> Dict[str, Any]:
|
||||
"""Resolve runtime credentials from Hermes's own Codex token store."""
|
||||
data = _read_codex_tokens()
|
||||
"""Resolve runtime credentials from Hermes's own Codex token store.
|
||||
|
||||
Falls back to the credential pool when the singleton (``providers.openai-codex.tokens``)
|
||||
has no usable access_token but the pool (``credential_pool.openai-codex``) does. This
|
||||
closes the divergence between the chat path (singleton-only via this function) and
|
||||
the auxiliary path (pool-first via ``_read_codex_access_token``). Without this
|
||||
fallback, a user whose tokens live only in the pool — for example after a manual
|
||||
pool seed, a partial re-auth, or pool-only restoration from a backup — gets a bare
|
||||
HTTP 401 ``Missing Authentication header`` from the wire instead of a usable
|
||||
credential. See issue #32992.
|
||||
"""
|
||||
try:
|
||||
data = _read_codex_tokens()
|
||||
except AuthError:
|
||||
pool_token = _pool_codex_access_token()
|
||||
if pool_token:
|
||||
base_url = (
|
||||
os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/")
|
||||
or DEFAULT_CODEX_BASE_URL
|
||||
)
|
||||
return {
|
||||
"provider": "openai-codex",
|
||||
"base_url": base_url,
|
||||
"api_key": pool_token,
|
||||
"source": "credential_pool",
|
||||
"last_refresh": None,
|
||||
"auth_mode": "chatgpt",
|
||||
}
|
||||
raise
|
||||
|
||||
tokens = dict(data["tokens"])
|
||||
access_token = str(tokens.get("access_token", "") or "").strip()
|
||||
refresh_timeout_seconds = float(os.getenv("HERMES_CODEX_REFRESH_TIMEOUT_SECONDS", "20"))
|
||||
|
|
@ -3555,6 +3583,46 @@ def resolve_codex_runtime_credentials(
|
|||
}
|
||||
|
||||
|
||||
def _pool_codex_access_token() -> str:
|
||||
"""Return the most-recent usable access_token from the openai-codex pool.
|
||||
|
||||
Used as a fallback by ``resolve_codex_runtime_credentials`` when the
|
||||
singleton has no creds. Reads ``credential_pool.openai-codex`` entries
|
||||
directly from auth.json and picks the first non-empty access_token,
|
||||
preferring entries that are not currently in an exhaustion cooldown.
|
||||
Returns ``""`` when no usable entry is found (caller handles by raising
|
||||
the original AuthError).
|
||||
"""
|
||||
try:
|
||||
with _auth_store_lock():
|
||||
auth_store = _load_auth_store()
|
||||
pool = auth_store.get("credential_pool")
|
||||
if not isinstance(pool, dict):
|
||||
return ""
|
||||
entries = pool.get("openai-codex")
|
||||
if not isinstance(entries, list):
|
||||
return ""
|
||||
|
||||
def _entry_usable(entry: Dict[str, Any]) -> bool:
|
||||
if not isinstance(entry, dict):
|
||||
return False
|
||||
token = entry.get("access_token")
|
||||
if not isinstance(token, str) or not token.strip():
|
||||
return False
|
||||
# Skip entries currently in an exhaustion cooldown window.
|
||||
reset_at = entry.get("last_error_reset_at")
|
||||
if isinstance(reset_at, (int, float)) and reset_at > time.time():
|
||||
return False
|
||||
return True
|
||||
|
||||
for entry in entries:
|
||||
if _entry_usable(entry):
|
||||
return str(entry.get("access_token", "")).strip()
|
||||
except Exception:
|
||||
logger.debug("Codex pool fallback lookup failed", exc_info=True)
|
||||
return ""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# xAI Grok OAuth — tokens stored in ~/.hermes/auth.json
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue