mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(cli): sync credential_pool on Codex re-auth
Codex re-auth via `hermes setup` / `hermes model` wrote fresh OAuth tokens to providers.openai-codex.tokens but left the credential_pool device_code entry holding the consumed refresh token and stale error markers. Since the runtime selects from the pool, the next request spent a dead token and got a 401 token_invalidated. Update the singleton-seeded pool entries in lockstep and clear their error state. Fixes #33000
This commit is contained in:
parent
4feb181eb4
commit
2bbd53493d
2 changed files with 110 additions and 0 deletions
|
|
@ -3223,6 +3223,48 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _sync_codex_pool_entries(
|
||||
auth_store: Dict[str, Any],
|
||||
tokens: Dict[str, str],
|
||||
last_refresh: Optional[str],
|
||||
) -> None:
|
||||
"""Mirror a fresh Codex re-auth into the credential_pool singleton 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.
|
||||
"""
|
||||
access_token = tokens.get("access_token")
|
||||
if not access_token:
|
||||
return
|
||||
refresh_token = tokens.get("refresh_token")
|
||||
pool = auth_store.get("credential_pool")
|
||||
if not isinstance(pool, dict):
|
||||
return
|
||||
entries = pool.get("openai-codex")
|
||||
if not isinstance(entries, list):
|
||||
return
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict) or entry.get("source") != "device_code":
|
||||
continue
|
||||
entry["access_token"] = access_token
|
||||
if refresh_token:
|
||||
entry["refresh_token"] = refresh_token
|
||||
if last_refresh:
|
||||
entry["last_refresh"] = last_refresh
|
||||
entry["last_status"] = None
|
||||
entry["last_status_at"] = None
|
||||
entry["last_error_code"] = None
|
||||
entry["last_error_reason"] = None
|
||||
entry["last_error_message"] = None
|
||||
entry["last_error_reset_at"] = None
|
||||
|
||||
|
||||
def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None) -> None:
|
||||
"""Save Codex OAuth tokens to Hermes auth store (~/.hermes/auth.json)."""
|
||||
if last_refresh is None:
|
||||
|
|
@ -3234,6 +3276,7 @@ def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None) -> None
|
|||
state["last_refresh"] = last_refresh
|
||||
state["auth_mode"] = "chatgpt"
|
||||
_save_provider_state(auth_store, "openai-codex", state)
|
||||
_sync_codex_pool_entries(auth_store, tokens, last_refresh)
|
||||
_save_auth_store(auth_store)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,73 @@ def test_save_codex_tokens_roundtrip(tmp_path, monkeypatch):
|
|||
assert data["tokens"]["refresh_token"] == "rt456"
|
||||
|
||||
|
||||
def test_save_codex_tokens_syncs_credential_pool(tmp_path, monkeypatch):
|
||||
"""Re-auth must update the credential_pool device_code entry, not just providers.
|
||||
|
||||
Regression for #33000: the runtime selects from credential_pool, so a
|
||||
re-auth that only refreshed providers.openai-codex.tokens left the pool
|
||||
holding a consumed refresh token and stale error markers, causing an
|
||||
immediate 401 token_invalidated on the next request.
|
||||
"""
|
||||
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": "abc123",
|
||||
"source": "device_code",
|
||||
"auth_type": "oauth",
|
||||
"access_token": "old-at",
|
||||
"refresh_token": "old-rt",
|
||||
"last_status": "exhausted",
|
||||
"last_error_code": 401,
|
||||
"last_error_reason": "token_invalidated",
|
||||
"last_error_reset_at": 9999999999,
|
||||
},
|
||||
{
|
||||
"id": "manual1",
|
||||
"source": "manual:codex",
|
||||
"auth_type": "oauth",
|
||||
"access_token": "manual-at",
|
||||
"refresh_token": "manual-rt",
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
_save_codex_tokens({"access_token": "new-at", "refresh_token": "new-rt"},
|
||||
last_refresh="2026-05-27T00:00:00Z")
|
||||
|
||||
auth = json.loads((hermes_home / "auth.json").read_text())
|
||||
pool = auth["credential_pool"]["openai-codex"]
|
||||
seeded = next(e for e in pool if e["source"] == "device_code")
|
||||
assert seeded["access_token"] == "new-at"
|
||||
assert seeded["refresh_token"] == "new-rt"
|
||||
assert seeded["last_refresh"] == "2026-05-27T00:00:00Z"
|
||||
assert seeded["last_status"] is None
|
||||
assert seeded["last_error_code"] is None
|
||||
assert seeded["last_error_reason"] is None
|
||||
assert seeded["last_error_reset_at"] is None
|
||||
|
||||
# Manual entries are independent credentials and must not be overwritten.
|
||||
manual = next(e for e in pool if e["source"] == "manual:codex")
|
||||
assert manual["access_token"] == "manual-at"
|
||||
assert manual["refresh_token"] == "manual-rt"
|
||||
|
||||
# Provider singleton is updated too.
|
||||
assert auth["providers"]["openai-codex"]["tokens"]["access_token"] == "new-at"
|
||||
|
||||
|
||||
def test_import_codex_cli_tokens(tmp_path, monkeypatch):
|
||||
codex_home = tmp_path / "codex-cli"
|
||||
codex_home.mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue