fix: address self-review findings for Vercel Sandbox salvage

- Add vercel_sandbox to hardline blocklist container bypass test
- Add vercel_sandbox to skills_tool remote backend parametrize test
- Deduplicate runtime set: doctor.py and setup.py now import
  _SUPPORTED_VERCEL_RUNTIMES from terminal_tool.py
- Add docstring to _run_bash explaining timeout/stdin_data discards
- Always stop sandbox during cleanup (unconditional, matching Modal/Daytona)
- Update security.md: container bypass text, production tip, comparison table
- Update environment-variables.md: TERMINAL_ENV list, Vercel auth vars,
  TERMINAL_VERCEL_RUNTIME
- Update inline comments in cli.py and config.py to include vercel_sandbox
This commit is contained in:
kshitijk4poor 2026-04-29 19:00:12 +05:30 committed by kshitij
parent 5a1d4f6804
commit 13c238327e
10 changed files with 36 additions and 14 deletions

2
cli.py
View File

@ -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",

View File

@ -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)

View File

@ -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"):

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 |

View File

@ -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}