From 590d547b40f36a6b2286fcd781a05545d199409c Mon Sep 17 00:00:00 2001 From: Daniel Magro <87080165+DanDo385@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:20:44 -0400 Subject: [PATCH] fix(auth): tolerate legacy Codex suppression data --- hermes_cli/auth.py | 35 ++++++++++++++++++++++---- tests/hermes_cli/test_auth_commands.py | 30 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 0ecd267513..502f23f892 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -1753,11 +1753,29 @@ def write_credential_pool( def suppress_credential_source(provider_id: str, source: str) -> None: - """Mark a credential source as suppressed so it won't be re-seeded.""" + """Mark a credential source as suppressed so it won't be re-seeded. + + Older auth stores may represent a provider's suppressed sources as a + mapping. Treat its keys as source names and migrate the value to the + canonical list form before appending the requested source. + """ with _auth_store_lock(): auth_store = _load_auth_store() - suppressed = auth_store.setdefault("suppressed_sources", {}) - provider_list = suppressed.setdefault(provider_id, []) + suppressed = auth_store.get("suppressed_sources") + if not isinstance(suppressed, dict): + suppressed = {} + auth_store["suppressed_sources"] = suppressed + + raw_sources = suppressed.get(provider_id) + if isinstance(raw_sources, list): + provider_list = raw_sources + elif isinstance(raw_sources, dict): + provider_list = [str(name) for name in raw_sources] + suppressed[provider_id] = provider_list + else: + provider_list = [] + suppressed[provider_id] = provider_list + if source not in provider_list: provider_list.append(source) _save_auth_store(auth_store) @@ -1783,8 +1801,15 @@ def unsuppress_credential_source(provider_id: str, source: str) -> bool: suppressed = auth_store.get("suppressed_sources") if not isinstance(suppressed, dict): return False - provider_list = suppressed.get(provider_id) - if not isinstance(provider_list, list) or source not in provider_list: + raw_sources = suppressed.get(provider_id) + if isinstance(raw_sources, dict): + provider_list = [str(name) for name in raw_sources] + suppressed[provider_id] = provider_list + elif isinstance(raw_sources, list): + provider_list = raw_sources + else: + return False + if source not in provider_list: return False provider_list.remove(source) if not provider_list: diff --git a/tests/hermes_cli/test_auth_commands.py b/tests/hermes_cli/test_auth_commands.py index 939be3674e..0e423d0cd9 100644 --- a/tests/hermes_cli/test_auth_commands.py +++ b/tests/hermes_cli/test_auth_commands.py @@ -521,6 +521,36 @@ def test_auth_remove_reindexes_priorities(tmp_path, monkeypatch): assert entries[0]["priority"] == 0 +def test_auth_remove_codex_migrates_legacy_dict_suppression(tmp_path, monkeypatch): + """Removing a Codex credential must tolerate legacy dict suppression data.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + store = _codex_pool_only_store() + primary = store["credential_pool"]["openai-codex"][0] + primary.update({"id": "codex-qb", "label": "qb"}) + store["suppressed_sources"] = {"openai-codex": {"legacy": True}} + _write_auth_store(tmp_path, store) + + from hermes_cli.auth_commands import auth_remove_command + + class _Args: + provider = "openai-codex" + target = "qb" + + auth_remove_command(_Args()) + + payload = json.loads((tmp_path / "hermes" / "auth.json").read_text(encoding="utf-8")) + assert payload.get("credential_pool", {}).get("openai-codex", []) == [] + assert payload["suppressed_sources"]["openai-codex"] == [ + "legacy", + "device_code", + "manual:device_code", + ] + + from agent.credential_pool import load_pool + + assert load_pool("openai-codex").peek() is None + + def test_clear_provider_auth_removes_provider_pool_entries(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) _write_auth_store(