{"repo":"NousResearch/hermes-agent","public_id":"15013","display_label":"OpenAI Codex auth state cleanup on logout","summary":"These items concern a bug where logging out of OpenAI Codex can leave stale provider state pinned, and related fixes to clear that state on logout.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":15061,"merged_at":"2026-04-24T10:17:18+00:00"},"resolution":{"state":"resolved","by":"pr-15061"},"freshness":{"state":"stale","last_verified_at":"2026-04-24T10:17:18+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":15013,"pr_number":15061,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-24T10:17:18+00:00"}],"members":[{"kind":"issue","number":15013,"title":"[Bug]: OpenAI Codex logout can leave Hermes pinned to openai-codex when auth state is already cleared","url":null,"state":"closed"},{"kind":"pr","number":15018,"title":"fix(auth): clear stale OpenAI Codex provider state on logout","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#15061","number":15061}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":15061,"title":"fix(auth): clear stale OpenAI Codex provider state on logout (salvage #15018)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/auth.py","scripts/release.py","tests/hermes_cli/test_auth_commands.py"],"fix_hunks":[{"file":"hermes_cli/auth.py","hunks":["@@ -955,10 +955,12 @@ def clear_provider_auth(provider_id: Optional[str] = None) -> bool:\n             del pool[target]\n             cleared = True\n \n-        if not cleared:\n-            return False\n         if auth_store.get(\"active_provider\") == target:\n             auth_store[\"active_provider\"] = None\n+            cleared = True\n+\n+        if not cleared:\n+            return False\n         _save_auth_store(auth_store)\n     return True\n ","@@ -2826,6 +2828,46 @@ def _update_config_for_provider(\n     return config_path\n \n \n+def _get_config_provider() -> Optional[str]:\n+    \"\"\"Return model.provider from config.yaml, normalized, if present.\"\"\"\n+    try:\n+        config = read_raw_config()\n+    except Exception:\n+        return None\n+    if not config:\n+        return None\n+    model = config.get(\"model\")\n+    if not isinstance(model, dict):\n+        return None\n+    provider = model.get(\"provider\")\n+    if not isinstance(provider, str):\n+        return None\n+    provider = provider.strip().lower()\n+    return provider or None\n+\n+\n+def _config_provider_matches(provider_id: Optional[str]) -> bool:\n+    \"\"\"Return True when config.yaml currently selects *provider_id*.\"\"\"\n+    if not provider_id:\n+        return False\n+    return _get_config_provider() == provider_id.strip().lower()\n+\n+\n+def _logout_default_provider_from_config() -> Optional[str]:\n+    \"\"\"Fallback logout target when auth.json has no active provider.\n+\n+    `hermes logout` historically keyed off auth.json.active_provider only.\n+    That left users stuck when auth state had already been cleared but\n+    config.yaml still selected an OAuth provider such as openai-codex for the\n+    agent model: there was no active auth provider to target, so logout printed\n+    \"No provider is currently logged in\" and never reset model.provider.\n+    \"\"\"\n+    provider = _get_config_provider()\n+    if provider in {\"nous\", \"openai-codex\"}:\n+        return provider\n+    return None\n+\n+\n def _reset_config_provider() -> Path:\n     \"\"\"Reset config.yaml provider back to auto after logout.\"\"\"\n     config_path = get_config_path()","@@ -3524,15 +3566,16 @@ def logout_command(args) -> None:\n         raise SystemExit(1)\n \n     active = get_active_provider()\n-    target = provider_id or active\n+    target = provider_id or active or _logout_default_provider_from_config()\n \n     if not target:\n         print(\"No provider is currently logged in.\")\n         return\n \n     provider_name = PROVIDER_REGISTRY[target].name if target in PROVIDER_REGISTRY else target\n+    config_matches = _config_provider_matches(target)\n \n-    if clear_provider_auth(target):\n+    if clear_provider_auth(target) or config_matches:\n         _reset_config_provider()\n         print(f\"Logged out of {provider_name}.\")\n         if os.getenv(\"OPENROUTER_API_KEY\"):"]},{"file":"scripts/release.py","hunks":["@@ -189,6 +189,7 @@\n     \"[redacted:email]\": \"christomitov\",\n     \"[redacted:email]\": \"NousResearch\",\n     \"[redacted:email]\": \"ReginaldasR\",\n+    \"[redacted:email]\": \"0xharryriddle\",\n     \"[redacted:email]\": \"benbarclay\",\n     \"[redacted:email]\": \"ChimingLiu\",\n     \"[redacted:email]\": \"openclaw\","]},{"file":"tests/hermes_cli/test_auth_commands.py","hunks":["@@ -504,6 +504,91 @@ def test_clear_provider_auth_removes_provider_pool_entries(tmp_path, monkeypatch\n     assert \"openrouter\" in payload.get(\"credential_pool\", {})\n \n \n+def test_logout_resets_codex_config_when_auth_state_already_cleared(tmp_path, monkeypatch, capsys):\n+    \"\"\"`hermes logout --provider openai-codex` must still clear model.provider.\n+\n+    Users can end up with auth.json already cleared but config.yaml still set to\n+    openai-codex.  Previously logout reported no auth state and left the agent\n+    pinned to the Codex provider.\n+    \"\"\"\n+    hermes_home = tmp_path / \"hermes\"\n+    monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+    _write_auth_store(tmp_path, {\"version\": 1, \"providers\": {}, \"credential_pool\": {}})\n+    (hermes_home / \"config.yaml\").write_text(\n+        \"model:\\n\"\n+        \"  default: gpt-5.3-codex\\n\"\n+        \"  provider: openai-codex\\n\"\n+        \"  base_url: https://chatgpt.com/backend-api/codex\\n\"\n+    )\n+\n+    from types import SimpleNamespace\n+    from hermes_cli.auth import logout_command\n+\n+    logout_command(SimpleNamespace(provider=\"openai-codex\"))\n+\n+    out = capsys.readouterr().out\n+    assert \"Logged out of OpenAI Codex.\" in out\n+    config_text = (hermes_home / \"config.yaml\").read_text()\n+    assert \"provider: auto\" in config_text\n+    assert \"base_url: https://openrouter.ai/api/v1\" in config_text\n+\n+\n+def test_logout_defaults_to_configured_codex_when_no_active_provider(tmp_path, monkeypatch, capsys):\n+    \"\"\"Bare `hermes logout` should target configured Codex if auth has no active provider.\"\"\"\n+    hermes_home = tmp_path / \"hermes\"\n+    monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+    _write_auth_store(tmp_path, {\"version\": 1, \"providers\": {}, \"credential_pool\": {}})\n+    (hermes_home / \"config.yaml\").write_text(\n+        \"model:\\n\"\n+        \"  default: gpt-5.3-codex\\n\"\n+        \"  provider: openai-codex\\n\"\n+        \"  base_url: https://chatgpt.com/backend-api/codex\\n\"\n+    )\n+\n+    from types import SimpleNamespace\n+    from hermes_cli.auth import logout_command\n+\n+    logout_command(SimpleNamespace(provider=None))\n+\n+    out = capsys.readouterr().out\n+    assert \"Logged out of OpenAI Codex.\" in out\n+    config_text = (hermes_home / \"config.yaml\").read_text()\n+    assert \"provider: auto\" in config_text\n+\n+\n+def test_logout_clears_stale_active_codex_without_provider_credentials(tmp_path, monkeypatch, capsys):\n+    \"\"\"Logout must clear active_provider even when provider credential payloads are gone.\"\"\"\n+    hermes_home = tmp_path / \"hermes\"\n+    monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+    _write_auth_store(\n+        tmp_path,\n+        {\n+            \"version\": 1,\n+            \"active_provider\": \"openai-codex\",\n+            \"providers\": {},\n+            \"credential_pool\": {},\n+        },\n+    )\n+    (hermes_home / \"config.yaml\").write_text(\n+        \"model:\\n\"\n+        \"  default: gpt-5.3-codex\\n\"\n+        \"  provider: openai-codex\\n\"\n+        \"  base_url: https://chatgpt.com/backend-api/codex\\n\"\n+    )\n+\n+    from types import SimpleNamespace\n+    from hermes_cli.auth import logout_command\n+\n+    logout_command(SimpleNamespace(provider=None))\n+\n+    out = capsys.readouterr().out\n+    assert \"Logged out of OpenAI Codex.\" in out\n+    auth_payload = json.loads((hermes_home / \"auth.json\").read_text())\n+    assert auth_payload.get(\"active_provider\") is None\n+    config_text = (hermes_home / \"config.yaml\").read_text()\n+    assert \"provider: auto\" in config_text\n+\n+\n def test_auth_list_does_not_call_mutating_select(monkeypatch, capsys):\n     from hermes_cli.auth_commands import auth_list_command\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}