Skip to content

fix(auth): codex chat path falls back to credential_pool when singleton is empty - #33189

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-5bf34d29
May 27, 2026
Merged

fix(auth): codex chat path falls back to credential_pool when singleton is empty#33189
teknium1 merged 1 commit into
mainfrom
hermes/hermes-5bf34d29

Conversation

@teknium1

@teknium1 teknium1 commented May 27, 2026

Copy link
Copy Markdown
Contributor

Closes #32992.

Summary

The reporter (#32992) hit a bare HTTP 401 Missing Authentication header on hermes chat with the openai-codex provider, despite a valid access_token being present in credential_pool.openai-codex[0]. They explicitly noted: "The auxiliary client appears to correctly attach the header via _pool_runtime_api_key()OpenAI(api_key=...). The chat-command path uses a different code path that doesn't go through this flow."

That's correct — there's a real divergence:

  • Auxiliary path (_read_codex_access_token in agent/auxiliary_client.py): tries pool first via _select_pool_entry, falls back to singleton.
  • Chat path (resolve_codex_runtime_credentials in hermes_cli/auth.py): reads ONLY the singleton (providers.openai-codex.tokens) via _read_codex_tokens. If the singleton is empty, it raises AuthError (visible) — but if a stale-singleton-but-fresh-pool state somehow returned an empty string instead of raising, OpenAI(api_key="") would silently attach no Authorization header and produce exactly the 401 the reporter saw.

This PR closes the divergence: when _read_codex_tokens() raises AuthError, resolve_codex_runtime_credentials now scans credential_pool.openai-codex for the first usable entry (non-empty access_token, not currently in an exhaustion cooldown via last_error_reset_at). If found, it returns that token with source="credential_pool". If no usable pool entry exists, the original AuthError propagates — the existing contract is preserved.

Changes

  • hermes_cli/auth.py:
    • resolve_codex_runtime_credentials wraps _read_codex_tokens() in try/except AuthError. On AuthError, calls new _pool_codex_access_token() helper; falls back to the pool token when found, re-raises otherwise.
    • New _pool_codex_access_token() helper reads credential_pool.openai-codex from auth.json, skips entries with empty access_token or currently in cooldown, returns the first usable token.
  • tests/hermes_cli/test_auth_codex_provider.py: three new regression tests
    • empty singleton + healthy pool entry → pool token returned
    • pool fallback skips entries in cooldown window
    • empty singleton + empty/wedged pool → AuthError propagates (existing contract preserved)

Why this complements #33164

PR #33164 (merged earlier today, also from @konsisumer) addressed the write side: when re-auth lands fresh tokens in the singleton, mirror them to the pool. This PR addresses the read side: when the chat path queries the singleton and finds nothing, fall back to the pool. Together they keep the two stores in sync regardless of which one received the write.

Validation

  • tests/hermes_cli/test_auth_codex_provider.py → 23/23 passing (3 new + 20 existing)
  • Manual repro of the reporter's state (singleton empty, pool valid) now resolves the pool token correctly via resolve_runtime_provider(requested="openai-codex")

Infographic

codex-auth-read-path-unified

…on 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)
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-5bf34d29 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9493 on HEAD, 9493 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5013 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists labels May 27, 2026
@teknium1
teknium1 merged commit 69dfcdc into main May 27, 2026
26 checks passed
@teknium1
teknium1 deleted the hermes/hermes-5bf34d29 branch May 27, 2026 10:43
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 comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openai-codex chat path returns HTTP 401 'Missing Authentication header' despite valid pool credential (v0.13 + v0.14)

2 participants