From f42be940491a33fc83e8a3f024e1e8d901cc6570 Mon Sep 17 00:00:00 2001 From: Fangliquan Date: Sat, 25 Jul 2026 20:50:37 +0800 Subject: [PATCH] fix(hermes_cli): preserve unset-active dashboard xAI OAuth and cover token save modes Use mark_provider_active_if_unset after dashboard token save, unsuppress device_code after TTS setup login, and lock default-active plus refresh active_provider contracts in tests. --- hermes_cli/setup.py | 4 ++ hermes_cli/web_server.py | 8 ++- .../test_auth_xai_oauth_provider.py | 70 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index d782d89016f5..53e6136a68a5 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -908,6 +908,7 @@ def _run_xai_oauth_login_from_setup() -> bool: _is_remote_session, _save_xai_oauth_tokens, _xai_oauth_device_code_login, + unsuppress_credential_source, ) except Exception as exc: print_warning(f"xAI Grok OAuth helpers unavailable: {exc}") @@ -926,6 +927,9 @@ def _run_xai_oauth_login_from_setup() -> bool: auth_mode="oauth_device_code", set_active=False, ) + # Mirror model/dashboard re-login: clear device_code suppression so + # the pool can seed from the singleton after a prior `auth remove`. + unsuppress_credential_source("xai-oauth", "device_code") return True except Exception as exc: print_warning(f"xAI Grok OAuth login failed: {exc}") diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index b98cf8ce9fb4..cbe4f06a5d27 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -11117,6 +11117,7 @@ def _xai_device_poller(session_id: str) -> None: _save_xai_oauth_tokens, _xai_oauth_discovery, _xai_oauth_poll_device_token, + mark_provider_active_if_unset, unsuppress_credential_source, ) @@ -11153,10 +11154,13 @@ def _xai_device_poller(session_id: str) -> None: discovery=discovery, last_refresh=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), auth_mode="oauth_device_code", - # Dashboard OAuth only bootstraps credentials for side tools; - # do not hijack the active chat inference provider. + # Persist credentials without hijacking an existing active + # chat provider. set_active=False, ) + # Mirror `hermes auth add xai-oauth`: first credential may become + # active when none is set yet; never overwrite an existing choice. + mark_provider_active_if_unset("xai-oauth") # The singleton write above is the single source of truth: the # credential-pool load seeds it as the canonical ``device_code`` # entry. Do NOT also insert a parallel ``manual:dashboard_*`` pool diff --git a/tests/hermes_cli/test_auth_xai_oauth_provider.py b/tests/hermes_cli/test_auth_xai_oauth_provider.py index 7210fd5e03e7..c01ad1bafb69 100644 --- a/tests/hermes_cli/test_auth_xai_oauth_provider.py +++ b/tests/hermes_cli/test_auth_xai_oauth_provider.py @@ -14,6 +14,7 @@ from hermes_cli.auth import ( XAI_OAUTH_CLIENT_ID, XAI_OAUTH_SCOPE, _read_xai_oauth_tokens, + _refresh_xai_oauth_tokens, _save_xai_oauth_tokens, _xai_access_token_is_expiring, _xai_oauth_poll_device_token, @@ -315,6 +316,75 @@ def test_save_xai_oauth_tokens_set_active_false_preserves_active_provider( assert raw["providers"]["xai-oauth"]["tokens"]["access_token"] == "side-tool-at" +def test_save_xai_oauth_tokens_default_sets_active_provider(tmp_path, monkeypatch): + """Intentional login default must promote xai-oauth to active_provider.""" + hermes_home = tmp_path / "hermes" + hermes_home.mkdir(parents=True, exist_ok=True) + auth_path = hermes_home / "auth.json" + auth_path.write_text( + json.dumps( + { + "version": 1, + "active_provider": "openrouter", + "providers": {}, + } + ) + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + _save_xai_oauth_tokens( + { + "access_token": "login-at", + "refresh_token": "login-rt", + "id_token": "", + "token_type": "Bearer", + }, + discovery={"token_endpoint": "https://auth.x.ai/oauth2/token"}, + ) + + raw = json.loads(auth_path.read_text()) + assert raw["active_provider"] == "xai-oauth" + assert raw["providers"]["xai-oauth"]["tokens"]["access_token"] == "login-at" + + +def test_refresh_xai_oauth_tokens_preserves_active_provider(tmp_path, monkeypatch): + """Token refresh must not flip active_provider away from the chat provider.""" + hermes_home = tmp_path / "hermes" + near = _jwt_with_exp(int(time.time()) + 30) + _setup_hermes_auth(hermes_home, access_token=near, refresh_token="rt-old") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + auth_path = hermes_home / "auth.json" + raw = json.loads(auth_path.read_text()) + raw["active_provider"] = "openrouter" + auth_path.write_text(json.dumps(raw)) + + new_access = _jwt_with_exp(int(time.time()) + 7200) + + def _fake_pure(access_token, refresh_token, **kwargs): + return { + "access_token": new_access, + "refresh_token": "rt-new", + "id_token": "", + "expires_in": 3600, + "token_type": "Bearer", + "last_refresh": "2026-07-25T12:00:00Z", + } + + monkeypatch.setattr("hermes_cli.auth.refresh_xai_oauth_pure", _fake_pure) + + tokens = _read_xai_oauth_tokens()["tokens"] + _refresh_xai_oauth_tokens( + tokens, + token_endpoint="https://auth.x.ai/oauth2/token", + timeout_seconds=5.0, + ) + + after = json.loads(auth_path.read_text()) + assert after["active_provider"] == "openrouter" + assert after["providers"]["xai-oauth"]["tokens"]["access_token"] == new_access + + def test_read_xai_oauth_tokens_missing(tmp_path, monkeypatch): hermes_home = tmp_path / "hermes" hermes_home.mkdir(parents=True, exist_ok=True)