From 43ad1aef066e814d97f193d343e074a8cac523b2 Mon Sep 17 00:00:00 2001 From: John Whitman Date: Wed, 19 Aug 2026 12:13:38 -0500 Subject: [PATCH] fix(auth): quarantine invalid xAI OAuth state into its source store, not the active profile store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a terminal xAI OAuth refresh failure fires in a profile-mode process, the quarantine path loads provider state (which may resolve from the global-root store via the #18594 fallback) but then persists the emptied state into the ACTIVE profile's store. That creates a shadowing providers.xai-oauth stub in the profile store which permanently hides the root grant from that profile (the #74339 shape) — the profile reads its own empty stub forever instead of falling back to root. Fix: resolve where the state actually came from with _load_provider_state_with_source and persist the quarantined state back to that source store (root when root-resolved) via _persist_provider_state_to_store; only fall back to the active store when the state was genuinely profile-local. Regression test: profile store empty + root store holds the grant -> terminal refresh failure -> quarantine lands in ROOT, and no providers.xai-oauth stub appears in the profile store. Verified red-on-revert against the unpatched tree. Companion to #81383 (cross-profile refresh serialization) — that PR fixes the rotation race; this fixes the quarantine-side stub that the race's failures leave behind. --- hermes_cli/auth.py | 20 +++++- tests/test_xai_quarantine_source_store.py | 88 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/test_xai_quarantine_source_store.py diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 666ef41507d6..3ffa156a4e50 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -5138,8 +5138,16 @@ def resolve_xai_oauth_runtime_credentials( # Clear dead tokens from auth.json so subsequent sessions fail fast # without a network retry. Mirrors credential_pool.py quarantine. try: + # 2026-08-17: quarantine into the SOURCE store the + # grant was resolved from. The old code stored the + # emptied state into the PROFILE store, creating a + # shadowing providers.xai-oauth stub that hides the + # root grant from this lane forever (#74339 shape). _q_store = _load_auth_store() - _q_state = _load_provider_state(_q_store, "xai-oauth") or {} + _q_state, _q_source = _load_provider_state_with_source( + _q_store, "xai-oauth" + ) + _q_state = _q_state or {} _q_tokens = dict(_q_state.get("tokens") or {}) _q_tokens.pop("access_token", None) _q_tokens.pop("refresh_token", None) @@ -5152,8 +5160,14 @@ def resolve_xai_oauth_runtime_credentials( "relogin_required": True, "at": datetime.now(timezone.utc).isoformat(), } - _store_provider_state(_q_store, "xai-oauth", _q_state, set_active=False) - _save_auth_store(_q_store) + _q_active = _auth_file_path() + if _q_source is not None and not _same_path(_q_source, _q_active): + _persist_provider_state_to_store( + "xai-oauth", _q_state, _q_source, set_active=False + ) + else: + _store_provider_state(_q_store, "xai-oauth", _q_state, set_active=False) + _save_auth_store(_q_store) except Exception as _save_exc: logger.debug( "xAI OAuth: failed to persist quarantined state: %s", _save_exc, diff --git a/tests/test_xai_quarantine_source_store.py b/tests/test_xai_quarantine_source_store.py new file mode 100644 index 000000000000..e900321243dc --- /dev/null +++ b/tests/test_xai_quarantine_source_store.py @@ -0,0 +1,88 @@ +"""Tests for xAI OAuth quarantine persistence cross-profile.""" + +import json +from pathlib import Path + +import pytest + +from hermes_cli.auth import AuthError, resolve_xai_oauth_runtime_credentials + + +@pytest.fixture() +def profile_env(tmp_path, monkeypatch): + """Set up a global root + an active profile under Path.home()/.hermes/profiles/coder. + + * Path.home() -> tmp_path + * Global root -> tmp_path/.hermes + * Profile -> tmp_path/.hermes/profiles/coder (active, HERMES_HOME points here) + """ + monkeypatch.setattr(Path, "home", lambda: tmp_path) + global_root = tmp_path / ".hermes" + global_root.mkdir() + profile_dir = global_root / "profiles" / "coder" + profile_dir.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(profile_dir)) + + return {"global": global_root, "profile": profile_dir} + + +def _make_auth_store(providers: dict | None = None) -> dict: + store: dict = {"version": 1} + if providers is not None: + store["providers"] = providers + return store + + +def test_terminal_refresh_failure_quarantines_to_source_store(profile_env, monkeypatch): + """ + Test that a terminal refresh failure quarantines the grant in the store + it was read from (the global root), rather than writing a shadowing + stub into the active profile store. + """ + # 1. Profile store is empty + root store holds the grant + (profile_env["global"] / "auth.json").write_text( + json.dumps(_make_auth_store(providers={ + "xai-oauth": { + "auth_mode": "oauth_pkce", + "tokens": { + "access_token": "expired_acc", + "refresh_token": "valid_ref", + "expires_in": 3600, + }, + "discovery": {"token_endpoint": "http://mock/token"}, + "last_refresh": "2026-05-14T00:00:00Z", + } + }), indent=2) + ) + (profile_env["profile"] / "auth.json").write_text( + json.dumps(_make_auth_store(), indent=2) + ) + + # 2. Force a terminal refresh failure + def _mock_refresh(*args, **kwargs): + raise AuthError( + provider="xai-oauth", + code="xai_refresh_failed", + message="invalid_grant: token revoked", + relogin_required=True, + ) + + monkeypatch.setattr("hermes_cli.auth._refresh_xai_oauth_tokens", _mock_refresh) + + # force_refresh=True bypasses the expiry check to force a network refresh call + with pytest.raises(AuthError) as exc_info: + resolve_xai_oauth_runtime_credentials(force_refresh=True) + + assert exc_info.value.code == "xai_refresh_failed" + + # 3. Assert quarantine landed in ROOT store + root_store = json.loads((profile_env["global"] / "auth.json").read_text()) + root_state = root_store.get("providers", {}).get("xai-oauth", {}) + assert "access_token" not in root_state.get("tokens", {}) + assert "refresh_token" not in root_state.get("tokens", {}) + assert root_state.get("last_auth_error", {}).get("reason") == "runtime_refresh_failure" + assert root_state.get("last_auth_error", {}).get("relogin_required") is True + + # 4. Assert NO providers.xai-oauth stub exists in the profile store + profile_store = json.loads((profile_env["profile"] / "auth.json").read_text()) + assert "xai-oauth" not in profile_store.get("providers", {})