Skip to content

fix(security): prevent Hermes Codex OAuth refresh-token reuse - #34820

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

fix(security): prevent Hermes Codex OAuth refresh-token reuse#34820
cooper-oai wants to merge 2 commits into
NousResearch:mainfrom
cooper-oai:fix/codex-oauth-refresh-ownership-1-core

Conversation

@cooper-oai

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

Copy link
Copy Markdown

Summary

Part 1 of 8, highest priority.

  • stop importing Codex CLI credentials into Hermes
  • require a fresh Hermes-owned device-code login before refresh
  • serialize Codex refresh through the Hermes auth-store lock
  • send User-Agent: hermes-cli/<version> on refresh requests

Why

OpenAI Codex refresh tokens rotate after use. Importing another client's token family or refreshing the same Hermes family concurrently can submit an already-consumed refresh token.

This is the upstream replacement for #34644. The remaining hardening is split into dependent drafts so each review stays bounded.

Validation

  • focused auth, pool, profile, gateway, and model suite: 388 passed, 2 skipped
  • ruff, py_compile, Windows footgun scan, and git diff --check: clean

Validated locally on Linux with direct pytest because this checkout has no .venv, venv, or $HOME/.hermes/hermes-agent/venv.

Stack

Priority order, highest to lowest:

  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 targets upstream main. Parts 2-8 are fork-local dependent drafts so each diff stays reviewable; promote each part upstream after the part below it lands.

@egilewski

Copy link
Copy Markdown
Contributor

merge conflicts

This PR does not merge cleanly with the base branch. Please rebase or merge current main and resolve the conflicts if it's still relevant.

Signed: GPT-5.5-medium in Codex

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jun 21, 2026
@teknium1

teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for this — it correctly identified a real bug: the credential-pool Codex refresh path (CredentialPool._refresh_entry) POSTs the single-use refresh token without holding the cross-process auth-store lock across the whole sync → POST → write-back sequence, so two concurrent Hermes processes can both spend the same token and the loser gets refresh_token_reused. I reproduced that deterministically with two real subprocesses against a mock single-use token endpoint.

I've shipped the narrow fix as #56233, which wraps that Codex branch in the existing reentrant _auth_store_lock (the same extended-timeout pattern resolve_codex_runtime_credentials() already uses) and adds the User-Agent: hermes-cli/<version> header you flagged. You're credited in the PR body and commit.

I went with the minimal lock-serialization rather than the separate Codex auth-store partition (_codex_auth_store_lock / _codex_auth_file_path / _require_codex_refresh_owner) from this PR, because:

  • The headline "stop auto-importing ~/.codex/auth.json tokens" is already on current main_seed_from_singletons no longer imports Codex CLI tokens at pool-load (see the comment at agent/credential_pool.py: "Hermes owns its own Codex auth state — we do NOT auto-import from ~/.codex/auth.json").
  • Singleton refresh already re-reads-under-lock via the shared _auth_store_lock.
  • The refresh timeout is already configurable via HERMES_CODEX_REFRESH_TIMEOUT_SECONDS.

That left the pool-path refresh serialization as the one genuinely-missing piece, which the shared lock closes without a second auth-store partition to maintain. Closing in favor of #56233 — thank you for the find and the clear write-up.

@teknium1 teknium1 closed this Jul 1, 2026
teknium1 added a commit that referenced this pull request Jul 1, 2026
…ck (#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…ck (NousResearch#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (NousResearch#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
Jasper6439 pushed a commit to Jasper6439/hermes-agent that referenced this pull request Jul 5, 2026
…ck (NousResearch#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (NousResearch#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…ck (NousResearch#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (NousResearch#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…ck (NousResearch#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (NousResearch#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…ck (NousResearch#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (NousResearch#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…ck (NousResearch#56233)

The credential-pool Codex refresh path synced tokens from auth.json and
then POSTed the refresh_token to OpenAI's token endpoint without holding
the cross-process auth-store lock across the whole read->POST->write-back
sequence. Because Codex refresh tokens are single-use, two concurrent
Hermes processes could both adopt the same on-disk token and both POST
it; the loser got refresh_token_reused / invalid_grant.

Wrap the Codex OAuth branch of _refresh_entry in the existing shared
_auth_store_lock (reentrant, cross-process flock) using the same
extended-timeout pattern resolve_codex_runtime_credentials() already
uses. A waiting process now blocks on the lock and, once inside, the
in-lock re-sync picks up the rotated token the winner persisted and
skips its own POST. Also send User-Agent: hermes-cli/<version> on the
refresh request.

Credit @cooper-oai (NousResearch#34820) for identifying the concurrent-refresh
reuse race; this ships the narrow lock-serialization fix without the
separate Codex auth-store partition.
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 P1 High — major feature broken, no workaround sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants