diff --git a/README.md b/README.md index 402abecc5..616a3eae7 100644 --- a/README.md +++ b/README.md @@ -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 # 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 diff --git a/manifest.json b/manifest.json index b58bb8454..3284608e0 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7ddfc6674..95af7202e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.0.44", + "version": "1.1.0", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/feature-flags.test.ts b/packages/cli/src/__tests__/feature-flags.test.ts index 4efdf2b03..4674f4d7b 100644 --- a/packages/cli/src/__tests__/feature-flags.test.ts +++ b/packages/cli/src/__tests__/feature-flags.test.ts @@ -236,7 +236,6 @@ describe("feature flags", () => { expect(expandFastProvisionVariant("test")).toEqual([ "images", "docker", - "sandbox", ]); }); diff --git a/packages/cli/src/__tests__/sandbox.test.ts b/packages/cli/src/__tests__/sandbox.test.ts index 7b622b336..2649aebf0 100644 --- a/packages/cli/src/__tests__/sandbox.test.ts +++ b/packages/cli/src/__tests__/sandbox.test.ts @@ -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 ─────────────────────────────────────────────── - -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); +// ─── sandbox cloud wiring ─────────────────────────────────────────────────── + +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("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("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("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); + } }); }); diff --git a/packages/cli/src/__tests__/update-check.test.ts b/packages/cli/src/__tests__/update-check.test.ts index 5dee05362..55316e6d9 100644 --- a/packages/cli/src/__tests__/update-check.test.ts +++ b/packages/cli/src/__tests__/update-check.test.ts @@ -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 : ""), diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7fcfabe87..8b9a7413c 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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 { "parallel", "docker", "recursive", - "sandbox", "skills", ]); const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn --beta parallel"); @@ -956,7 +954,6 @@ async function main(): Promise { 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 { // 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) { diff --git a/packages/cli/src/local/main.ts b/packages/cli/src/local/main.ts index 1823fb56e..d5ec1bb0d 100644 --- a/packages/cli/src/local/main.ts +++ b/packages/cli/src/local/main.ts @@ -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); diff --git a/packages/cli/src/local/run.ts b/packages/cli/src/local/run.ts new file mode 100644 index 000000000..0611002fe --- /dev/null +++ b/packages/cli/src/local/run.ts @@ -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 { + 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); +} diff --git a/packages/cli/src/sandbox/main.ts b/packages/cli/src/sandbox/main.ts new file mode 100644 index 000000000..6771b899f --- /dev/null +++ b/packages/cli/src/sandbox/main.ts @@ -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 "); + 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); +}); diff --git a/packages/cli/src/shared/feature-flags.ts b/packages/cli/src/shared/feature-flags.ts index 871e62495..c957e2d0b 100644 --- a/packages/cli/src/shared/feature-flags.ts +++ b/packages/cli/src/shared/feature-flags.ts @@ -215,7 +215,6 @@ export function expandFastProvisionVariant(variant: string): readonly string[] { return [ "images", "docker", - "sandbox", ]; } return []; diff --git a/sh/sandbox/README.md b/sh/sandbox/README.md new file mode 100644 index 000000000..1ab16efe0 --- /dev/null +++ b/sh/sandbox/README.md @@ -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-: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/.Dockerfile`. +- For host-native execution (no container), use the [`local`](../local/README.md) cloud instead. diff --git a/sh/sandbox/claude.sh b/sh/sandbox/claude.sh new file mode 100755 index 000000000..08f120b2e --- /dev/null +++ b/sh/sandbox/claude.sh @@ -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 "$@" diff --git a/sh/sandbox/codex.sh b/sh/sandbox/codex.sh new file mode 100755 index 000000000..d3631a00f --- /dev/null +++ b/sh/sandbox/codex.sh @@ -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 "$@" diff --git a/sh/sandbox/cursor.sh b/sh/sandbox/cursor.sh new file mode 100755 index 000000000..56f46f22d --- /dev/null +++ b/sh/sandbox/cursor.sh @@ -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 "$@" diff --git a/sh/sandbox/hermes.sh b/sh/sandbox/hermes.sh new file mode 100755 index 000000000..96bfb1378 --- /dev/null +++ b/sh/sandbox/hermes.sh @@ -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 "$@" diff --git a/sh/sandbox/junie.sh b/sh/sandbox/junie.sh new file mode 100755 index 000000000..4ed4c89fe --- /dev/null +++ b/sh/sandbox/junie.sh @@ -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 "$@" diff --git a/sh/sandbox/kilocode.sh b/sh/sandbox/kilocode.sh new file mode 100755 index 000000000..a23c2ea3b --- /dev/null +++ b/sh/sandbox/kilocode.sh @@ -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 "$@" diff --git a/sh/sandbox/openclaw.sh b/sh/sandbox/openclaw.sh new file mode 100755 index 000000000..41dc2effc --- /dev/null +++ b/sh/sandbox/openclaw.sh @@ -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 "$@" diff --git a/sh/sandbox/opencode.sh b/sh/sandbox/opencode.sh new file mode 100755 index 000000000..73843b381 --- /dev/null +++ b/sh/sandbox/opencode.sh @@ -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 "$@" diff --git a/sh/sandbox/pi.sh b/sh/sandbox/pi.sh new file mode 100755 index 000000000..6096597a8 --- /dev/null +++ b/sh/sandbox/pi.sh @@ -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 "$@"