mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend Both Vercel-hosted integrations are removed end-to-end. Users on the AI Gateway should switch to OpenRouter or one of the other aggregators (Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should switch to Docker, Modal, Daytona, or SSH. What's removed: - `plugins/model-providers/ai-gateway/` provider plugin - `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper - `tools/environments/vercel_sandbox.py` terminal backend - `ai-gateway` provider wiring across auth, doctor, setup, models, config, status, providers, main, web_server, model_normalize, dump - `vercel_sandbox` backend wiring across terminal_tool, file_tools, code_execution_tool, file_operations, approval, skills_tool, environments/local, credential_files, lazy_deps, prompt_builder, cli, gateway/run - `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client header set, run_agent base-URL header/reasoning special-cases - `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock - env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`, `TERMINAL_VERCEL_RUNTIME` - Tests: deletes test_ai_gateway_models.py and test_vercel_sandbox_environment.py; scrubs references across 23 surviving test files (no entire tests deleted unless they were dedicated to AI Gateway / Sandbox) - Docs: provider tables, env-var reference, setup guides, security notes, tool config, terminal-backend tables — English plus zh-Hans i18n parity - `hermes-agent` skill: provider table entry and remote-backend list What stays (intentional): - `popular-web-designs/templates/vercel.md` — CSS design reference, unrelated to Vercel-the-AI-product - `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN response header, useful diag signal on any Vercel-hosted endpoint - `vercel-labs/agent-browser` URL in browser config — lightpanda browser project, different OSS effort - `userStories.json` historical contributor entry mentioning Vercel Sandbox — archive, not active docs Validation: - 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`) - Full repo `py_compile` clean - Live import of every touched module + invariant check (no `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`) * test: convert profile-count check from change-detector to invariant The hardcoded "== 34" assertion broke when ai-gateway was removed. Per AGENTS.md change-detector-test guidance, assert the relationship (registry count >= number of plugin dirs) instead of a literal count. Counts shift when providers are added/removed; that's expected.
188 lines
7.2 KiB
Python
188 lines
7.2 KiB
Python
import importlib
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
terminal_tool_module = importlib.import_module("tools.terminal_tool")
|
|
|
|
|
|
def _clear_terminal_env(monkeypatch):
|
|
"""Remove terminal env vars that could affect requirements checks."""
|
|
keys = [
|
|
"TERMINAL_ENV",
|
|
"TERMINAL_CONTAINER_CPU",
|
|
"TERMINAL_CONTAINER_DISK",
|
|
"TERMINAL_CONTAINER_MEMORY",
|
|
"TERMINAL_DOCKER_FORWARD_ENV",
|
|
"TERMINAL_DOCKER_VOLUMES",
|
|
"TERMINAL_LIFETIME_SECONDS",
|
|
"TERMINAL_MODAL_MODE",
|
|
"TERMINAL_SSH_HOST",
|
|
"TERMINAL_SSH_PORT",
|
|
"TERMINAL_SSH_USER",
|
|
"TERMINAL_TIMEOUT",
|
|
"MODAL_TOKEN_ID",
|
|
"MODAL_TOKEN_SECRET",
|
|
"HOME",
|
|
"USERPROFILE",
|
|
]
|
|
for key in keys:
|
|
monkeypatch.delenv(key, raising=False)
|
|
# Default: no Nous subscription — patch both the terminal_tool local
|
|
# binding and tool_backend_helpers (used by resolve_modal_backend_state).
|
|
monkeypatch.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: False)
|
|
import tools.tool_backend_helpers as _tbh
|
|
monkeypatch.setattr(_tbh, "managed_nous_tools_enabled", lambda: False)
|
|
|
|
|
|
def test_local_terminal_requirements(monkeypatch, caplog):
|
|
"""Local backend uses Hermes' own LocalEnvironment wrapper."""
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setenv("TERMINAL_ENV", "local")
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
ok = terminal_tool_module.check_terminal_requirements()
|
|
|
|
assert ok is True
|
|
assert "Terminal requirements check failed" not in caplog.text
|
|
|
|
|
|
def test_unknown_terminal_env_logs_error_and_returns_false(monkeypatch, caplog):
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setenv("TERMINAL_ENV", "unknown-backend")
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
ok = terminal_tool_module.check_terminal_requirements()
|
|
|
|
assert ok is False
|
|
assert any(
|
|
"Unknown TERMINAL_ENV 'unknown-backend'" in record.getMessage()
|
|
for record in caplog.records
|
|
)
|
|
|
|
|
|
def test_ssh_backend_without_host_or_user_logs_and_returns_false(monkeypatch, caplog):
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setenv("TERMINAL_ENV", "ssh")
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
ok = terminal_tool_module.check_terminal_requirements()
|
|
|
|
assert ok is False
|
|
assert any(
|
|
"SSH backend selected but TERMINAL_SSH_HOST and TERMINAL_SSH_USER" in record.getMessage()
|
|
for record in caplog.records
|
|
)
|
|
|
|
|
|
def test_modal_backend_without_token_or_config_logs_specific_error(monkeypatch, caplog, tmp_path):
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
|
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)
|
|
monkeypatch.setattr(terminal_tool_module.importlib.util, "find_spec", lambda _name: object())
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
ok = terminal_tool_module.check_terminal_requirements()
|
|
|
|
assert ok is False
|
|
assert any(
|
|
"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.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: True)
|
|
import tools.tool_backend_helpers as _tbh
|
|
monkeypatch.setattr(_tbh, "managed_nous_tools_enabled", lambda: True)
|
|
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
monkeypatch.setenv("TERMINAL_MODAL_MODE", "managed")
|
|
monkeypatch.setattr(terminal_tool_module, "is_managed_tool_gateway_ready", lambda _vendor: True)
|
|
monkeypatch.setattr(
|
|
terminal_tool_module.importlib.util,
|
|
"find_spec",
|
|
lambda _name: (_ for _ in ()).throw(AssertionError("should not be called")),
|
|
)
|
|
|
|
assert terminal_tool_module.check_terminal_requirements() is True
|
|
|
|
|
|
def test_modal_backend_auto_mode_prefers_managed_gateway_over_direct_creds(monkeypatch, tmp_path):
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: True)
|
|
import tools.tool_backend_helpers as _tbh
|
|
monkeypatch.setattr(_tbh, "managed_nous_tools_enabled", lambda: True)
|
|
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
|
monkeypatch.setenv("MODAL_TOKEN_ID", "tok-id")
|
|
monkeypatch.setenv("MODAL_TOKEN_SECRET", "tok-secret")
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
monkeypatch.setattr(terminal_tool_module, "is_managed_tool_gateway_ready", lambda _vendor: True)
|
|
monkeypatch.setattr(
|
|
terminal_tool_module.importlib.util,
|
|
"find_spec",
|
|
lambda _name: (_ for _ in ()).throw(AssertionError("should not be called")),
|
|
)
|
|
|
|
assert terminal_tool_module.check_terminal_requirements() is True
|
|
|
|
|
|
def test_modal_backend_direct_mode_does_not_fall_back_to_managed(monkeypatch, caplog, tmp_path):
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
|
monkeypatch.setenv("TERMINAL_MODAL_MODE", "direct")
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
monkeypatch.setattr(terminal_tool_module, "is_managed_tool_gateway_ready", lambda _vendor: True)
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
ok = terminal_tool_module.check_terminal_requirements()
|
|
|
|
assert ok is False
|
|
assert any(
|
|
"TERMINAL_MODAL_MODE=direct" in record.getMessage()
|
|
for record in caplog.records
|
|
)
|
|
|
|
|
|
def test_modal_backend_managed_mode_does_not_fall_back_to_direct(monkeypatch, caplog, tmp_path):
|
|
_clear_terminal_env(monkeypatch)
|
|
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
|
monkeypatch.setenv("TERMINAL_MODAL_MODE", "managed")
|
|
monkeypatch.setenv("MODAL_TOKEN_ID", "tok-id")
|
|
monkeypatch.setenv("MODAL_TOKEN_SECRET", "tok-secret")
|
|
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(
|
|
"paid Nous subscription is required" 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(
|
|
"paid Nous subscription is required" in record.getMessage()
|
|
for record in caplog.records
|
|
)
|