Skip to content

fix(security): harden Codex OAuth refresh ownership - #34644

Closed
cooper-oai wants to merge 27 commits into
NousResearch:mainfrom
cooper-oai:fix/codex-oauth-refresh-ownership
Closed

fix(security): harden Codex OAuth refresh ownership#34644
cooper-oai wants to merge 27 commits into
NousResearch:mainfrom
cooper-oai:fix/codex-oauth-refresh-ownership

Conversation

@cooper-oai

@cooper-oai cooper-oai commented May 29, 2026

Copy link
Copy Markdown

Summary

  • stop importing Codex CLI credentials into Hermes and require a Hermes-owned device-code login
  • serialize Codex OAuth refresh transactions across singleton, credential-pool, process, and named-profile boundaries
  • store profile-mode Codex OAuth state in one canonical root auth.json, while leaving other providers and independent manual Codex entries profile-local
  • preserve newer shared and independent manual Codex rows during stale root and profile pool flushes without resurrecting removed rows
  • sanitize final merged Codex pool payloads and prune stale borrowed rows in classic and named-profile stores
  • keep manual Codex OAuth rows independent during canonical reauth and quarantine only the failed family
  • clear canonical provider state atomically when the shared Codex row is removed
  • remove refresh-linked legacy aliases from the root and all named profiles when the canonical family is removed
  • remove refresh-linked legacy aliases, including recorded superseded families, from sibling profiles when the canonical family is logged out
  • remove refresh-linked legacy aliases from sibling profiles when terminal refresh quarantine invalidates the canonical family
  • distinguish shared and profile-local Codex rows by ID and source during in-memory routing and persistence
  • reject quarantined DEAD Codex rows when resolving the access-token fallback
  • honor pre-migration profile-local Codex suppression markers and filter stale suppressed shared rows before use
  • assign durable IDs to legacy Codex pool rows under the shared lock before exposing them
  • continue canonical logout when a sibling-profile alias cleanup lock or I/O operation fails
  • skip busy sibling profiles immediately during best-effort legacy-alias migration so durable refresh persistence does not wait behind unrelated profile locks
  • let shared Codex-store readers wait through the configured refresh window so a slow successful refresh in another profile does not surface as a lock timeout
  • isolate colliding merged-row lease accounting by (id, source) and reject ambiguous string-ID removal targets
  • mark the current credential in hermes auth list by (id, source) so colliding shared and profile-local IDs do not both appear active
  • return a clean fallback directly when terminal quarantine removes the failed current row, so reactive recovery cannot attach the original auth error to an unrelated credential
  • keep shared-row round-robin ordering profile-local without copying shared OAuth tokens
  • clear stale matching active-provider markers during shared logout and removal
  • migrate refresh-linked legacy manual aliases and reject surviving superseded aliases before refresh POST
  • route still-linked legacy aliases through canonical save and quarantine paths even when their cached access tokens are stale
  • deny media delivery of the canonical root credential store from named profiles
  • invalidate named-profile Codex model caches when the canonical root credential store changes
  • send User-Agent: hermes-cli/<version> on refresh requests
  • direct terminal Codex refresh guidance to a fresh Hermes-owned device-code login rather than the independent Codex CLI session
  • refuse to spend ambiguous pre-upgrade tokens until a fresh Hermes login claims the canonical token family

Why

OpenAI Codex OAuth refresh tokens rotate after use. Copying a Codex CLI refresh token into Hermes or refreshing the same Hermes token concurrently can submit an already-consumed token to the token endpoint.

The refresh transaction must hold one lock across:

  1. reading the latest durable credential
  2. deciding whether refresh is still needed
  3. submitting the refresh token
  4. persisting the rotated credential

The existing pool path synchronized before and after refresh but released the auth-store lock during the network request. Named profiles also used separate auth locks while reading fallback Codex state from the global root. Both left reuse windows. Later snapshot writes could also replay stale shared or independent manual rows after another process refreshed or removed them. Canonical reauth and terminal quarantine could also overwrite or remove independent families.

This is security-sensitive auth hardening. No live credentials or OpenAI auth endpoints were used during validation.

Profile migration

Named profiles now share one canonical Hermes-owned Codex token family at the Hermes root. Existing credentials without the new ownership marker are intentionally not refreshed in either classic or profile mode: users must run hermes model, choose OpenAI Codex, and complete one fresh login so Hermes can claim an unambiguous token family.

Relationship to #34121

This includes the profile-sharing intent from #34121 and extends it:

  • the shared lock spans the token POST and durable persistence
  • singleton refresh uses the same canonical store and lock
  • manual Codex OAuth pool entries also serialize by persisted entry id
  • root and profile pool flushes cannot overwrite newer or removed shared and independent manual rows
  • final merged Codex pool writes cannot preserve raw borrowed secrets or restore pruned borrowed rows
  • canonical reauth cannot overwrite independent manual OAuth rows
  • terminal failures and explicit removals cannot quarantine or resurrect an unrelated token family
  • explicit canonical removal deletes refresh-linked aliases from the root and all named profiles
  • logout deletes current and recorded superseded refresh-linked aliases from sibling profiles while preserving independent rows
  • terminal canonical quarantine deletes refresh-linked aliases from sibling profiles while preserving independent rows
  • profile-local refresh cannot adopt a colliding shared-row ID
  • shared removal and aged DEAD pruning cannot delete an independent row with a colliding ID
  • runtime fallback cannot return a quarantined DEAD access token
  • pre-migration profile-local suppressions continue to hide the canonical row until explicit re-link
  • classic and profile reads filter stale suppressed shared rows before runtime use
  • identity-less legacy rows receive durable IDs before selection, removal, or stale-snapshot merge
  • one busy or malformed sibling profile cannot block canonical logout or cleanup of healthy siblings
  • one busy sibling profile cannot stall canonical token persistence during best-effort linked-alias migration
  • shared-store readers wait long enough for another profile's valid slow refresh to commit
  • colliding shared and profile-local row IDs retain independent leases and require numeric removal targeting
  • hermes auth list uses the same composite identity as runtime routing when displaying the active row
  • terminal quarantine returns a surviving clean fallback directly instead of letting reactive recovery quarantine it with the original request failure
  • shared status is canonical while round-robin ordering remains profile-relative
  • superseded canonical refresh tokens are tracked by bounded SHA-256 fingerprints so stale legacy aliases fail locally before token-endpoint use
  • refresh-linked legacy aliases refresh through the canonical family and quarantine together even when cached access tokens differ, while independent manual accounts remain isolated
  • named-profile media delivery and Codex model-cache consumers observe the canonical root store
  • terminal refresh guidance sends users through a fresh Hermes-owned login instead of the independent Codex CLI session
  • pre-upgrade classic and profile credentials fail closed until a fresh login

Validation

  • python3 -m pytest -q tests/hermes_cli/test_auth_profile_fallback.py tests/hermes_cli/test_auth_codex_provider.py tests/agent/test_credential_pool.py tests/hermes_cli/test_auth_commands.py tests/hermes_cli/test_codex_cli_model_picker.py tests/hermes_cli/test_doctor.py tests/hermes_cli/test_codex_models.py tests/hermes_cli/test_status.py tests/hermes_cli/test_status_model_provider.py tests/gateway/test_platform_base.py
    • 448 passed, 2 skipped
  • python3 -m pytest -q tests/hermes_cli/test_auth*.py tests/hermes_cli/test_web_oauth_dispatch.py
    • 369 passed
  • python3 -m pytest -q tests/run_agent/test_codex_xai_oauth_recovery.py tests/agent/test_auxiliary_client.py tests/hermes_cli/test_proxy.py
    • 263 passed
  • expanded auth, pool, runtime, proxy, media, routing, and interrupt matrix
    • 555 passed
  • python3 -m pytest -q tests/tools/test_delegate.py
    • 135 passed
  • repeated thirty-three localhost refresh-race, stale-snapshot, and borrowed-row regressions ten times
    • classic cross-process pool refresh
    • named-profile cross-process pool refresh
    • manual OAuth pool refresh
    • named-profile local manual OAuth pool refresh
    • pool versus forced singleton refresh
    • classic stale add after manual-row deletion
    • named-profile stale add after manual-row deletion
    • atomic shared-row removal versus stale profile add
    • manual terminal refresh failure preserving the shared family
    • classic shared-row removal preserving newer manual rows
    • named-profile shared-row removal preserving newer manual rows
    • shared terminal quarantine preserving newer manual rows
    • profile-local shared-row round-robin ordering
    • stale legacy alias rejection before refresh POST
    • linked legacy alias refresh through canonical state
    • stale-access linked alias quarantine with its revoked canonical family while preserving the selected clean fallback
    • unclaimed pool-only canonical row quarantine before refresh POST
    • explicit canonical removal deleting root and sibling-profile linked aliases
    • stale borrowed Codex row pruning in classic and named-profile stores
    • logout deleting linked aliases from sibling profiles
    • profile-local refresh ignoring a colliding shared-row ID
    • runtime fallback ignoring quarantined DEAD rows
    • shared removal preserving a colliding independent manual row
    • aged DEAD local-row pruning preserving a colliding healthy shared row
    • terminal quarantine deleting linked aliases from sibling profiles
    • pre-migration profile-local suppression hiding the canonical shared row until re-link
    • identity-less legacy Codex rows receiving durable IDs before merged removal
    • classic reads hiding stale suppressed canonical rows while preserving independent rows
    • canonical logout continuing after one sibling alias-cleanup lock failure
    • canonical refresh persistence skipping one busy sibling alias-migration lock without delaying healthy siblings
    • refresh-aware shared-store readers waiting through the configured token-endpoint window
    • colliding shared and local row IDs retaining independent leases and rejecting ambiguous string targeting
    • result: 330/330 scenarios passed with zero duplicate initial refresh-token submissions, stale-row resurrection, cross-profile ordering leaks, superseded alias POSTs, unclaimed canonical POSTs, refreshable aliases left behind after canonical quarantine, linked aliases surviving canonical removal, logout, or sibling-profile terminal quarantine, original auth failures attributed to surviving clean fallbacks, quarantined DEAD access-token fallback, stale borrowed rows restored by shared-store merges, cross-row deletion or pruning through colliding IDs, suppressed canonical rows exposed after removal, identity-less removed rows restored after reload, sibling cleanup failure blocking canonical logout, busy sibling migration locks delaying canonical persistence, shared-store readers timing out before valid slow refreshes commit, colliding-row lease aliasing, ambiguous string-ID removal, or profile-local refreshes routed through colliding shared rows
  • ruff check hermes_cli/auth.py hermes_cli/models.py gateway/platforms/base.py agent/credential_pool.py agent/credential_sources.py tests/hermes_cli/test_auth_codex_provider.py tests/hermes_cli/test_auth_profile_fallback.py tests/hermes_cli/test_auth_commands.py tests/hermes_cli/test_codex_cli_model_picker.py tests/agent/test_credential_pool.py tests/gateway/test_platform_base.py
  • python3 scripts/check-windows-footguns.py hermes_cli/auth.py hermes_cli/models.py gateway/platforms/base.py agent/credential_pool.py agent/credential_sources.py tests/hermes_cli/test_auth_codex_provider.py tests/hermes_cli/test_auth_profile_fallback.py tests/hermes_cli/test_auth_commands.py tests/hermes_cli/test_codex_cli_model_picker.py tests/agent/test_credential_pool.py tests/gateway/test_platform_base.py
  • python3 -m py_compile hermes_cli/auth.py hermes_cli/models.py gateway/platforms/base.py agent/credential_pool.py agent/credential_sources.py
  • git diff --check

Validated locally on Linux. scripts/run_tests.sh could not start in this checkout because .venv, venv, and $HOME/.hermes/hermes-agent/venv are absent. The direct pytest commands above used the available environment.

@cooper-oai
cooper-oai marked this pull request as ready for review May 29, 2026 13:40
@cooper-oai

Copy link
Copy Markdown
Author

@codex review

@alt-glitch alt-glitch added type/security Security vulnerability or hardening area/auth Authentication, OAuth, credential pools provider/openai OpenAI / Codex Responses API codex comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists labels May 29, 2026
@cooper-oai

Copy link
Copy Markdown
Author

Local PR readiness review is clean on e1b9053cdbe7ba544691268b103fd7410145b19b against origin/main@5641ae646997e61a7c88a5f66491f301ed876fa9.

Validation:

  • PR-focused auth, pool, profile, gateway, and model suite: 448 passed, 2 skipped
  • auth-wide suite: 369 passed
  • expanded auth, pool, runtime, proxy, media, routing, and interrupt matrix: 555 passed
  • delegate lease round-trip suite: 135 passed
  • repeated localhost chaos suite: 330/330 selected scenarios passed
  • ruff, py_compile, Windows footgun scan, and git diff --check: clean

Review ledger:

  • resolved canonical store, lock-span, stale snapshot, cooldown, profile cleanup, ownership migration, user-agent, manual-family isolation, atomic removal, profile-relative ordering, legacy-alias migration, linked-alias canonical refresh, refresh-family classification, shared-store delivery/cache, classic ownership fail-closed, explicit linked-family cleanup, borrowed-row merge sanitization, sibling logout cleanup, composite in-memory routing, DEAD fallback rejection, sibling terminal quarantine, composite persistence, superseded-alias logout, legacy profile suppression, identity-less row migration, sibling-cleanup isolation, composite lease accounting, ambiguity-safe removal targeting, linked-alias fallback recovery, non-blocking best-effort sibling migration, refresh-aware shared-store reads, Hermes-owned relogin guidance, and composite auth-list marker findings
  • GitHub Connector review was requested earlier but timed out/unacknowledged before the explicit switch to local mode; it was not used as the clean signal
  • strict local backend returned clean on the current head

@cooper-oai

Copy link
Copy Markdown
Author

Superseded by a priority-ordered stack so each review stays bounded:

  1. NousResearch/hermes-agent#34820 — prevent Hermes Codex OAuth refresh-token reuse
  2. cooper-oai/hermes-agent#1 — share canonical Codex state across profiles
  3. cooper-oai/hermes-agent#2 — reconcile stale Codex pool snapshots
  4. cooper-oai/hermes-agent#3 — migrate linked Codex aliases safely
  5. cooper-oai/hermes-agent#4 — guard shared Codex store consumers
  6. cooper-oai/hermes-agent#5 — quarantine Codex families by composite identity
  7. cooper-oai/hermes-agent#6 — harden Codex migration and fallback routing
  8. cooper-oai/hermes-agent#7 — avoid Codex refresh contention and fix guidance

Part 1 contains the highest-priority refresh-token reuse prevention and targets upstream main. Parts 2-8 are dependent fork-local drafts that can be promoted upstream in order after the preceding slice lands.

@cooper-oai cooper-oai closed this May 29, 2026
@cooper-oai

Copy link
Copy Markdown
Author

Superseded by a priority-ordered stack so each review stays bounded:

  1. NousResearch/hermes-agent#34820 — prevent Hermes Codex OAuth refresh-token reuse
  2. cooper-oai/hermes-agent#1 — share canonical Codex state across profiles
  3. cooper-oai/hermes-agent#2 — reconcile stale Codex pool snapshots
  4. cooper-oai/hermes-agent#3 — migrate linked Codex aliases safely
  5. cooper-oai/hermes-agent#4 — guard shared Codex store consumers
  6. cooper-oai/hermes-agent#5 — quarantine Codex families by composite identity
  7. cooper-oai/hermes-agent#6 — harden Codex migration and fallback routing
  8. cooper-oai/hermes-agent#7 — avoid Codex refresh contention and fix guidance

Part 1 contains the highest-priority refresh-token reuse prevention and targets upstream main. Parts 2-8 are dependent fork-local drafts that can be promoted upstream in order after the preceding slice lands.

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 codex comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists provider/openai OpenAI / Codex Responses API type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants