mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Check no committed infographics (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Waiting to run
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build, Test, and Publish / merge (push) Blocked by required conditions
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
212 lines
8.2 KiB
Python
212 lines
8.2 KiB
Python
import os
|
|
import json
|
|
from datetime import datetime, timedelta, timezone
|
|
from importlib.util import module_from_spec, spec_from_file_location
|
|
from pathlib import Path
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
MODULE_PATH = Path(__file__).resolve().parents[2] / "tools" / "managed_tool_gateway.py"
|
|
MODULE_SPEC = spec_from_file_location("managed_tool_gateway_test_module", MODULE_PATH)
|
|
assert MODULE_SPEC and MODULE_SPEC.loader
|
|
managed_tool_gateway = module_from_spec(MODULE_SPEC)
|
|
sys.modules[MODULE_SPEC.name] = managed_tool_gateway
|
|
MODULE_SPEC.loader.exec_module(managed_tool_gateway)
|
|
is_managed_tool_gateway_ready = managed_tool_gateway.is_managed_tool_gateway_ready
|
|
resolve_managed_tool_gateway = managed_tool_gateway.resolve_managed_tool_gateway
|
|
|
|
|
|
def test_resolve_managed_tool_gateway_derives_vendor_origin_from_shared_domain():
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
|
},
|
|
clear=False,
|
|
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
result = resolve_managed_tool_gateway(
|
|
"firecrawl",
|
|
token_reader=lambda: "nous-token",
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.gateway_origin == "https://firecrawl-gateway.nousresearch.com"
|
|
assert result.nous_user_token == "nous-token"
|
|
assert result.managed_mode is True
|
|
|
|
|
|
def test_resolve_managed_tool_gateway_uses_vendor_specific_override():
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"BROWSER_USE_GATEWAY_URL": "http://browser-use-gateway.localhost:3009/",
|
|
},
|
|
clear=False,
|
|
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
result = resolve_managed_tool_gateway(
|
|
"browser-use",
|
|
token_reader=lambda: "nous-token",
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.gateway_origin == "http://browser-use-gateway.localhost:3009"
|
|
|
|
|
|
def test_resolve_managed_tool_gateway_is_inactive_without_nous_token():
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
|
},
|
|
clear=False,
|
|
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
result = resolve_managed_tool_gateway(
|
|
"firecrawl",
|
|
token_reader=lambda: None,
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
def test_resolve_managed_tool_gateway_is_disabled_without_subscription():
|
|
with patch.dict(os.environ, {"TOOL_GATEWAY_DOMAIN": "nousresearch.com"}, clear=False), \
|
|
patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=False):
|
|
result = resolve_managed_tool_gateway(
|
|
"firecrawl",
|
|
token_reader=lambda: "nous-token",
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
def test_read_nous_access_token_refreshes_expiring_cached_token(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("TOOL_GATEWAY_USER_TOKEN", raising=False)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=30)).isoformat()
|
|
(tmp_path / "auth.json").write_text(json.dumps({
|
|
"providers": {
|
|
"nous": {
|
|
"access_token": "stale-token",
|
|
"refresh_token": "refresh-token",
|
|
"expires_at": expires_at,
|
|
}
|
|
}
|
|
}))
|
|
monkeypatch.setattr(
|
|
"hermes_cli.auth.resolve_nous_access_token",
|
|
lambda refresh_skew_seconds=120: "fresh-token",
|
|
)
|
|
|
|
assert managed_tool_gateway.read_nous_access_token() == "fresh-token"
|
|
|
|
|
|
def test_managed_vendor_endpoints_pin_the_deployed_gateway_url():
|
|
"""The exact URL an agent may connect to is a code fact, not a lookup.
|
|
|
|
Exercises the real ``build_vendor_gateway_url`` (which once resolved a
|
|
typo'd pseudo-vendor to a non-existent host while every other test stubbed
|
|
it): default builder, real deployed host, pinned vendor path.
|
|
"""
|
|
with patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True), \
|
|
patch.dict(
|
|
os.environ,
|
|
{"TOOL_GATEWAY_DOMAIN": "nousresearch.com", "TOOL_GATEWAY_SCHEME": "https"},
|
|
clear=False,
|
|
):
|
|
os.environ.pop("TOOL_GATEWAY_URL", None)
|
|
endpoints = managed_tool_gateway.managed_vendor_endpoints("bfl")
|
|
|
|
assert endpoints == {
|
|
"origin": "https://tool-gateway.nousresearch.com",
|
|
"base_url": "https://tool-gateway.nousresearch.com/api/bfl",
|
|
"upload_path": "/api/uploads/bfl",
|
|
}
|
|
|
|
|
|
def test_managed_vendor_endpoints_unreachable_when_managed_tools_disabled():
|
|
with patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=False):
|
|
assert managed_tool_gateway.managed_vendor_endpoints("bfl") is None
|
|
|
|
|
|
def test_managed_gateway_auth_headers_carry_the_bearer():
|
|
with patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
headers = managed_tool_gateway.managed_gateway_auth_headers(
|
|
"https://tool-gateway.example.com/api/bfl/generations",
|
|
gateway_builder=lambda vendor: f"https://{vendor}-gateway.example.com",
|
|
token_reader=lambda: "nous-token",
|
|
)
|
|
|
|
assert headers == {"Authorization": "Bearer nous-token"}
|
|
|
|
|
|
def test_managed_gateway_auth_headers_reflect_a_rotated_token():
|
|
# Read fresh on every call: a Nous access token expires within the hour,
|
|
# and a long session must not keep presenting a dead bearer.
|
|
tokens = iter(["first-token", "second-token"])
|
|
builder = lambda vendor: f"https://{vendor}-gateway.example.com"
|
|
url = "https://tool-gateway.example.com/api/bfl/generations"
|
|
|
|
with patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
first = managed_tool_gateway.managed_gateway_auth_headers(url, builder, lambda: next(tokens))
|
|
second = managed_tool_gateway.managed_gateway_auth_headers(url, builder, lambda: next(tokens))
|
|
|
|
assert first["Authorization"] == "Bearer first-token"
|
|
assert second["Authorization"] == "Bearer second-token"
|
|
|
|
|
|
def test_managed_gateway_auth_headers_refuse_a_url_off_the_gateway_origin():
|
|
# Gated on the URL, never a name: our bearer must never be handed to a
|
|
# host that merely looks managed.
|
|
with patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
assert managed_tool_gateway.managed_gateway_auth_headers(
|
|
"https://attacker.example/api/bfl/generations",
|
|
gateway_builder=lambda vendor: f"https://{vendor}-gateway.example.com",
|
|
token_reader=lambda: "nous-token",
|
|
) == {}
|
|
|
|
|
|
def test_managed_gateway_auth_headers_empty_without_a_token():
|
|
# Empty rather than raising, so a caller can say "sign in" instead of
|
|
# sending an unauthenticated request.
|
|
with patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
assert managed_tool_gateway.managed_gateway_auth_headers(
|
|
"https://tool-gateway.example.com/api/bfl/generations",
|
|
gateway_builder=lambda vendor: f"https://{vendor}-gateway.example.com",
|
|
token_reader=lambda: None,
|
|
) == {}
|
|
|
|
|
|
def test_is_managed_tool_gateway_ready_skips_refresh_for_expired_cached_token(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("TOOL_GATEWAY_USER_TOKEN", raising=False)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
expired_at = (datetime.now(timezone.utc) - timedelta(seconds=30)).isoformat()
|
|
(tmp_path / "auth.json").write_text(json.dumps({
|
|
"providers": {
|
|
"nous": {
|
|
"access_token": "expired-token",
|
|
"refresh_token": "refresh-token",
|
|
"expires_at": expired_at,
|
|
}
|
|
}
|
|
}))
|
|
refresh_calls = []
|
|
|
|
def _record_refresh(*, refresh_skew_seconds=120, **_kwargs):
|
|
refresh_calls.append(refresh_skew_seconds)
|
|
return "fresh-token"
|
|
|
|
monkeypatch.setattr(
|
|
"hermes_cli.auth.resolve_nous_access_token",
|
|
_record_refresh,
|
|
)
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TOOL_GATEWAY_DOMAIN": "nousresearch.com"},
|
|
clear=False,
|
|
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
|
assert is_managed_tool_gateway_ready("modal") is True
|
|
|
|
assert refresh_calls == []
|