From 21bde99423f40fd34ee46fa94a8c4fc8b26c89f3 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Mon, 8 Jun 2026 21:03:35 +0800 Subject: [PATCH] fix(auth): propagate credential label to pool entries on re-auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user runs `hermes auth add openai-codex` (or any OAuth provider), the computed label was saved to auth.json providers state but never propagated to credential_pool entries. Two bugs caused this: 1. `_sync_codex_pool_entries()` synced tokens and error markers but not the label field, leaving pool entries with stale labels. 2. `_upsert_entry()` unconditionally skipped label updates when the existing entry already had a label, preventing the label from being refreshed even when the user explicitly provided `--label` or the auto-generated label incremented (e.g., oauth-1 → oauth-2). Fixes #42102 --- agent/credential_pool.py | 2 +- hermes_cli/auth.py | 5 +- tests/hermes_cli/test_auth_codex_provider.py | 137 +++++++++++++++++++ 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 53cc31daf6d0..4ae5bf85b78d 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1567,7 +1567,7 @@ def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, p for key, value in payload.items(): if key in {"id", "priority"} or value is None: continue - if key == "label" and existing.label: + if key == "label" and existing.label and value == existing.label: continue if key in _field_names: if getattr(existing, key) != value: diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 021905c3ec05..f6b9bed12a8c 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -3340,6 +3340,7 @@ def _sync_codex_pool_entries( auth_store: Dict[str, Any], tokens: Dict[str, str], last_refresh: Optional[str], + label: Optional[str] = None, ) -> None: """Mirror a fresh Codex re-auth into the credential_pool OAuth entries. @@ -3405,6 +3406,8 @@ def _sync_codex_pool_entries( entry["last_error_reason"] = None entry["last_error_message"] = None entry["last_error_reset_at"] = None + if label: + entry["label"] = label def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None, label: str = None) -> None: @@ -3420,7 +3423,7 @@ def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None, label: if label and str(label).strip(): state["label"] = str(label).strip() _save_provider_state(auth_store, "openai-codex", state) - _sync_codex_pool_entries(auth_store, tokens, last_refresh) + _sync_codex_pool_entries(auth_store, tokens, last_refresh, label=label) _save_auth_store(auth_store) diff --git a/tests/hermes_cli/test_auth_codex_provider.py b/tests/hermes_cli/test_auth_codex_provider.py index 52a8a4a2c455..7725f6b521fd 100644 --- a/tests/hermes_cli/test_auth_codex_provider.py +++ b/tests/hermes_cli/test_auth_codex_provider.py @@ -658,3 +658,140 @@ def _fake_save(tokens, last_refresh=None): assert called["device_login"] == 1 assert called["tokens"]["access_token"] == "fresh-at" + + +def test_save_codex_tokens_syncs_label_to_pool(tmp_path, monkeypatch): + """Re-auth with --label must update the label on pool entries. + + Regression for #42102: ``hermes auth add openai-codex --label foo`` + saved the label to providers state but _sync_codex_pool_entries did not + propagate it to credential_pool entries, so the pool entry kept its + stale label. + """ + 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", + "label": "openai-codex-oauth-1", + }, + }, + "credential_pool": { + "openai-codex": [ + { + "id": "abc123", + "source": "device_code", + "auth_type": "oauth", + "access_token": "old-at", + "refresh_token": "old-rt", + "label": "openai-codex-oauth-1", + }, + ], + }, + })) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + _save_codex_tokens( + {"access_token": "new-at", "refresh_token": "new-rt"}, + last_refresh="2026-06-08T00:00:00Z", + label="openai-codex-oauth-2", + ) + + auth = json.loads((hermes_home / "auth.json").read_text()) + pool = auth["credential_pool"]["openai-codex"] + entry = next(e for e in pool if e["source"] == "device_code") + assert entry["label"] == "openai-codex-oauth-2" + assert entry["access_token"] == "new-at" + + # Provider state also has the new label. + assert auth["providers"]["openai-codex"]["label"] == "openai-codex-oauth-2" + + +def test_save_codex_tokens_syncs_custom_label(tmp_path, monkeypatch): + """Custom --label is propagated to pool entries on re-auth.""" + 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"}, + "auth_mode": "chatgpt", + }, + }, + "credential_pool": { + "openai-codex": [ + { + "id": "abc123", + "source": "device_code", + "auth_type": "oauth", + "access_token": "old-at", + "refresh_token": "old-rt", + "label": "device_code", + }, + ], + }, + })) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + _save_codex_tokens( + {"access_token": "new-at", "refresh_token": "new-rt"}, + label="my-custom-label", + ) + + auth = json.loads((hermes_home / "auth.json").read_text()) + pool = auth["credential_pool"]["openai-codex"] + entry = next(e for e in pool if e["source"] == "device_code") + assert entry["label"] == "my-custom-label" + + +def test_upsert_entry_updates_label_when_different(): + """_upsert_entry must update the label when the new value differs.""" + from agent.credential_pool import _upsert_entry, PooledCredential + + existing = PooledCredential( + provider="openai-codex", + id="abc123", + label="openai-codex-oauth-1", + auth_type="oauth", + priority=0, + source="device_code", + access_token="old-at", + ) + entries = [existing] + changed = _upsert_entry( + entries, "openai-codex", "device_code", + {"source": "device_code", "auth_type": "oauth", + "access_token": "new-at", "label": "openai-codex-oauth-2"}, + ) + assert changed is True + assert entries[0].label == "openai-codex-oauth-2" + assert entries[0].access_token == "new-at" + + +def test_upsert_entry_preserves_label_when_same(): + """_upsert_entry is a no-op when the label hasn't changed.""" + from agent.credential_pool import _upsert_entry, PooledCredential + + existing = PooledCredential( + provider="openai-codex", + id="abc123", + label="openai-codex-oauth-1", + auth_type="oauth", + priority=0, + source="device_code", + access_token="same-at", + ) + entries = [existing] + changed = _upsert_entry( + entries, "openai-codex", "device_code", + {"source": "device_code", "auth_type": "oauth", + "access_token": "same-at", "label": "openai-codex-oauth-1"}, + ) + # Only the label+token are same → no change + assert changed is False + assert entries[0].label == "openai-codex-oauth-1"