Gate tool-gateway behind an env var, so it's not in users' faces until we're ready. Even if users enable it, it'll be blocked server-side for now, until we unlock for non-admin users on tool-gateway.

This commit is contained in:
Robin Fernandes 2026-03-30 13:28:10 +09:00
parent e95965d76a
commit 1cbb1b99cc
35 changed files with 426 additions and 147 deletions

View file

@ -45,6 +45,11 @@ def _restore_tool_and_agent_modules():
sys.modules.update(original_modules)
@pytest.fixture(autouse=True)
def _enable_managed_nous_tools(monkeypatch):
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
def _install_fake_tools_package():
_reset_modules(("tools", "agent"))

View file

@ -44,6 +44,11 @@ def _restore_tool_and_agent_modules():
sys.modules.update(original_modules)
@pytest.fixture(autouse=True)
def _enable_managed_nous_tools(monkeypatch):
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
def _install_fake_tools_package():
tools_package = types.ModuleType("tools")
tools_package.__path__ = [str(TOOLS_DIR)] # type: ignore[attr-defined]

View file

@ -16,7 +16,14 @@ 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):
with patch.dict(
os.environ,
{
"HERMES_ENABLE_NOUS_MANAGED_TOOLS": "1",
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
},
clear=False,
):
result = resolve_managed_tool_gateway(
"firecrawl",
token_reader=lambda: "nous-token",
@ -29,7 +36,14 @@ def test_resolve_managed_tool_gateway_derives_vendor_origin_from_shared_domain()
def test_resolve_managed_tool_gateway_uses_vendor_specific_override():
with patch.dict(os.environ, {"BROWSERBASE_GATEWAY_URL": "http://browserbase-gateway.localhost:3009/"}, clear=False):
with patch.dict(
os.environ,
{
"HERMES_ENABLE_NOUS_MANAGED_TOOLS": "1",
"BROWSERBASE_GATEWAY_URL": "http://browserbase-gateway.localhost:3009/",
},
clear=False,
):
result = resolve_managed_tool_gateway(
"browserbase",
token_reader=lambda: "nous-token",
@ -40,7 +54,14 @@ def test_resolve_managed_tool_gateway_uses_vendor_specific_override():
def test_resolve_managed_tool_gateway_is_inactive_without_nous_token():
with patch.dict(os.environ, {"TOOL_GATEWAY_DOMAIN": "nousresearch.com"}, clear=False):
with patch.dict(
os.environ,
{
"HERMES_ENABLE_NOUS_MANAGED_TOOLS": "1",
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
},
clear=False,
):
result = resolve_managed_tool_gateway(
"firecrawl",
token_reader=lambda: None,
@ -49,6 +70,16 @@ def test_resolve_managed_tool_gateway_is_inactive_without_nous_token():
assert result is None
def test_resolve_managed_tool_gateway_is_disabled_without_feature_flag():
with patch.dict(os.environ, {"TOOL_GATEWAY_DOMAIN": "nousresearch.com"}, clear=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))

View file

@ -7,6 +7,7 @@ terminal_tool_module = importlib.import_module("tools.terminal_tool")
def _clear_terminal_env(monkeypatch):
"""Remove terminal env vars that could affect requirements checks."""
keys = [
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
"TERMINAL_ENV",
"TERMINAL_MODAL_MODE",
"TERMINAL_SSH_HOST",
@ -73,13 +74,14 @@ def test_modal_backend_without_token_or_config_logs_specific_error(monkeypatch,
assert ok is False
assert any(
"Modal backend selected but no direct Modal credentials/config or managed tool gateway was found" in record.getMessage()
"Modal backend selected but no direct Modal credentials/config was found" in record.getMessage()
for record in caplog.records
)
def test_modal_backend_with_managed_gateway_does_not_require_direct_creds_or_minisweagent(monkeypatch, tmp_path):
_clear_terminal_env(monkeypatch)
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
monkeypatch.setenv("TERMINAL_ENV", "modal")
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
@ -115,3 +117,21 @@ def test_modal_backend_direct_mode_does_not_fall_back_to_managed(monkeypatch, ca
"TERMINAL_MODAL_MODE=direct" in record.getMessage()
for record in caplog.records
)
def test_modal_backend_managed_mode_without_feature_flag_logs_clear_error(monkeypatch, caplog, tmp_path):
_clear_terminal_env(monkeypatch)
monkeypatch.setenv("TERMINAL_ENV", "modal")
monkeypatch.setenv("TERMINAL_MODAL_MODE", "managed")
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.setattr(terminal_tool_module, "is_managed_tool_gateway_ready", lambda _vendor: False)
with caplog.at_level(logging.ERROR):
ok = terminal_tool_module.check_terminal_requirements()
assert ok is False
assert any(
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled" in record.getMessage()
for record in caplog.records
)

View file

@ -28,6 +28,7 @@ class TestTerminalRequirements:
assert {"read_file", "write_file", "patch", "search_files"}.issubset(names)
def test_terminal_and_execute_code_tools_resolve_for_managed_modal(self, monkeypatch, tmp_path):
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.delenv("MODAL_TOKEN_ID", raising=False)

View file

@ -11,6 +11,8 @@ Coverage:
import importlib
import json
import os
import sys
import types
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
@ -24,6 +26,7 @@ class TestFirecrawlClientConfig:
tools.web_tools._firecrawl_client = None
tools.web_tools._firecrawl_client_config = None
for key in (
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
"FIRECRAWL_API_KEY",
"FIRECRAWL_API_URL",
"FIRECRAWL_GATEWAY_URL",
@ -32,6 +35,7 @@ class TestFirecrawlClientConfig:
"TOOL_GATEWAY_USER_TOKEN",
):
os.environ.pop(key, None)
os.environ["HERMES_ENABLE_NOUS_MANAGED_TOOLS"] = "1"
def teardown_method(self):
"""Reset client after each test."""
@ -39,6 +43,7 @@ class TestFirecrawlClientConfig:
tools.web_tools._firecrawl_client = None
tools.web_tools._firecrawl_client_config = None
for key in (
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
"FIRECRAWL_API_KEY",
"FIRECRAWL_API_URL",
"FIRECRAWL_GATEWAY_URL",
@ -293,6 +298,7 @@ class TestBackendSelection:
"""
_ENV_KEYS = (
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
"PARALLEL_API_KEY",
"FIRECRAWL_API_KEY",
"FIRECRAWL_API_URL",
@ -304,8 +310,10 @@ class TestBackendSelection:
)
def setup_method(self):
os.environ["HERMES_ENABLE_NOUS_MANAGED_TOOLS"] = "1"
for key in self._ENV_KEYS:
os.environ.pop(key, None)
if key != "HERMES_ENABLE_NOUS_MANAGED_TOOLS":
os.environ.pop(key, None)
def teardown_method(self):
for key in self._ENV_KEYS:
@ -417,11 +425,25 @@ class TestParallelClientConfig:
import tools.web_tools
tools.web_tools._parallel_client = None
os.environ.pop("PARALLEL_API_KEY", None)
fake_parallel = types.ModuleType("parallel")
class Parallel:
def __init__(self, api_key):
self.api_key = api_key
class AsyncParallel:
def __init__(self, api_key):
self.api_key = api_key
fake_parallel.Parallel = Parallel
fake_parallel.AsyncParallel = AsyncParallel
sys.modules["parallel"] = fake_parallel
def teardown_method(self):
import tools.web_tools
tools.web_tools._parallel_client = None
os.environ.pop("PARALLEL_API_KEY", None)
sys.modules.pop("parallel", None)
def test_creates_client_with_key(self):
"""PARALLEL_API_KEY set → creates Parallel client."""
@ -479,6 +501,7 @@ class TestCheckWebApiKey:
"""Test suite for check_web_api_key() unified availability check."""
_ENV_KEYS = (
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
"PARALLEL_API_KEY",
"FIRECRAWL_API_KEY",
"FIRECRAWL_API_URL",
@ -490,8 +513,10 @@ class TestCheckWebApiKey:
)
def setup_method(self):
os.environ["HERMES_ENABLE_NOUS_MANAGED_TOOLS"] = "1"
for key in self._ENV_KEYS:
os.environ.pop(key, None)
if key != "HERMES_ENABLE_NOUS_MANAGED_TOOLS":
os.environ.pop(key, None)
def teardown_method(self):
for key in self._ENV_KEYS: