From 4e4984a11a417c684658e781e5609aa86975f64f Mon Sep 17 00:00:00 2001 From: Robin Fernandes Date: Fri, 29 May 2026 08:17:58 +1000 Subject: [PATCH] test(auth): update nous jwt-only expectations --- tests/hermes_cli/test_auth_commands.py | 8 ++++---- .../test_nous_inference_url_validation.py | 16 ++++++++-------- tests/hermes_cli/test_proxy.py | 6 +++--- tests/run_agent/test_provider_parity.py | 19 ++++++++++++++++++- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/tests/hermes_cli/test_auth_commands.py b/tests/hermes_cli/test_auth_commands.py index 371a7080c..ae95c2747 100644 --- a/tests/hermes_cli/test_auth_commands.py +++ b/tests/hermes_cli/test_auth_commands.py @@ -155,17 +155,17 @@ def test_auth_add_nous_oauth_persists_pool_entry(tmp_path, monkeypatch): assert not any(item["source"] == "manual:device_code" for item in entries) entry = device_code_entries[0] assert entry["source"] == "device_code" - assert entry["agent_key"] == "ak-test" + assert entry["agent_key"] == token assert entry["portal_base_url"] == "https://portal.example.com" # `hermes auth add nous` must also populate providers.nous so the - # 401-recovery path (resolve_nous_runtime_credentials) can mint a fresh - # agent_key when the 24h TTL expires. If this mirror is missing, recovery + # 401-recovery path (resolve_nous_runtime_credentials) can refresh an + # invoke JWT when the token expires. If this mirror is missing, recovery # raises "Hermes is not logged into Nous Portal" and the agent dies. singleton = payload["providers"]["nous"] assert singleton["access_token"] == token assert singleton["refresh_token"] == "refresh-token" - assert singleton["agent_key"] == "ak-test" + assert singleton["agent_key"] == token assert singleton["portal_base_url"] == "https://portal.example.com" assert singleton["inference_base_url"] == "https://inference.example.com/v1" diff --git a/tests/hermes_cli/test_nous_inference_url_validation.py b/tests/hermes_cli/test_nous_inference_url_validation.py index f4f899462..e4c70786b 100644 --- a/tests/hermes_cli/test_nous_inference_url_validation.py +++ b/tests/hermes_cli/test_nous_inference_url_validation.py @@ -1,8 +1,8 @@ """Regression tests for Nous Portal inference_base_url host-allowlist validation. -A poisoned ``inference_base_url`` from the Portal refresh / agent-key-mint -response (network MITM, malicious response injection) would otherwise be -persisted to auth.json and forwarded the user's legitimate agent_key +A poisoned ``inference_base_url`` from a Portal refresh response (network +MITM, malicious response injection) would otherwise be persisted to +auth.json and forwarded with the user's legitimate invoke JWT bearer on every subsequent proxy request, exfiltrating their inference budget and opening a response-injection channel into the IDE / chat client. ``_validate_nous_inference_url_from_network()`` blocks any URL @@ -11,7 +11,7 @@ outside the allowlist at the source. These tests verify: 1. The validator's host + scheme rules. -2. Each of the five NETWORK call sites in ``auth.py`` calls the validator +2. Each of the two NETWORK call sites in ``auth.py`` calls the validator rather than the unrestricted ``_optional_base_url`` helper. 3. The proxy adapter applies the validator as belt-and-suspenders. 4. The env-var override path (``NOUS_INFERENCE_BASE_URL``) is NOT @@ -124,7 +124,7 @@ class TestValidatorRules: class TestCallSiteWiring: - """Verify the validator is actually wired into all 5 NETWORK call sites. + """Verify the validator is actually wired into all auth.py NETWORK call sites. These are not behaviour-end-to-end tests (the surrounding code is several hundred lines per site with extensive HTTP mocking @@ -161,7 +161,7 @@ class TestCallSiteWiring: ) def test_validator_wired_at_all_known_call_sites(self): - """All 5 known NETWORK sites use the validator. If this count + """All 2 known auth.py NETWORK sites use the validator. If this count drops, someone removed protection; if it grows, audit the new site to be sure validation is appropriate.""" source = self._read_auth_source() @@ -171,8 +171,8 @@ class TestCallSiteWiring: mint_count = source.count( '_validate_nous_inference_url_from_network(mint_payload.get("inference_base_url"))' ) - assert refresh_count == 3, f"expected 3 refresh sites, found {refresh_count}" - assert mint_count == 2, f"expected 2 mint sites, found {mint_count}" + assert refresh_count == 2, f"expected 2 refresh sites, found {refresh_count}" + assert mint_count == 0, f"expected 0 mint sites, found {mint_count}" def test_proxy_adapter_also_validates(self): """The Nous proxy adapter applies the validator as defense-in-depth diff --git a/tests/hermes_cli/test_proxy.py b/tests/hermes_cli/test_proxy.py index 0e2a4efde..6545bbd51 100644 --- a/tests/hermes_cli/test_proxy.py +++ b/tests/hermes_cli/test_proxy.py @@ -260,8 +260,8 @@ def test_nous_adapter_quarantines_terminal_refresh_failure(tmp_path, monkeypatch assert stored.get("credential_pool", {}).get("nous") == [] -def test_nous_adapter_get_credential_raises_when_no_agent_key_returned(tmp_path, monkeypatch): - """If the refresh helper succeeds but produces no agent_key, we surface a clear error.""" +def test_nous_adapter_get_credential_raises_when_no_jwt_returned(tmp_path, monkeypatch): + """If the refresh helper succeeds but produces no JWT, we surface a clear error.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path)) _write_auth_store(tmp_path, { "access_token": "access-tok", @@ -273,7 +273,7 @@ def test_nous_adapter_get_credential_raises_when_no_agent_key_returned(tmp_path, return_value={"access_token": "a", "refresh_token": "r"}, ): adapter = NousPortalAdapter() - with pytest.raises(RuntimeError, match="did not return a usable agent_key"): + with pytest.raises(RuntimeError, match="did not return a usable inference JWT"): adapter.get_credential() diff --git a/tests/run_agent/test_provider_parity.py b/tests/run_agent/test_provider_parity.py index 4b80d2e1b..523c5b09d 100644 --- a/tests/run_agent/test_provider_parity.py +++ b/tests/run_agent/test_provider_parity.py @@ -4,6 +4,8 @@ and handles responses properly for all supported providers. Ensures changes to one provider path don't silently break another. """ +import base64 +import json import sys import types from types import SimpleNamespace @@ -35,6 +37,17 @@ def _tool_defs(*names): ] +def _fake_invoke_jwt() -> str: + def _part(payload): + raw = json.dumps(payload, separators=(",", ":")).encode("utf-8") + return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + + return ( + f"{_part({'alg': 'none', 'typ': 'JWT'})}." + f"{_part({'scope': 'inference:invoke', 'exp': 4102444800})}.sig" + ) + + class _FakeOpenAI: def __init__(self, **kw): self.api_key = kw.get("api_key", "test") @@ -925,7 +938,11 @@ class TestAuxiliaryClientProviderPriority: def test_nous_when_no_openrouter(self, monkeypatch): monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) from agent.auxiliary_client import get_text_auxiliary_client - with patch("agent.auxiliary_client._read_nous_auth", return_value={"access_token": "nous-tok"}), \ + nous_auth = { + "access_token": _fake_invoke_jwt(), + "scope": "inference:invoke", + } + with patch("agent.auxiliary_client._read_nous_auth", return_value=nous_auth), \ patch("agent.auxiliary_client.OpenAI") as mock, \ patch("hermes_cli.models.get_nous_recommended_aux_model", return_value=None): client, model = get_text_auxiliary_client()