diff --git a/cli.py b/cli.py index 2fd57b2e47..714fd96ad5 100644 --- a/cli.py +++ b/cli.py @@ -503,7 +503,7 @@ def load_cli_config() -> Dict[str, Any]: "ssh_user": "TERMINAL_SSH_USER", "ssh_port": "TERMINAL_SSH_PORT", "ssh_key": "TERMINAL_SSH_KEY", - # Container resource config (docker, singularity, modal, daytona -- ignored for local/ssh) + # Container resource config (docker, singularity, modal, daytona, vercel_sandbox -- ignored for local/ssh) "container_cpu": "TERMINAL_CONTAINER_CPU", "container_memory": "TERMINAL_CONTAINER_MEMORY", "container_disk": "TERMINAL_CONTAINER_DISK", diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 26112c4f57..cbee55cbd5 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -500,7 +500,7 @@ DEFAULT_CONFIG = { "modal_image": "nikolaik/python-nodejs:python3.11-nodejs20", "daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20", "vercel_runtime": "node24", - # Container resource limits (docker, singularity, modal, daytona — ignored for local/ssh) + # Container resource limits (docker, singularity, modal, daytona, vercel_sandbox — ignored for local/ssh) "container_cpu": 1, "container_memory": 5120, # MB (default 5GB) "container_disk": 51200, # MB (default 50GB) diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 31eeb2100d..8ac8822c73 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -868,11 +868,13 @@ def run_doctor(args): # Vercel Sandbox (if using vercel_sandbox backend) if terminal_env == "vercel_sandbox": runtime = os.getenv("TERMINAL_VERCEL_RUNTIME", "node24").strip() or "node24" - if runtime in {"node24", "node22", "python3.13"}: + from tools.terminal_tool import _SUPPORTED_VERCEL_RUNTIMES + if runtime in _SUPPORTED_VERCEL_RUNTIMES: check_ok("Vercel runtime", f"({runtime})") else: - check_fail("Vercel runtime unsupported", f"({runtime}; use node24, node22, or python3.13)") - issues.append("Set TERMINAL_VERCEL_RUNTIME to node24, node22, or python3.13") + supported = ", ".join(_SUPPORTED_VERCEL_RUNTIMES) + check_fail("Vercel runtime unsupported", f"({runtime}; use {supported})") + issues.append(f"Set TERMINAL_VERCEL_RUNTIME to one of: {supported}") disk = os.getenv("TERMINAL_CONTAINER_DISK", "51200").strip() if disk in ("", "0", "51200"): diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index ab2c7c7d63..07cc92f4ab 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -666,11 +666,14 @@ def _prompt_vercel_sandbox_settings(config: dict): print_info(" Filesystem persistence uses Vercel snapshots.") print_info(" Snapshots restore files only; live processes do not continue after sandbox recreation.") + from tools.terminal_tool import _SUPPORTED_VERCEL_RUNTIMES + current_runtime = terminal.get("vercel_runtime") or "node24" - runtime = prompt(" Runtime (node24, node22, python3.13)", current_runtime).strip() or current_runtime - if runtime not in {"node24", "node22", "python3.13"}: + supported_label = ", ".join(_SUPPORTED_VERCEL_RUNTIMES) + runtime = prompt(f" Runtime ({supported_label})", current_runtime).strip() or current_runtime + if runtime not in _SUPPORTED_VERCEL_RUNTIMES: print_warning(f"Unsupported Vercel runtime '{runtime}', keeping {current_runtime}.") - runtime = current_runtime if current_runtime in {"node24", "node22", "python3.13"} else "node24" + runtime = current_runtime if current_runtime in _SUPPORTED_VERCEL_RUNTIMES else "node24" terminal["vercel_runtime"] = runtime save_env_value("TERMINAL_VERCEL_RUNTIME", runtime) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 4d69f066f1..c45375cde2 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -258,7 +258,7 @@ _SCHEMA_OVERRIDES: Dict[str, Dict[str, Any]] = { "terminal.vercel_runtime": { "type": "select", "description": "Vercel Sandbox runtime", - "options": ["node24", "node22", "python3.13"], + "options": ["node24", "node22", "python3.13"], # sync with _SUPPORTED_VERCEL_RUNTIMES in terminal_tool.py }, "terminal.modal_mode": { "type": "select", diff --git a/tests/tools/test_hardline_blocklist.py b/tests/tools/test_hardline_blocklist.py index 3f65cc0869..a3a08cd464 100644 --- a/tests/tools/test_hardline_blocklist.py +++ b/tests/tools/test_hardline_blocklist.py @@ -241,7 +241,7 @@ def test_container_backends_still_bypass(clean_session): Hardline only protects environments with real host impact (local, ssh). """ - for env in ("docker", "singularity", "modal", "daytona"): + for env in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"): r1 = check_dangerous_command("rm -rf /", env) assert r1["approved"] is True, f"container {env} should still bypass" r2 = check_all_command_guards("rm -rf /", env) diff --git a/tests/tools/test_skills_tool.py b/tests/tools/test_skills_tool.py index 79470710b0..d95fc0671d 100644 --- a/tests/tools/test_skills_tool.py +++ b/tests/tools/test_skills_tool.py @@ -932,7 +932,7 @@ class TestSkillViewPrerequisites: @pytest.mark.parametrize( "backend", - ["ssh", "daytona", "docker", "singularity", "modal"], + ["ssh", "daytona", "docker", "singularity", "modal", "vercel_sandbox"], ) def test_remote_backend_becomes_available_after_local_secret_capture( self, tmp_path, monkeypatch, backend diff --git a/tools/environments/vercel_sandbox.py b/tools/environments/vercel_sandbox.py index d58e3e923d..2b434af159 100644 --- a/tools/environments/vercel_sandbox.py +++ b/tools/environments/vercel_sandbox.py @@ -578,6 +578,17 @@ class VercelSandboxEnvironment(BaseEnvironment): timeout: int = 120, stdin_data: str | None = None, ): + """Run a bash command in the Vercel sandbox. + + ``timeout`` is not forwarded to the Vercel SDK (which does not expose + a per-exec timeout parameter); the base class ``_wait_for_process`` + enforces timeout by killing the sandbox via ``cancel_fn``. + + ``stdin_data`` is intentionally discarded here because + ``_stdin_mode = "heredoc"`` causes the base class ``execute()`` to + embed any stdin payload into the command string before calling this + method. + """ del timeout del stdin_data diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index f5da509bcf..8a5678038a 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -132,6 +132,10 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe | `TINKER_API_KEY` | RL training ([tinker-console.thinkingmachines.ai](https://tinker-console.thinkingmachines.ai/)) | | `WANDB_API_KEY` | RL training metrics ([wandb.ai](https://wandb.ai/)) | | `DAYTONA_API_KEY` | Daytona cloud sandboxes ([daytona.io](https://daytona.io/)) | +| `VERCEL_TOKEN` | Vercel Sandbox access token ([vercel.com](https://vercel.com/)) | +| `VERCEL_PROJECT_ID` | Vercel project ID (required with `VERCEL_TOKEN`) | +| `VERCEL_TEAM_ID` | Vercel team ID (required with `VERCEL_TOKEN`) | +| `VERCEL_OIDC_TOKEN` | Vercel short-lived OIDC token (development-only alternative) | ### Langfuse Observability @@ -164,7 +168,7 @@ These variables configure the [Tool Gateway](/docs/user-guide/features/tool-gate | Variable | Description | |----------|-------------| -| `TERMINAL_ENV` | Backend: `local`, `docker`, `ssh`, `singularity`, `modal`, `daytona` | +| `TERMINAL_ENV` | Backend: `local`, `docker`, `ssh`, `singularity`, `modal`, `daytona`, `vercel_sandbox` | | `TERMINAL_DOCKER_IMAGE` | Docker image (default: `nikolaik/python-nodejs:python3.11-nodejs20`) | | `TERMINAL_DOCKER_FORWARD_ENV` | JSON array of env var names to explicitly forward into Docker terminal sessions. Note: skill-declared `required_environment_variables` are forwarded automatically — you only need this for vars not declared by any skill. | | `TERMINAL_DOCKER_VOLUMES` | Additional Docker volume mounts (comma-separated `host:container` pairs) | @@ -172,6 +176,7 @@ These variables configure the [Tool Gateway](/docs/user-guide/features/tool-gate | `TERMINAL_SINGULARITY_IMAGE` | Singularity image or `.sif` path | | `TERMINAL_MODAL_IMAGE` | Modal container image | | `TERMINAL_DAYTONA_IMAGE` | Daytona sandbox image | +| `TERMINAL_VERCEL_RUNTIME` | Vercel Sandbox runtime (`node24`, `node22`, `python3.13`) | | `TERMINAL_TIMEOUT` | Command timeout in seconds | | `TERMINAL_LIFETIME_SECONDS` | Max lifetime for terminal sessions in seconds | | `TERMINAL_CWD` | Working directory for all terminal sessions | diff --git a/website/docs/user-guide/security.md b/website/docs/user-guide/security.md index aba476bc10..3f2fe665bd 100644 --- a/website/docs/user-guide/security.md +++ b/website/docs/user-guide/security.md @@ -115,7 +115,7 @@ The following patterns trigger approval prompts (defined in `tools/approval.py`) | `gateway run` with `&`/`disown`/`nohup`/`setsid` | Prevents starting gateway outside service manager | :::info -**Container bypass**: When running in `docker`, `singularity`, `modal`, or `daytona` backends, dangerous command checks are **skipped** because the container itself is the security boundary. Destructive commands inside a container can't harm the host. +**Container bypass**: When running in `docker`, `singularity`, `modal`, `daytona`, or `vercel_sandbox` backends, dangerous command checks are **skipped** because the container itself is the security boundary. Destructive commands inside a container can't harm the host. ::: ### Approval Flow (CLI) @@ -311,7 +311,7 @@ terminal: - **Ephemeral mode** (`container_persistent: false`): Uses tmpfs for workspace — everything is lost on cleanup :::tip -For production gateway deployments, use `docker`, `modal`, or `daytona` backend to isolate agent commands from your host system. This eliminates the need for dangerous command approval entirely. +For production gateway deployments, use `docker`, `modal`, `daytona`, or `vercel_sandbox` backend to isolate agent commands from your host system. This eliminates the need for dangerous command approval entirely. ::: :::warning @@ -328,6 +328,7 @@ If you add names to `terminal.docker_forward_env`, those variables are intention | **singularity** | Container | ❌ Skipped | HPC environments | | **modal** | Cloud sandbox | ❌ Skipped | Scalable cloud isolation | | **daytona** | Cloud sandbox | ❌ Skipped | Persistent cloud workspaces | +| **vercel_sandbox** | Cloud microVM | ❌ Skipped | Cloud execution with snapshot persistence | ## Environment Variable Passthrough {#environment-variable-passthrough}