# NousResearch/hermes-agent · Codex OAuth credential pool sync and lifecycle fixes

## Open now

- Nothing open on GitHub.

These issues and pull requests address stale credentials, pool key mismatches, auth.json synchronization, and race conditions in the Codex OAuth authentication flow. Several entries also touch re-authorization paths and persistence alignment for device-code and manual auth methods.

## TL;DR

- 5 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #33744. Verify it addresses your case.
- **Recommended:** read PR #33744 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-33744`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.27`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #33538: openai-codex OAuth reauthorization can leave stale credential; auth add workaround required
- #42712: \[Bug\]: hermes auth add openai-codex does not go to Oauth
- #43461: \[Bug\]: OpenAI Codex usage completely Broken/Being Treated as Custom API

## Prior & competing PRs

- **#33744: fix(auth): sync manual:device_code Codex pool entries on re-auth**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/auth.py`, `tests/hermes_cli/test_auth_codex_provider.py`
  - fix hunks:
  - `hermes_cli/auth.py`

    ```diff
    @@ -3312,16 +3312,38 @@ def _sync_codex_pool_entries(
         tokens: [redacted:secret-assignment] str],
         last_refresh: Optional[str],
     ) -> None:
    -    """Mirror a fresh Codex re-auth into the credential_pool singleton entries.
    +    """Mirror a fresh Codex re-auth into the credential_pool OAuth entries.
     
         The runtime selects credentials from ``credential_pool.openai-codex``, not
         from ``providers.openai-codex.tokens``.  A re-auth invalidates the prior
    -    OAuth pair server-side, but the pool's ``device_code`` entry keeps holding
    -    the now-consumed refresh token plus any stale error markers — so the next
    -    request spends a dead token and gets a 401 ``token_invalidated``.  Update
    -    the singleton-seeded entries in lockstep with the provider tokens and clear
    -    the error state so the fresh credentials take effect immediately.  Manual
    -    (``manual:*``) entries are independent credentials and are left untouched.
    +    OAuth pair server-side, but pool entries keep holding the now-consumed
    +    refresh token plus any stale error markers — so the next request spends a
    +    dead token and gets a 401 ``token_invalidated``.
    +
    +    What gets refreshed:
    +
    +    * ``device_code`` — the singleton-seeded entry written by the device-code
    +      OAuth flow when the user logged in via ``hermes setup`` / the model
    +      picker.  Always synced with the fresh tokens.
    +    * ``manual:device_code`` — entries created by ``hermes auth add openai-codex``
    +      that use the same device-code OAuth mechanism.  An interactive re-auth
    +      proves the user owns the ChatGPT account, so it is safe (and expected)
    +      to refresh these entries too.  Without this, a user who once ran the
    +      ``hermes auth add`` workaround for #33000 would silently leave that
    +      manual entry stale on every subsequent re-auth, recreating the issue
    +      reported in #33538.
    +
    +    What does NOT get refreshed:
    +
    +    * ``manual:api_key`` and any other non-device-code manual sources — those
    +      are independent credentials (an explicit API key, a different ChatGPT
    +      account, etc.) and must not be overwritten by a single re-auth.
    +
    +    Error markers (``last_status``, ``last_error_*``) are also cleared on
    +    every device-code-backed entry — even those whose tokens we did not
    +    rewrite — so that an interactive re-auth gives every relevant pool entry
    +    a fresh selection chance instead of leaving them marked unhealthy from a
    +    pre-re-auth 401.
         """
         access_token = [redacted:secret-assignment]"access_token")
         if not access_token:
    ```

    ```diff
    @@ -3333,8 +3355,15 @@ def _sync_codex_pool_entries(
         entries = pool.get("openai-codex")
         if not isinstance(entries, list):
             return
    +    # Sources whose tokens should be rewritten by a fresh Codex device-code
    +    # OAuth re-auth.  ``manual:api_key`` and unknown sources are intentionally
    +    # excluded — they represent independent credentials.
    +    REFRESHABLE_SOURCES = {"device_code", "manual:device_code"}
         for entry in entries:
    -        if not isinstance(entry, dict) or entry.get("source") != "device_code":
    +        if not isinstance(entry, dict):
    +            continue
    +        source = entry.get("source")
    +        if source not in REFRESHABLE_SOURCES:
                 continue
             entry["access_token"] = access_token
             if refresh_token:
    ```
  - `tests/hermes_cli/test_auth_codex_provider.py`

    ```diff
    @@ -303,6 +303,88 @@ def test_save_codex_tokens_syncs_credential_pool(tmp_path, monkeypatch):
         assert auth["providers"]["openai-codex"]["tokens"]["access_token"] == "new-at"
     
     
    +def test_save_codex_tokens_syncs_manual_device_code_entries(tmp_path, monkeypatch):
    +    """Re-auth must also refresh ``manual:device_code`` pool entries.
    +
    +    Regression for #33538: a user who hit #33000 before the #33164 fix landed
    +    would have run ``hermes auth add openai-codex`` as a workaround, leaving
    +    a pool entry with ``source="manual:device_code"``.  On every subsequent
    +    re-auth via setup/model picker, the singleton-seeded ``device_code`` entry
    +    got refreshed but the ``manual:device_code`` entry stayed stale, recreating
    +    the same 401 token_invalidated symptom that #33164 was supposed to fix.
    +
    +    An interactive Codex device-code re-auth proves the user owns the ChatGPT
    +    account, so it is safe to refresh every device-code-backed entry in the
    +    pool — but NOT independent ``manual:api_key`` entries (separate accounts /
    +    explicit API keys).
    +    """
    +    hermes_home = tmp_path / "hermes"
    +    hermes_home.mkdir(parents=True, exist_ok=True)
    +    (hermes_home / "auth.json").write_text(json.dumps({
    +        "version": 1,
    +        "providers": {
    +            "openai-codex": {
    +                "tokens": {"access_token": "old-at", "refresh_token": "old-rt"},
    +                "last_refresh": "2026-01-01T00:00:00Z",
    +                "auth_mode": "chatgpt",
    +            },
    +        },
    +        "credential_pool": {
    +            "openai-codex": [
    +                {
    +                    "id": "seeded",
    +                    "source": "device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "old-at",
    +                    "refresh_token": "old-rt",
    +                },
    +                {
    +                    "id": "auth-add",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "stale-manual-at",
    +                    "refresh_token": "stale-manual-rt",
    +                    "last_status": "exhausted",
    +                    "last_error_code": 401,
    +                    "last_error_reason": "token_invalidated",
    +                },
    +                {
    +                    "id": "api-key",
    +                    "source": "manual:api_key",
    +                    "auth_type": "api_key",
    +                    "access_token": "user-api-key",
    +                },
    +            ],
    +        },
    +    }))
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +    _save_codex_tokens({"access_token": "fresh-at", "refresh_token": "fresh-rt"},
    +                       last_refresh="2026-05-28T00:00:00Z")
    +
    +    auth = json.loads((hermes_home / "auth.json").read_text())
    +    pool = auth["credential_pool"]["openai-codex"]
    +
    +    # Singleton-seeded device_code entry: refreshed and error markers cleared.
    +    seeded = next(e for e in pool if e["source"] == "device_code")
    +    assert seeded["access_token"] == "fresh-at"
    +    assert seeded["refresh_token"] == "fresh-rt"
    +
    +    # manual:device_code entry: ALSO refreshed (the new behavior).
    +    manual_dc = next(e for e in pool if e["source"] == "manual:device_code")
    +    assert manual_dc["access_token"] == "fresh-at"
    +    assert manual_dc["refresh_token"] == "fresh-rt"
    +    assert manual_dc["last_refresh"] == "2026-05-28T00:00:00Z"
    +    assert manual_dc["last_status"] is None
    +    assert manual_dc["last_error_code"] is None
    +    assert manual_dc["last_error_reason"] is None
    +
    +    # manual:api_key entry: untouched — independent credential.
    +    api_key = [redacted:secret-assignment] for e in pool if e["source"] == "manual:api_key")
    +    assert api_key["access_token"] == "user-api-key"
    +    assert "refresh_token" not in api_key or api_key.get("refresh_token") is None
    +
    +
     def test_import_codex_cli_tokens(tmp_path, monkeypatch):
         codex_home = tmp_path / "codex-cli"
         codex_home.mkdir(parents=True, exist_ok=True)
    ```
- **#36932: config-driven MOA; Credential pool reload on /model switch, Incremental token count for sessions**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`

## Fix status

- Merged to main in PR #33744 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-28`.
- Subsystem: `agent`
