test(auth): update nous jwt-only expectations
This commit is contained in:
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
Reference in New Issue
Block a user