feat(sandbox): promote sandbox from --beta flag to first-class cloud (#3432)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run

Running an agent in a local Docker container is now a cloud target:
`spawn <agent> sandbox`, instead of `spawn <agent> local --beta sandbox`.

- Extract local orchestration into local/run.ts (runLocalAgent), shared
  by the local and sandbox clouds; add sandbox/main.ts entry point.
- Add 9 sh/sandbox/*.sh shims + README for the Docker-capable agents.
- Register the sandbox cloud and matrix entries in manifest.json.
- Remove sandbox from the --beta gate and the fast_provision experiment.
- Bump CLI to 1.1.0; update tests for the new wiring.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ahmed Abushagur 2026-05-20 23:09:04 -07:00 committed by GitHub
parent 8661ab26ec
commit 18bbe3e9a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 541 additions and 136 deletions

View file

@ -156,9 +156,8 @@ spawn claude gcp --beta tarball --beta parallel
| `images` | Use pre-built cloud images/snapshots (faster boot) |
| `parallel` | Parallelize server boot with setup prompts |
| `recursive` | Install spawn CLI on VM so it can spawn child VMs |
| `sandbox` | Run local agents in a Docker container (sandboxed) |
`--fast` enables `tarball`, `images`, and `parallel` (not `recursive` or `sandbox`).
`--fast` enables `tarball`, `images`, and `parallel` (not `recursive`).
#### Recursive Spawn
@ -188,12 +187,12 @@ Tear down an entire tree:
spawn delete --cascade <id> # Delete a VM and all its children
```
#### Sandboxed Local
#### Local Sandbox
Use `--beta sandbox` to run local agents inside a Docker container instead of directly on your machine:
The `sandbox` cloud runs an agent inside a throwaway Docker container on your own machine, instead of directly on your host:
```bash
spawn claude local --beta sandbox
spawn claude sandbox
```
What this does:
@ -202,12 +201,7 @@ What this does:
- **Auto-installs Docker** if not present (OrbStack on macOS, docker.io on Linux)
- **Cleans up the container** automatically when the session ends
In the interactive picker, `--beta sandbox` adds a "Local Machine (Sandboxed)" option alongside the regular "Local Machine":
```bash
spawn --beta sandbox # Interactive picker shows both local options
spawn openclaw local --beta sandbox # Direct launch, sandboxed
```
It's the same setup as the `local` cloud, but the agent can't touch your host filesystem, shell, or SSH keys. (This was previously the `--beta sandbox` flag.)
### Without the CLI

View file

@ -392,6 +392,18 @@
"interactive_method": "exec",
"notes": "No cloud provisioning needed. Installs agents and injects OpenRouter credentials locally. Useful for local development and testing."
},
"sandbox": {
"name": "Local Sandbox",
"price": "Free",
"description": "Your computer, in a throwaway Docker container — no account or payment needed",
"url": "https://github.com/OpenRouterTeam/spawn",
"type": "local",
"auth": "none",
"provision_method": "docker run (local container)",
"exec_method": "docker exec",
"interactive_method": "docker exec -it",
"notes": "Runs the agent inside an isolated Docker container on your machine. Docker is auto-installed if missing, and the container is removed on exit. Same setup as the local cloud, but the agent cannot touch your host filesystem."
},
"hetzner": {
"name": "Hetzner Cloud",
"price": "~€3/mo",
@ -498,6 +510,16 @@
"local/codex": "implemented",
"local/opencode": "implemented",
"local/kilocode": "implemented",
"sandbox/claude": "implemented",
"sandbox/openclaw": "implemented",
"sandbox/codex": "implemented",
"sandbox/opencode": "implemented",
"sandbox/kilocode": "implemented",
"sandbox/hermes": "implemented",
"sandbox/junie": "implemented",
"sandbox/pi": "implemented",
"sandbox/cursor": "implemented",
"sandbox/t3code": "missing",
"hetzner/claude": "implemented",
"hetzner/openclaw": "implemented",
"hetzner/codex": "implemented",

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "1.0.44",
"version": "1.1.0",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -236,7 +236,6 @@ describe("feature flags", () => {
expect(expandFastProvisionVariant("test")).toEqual([
"images",
"docker",
"sandbox",
]);
});

View file

@ -1,4 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { mockBunSpawn, mockClackPrompts } from "./test-helpers";
mockClackPrompts();
@ -394,20 +396,39 @@ describe("cleanupContainer", () => {
});
});
// ─── sandbox mode integration ───────────────────────────────────────────────
// ─── sandbox cloud wiring ───────────────────────────────────────────────────
describe("sandbox mode", () => {
it("sandbox beta feature is detected from SPAWN_BETA", () => {
process.env.SPAWN_BETA = "sandbox";
const betaFeatures = (process.env.SPAWN_BETA ?? "").split(",");
expect(betaFeatures.includes("sandbox")).toBe(true);
describe("sandbox cloud", () => {
const REPO_ROOT = resolve(import.meta.dir, "../../../..");
const manifest = JSON.parse(readFileSync(resolve(REPO_ROOT, "manifest.json"), "utf-8"));
// Docker-capable agents — `sandbox` runs each inside a container image.
const SANDBOX_AGENTS = [
"claude",
"codex",
"cursor",
"hermes",
"junie",
"kilocode",
"openclaw",
"opencode",
"pi",
];
it("is registered as a no-auth cloud in the manifest", () => {
expect(manifest.clouds.sandbox).toBeDefined();
expect(manifest.clouds.sandbox.auth).toBe("none");
});
it("sandbox can coexist with other beta features", () => {
process.env.SPAWN_BETA = "tarball,sandbox,parallel";
const betaFeatures = (process.env.SPAWN_BETA ?? "").split(",");
expect(betaFeatures.includes("sandbox")).toBe(true);
expect(betaFeatures.includes("tarball")).toBe(true);
it("is marked implemented in the matrix for every Docker-capable agent", () => {
for (const agent of SANDBOX_AGENTS) {
expect(manifest.matrix[`sandbox/${agent}`]).toBe("implemented");
}
});
it("ships a shim script for every implemented agent", () => {
for (const agent of SANDBOX_AGENTS) {
expect(existsSync(resolve(REPO_ROOT, "sh", "sandbox", `${agent}.sh`))).toBe(true);
}
});
});

View file

@ -486,9 +486,9 @@ describe("update-check", () => {
// - SPAWN_NO_AUTO_UPDATE=1 suppresses auto-install entirely
describe("update policy", () => {
it("auto-installs patch bumps even without SPAWN_AUTO_UPDATE=1", async () => {
// 1.0.20 -> 1.0.99 is a patch bump (same major.minor)
// current -> 1.1.99 is a patch bump (same major.minor as the 1.1.x line)
process.env.SPAWN_AUTO_UPDATE = undefined;
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.0.99\n")));
const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.1.99\n")));
const { executor } = await import("../update-check.js");
const execFileSyncSpy = spyOn(executor, "execFileSync").mockImplementation((file: string) =>
Buffer.from(file === "curl" ? FAKE_INSTALL_SCRIPT : ""),

View file

@ -153,7 +153,6 @@ function checkUnknownFlags(args: string[]): void {
console.error(` ${pc.cyan("--beta images")} Use pre-built DO marketplace images (faster boot)`);
console.error(` ${pc.cyan("--beta parallel")} Parallelize server boot with setup prompts`);
console.error(` ${pc.cyan("--beta docker")} Use Docker CE app image on Hetzner/GCP (faster boot)`);
console.error(` ${pc.cyan("--beta sandbox")} Run local agents in a Docker container (sandboxed)`);
console.error(` ${pc.cyan("--beta recursive")} Install spawn CLI on VM for recursive spawning`);
console.error(` ${pc.cyan("--help, -h")} Show help information`);
console.error(` ${pc.cyan("--version, -v")} Show version`);
@ -943,7 +942,6 @@ async function main(): Promise<void> {
"parallel",
"docker",
"recursive",
"sandbox",
"skills",
]);
const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn <agent> <cloud> --beta parallel");
@ -956,7 +954,6 @@ async function main(): Promise<void> {
console.error(` ${pc.cyan("images")} Use pre-built DO marketplace images (faster boot)`);
console.error(` ${pc.cyan("parallel")} Parallelize server boot with setup prompts`);
console.error(` ${pc.cyan("docker")} Use Docker CE app image on Hetzner/GCP (faster boot)`);
console.error(` ${pc.cyan("sandbox")} Run local agents in a Docker container (sandboxed)`);
console.error(` ${pc.cyan("skills")} Pre-install MCP servers and tools on the VM`);
console.error(` ${pc.cyan("recursive")} Install spawn CLI on VM for recursive spawning`);
process.exit(1);
@ -969,10 +966,9 @@ async function main(): Promise<void> {
// fast_provision experiment: if the user did NOT pass --beta or --fast,
// bucket them on the PostHog `fast_provision` flag. The `test` variant
// turns on images + docker + sandbox by default; control behaves as before.
// turns on images + docker by default; control behaves as before.
// - images: pre-built DO marketplace images (cloud-side faster boot)
// - docker: Docker CE host image on Hetzner/GCP (cloud-side faster boot)
// - sandbox: local agents run in a Docker container (local-side faster boot)
// Exposure is captured for both variants so PostHog can compute conversion.
// Bundle composition lives in expandFastProvisionVariant() for unit testing.
if (!userOptedIntoBeta) {

View file

@ -1,27 +1,15 @@
#!/usr/bin/env bun
// local/main.ts — Orchestrator: deploys an agent on the local machine
// local/main.ts — Orchestrator: deploys an agent on the local machine.
//
// For the isolated Docker-container variant, see sandbox/main.ts — both share
// the orchestration in local/run.ts.
import type { CloudOrchestrator } from "../shared/orchestrate.js";
import * as p from "@clack/prompts";
import { getErrorMessage } from "@openrouter/spawn-shared";
import pkg from "../../package.json" with { type: "json" };
import { createCloudAgents } from "../shared/agent-setup.js";
import { makeDockerRunner, runOrchestration } from "../shared/orchestrate.js";
import { initTelemetry } from "../shared/telemetry.js";
import { logWarn } from "../shared/ui.js";
import { agents, resolveAgent } from "./agents.js";
import {
cleanupContainer,
dockerInteractiveSession,
downloadFile,
ensureDocker,
interactiveSession,
pullAndStartContainer,
runLocal,
uploadFile,
} from "./local.js";
import { agents } from "./agents.js";
import { runLocalAgent } from "./run.js";
async function main() {
const agentName = process.argv[2];
@ -31,92 +19,7 @@ async function main() {
process.exit(1);
}
// Check if --beta sandbox is active
const betaFeatures = (process.env.SPAWN_BETA ?? "").split(",");
const useSandbox = betaFeatures.includes("sandbox");
const baseRunner = {
runServer: runLocal,
uploadFile: async (l: string, r: string) => uploadFile(l, r),
downloadFile: async (r: string, l: string) => downloadFile(r, l),
};
// When sandboxed, recreate agents with the Docker-wrapped runner so that
// agent.configure() / agent.install() closures execute inside the container
// instead of writing config files directly to the host filesystem.
const agent = useSandbox
? createCloudAgents(makeDockerRunner(baseRunner)).resolveAgent(agentName)
: resolveAgent(agentName);
// If sandboxed, ensure Docker is installed (auto-install if missing)
if (useSandbox) {
await ensureDocker();
}
// Warn about security implications of installing OpenClaw locally
// (skip warning in sandbox mode — the container provides isolation)
if (agentName === "openclaw" && !useSandbox && process.env.SPAWN_NON_INTERACTIVE !== "1") {
process.stderr.write("\n");
logWarn("⚠ Local installation warning");
logWarn(` This will install ${agent.name} directly on your machine.`);
logWarn(" The agent will have full access to your filesystem, shell, and network.");
logWarn(" For isolation, consider running on a cloud VM instead.\n");
const confirmed = await p.confirm({
message: "Continue with local installation?",
initialValue: true,
});
if (p.isCancel(confirmed) || !confirmed) {
p.log.info("Installation cancelled.");
process.exit(0);
}
}
const cloud: CloudOrchestrator = {
cloudName: "local",
cloudLabel: useSandbox ? "local (sandboxed)" : "local",
skipAgentInstall: false,
runner: useSandbox ? makeDockerRunner(baseRunner) : baseRunner,
async authenticate() {},
async promptSize() {},
async createServer(_name: string) {
return {
ip: "localhost",
user: process.env.USER || "local",
cloud: "local",
};
},
async getServerName() {
const result = Bun.spawnSync(
[
"hostname",
],
{
stdio: [
"ignore",
"pipe",
"ignore",
],
},
);
return new TextDecoder().decode(result.stdout).trim() || "local";
},
async waitForReady() {
if (useSandbox) {
await pullAndStartContainer(agentName);
cloud.skipAgentInstall = true;
}
},
interactiveSession: useSandbox ? dockerInteractiveSession : interactiveSession,
};
// Clean up sandbox container on exit
if (useSandbox) {
process.on("exit", cleanupContainer);
}
await runOrchestration(cloud, agent, agentName);
await runLocalAgent(agentName, false);
}
initTelemetry(pkg.version);

View file

@ -0,0 +1,122 @@
// local/run.ts — Shared orchestration for the `local` and `sandbox` clouds.
//
// `local` runs the agent directly on the host machine.
// `sandbox` runs the agent inside a throwaway Docker container on the host.
//
// Both share one code path; `useSandbox` swaps in the Docker-wrapped runner,
// container lifecycle, and interactive session. The orchestrator's internal
// `cloudName` stays "local" either way — orchestrate.ts has ~20 `!== "local"`
// branches (tarball install, repo cloning, restart loops, reconnects, skills)
// that must treat the sandbox as local execution. The user-facing `sandbox`
// cloud name is tracked separately by the run/headless command layer.
import type { CloudOrchestrator } from "../shared/orchestrate.js";
import * as p from "@clack/prompts";
import { createCloudAgents } from "../shared/agent-setup.js";
import { makeDockerRunner, runOrchestration } from "../shared/orchestrate.js";
import { logWarn } from "../shared/ui.js";
import { resolveAgent } from "./agents.js";
import {
cleanupContainer,
dockerInteractiveSession,
downloadFile,
ensureDocker,
interactiveSession,
pullAndStartContainer,
runLocal,
uploadFile,
} from "./local.js";
/**
* Deploy an agent on the local machine.
*
* @param agentName Agent key (e.g. "claude", "hermes").
* @param useSandbox When true, the agent runs inside a Docker container
* (the `sandbox` cloud); otherwise it runs directly on the
* host (the `local` cloud).
*/
export async function runLocalAgent(agentName: string, useSandbox: boolean): Promise<void> {
const baseRunner = {
runServer: runLocal,
uploadFile: async (l: string, r: string) => uploadFile(l, r),
downloadFile: async (r: string, l: string) => downloadFile(r, l),
};
// When sandboxed, recreate agents with the Docker-wrapped runner so that
// agent.configure() / agent.install() closures execute inside the container
// instead of writing config files directly to the host filesystem.
const agent = useSandbox
? createCloudAgents(makeDockerRunner(baseRunner)).resolveAgent(agentName)
: resolveAgent(agentName);
// If sandboxed, ensure Docker is installed (auto-install if missing)
if (useSandbox) {
await ensureDocker();
}
// Warn about security implications of installing OpenClaw locally
// (skip warning in sandbox mode — the container provides isolation)
if (agentName === "openclaw" && !useSandbox && process.env.SPAWN_NON_INTERACTIVE !== "1") {
process.stderr.write("\n");
logWarn("⚠ Local installation warning");
logWarn(` This will install ${agent.name} directly on your machine.`);
logWarn(" The agent will have full access to your filesystem, shell, and network.");
logWarn(" For isolation, consider running on a cloud VM instead.\n");
const confirmed = await p.confirm({
message: "Continue with local installation?",
initialValue: true,
});
if (p.isCancel(confirmed) || !confirmed) {
p.log.info("Installation cancelled.");
process.exit(0);
}
}
const cloud: CloudOrchestrator = {
cloudName: "local",
cloudLabel: useSandbox ? "local (sandboxed)" : "local",
skipAgentInstall: false,
runner: useSandbox ? makeDockerRunner(baseRunner) : baseRunner,
async authenticate() {},
async promptSize() {},
async createServer(_name: string) {
return {
ip: "localhost",
user: process.env.USER || "local",
cloud: "local",
};
},
async getServerName() {
const result = Bun.spawnSync(
[
"hostname",
],
{
stdio: [
"ignore",
"pipe",
"ignore",
],
},
);
return new TextDecoder().decode(result.stdout).trim() || "local";
},
async waitForReady() {
if (useSandbox) {
await pullAndStartContainer(agentName);
cloud.skipAgentInstall = true;
}
},
interactiveSession: useSandbox ? dockerInteractiveSession : interactiveSession,
};
// Clean up sandbox container on exit
if (useSandbox) {
process.on("exit", cleanupContainer);
}
await runOrchestration(cloud, agent, agentName);
}

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bun
// sandbox/main.ts — Orchestrator: deploys an agent inside a local Docker sandbox.
//
// The `sandbox` cloud reuses the `local` orchestrator with the Docker-wrapped
// runner enabled, so the agent runs in a throwaway container on the host
// machine. Docker is auto-installed if missing; the container is removed on
// exit. See local/run.ts for the shared implementation.
import { getErrorMessage } from "@openrouter/spawn-shared";
import pkg from "../../package.json" with { type: "json" };
import { agents } from "../local/agents.js";
import { runLocalAgent } from "../local/run.js";
import { initTelemetry } from "../shared/telemetry.js";
async function main() {
const agentName = process.argv[2];
if (!agentName) {
console.error("Usage: bun run sandbox/main.ts <agent>");
console.error(`Agents: ${Object.keys(agents).join(", ")}`);
process.exit(1);
}
await runLocalAgent(agentName, true);
}
initTelemetry(pkg.version);
main().catch((err) => {
process.stderr.write(`\x1b[0;31mFatal: ${getErrorMessage(err)}\x1b[0m\n`);
process.exit(1);
});

View file

@ -215,7 +215,6 @@ export function expandFastProvisionVariant(variant: string): readonly string[] {
return [
"images",
"docker",
"sandbox",
];
}
return [];

57
sh/sandbox/README.md Normal file
View file

@ -0,0 +1,57 @@
# Local Sandbox
Run agents inside a throwaway Docker container on your own machine.
> Same setup as the `local` cloud, but the agent runs in an isolated Docker container instead of directly on your host. No account or payment needed. Docker is auto-installed if missing, and the container is removed when the session ends — so the agent can't touch your host filesystem, shell, or SSH keys.
This was previously the `--beta sandbox` flag on the `local` cloud. It is now a first-class cloud.
## Quick Start
If you have the [spawn CLI](https://github.com/OpenRouterTeam/spawn) installed:
```bash
spawn claude sandbox
spawn openclaw sandbox
spawn codex sandbox
spawn opencode sandbox
spawn kilocode sandbox
spawn hermes sandbox
spawn junie sandbox
spawn cursor sandbox
spawn pi sandbox
```
Or run directly without the CLI:
```bash
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/claude.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/openclaw.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/codex.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/opencode.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/kilocode.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/hermes.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/cursor.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sandbox/pi.sh)
```
## Requirements
- **Docker** — auto-installed if missing (OrbStack on macOS, `docker.io` via apt on Linux).
- **`OPENROUTER_API_KEY`** — prompted interactively, or set in the environment.
## How it works
The `sandbox` cloud reuses the `local` orchestrator with a Docker-wrapped runner:
1. Ensure Docker is installed and running.
2. Pull the agent image `ghcr.io/openrouterteam/spawn-<agent>:latest` and start a container.
3. Inject OpenRouter credentials and write agent config files **inside the container**.
4. Drop into an interactive session via `docker exec -it`.
5. Remove the container on exit.
## Notes
- Agents that need a Docker image: `claude`, `codex`, `cursor`, `hermes`, `junie`, `kilocode`, `openclaw`, `opencode`, `pi`. The container images are built from `sh/docker/<agent>.Dockerfile`.
- For host-native execution (no container), use the [`local`](../local/README.md) cloud instead.

29
sh/sandbox/claude.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" claude "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" claude "$@"

29
sh/sandbox/codex.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" codex "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" codex "$@"

29
sh/sandbox/cursor.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" cursor "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" cursor "$@"

29
sh/sandbox/hermes.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" hermes "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" hermes "$@"

29
sh/sandbox/junie.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" junie "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" junie "$@"

29
sh/sandbox/kilocode.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" kilocode "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" kilocode "$@"

29
sh/sandbox/openclaw.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" openclaw "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" openclaw "$@"

29
sh/sandbox/opencode.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" opencode "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" opencode "$@"

29
sh/sandbox/pi.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Thin shim for the `sandbox` cloud: runs the agent inside a throwaway Docker
# container on this machine. Ensures bun is available, then runs bundled
# sandbox.js (local source or from the GitHub release).
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sandbox/main.ts" pi "$@"
fi
# Remote — download bundled sandbox.js from GitHub release
SANDBOX_JS=$(mktemp)
trap 'rm -f "$SANDBOX_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sandbox-latest/sandbox.js" -o "$SANDBOX_JS" \
|| { printf '\033[0;31mFailed to download sandbox.js\033[0m\n' >&2; exit 1; }
exec bun run "$SANDBOX_JS" pi "$@"