fix(auth): use refresh-free Nous status snapshot on read-only display paths

Follow-up widening of the /api/status fix: add get_nous_auth_status_local(),
a refresh-free auth-store snapshot (local invoke-JWT decode only), and use it
on the read-only display surfaces that previously called
get_nous_auth_status() -> resolve_nous_runtime_credentials() -> live OAuth
refresh POST:

- hermes_cli/status.py  (hermes status auth-provider panel)
- hermes_cli/doctor.py  (hermes doctor auth-provider checks)
- hermes_cli/portal_cli.py  (hermes portal status display)
- hermes_cli/web_server.py  /api/portal endpoint and the accounts-tab
  provider card dispatcher (_resolve_provider_status nous branch)

Action paths (login flows, portal operations needing a live credential)
keep using get_nous_auth_status(). Part of NS-592.
This commit is contained in:
Teknium 2026-07-28 21:57:19 -07:00
parent 0764163f80
commit 5ea2e0a0bc
9 changed files with 176 additions and 27 deletions

View file

@ -6519,6 +6519,69 @@ def _compute_nous_auth_status() -> Dict[str, Any]:
return _snapshot_nous_pool_status()
def get_nous_auth_status_local() -> Dict[str, Any]:
"""Refresh-free Nous auth snapshot for read-only display surfaces.
Unlike :func:`get_nous_auth_status`, this NEVER calls
``resolve_nous_runtime_credentials()`` and therefore never performs an
OAuth refresh POST or consumes a single-use refresh token. It reports the
persisted auth-store state, classifying the access token with a local
invoke-JWT decode only.
Use this from status panels, doctor checks, and polled dashboard
endpoints. Explicit auth actions (login flows, portal operations that
need a live credential) should keep using ``get_nous_auth_status()``.
``logged_in`` here means "a persisted login exists that the runtime can
use or refresh": a currently-usable invoke JWT, or a refresh token that
has not been terminally quarantined. It does not prove the refresh token
is still accepted server-side only a live resolve can do that.
"""
try:
state = get_provider_auth_state("nous")
except Exception:
state = None
if not state:
return _snapshot_nous_pool_status()
access_token = state.get("access_token")
jwt_reason = _nous_invoke_jwt_status(
access_token,
scope=state.get("scope"),
expires_at=state.get("expires_at"),
)
last_err = state.get("last_auth_error")
terminal = bool(
isinstance(last_err, dict)
and last_err.get("relogin_required")
and not (access_token or state.get("refresh_token"))
)
logged_in = (jwt_reason is None) or (
bool(state.get("refresh_token")) and not terminal
)
status: Dict[str, Any] = {
"logged_in": logged_in,
"portal_base_url": state.get("portal_base_url"),
"inference_base_url": state.get("inference_base_url"),
"access_token": access_token,
"access_expires_at": state.get("expires_at"),
"agent_key_expires_at": state.get("agent_key_expires_at"),
"has_refresh_token": bool(state.get("refresh_token")),
"inference_credential_present": bool(
access_token or state.get("agent_key")
),
"credential_source": "auth_store",
"source": "auth_store_local",
}
if terminal and isinstance(last_err, dict):
status["relogin_required"] = True
status["error_code"] = last_err.get("code")
status["error"] = last_err.get("message") or "re-login required"
return status
# Enum values reported on the dashboard /api/status as ``nous_session_valid``.
# NAS's health sweep re-mints the bootstrap session ONLY on "terminal"; "valid"
# and "unknown" are no-ops. Keep this set small and stable — NAS parses it with

View file

@ -1356,12 +1356,14 @@ def run_doctor(args):
try:
from hermes_cli.auth import (
get_nous_auth_status,
get_nous_auth_status_local,
get_codex_auth_status,
get_minimax_oauth_auth_status,
)
nous_status = get_nous_auth_status()
# Read-only display: refresh-free snapshot — doctor must never
# trigger an OAuth refresh as a side effect of a health check.
nous_status = get_nous_auth_status_local()
if nous_status.get("logged_in"):
check_ok("Nous Portal auth", "(logged in)")
else:

View file

@ -33,13 +33,14 @@ DOCS_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/tool-
def _cmd_status(args) -> int:
"""Show Portal auth + Tool Gateway routing summary."""
from hermes_cli.auth import get_nous_auth_status
from hermes_cli.auth import get_nous_auth_status_local
from hermes_cli.nous_subscription import get_nous_subscription_features
config = load_config() or {}
try:
auth = get_nous_auth_status() or {}
# Read-only status display: refresh-free snapshot (no OAuth refresh).
auth = get_nous_auth_status_local() or {}
except Exception:
auth = {}

View file

@ -195,12 +195,14 @@ def show_status(args):
try:
from hermes_cli.auth import (
get_nous_auth_status,
get_nous_auth_status_local,
get_codex_auth_status,
get_qwen_auth_status,
get_minimax_oauth_auth_status,
)
nous_status = get_nous_auth_status()
# Read-only display: use the refresh-free snapshot so `hermes status`
# never performs an OAuth refresh or burns a single-use refresh token.
nous_status = get_nous_auth_status_local()
codex_status = get_codex_auth_status()
qwen_status = get_qwen_auth_status()
minimax_status = get_minimax_oauth_auth_status()

View file

@ -3696,9 +3696,11 @@ async def get_portal_status():
cfg = load_config() or {}
auth: Dict[str, Any] = {}
try:
from hermes_cli.auth import get_nous_auth_status
from hermes_cli.auth import get_nous_auth_status_local
auth = get_nous_auth_status() or {}
# Read-only dashboard endpoint: refresh-free snapshot so polling
# never performs an OAuth refresh or burns a refresh token.
auth = get_nous_auth_status_local() or {}
except Exception:
auth = {}
@ -10331,7 +10333,9 @@ def _resolve_provider_status(provider_id: str, status_fn) -> Dict[str, Any]:
try:
from hermes_cli import auth as hauth
if provider_id == "nous":
raw = hauth.get_nous_auth_status()
# Read-only accounts-tab card: refresh-free snapshot so listing
# providers never performs an OAuth refresh.
raw = hauth.get_nous_auth_status_local()
return {
"logged_in": bool(raw.get("logged_in")),
"source": "nous_portal",

View file

@ -354,7 +354,7 @@ class TestDoctorMemoryProviderSection:
# Stub auth checks to avoid real API calls
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -461,7 +461,7 @@ def test_run_doctor_accepts_named_provider_from_providers_section(monkeypatch, t
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -499,7 +499,7 @@ def test_run_doctor_accepts_bare_custom_provider(monkeypatch, tmp_path):
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -539,7 +539,7 @@ def test_run_doctor_flags_missing_credentials_for_active_openrouter_provider(mon
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {})
except Exception:
@ -589,7 +589,7 @@ def test_run_doctor_accepts_hermes_provider_ids_that_catalog_aliases(
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -636,7 +636,7 @@ def test_run_doctor_accepts_vendor_slugs_for_named_custom_provider(monkeypatch,
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -683,7 +683,7 @@ def test_run_doctor_accepts_kimi_coding_cn_provider(monkeypatch, tmp_path):
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_auth_status", lambda provider: {"logged_in": True})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
@ -723,7 +723,7 @@ def test_run_doctor_termux_does_not_mark_browser_available_without_agent_browser
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -763,7 +763,7 @@ def test_run_doctor_kimi_cn_env_is_detected_and_probe_is_null_safe(monkeypatch,
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except Exception:
@ -812,7 +812,7 @@ def test_run_doctor_dashscope_retries_china_endpoint_after_intl_unauthorized(mon
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except ImportError:
@ -871,7 +871,7 @@ def test_run_doctor_opencode_go_skips_invalid_models_probe(monkeypatch, tmp_path
try:
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {})
except ImportError:
@ -1031,7 +1031,7 @@ def _run_doctor_with_healthy_oauth_fallback(
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": True})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {"logged_in": True})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: minimax_oauth_status)
_xai_status = xai_oauth_status if xai_oauth_status is not None else {}
@ -1162,7 +1162,7 @@ class TestDoctorXaiOAuthStatus:
monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", xai_auth_fn)
@ -1236,7 +1236,7 @@ class TestDoctorXaiOAuthStatus:
monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
monkeypatch.delattr(_auth_mod, "get_xai_oauth_auth_status", raising=False)
@ -1267,7 +1267,7 @@ class TestDoctorXaiOAuthStatus:
monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": True})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {"logged_in": True})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
monkeypatch.delattr(_auth_mod, "get_xai_oauth_auth_status", raising=False)
@ -1327,7 +1327,7 @@ class TestDoctorCodexCliHintPlacement:
monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
from hermes_cli import auth as _auth_mod
monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_nous_auth_status_local", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {"logged_in": codex_logged_in})
monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {"logged_in": False})
monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {"logged_in": False})

View file

@ -154,3 +154,77 @@ def test_provider_state_exception_is_unknown(monkeypatch):
_block_live_auth(monkeypatch)
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
# ── get_nous_auth_status_local — refresh-free display snapshot ──
def test_local_status_logged_in_with_usable_jwt_never_refreshes(monkeypatch):
monkeypatch.setattr(
auth,
"get_provider_auth_state",
lambda provider: {
"access_token": _invoke_jwt(),
"refresh_token": "rt",
"scope": auth.DEFAULT_NOUS_SCOPE,
"portal_base_url": "https://portal.example",
},
)
_block_live_auth(monkeypatch)
status = auth.get_nous_auth_status_local()
assert status["logged_in"] is True
assert status["source"] == "auth_store_local"
assert status["portal_base_url"] == "https://portal.example"
def test_local_status_logged_in_via_refresh_token_when_jwt_expired(monkeypatch):
monkeypatch.setattr(
auth,
"get_provider_auth_state",
lambda provider: {
"access_token": _invoke_jwt(seconds=-60),
"refresh_token": "rt",
"scope": auth.DEFAULT_NOUS_SCOPE,
},
)
_block_live_auth(monkeypatch)
# Runtime can still refresh — display should not claim logged-out.
assert auth.get_nous_auth_status_local()["logged_in"] is True
def test_local_status_not_logged_in_after_terminal_quarantine(monkeypatch):
monkeypatch.setattr(
auth,
"get_provider_auth_state",
lambda provider: {
"last_auth_error": {
"relogin_required": True,
"code": "invalid_grant",
},
},
)
_block_live_auth(monkeypatch)
status = auth.get_nous_auth_status_local()
assert status["logged_in"] is False
assert status["relogin_required"] is True
assert status["error_code"] == "invalid_grant"
def test_local_status_repeated_polling_never_uses_live_auth(monkeypatch):
monkeypatch.setattr(
auth,
"get_provider_auth_state",
lambda provider: {
"access_token": _invoke_jwt(),
"refresh_token": "rt",
"scope": auth.DEFAULT_NOUS_SCOPE,
},
)
_block_live_auth(monkeypatch)
assert all(
auth.get_nous_auth_status_local()["logged_in"] for _ in range(10)
)

View file

@ -19,6 +19,9 @@ def _patch_common_status_deps(monkeypatch, status_mod, tmp_path, *, openai_base_
monkeypatch.setattr(status_mod, "get_env_value", _get_env_value, raising=False)
monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(
auth_mod, "get_nous_auth_status_local", lambda: {}, raising=False
)
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(
status_mod.subprocess,
@ -135,7 +138,7 @@ def test_show_status_reports_exhausted_nous_credits(monkeypatch, capsys, tmp_pat
_patch_common_status_deps(monkeypatch, status_mod, tmp_path)
monkeypatch.setattr(
auth_mod,
"get_nous_auth_status",
"get_nous_auth_status_local",
lambda: {
"logged_in": False,
"access_token": "jwt",

View file

@ -867,7 +867,7 @@ def test_status_hardcoded_branch_wins_over_generic_fallback():
import hermes_cli.web_server as ws
with patch(
"hermes_cli.auth.get_nous_auth_status",
"hermes_cli.auth.get_nous_auth_status_local",
return_value={"logged_in": True, "portal_base_url": "https://portal.test"},
):
out = ws._resolve_provider_status("nous", None)