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:
Teknium 2026-05-27 03:33:52 -07:00
parent 3e33e14335
commit 69dfcdcc15
2 changed files with 162 additions and 2 deletions

View file

@ -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
# =============================================================================