mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(auth): stop JWT refreshes from status polling
This commit is contained in:
parent
3ab583044a
commit
0764163f80
3 changed files with 154 additions and 100 deletions
|
|
@ -613,13 +613,18 @@ def test_nous_inference_auth_logs_do_not_include_secret_values(
|
|||
|
||||
monkeypatch.setattr(auth_mod, "_refresh_access_token", _fake_refresh_access_token)
|
||||
|
||||
caplog.set_level(logging.INFO, logger="hermes_cli.auth")
|
||||
caplog.set_level(logging.DEBUG, logger="hermes_cli.auth")
|
||||
auth_mod.resolve_nous_runtime_credentials(
|
||||
force_refresh=True,
|
||||
)
|
||||
|
||||
logged = caplog.text
|
||||
assert "using NAS invoke JWT" in logged
|
||||
assert not any(
|
||||
record.levelno >= logging.INFO
|
||||
and "using NAS invoke JWT" in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
assert token not in logged
|
||||
assert refreshed_token not in logged
|
||||
assert refresh_token not in logged
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
"""Tests for get_nous_session_validity — the /api/status classifier NAS reads
|
||||
to decide whether to re-mint a hosted-agent bootstrap session.
|
||||
"""Tests for the local-only Nous session classifier exposed on /api/status."""
|
||||
|
||||
The anti-flap contract is the load-bearing property: only a *terminal* auth
|
||||
failure may report "terminal" (a spurious "terminal" triggers an unnecessary
|
||||
NAS re-mint + machine restart on a healthy box). A mid-rotation blip, a
|
||||
transient error, or a merely-expiring token must NOT report "terminal".
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
|
||||
import hermes_cli.auth as auth
|
||||
from hermes_cli.auth import (
|
||||
|
|
@ -16,89 +13,144 @@ from hermes_cli.auth import (
|
|||
)
|
||||
|
||||
|
||||
def _clear_cache():
|
||||
auth.invalidate_nous_auth_status_cache()
|
||||
def _invoke_jwt(*, seconds: int = 3600) -> str:
|
||||
def _encode(value: dict) -> str:
|
||||
raw = json.dumps(value, separators=(",", ":")).encode()
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
|
||||
return ".".join(
|
||||
(
|
||||
_encode({"alg": "none", "typ": "JWT"}),
|
||||
_encode(
|
||||
{
|
||||
"sub": "test-user",
|
||||
"scope": auth.DEFAULT_NOUS_SCOPE,
|
||||
"exp": int(time.time() + seconds),
|
||||
}
|
||||
),
|
||||
"signature",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_valid_when_logged_in(monkeypatch):
|
||||
"""A healthy login → 'valid'."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: {
|
||||
"access_token": "at", "refresh_token": "rt",
|
||||
})
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
def _fail_if_live_auth_is_used(*args, **kwargs):
|
||||
raise AssertionError("session validity must not resolve or refresh credentials")
|
||||
|
||||
|
||||
def _block_live_auth(monkeypatch):
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", _fail_if_live_auth_is_used)
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"resolve_nous_runtime_credentials",
|
||||
_fail_if_live_auth_is_used,
|
||||
)
|
||||
|
||||
|
||||
def test_valid_when_local_invoke_jwt_is_usable(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 get_nous_session_validity() == NOUS_SESSION_VALID
|
||||
|
||||
|
||||
def test_repeated_status_checks_never_use_live_auth_resolution(monkeypatch):
|
||||
state = {
|
||||
"access_token": _invoke_jwt(),
|
||||
"refresh_token": "rt",
|
||||
"scope": auth.DEFAULT_NOUS_SCOPE,
|
||||
}
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda provider: state)
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
assert [get_nous_session_validity() for _ in range(10)] == [
|
||||
NOUS_SESSION_VALID
|
||||
] * 10
|
||||
|
||||
|
||||
def test_terminal_on_persisted_quarantine_marker(monkeypatch):
|
||||
"""A persisted last_auth_error.relogin_required with tokens cleared →
|
||||
'terminal'. This is the exact on-disk state the incident produced."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: {
|
||||
# tokens cleared by the quarantine path
|
||||
"last_auth_error": {"relogin_required": True, "code": "invalid_grant"},
|
||||
})
|
||||
# status would also say not-logged-in, but the marker short-circuits first
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {"logged_in": False})
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"get_provider_auth_state",
|
||||
lambda provider: {
|
||||
"last_auth_error": {
|
||||
"relogin_required": True,
|
||||
"code": "invalid_grant",
|
||||
},
|
||||
},
|
||||
)
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
assert get_nous_session_validity() == NOUS_SESSION_TERMINAL
|
||||
|
||||
|
||||
def test_terminal_on_relogin_required_status(monkeypatch):
|
||||
"""Not logged in + relogin_required from the live status → 'terminal'."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: {
|
||||
"refresh_token": "rt", # present, but status resolution fails terminally
|
||||
})
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {
|
||||
"logged_in": False, "relogin_required": True, "error_code": "invalid_grant",
|
||||
})
|
||||
assert get_nous_session_validity() == NOUS_SESSION_TERMINAL
|
||||
|
||||
|
||||
def test_unknown_when_no_provider_state(monkeypatch):
|
||||
"""No Nous provider state at all → 'unknown' (never terminal)."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: None)
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {"logged_in": False})
|
||||
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
|
||||
|
||||
|
||||
def test_anti_flap_transient_not_logged_in_is_unknown(monkeypatch):
|
||||
"""ANTI-FLAP: not-logged-in WITHOUT relogin_required (a transient/network
|
||||
blip) must be 'unknown', NOT 'terminal' — otherwise a healthy box mid-blip
|
||||
triggers a spurious re-mint."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: {
|
||||
"access_token": "at", "refresh_token": "rt",
|
||||
})
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {
|
||||
"logged_in": False, "error": "connection reset", # no relogin_required
|
||||
})
|
||||
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
|
||||
|
||||
|
||||
def test_stale_quarantine_marker_ignored_after_relogin(monkeypatch):
|
||||
"""ANTI-FLAP: a leftover last_auth_error marker must NOT report 'terminal'
|
||||
once a subsequent login has repopulated tokens."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: {
|
||||
"access_token": "new-at", "refresh_token": "new-rt",
|
||||
"last_auth_error": {"relogin_required": True, "code": "invalid_grant"},
|
||||
})
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"get_provider_auth_state",
|
||||
lambda provider: {
|
||||
"access_token": _invoke_jwt(),
|
||||
"refresh_token": "new-rt",
|
||||
"scope": auth.DEFAULT_NOUS_SCOPE,
|
||||
"last_auth_error": {
|
||||
"relogin_required": True,
|
||||
"code": "invalid_grant",
|
||||
},
|
||||
},
|
||||
)
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
assert get_nous_session_validity() == NOUS_SESSION_VALID
|
||||
|
||||
|
||||
def test_status_exception_is_unknown_not_terminal(monkeypatch):
|
||||
"""If status computation itself throws, that's indeterminate → 'unknown'."""
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda p: {"refresh_token": "rt"})
|
||||
def test_expiring_token_is_unknown_without_refreshing(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"get_provider_auth_state",
|
||||
lambda provider: {
|
||||
"access_token": _invoke_jwt(seconds=30),
|
||||
"refresh_token": "rt",
|
||||
"scope": auth.DEFAULT_NOUS_SCOPE,
|
||||
},
|
||||
)
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
def _boom():
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", _boom)
|
||||
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
|
||||
|
||||
|
||||
def test_provider_state_exception_falls_through_to_status(monkeypatch):
|
||||
"""If reading provider state throws, fall through to status (don't crash)."""
|
||||
def _boom(p):
|
||||
def test_invalid_token_is_unknown_without_refreshing(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"get_provider_auth_state",
|
||||
lambda provider: {
|
||||
"access_token": "not-a-jwt",
|
||||
"refresh_token": "rt",
|
||||
},
|
||||
)
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
|
||||
|
||||
|
||||
def test_no_provider_state_is_unknown(monkeypatch):
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", lambda provider: None)
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
|
||||
|
||||
|
||||
def test_provider_state_exception_is_unknown(monkeypatch):
|
||||
def _boom(provider):
|
||||
raise RuntimeError("disk error")
|
||||
|
||||
monkeypatch.setattr(auth, "get_provider_auth_state", _boom)
|
||||
monkeypatch.setattr(auth, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
assert get_nous_session_validity() == NOUS_SESSION_VALID
|
||||
_block_live_auth(monkeypatch)
|
||||
|
||||
assert get_nous_session_validity() == NOUS_SESSION_UNKNOWN
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue