From ac25e6c99a686964ac268b35235830e1e5519d88 Mon Sep 17 00:00:00 2001 From: Harry Riddle Date: Fri, 24 Apr 2026 16:07:32 +0700 Subject: [PATCH] feat(auth-codex): add config-provider fallback detection for logout in hermes-agent/hermes_cli/auth.py --- hermes_cli/auth.py | 51 ++++++++++++++-- tests/hermes_cli/test_auth_commands.py | 85 ++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 273d832f5..8a625fbb2 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -955,10 +955,12 @@ def clear_provider_auth(provider_id: Optional[str] = None) -> bool: del pool[target] cleared = True - if not cleared: - return False if auth_store.get("active_provider") == target: auth_store["active_provider"] = None + cleared = True + + if not cleared: + return False _save_auth_store(auth_store) return True @@ -2826,6 +2828,46 @@ def _update_config_for_provider( return config_path +def _get_config_provider() -> Optional[str]: + """Return model.provider from config.yaml, normalized, if present.""" + try: + config = read_raw_config() + except Exception: + return None + if not config: + return None + model = config.get("model") + if not isinstance(model, dict): + return None + provider = model.get("provider") + if not isinstance(provider, str): + return None + provider = provider.strip().lower() + return provider or None + + +def _config_provider_matches(provider_id: Optional[str]) -> bool: + """Return True when config.yaml currently selects *provider_id*.""" + if not provider_id: + return False + return _get_config_provider() == provider_id.strip().lower() + + +def _logout_default_provider_from_config() -> Optional[str]: + """Fallback logout target when auth.json has no active provider. + + `hermes logout` historically keyed off auth.json.active_provider only. + That left users stuck when auth state had already been cleared but + config.yaml still selected an OAuth provider such as openai-codex for the + agent model: there was no active auth provider to target, so logout printed + "No provider is currently logged in" and never reset model.provider. + """ + provider = _get_config_provider() + if provider in {"nous", "openai-codex"}: + return provider + return None + + def _reset_config_provider() -> Path: """Reset config.yaml provider back to auto after logout.""" config_path = get_config_path() @@ -3524,15 +3566,16 @@ def logout_command(args) -> None: raise SystemExit(1) active = get_active_provider() - target = provider_id or active + target = provider_id or active or _logout_default_provider_from_config() if not target: print("No provider is currently logged in.") return provider_name = PROVIDER_REGISTRY[target].name if target in PROVIDER_REGISTRY else target + config_matches = _config_provider_matches(target) - if clear_provider_auth(target): + if clear_provider_auth(target) or config_matches: _reset_config_provider() print(f"Logged out of {provider_name}.") if os.getenv("OPENROUTER_API_KEY"): diff --git a/tests/hermes_cli/test_auth_commands.py b/tests/hermes_cli/test_auth_commands.py index fb749b6ae..388386a6b 100644 --- a/tests/hermes_cli/test_auth_commands.py +++ b/tests/hermes_cli/test_auth_commands.py @@ -504,6 +504,91 @@ def test_clear_provider_auth_removes_provider_pool_entries(tmp_path, monkeypatch assert "openrouter" in payload.get("credential_pool", {}) +def test_logout_resets_codex_config_when_auth_state_already_cleared(tmp_path, monkeypatch, capsys): + """`hermes logout --provider openai-codex` must still clear model.provider. + + Users can end up with auth.json already cleared but config.yaml still set to + openai-codex. Previously logout reported no auth state and left the agent + pinned to the Codex provider. + """ + hermes_home = tmp_path / "hermes" + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + _write_auth_store(tmp_path, {"version": 1, "providers": {}, "credential_pool": {}}) + (hermes_home / "config.yaml").write_text( + "model:\n" + " default: gpt-5.3-codex\n" + " provider: openai-codex\n" + " base_url: https://chatgpt.com/backend-api/codex\n" + ) + + from types import SimpleNamespace + from hermes_cli.auth import logout_command + + logout_command(SimpleNamespace(provider="openai-codex")) + + out = capsys.readouterr().out + assert "Logged out of OpenAI Codex." in out + config_text = (hermes_home / "config.yaml").read_text() + assert "provider: auto" in config_text + assert "base_url: https://openrouter.ai/api/v1" in config_text + + +def test_logout_defaults_to_configured_codex_when_no_active_provider(tmp_path, monkeypatch, capsys): + """Bare `hermes logout` should target configured Codex if auth has no active provider.""" + hermes_home = tmp_path / "hermes" + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + _write_auth_store(tmp_path, {"version": 1, "providers": {}, "credential_pool": {}}) + (hermes_home / "config.yaml").write_text( + "model:\n" + " default: gpt-5.3-codex\n" + " provider: openai-codex\n" + " base_url: https://chatgpt.com/backend-api/codex\n" + ) + + from types import SimpleNamespace + from hermes_cli.auth import logout_command + + logout_command(SimpleNamespace(provider=None)) + + out = capsys.readouterr().out + assert "Logged out of OpenAI Codex." in out + config_text = (hermes_home / "config.yaml").read_text() + assert "provider: auto" in config_text + + +def test_logout_clears_stale_active_codex_without_provider_credentials(tmp_path, monkeypatch, capsys): + """Logout must clear active_provider even when provider credential payloads are gone.""" + hermes_home = tmp_path / "hermes" + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + _write_auth_store( + tmp_path, + { + "version": 1, + "active_provider": "openai-codex", + "providers": {}, + "credential_pool": {}, + }, + ) + (hermes_home / "config.yaml").write_text( + "model:\n" + " default: gpt-5.3-codex\n" + " provider: openai-codex\n" + " base_url: https://chatgpt.com/backend-api/codex\n" + ) + + from types import SimpleNamespace + from hermes_cli.auth import logout_command + + logout_command(SimpleNamespace(provider=None)) + + out = capsys.readouterr().out + assert "Logged out of OpenAI Codex." in out + auth_payload = json.loads((hermes_home / "auth.json").read_text()) + assert auth_payload.get("active_provider") is None + config_text = (hermes_home / "config.yaml").read_text() + assert "provider: auto" in config_text + + def test_auth_list_does_not_call_mutating_select(monkeypatch, capsys): from hermes_cli.auth_commands import auth_list_command