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:
@@ -10,6 +10,7 @@ import requests
|
||||
|
||||
from tools.browser_providers.base import CloudBrowserProvider
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_pending_create_keys: Dict[str, str] = {}
|
||||
@@ -93,10 +94,15 @@ class BrowserbaseProvider(CloudBrowserProvider):
|
||||
def _get_config(self) -> Dict[str, Any]:
|
||||
config = self._get_config_or_none()
|
||||
if config is None:
|
||||
raise ValueError(
|
||||
"Browserbase requires either direct BROWSERBASE_API_KEY/BROWSERBASE_PROJECT_ID credentials "
|
||||
"or a managed Browserbase gateway configuration."
|
||||
message = (
|
||||
"Browserbase requires direct BROWSERBASE_API_KEY/BROWSERBASE_PROJECT_ID credentials."
|
||||
)
|
||||
if managed_nous_tools_enabled():
|
||||
message = (
|
||||
"Browserbase requires either direct BROWSERBASE_API_KEY/BROWSERBASE_PROJECT_ID "
|
||||
"credentials or a managed Browserbase gateway configuration."
|
||||
)
|
||||
raise ValueError(message)
|
||||
return config
|
||||
|
||||
def create_session(self, task_id: str) -> Dict[str, object]:
|
||||
|
||||
@@ -39,6 +39,7 @@ from urllib.parse import urlencode
|
||||
import fal_client
|
||||
from tools.debug_helpers import DebugSession
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -416,9 +417,10 @@ def image_generate_tool(
|
||||
|
||||
# Check API key availability
|
||||
if not (os.getenv("FAL_KEY") or _resolve_managed_fal_gateway()):
|
||||
raise ValueError(
|
||||
"FAL_KEY environment variable not set and managed FAL gateway is unavailable"
|
||||
)
|
||||
message = "FAL_KEY environment variable not set"
|
||||
if managed_nous_tools_enabled():
|
||||
message += " and managed FAL gateway is unavailable"
|
||||
raise ValueError(message)
|
||||
|
||||
# Validate other parameters
|
||||
validated_params = _validate_parameters(
|
||||
|
||||
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
from hermes_cli.config import get_hermes_home
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled
|
||||
|
||||
_DEFAULT_TOOL_GATEWAY_DOMAIN = "nousresearch.com"
|
||||
_DEFAULT_TOOL_GATEWAY_SCHEME = "https"
|
||||
@@ -131,6 +132,9 @@ def resolve_managed_tool_gateway(
|
||||
token_reader: Optional[Callable[[], Optional[str]]] = None,
|
||||
) -> Optional[ManagedToolGatewayConfig]:
|
||||
"""Resolve shared managed-tool gateway config for a vendor."""
|
||||
if not managed_nous_tools_enabled():
|
||||
return None
|
||||
|
||||
resolved_gateway_builder = gateway_builder or build_vendor_gateway_url
|
||||
resolved_token_reader = token_reader or read_nous_access_token
|
||||
|
||||
|
||||
@@ -65,7 +65,12 @@ def ensure_minisweagent_on_path(_repo_root: Path | None = None) -> None:
|
||||
|
||||
# Singularity helpers (scratch dir, SIF cache) now live in tools/environments/singularity.py
|
||||
from tools.environments.singularity import _get_scratch_dir
|
||||
from tools.tool_backend_helpers import has_direct_modal_credentials, normalize_modal_mode
|
||||
from tools.tool_backend_helpers import (
|
||||
coerce_modal_mode,
|
||||
has_direct_modal_credentials,
|
||||
managed_nous_tools_enabled,
|
||||
normalize_modal_mode,
|
||||
)
|
||||
|
||||
|
||||
# Disk usage warning threshold (in GB)
|
||||
@@ -506,7 +511,7 @@ def _get_env_config() -> Dict[str, Any]:
|
||||
|
||||
return {
|
||||
"env_type": env_type,
|
||||
"modal_mode": normalize_modal_mode(os.getenv("TERMINAL_MODAL_MODE", "auto")),
|
||||
"modal_mode": coerce_modal_mode(os.getenv("TERMINAL_MODAL_MODE", "auto")),
|
||||
"docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image),
|
||||
"docker_forward_env": _parse_env_var("TERMINAL_DOCKER_FORWARD_ENV", "[]", json.loads, "valid JSON"),
|
||||
"singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"),
|
||||
@@ -541,9 +546,13 @@ def _get_env_config() -> Dict[str, Any]:
|
||||
|
||||
def _get_modal_backend_state(modal_mode: object | None) -> Dict[str, Any]:
|
||||
"""Resolve direct vs managed Modal backend selection."""
|
||||
requested_mode = coerce_modal_mode(modal_mode)
|
||||
normalized_mode = normalize_modal_mode(modal_mode)
|
||||
has_direct = has_direct_modal_credentials()
|
||||
managed_ready = is_managed_tool_gateway_ready("modal")
|
||||
managed_mode_blocked = (
|
||||
requested_mode == "managed" and not managed_nous_tools_enabled()
|
||||
)
|
||||
|
||||
if normalized_mode == "managed":
|
||||
selected_backend = "managed" if managed_ready else None
|
||||
@@ -553,9 +562,11 @@ def _get_modal_backend_state(modal_mode: object | None) -> Dict[str, Any]:
|
||||
selected_backend = "direct" if has_direct else "managed" if managed_ready else None
|
||||
|
||||
return {
|
||||
"requested_mode": requested_mode,
|
||||
"mode": normalized_mode,
|
||||
"has_direct": has_direct,
|
||||
"managed_ready": managed_ready,
|
||||
"managed_mode_blocked": managed_mode_blocked,
|
||||
"selected_backend": selected_backend,
|
||||
}
|
||||
|
||||
@@ -636,6 +647,13 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
|
||||
)
|
||||
|
||||
if modal_state["selected_backend"] != "direct":
|
||||
if modal_state["managed_mode_blocked"]:
|
||||
raise ValueError(
|
||||
"Modal backend is configured for managed mode, but "
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled and no direct "
|
||||
"Modal credentials/config were found. Enable the feature flag or "
|
||||
"choose TERMINAL_MODAL_MODE=direct/auto."
|
||||
)
|
||||
if modal_state["mode"] == "managed":
|
||||
raise ValueError(
|
||||
"Modal backend is configured for managed mode, but the managed tool gateway is unavailable."
|
||||
@@ -644,9 +662,12 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
|
||||
raise ValueError(
|
||||
"Modal backend is configured for direct mode, but no direct Modal credentials/config were found."
|
||||
)
|
||||
raise ValueError(
|
||||
"Modal backend selected but no direct Modal credentials/config or managed tool gateway was found."
|
||||
)
|
||||
message = "Modal backend selected but no direct Modal credentials/config was found."
|
||||
if managed_nous_tools_enabled():
|
||||
message = (
|
||||
"Modal backend selected but no direct Modal credentials/config or managed tool gateway was found."
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return _ModalEnvironment(
|
||||
image=image, cwd=cwd, timeout=timeout,
|
||||
@@ -1283,25 +1304,48 @@ def check_terminal_requirements() -> bool:
|
||||
return True
|
||||
|
||||
if modal_state["selected_backend"] != "direct":
|
||||
if modal_state["managed_mode_blocked"]:
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=managed, but "
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled and no direct "
|
||||
"Modal credentials/config were found. Enable the feature flag "
|
||||
"or choose TERMINAL_MODAL_MODE=direct/auto."
|
||||
)
|
||||
return False
|
||||
if modal_state["mode"] == "managed":
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=managed, but the managed "
|
||||
"tool gateway is unavailable. Configure the managed gateway or choose "
|
||||
"TERMINAL_MODAL_MODE=direct/auto."
|
||||
)
|
||||
return False
|
||||
elif modal_state["mode"] == "direct":
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=direct, but no direct "
|
||||
"Modal credentials/config were found. Configure Modal or choose "
|
||||
"TERMINAL_MODAL_MODE=managed/auto."
|
||||
)
|
||||
if managed_nous_tools_enabled():
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=direct, but no direct "
|
||||
"Modal credentials/config were found. Configure Modal or choose "
|
||||
"TERMINAL_MODAL_MODE=managed/auto."
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=direct, but no direct "
|
||||
"Modal credentials/config were found. Configure Modal or choose "
|
||||
"TERMINAL_MODAL_MODE=auto."
|
||||
)
|
||||
return False
|
||||
else:
|
||||
logger.error(
|
||||
"Modal backend selected but no direct Modal credentials/config or managed "
|
||||
"tool gateway was found. Configure Modal, set up the managed gateway, "
|
||||
"or choose a different TERMINAL_ENV."
|
||||
)
|
||||
return False
|
||||
if managed_nous_tools_enabled():
|
||||
logger.error(
|
||||
"Modal backend selected but no direct Modal credentials/config or managed "
|
||||
"tool gateway was found. Configure Modal, set up the managed gateway, "
|
||||
"or choose a different TERMINAL_ENV."
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"Modal backend selected but no direct Modal credentials/config was found. "
|
||||
"Configure Modal or choose a different TERMINAL_ENV."
|
||||
)
|
||||
return False
|
||||
|
||||
if importlib.util.find_spec("swerex") is None:
|
||||
logger.error("swe-rex is required for direct modal terminal backend: pip install 'swe-rex[modal]'")
|
||||
|
||||
@@ -5,26 +5,40 @@ from __future__ import annotations
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from utils import env_var_enabled
|
||||
|
||||
_DEFAULT_BROWSER_PROVIDER = "local"
|
||||
_DEFAULT_MODAL_MODE = "auto"
|
||||
_VALID_MODAL_MODES = {"auto", "direct", "managed"}
|
||||
|
||||
|
||||
def managed_nous_tools_enabled() -> bool:
|
||||
"""Return True when the hidden Nous-managed tools feature flag is enabled."""
|
||||
return env_var_enabled("HERMES_ENABLE_NOUS_MANAGED_TOOLS")
|
||||
|
||||
|
||||
def normalize_browser_cloud_provider(value: object | None) -> str:
|
||||
"""Return a normalized browser provider key."""
|
||||
provider = str(value or _DEFAULT_BROWSER_PROVIDER).strip().lower()
|
||||
return provider or _DEFAULT_BROWSER_PROVIDER
|
||||
|
||||
|
||||
def normalize_modal_mode(value: object | None) -> str:
|
||||
"""Return a normalized modal execution mode."""
|
||||
def coerce_modal_mode(value: object | None) -> str:
|
||||
"""Return the requested modal mode when valid, else the default."""
|
||||
mode = str(value or _DEFAULT_MODAL_MODE).strip().lower()
|
||||
if mode in _VALID_MODAL_MODES:
|
||||
return mode
|
||||
return _DEFAULT_MODAL_MODE
|
||||
|
||||
|
||||
def normalize_modal_mode(value: object | None) -> str:
|
||||
"""Return a normalized modal execution mode."""
|
||||
mode = coerce_modal_mode(value)
|
||||
if mode == "managed" and not managed_nous_tools_enabled():
|
||||
return "direct"
|
||||
return mode
|
||||
|
||||
|
||||
def has_direct_modal_credentials() -> bool:
|
||||
"""Return True when direct Modal credentials/config are available."""
|
||||
return bool(
|
||||
|
||||
@@ -33,8 +33,9 @@ from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from utils import is_truthy_value
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import resolve_openai_audio_api_key
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled, resolve_openai_audio_api_key
|
||||
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
@@ -122,11 +123,7 @@ def is_stt_enabled(stt_config: Optional[dict] = None) -> bool:
|
||||
if stt_config is None:
|
||||
stt_config = _load_stt_config()
|
||||
enabled = stt_config.get("enabled", True)
|
||||
if isinstance(enabled, str):
|
||||
return enabled.strip().lower() in ("true", "1", "yes", "on")
|
||||
if enabled is None:
|
||||
return True
|
||||
return bool(enabled)
|
||||
return is_truthy_value(enabled, default=True)
|
||||
|
||||
|
||||
def _has_openai_audio_backend() -> bool:
|
||||
@@ -586,9 +583,10 @@ def _resolve_openai_audio_client_config() -> tuple[str, str]:
|
||||
|
||||
managed_gateway = resolve_managed_tool_gateway("openai-audio")
|
||||
if managed_gateway is None:
|
||||
raise ValueError(
|
||||
"Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is set, and the managed OpenAI audio gateway is unavailable"
|
||||
)
|
||||
message = "Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is set"
|
||||
if managed_nous_tools_enabled():
|
||||
message += ", and the managed OpenAI audio gateway is unavailable"
|
||||
raise ValueError(message)
|
||||
|
||||
return managed_gateway.nous_user_token, urljoin(
|
||||
f"{managed_gateway.gateway_origin.rstrip('/')}/", "v1"
|
||||
|
||||
@@ -40,7 +40,7 @@ from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import resolve_openai_audio_api_key
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled, resolve_openai_audio_api_key
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lazy imports -- providers are imported only when actually used to avoid
|
||||
@@ -565,9 +565,10 @@ def _resolve_openai_audio_client_config() -> tuple[str, str]:
|
||||
|
||||
managed_gateway = resolve_managed_tool_gateway("openai-audio")
|
||||
if managed_gateway is None:
|
||||
raise ValueError(
|
||||
"Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is set, and the managed OpenAI audio gateway is unavailable"
|
||||
)
|
||||
message = "Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is set"
|
||||
if managed_nous_tools_enabled():
|
||||
message += ", and the managed OpenAI audio gateway is unavailable"
|
||||
raise ValueError(message)
|
||||
|
||||
return managed_gateway.nous_user_token, urljoin(
|
||||
f"{managed_gateway.gateway_origin.rstrip('/')}/", "v1"
|
||||
|
||||
@@ -54,6 +54,7 @@ from tools.managed_tool_gateway import (
|
||||
read_nous_access_token as _read_nous_access_token,
|
||||
resolve_managed_tool_gateway,
|
||||
)
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled
|
||||
from tools.url_safety import is_safe_url
|
||||
from tools.website_policy import check_website_access
|
||||
|
||||
@@ -152,12 +153,46 @@ def _has_direct_firecrawl_config() -> bool:
|
||||
|
||||
def _raise_web_backend_configuration_error() -> None:
|
||||
"""Raise a clear error for unsupported web backend configuration."""
|
||||
raise ValueError(
|
||||
message = (
|
||||
"Web tools are not configured. "
|
||||
"Set FIRECRAWL_API_KEY for cloud Firecrawl, set FIRECRAWL_API_URL for a self-hosted Firecrawl instance, "
|
||||
"or, if you are a Nous Subscriber, login to Nous (`hermes model`) and provide "
|
||||
"FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN."
|
||||
"Set FIRECRAWL_API_KEY for cloud Firecrawl or set FIRECRAWL_API_URL for a self-hosted Firecrawl instance."
|
||||
)
|
||||
if managed_nous_tools_enabled():
|
||||
message += (
|
||||
" If you have the hidden Nous-managed tools flag enabled, you can also login to Nous "
|
||||
"(`hermes model`) and provide FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN."
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def _firecrawl_backend_help_suffix() -> str:
|
||||
"""Return optional managed-gateway guidance for Firecrawl help text."""
|
||||
if not managed_nous_tools_enabled():
|
||||
return ""
|
||||
return (
|
||||
", or, if you have the hidden Nous-managed tools flag enabled, login to Nous and use "
|
||||
"FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN"
|
||||
)
|
||||
|
||||
|
||||
def _web_requires_env() -> list[str]:
|
||||
"""Return tool metadata env vars for the currently enabled web backends."""
|
||||
requires = [
|
||||
"PARALLEL_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
"FIRECRAWL_API_KEY",
|
||||
"FIRECRAWL_API_URL",
|
||||
]
|
||||
if managed_nous_tools_enabled():
|
||||
requires.extend(
|
||||
[
|
||||
"FIRECRAWL_GATEWAY_URL",
|
||||
"TOOL_GATEWAY_DOMAIN",
|
||||
"TOOL_GATEWAY_SCHEME",
|
||||
"TOOL_GATEWAY_USER_TOKEN",
|
||||
]
|
||||
)
|
||||
return requires
|
||||
|
||||
|
||||
def _get_firecrawl_client():
|
||||
@@ -1410,10 +1445,8 @@ async def web_crawl_tool(
|
||||
# web_crawl requires Firecrawl or the Firecrawl tool-gateway — Parallel has no crawl API
|
||||
if not check_firecrawl_api_key():
|
||||
return json.dumps({
|
||||
"error": "web_crawl requires Firecrawl. Set FIRECRAWL_API_KEY, FIRECRAWL_API_URL, "
|
||||
"or, if you are a Nous Subscriber, login to Nous and use FIRECRAWL_GATEWAY_URL, "
|
||||
"or TOOL_GATEWAY_DOMAIN, "
|
||||
"or use web_search + web_extract instead.",
|
||||
"error": "web_crawl requires Firecrawl. Set FIRECRAWL_API_KEY, FIRECRAWL_API_URL"
|
||||
f"{_firecrawl_backend_help_suffix()}, or use web_search + web_extract instead.",
|
||||
"success": False,
|
||||
}, ensure_ascii=False)
|
||||
|
||||
@@ -1754,9 +1787,8 @@ if __name__ == "__main__":
|
||||
else:
|
||||
print("❌ No web search backend configured")
|
||||
print(
|
||||
"Set PARALLEL_API_KEY, TAVILY_API_KEY, FIRECRAWL_API_KEY, FIRECRAWL_API_URL, "
|
||||
"or, if you are a Nous Subscriber, login to Nous and use "
|
||||
"FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN"
|
||||
"Set PARALLEL_API_KEY, TAVILY_API_KEY, FIRECRAWL_API_KEY, FIRECRAWL_API_URL"
|
||||
f"{_firecrawl_backend_help_suffix()}"
|
||||
)
|
||||
|
||||
if not nous_available:
|
||||
@@ -1867,16 +1899,7 @@ registry.register(
|
||||
schema=WEB_SEARCH_SCHEMA,
|
||||
handler=lambda args, **kw: web_search_tool(args.get("query", ""), limit=5),
|
||||
check_fn=check_web_api_key,
|
||||
requires_env=[
|
||||
"PARALLEL_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
"FIRECRAWL_GATEWAY_URL",
|
||||
"TOOL_GATEWAY_DOMAIN",
|
||||
"TOOL_GATEWAY_SCHEME",
|
||||
"TOOL_GATEWAY_USER_TOKEN",
|
||||
"FIRECRAWL_API_KEY",
|
||||
"FIRECRAWL_API_URL",
|
||||
],
|
||||
requires_env=_web_requires_env(),
|
||||
emoji="🔍",
|
||||
)
|
||||
registry.register(
|
||||
@@ -1886,16 +1909,7 @@ registry.register(
|
||||
handler=lambda args, **kw: web_extract_tool(
|
||||
args.get("urls", [])[:5] if isinstance(args.get("urls"), list) else [], "markdown"),
|
||||
check_fn=check_web_api_key,
|
||||
requires_env=[
|
||||
"PARALLEL_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
"FIRECRAWL_GATEWAY_URL",
|
||||
"TOOL_GATEWAY_DOMAIN",
|
||||
"TOOL_GATEWAY_SCHEME",
|
||||
"TOOL_GATEWAY_USER_TOKEN",
|
||||
"FIRECRAWL_API_KEY",
|
||||
"FIRECRAWL_API_URL",
|
||||
],
|
||||
requires_env=_web_requires_env(),
|
||||
is_async=True,
|
||||
emoji="📄",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user