Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
88 changes: 88 additions & 0 deletions tests/test_xai_quarantine_source_store.py
Original file line number Diff line number Diff line change
@@ -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", {})