From 5dd8a13845030f7e304ca8d7af728a5acbab5457 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 28 Jan 2026 01:23:51 -0800 Subject: [PATCH] fix: add OpenSSL build for musl in runtime Dockerfile --- CLAUDE.md | 1 - docker/runtime/Dockerfile | 89 ++++++++++- examples/daytona/package.json | 1 - examples/daytona/src/daytona-fallback.ts | 18 +-- examples/daytona/src/daytona.ts | 8 +- examples/docker/package.json | 3 +- examples/docker/src/docker.ts | 168 ++++++-------------- examples/docker/tsconfig.json | 16 ++ examples/e2b/package.json | 3 +- examples/e2b/src/e2b.ts | 101 +++--------- examples/e2b/tsconfig.json | 16 ++ examples/shared/src/sandbox-agent-client.ts | 40 +---- examples/vercel/package.json | 3 +- examples/vercel/src/vercel-sandbox.ts | 141 +++++----------- examples/vercel/tsconfig.json | 16 ++ todo.md | 14 -- 16 files changed, 262 insertions(+), 376 deletions(-) create mode 100644 examples/docker/tsconfig.json create mode 100644 examples/e2b/tsconfig.json create mode 100644 examples/vercel/tsconfig.json delete mode 100644 todo.md diff --git a/CLAUDE.md b/CLAUDE.md index c747930..9280f9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,6 @@ Universal schema guidance: ## Spec Tracking -- Update `todo.md` as work progresses; add new tasks as they arise. - Keep CLI subcommands in sync with every HTTP endpoint. - Update `CLAUDE.md` to keep CLI endpoints in sync with HTTP API changes. - When changing the HTTP API, update the TypeScript SDK and CLI together. diff --git a/docker/runtime/Dockerfile b/docker/runtime/Dockerfile index 0e71c2f..6eaf9b5 100644 --- a/docker/runtime/Dockerfile +++ b/docker/runtime/Dockerfile @@ -3,29 +3,102 @@ # Build stage - compile the binary FROM rust:1.88.0 AS builder +ARG TARGETARCH + ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies RUN apt-get update && apt-get install -y \ musl-tools \ musl-dev \ + llvm-14-dev \ + libclang-14-dev \ + clang-14 \ + libssl-dev \ pkg-config \ ca-certificates \ - git && \ - apt-get clean && \ + g++ \ + g++-multilib \ + git \ + curl \ + wget && \ rm -rf /var/lib/apt/lists/* -RUN rustup target add x86_64-unknown-linux-musl +# Install musl cross toolchain based on architecture +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + wget -q https://github.com/cross-tools/musl-cross/releases/latest/download/x86_64-unknown-linux-musl.tar.xz && \ + tar -xf x86_64-unknown-linux-musl.tar.xz -C /opt/ && \ + rm x86_64-unknown-linux-musl.tar.xz && \ + rustup target add x86_64-unknown-linux-musl; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + wget -q https://github.com/cross-tools/musl-cross/releases/latest/download/aarch64-unknown-linux-musl.tar.xz && \ + tar -xf aarch64-unknown-linux-musl.tar.xz -C /opt/ && \ + rm aarch64-unknown-linux-musl.tar.xz && \ + rustup target add aarch64-unknown-linux-musl; \ + fi + +# Set environment variables based on architecture +ENV LIBCLANG_PATH=/usr/lib/llvm-14/lib \ + CLANG_PATH=/usr/bin/clang-14 \ + CARGO_INCREMENTAL=0 \ + CARGO_NET_GIT_FETCH_WITH_CLI=true + +# Build OpenSSL for musl target +ENV SSL_VER=1.1.1w +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + export PATH="/opt/x86_64-unknown-linux-musl/bin:$PATH" && \ + wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz && \ + tar -xzf openssl-$SSL_VER.tar.gz && \ + cd openssl-$SSL_VER && \ + ./Configure no-shared no-async --prefix=/musl --openssldir=/musl/ssl linux-x86_64 && \ + make -j$(nproc) && \ + make install_sw && \ + cd .. && \ + rm -rf openssl-$SSL_VER*; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + export PATH="/opt/aarch64-unknown-linux-musl/bin:$PATH" && \ + wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz && \ + tar -xzf openssl-$SSL_VER.tar.gz && \ + cd openssl-$SSL_VER && \ + ./Configure no-shared no-async --prefix=/musl --openssldir=/musl/ssl linux-aarch64 && \ + make -j$(nproc) && \ + make install_sw && \ + cd .. && \ + rm -rf openssl-$SSL_VER*; \ + fi + +# Set OpenSSL environment variables +ENV OPENSSL_DIR=/musl \ + OPENSSL_INCLUDE_DIR=/musl/include \ + OPENSSL_LIB_DIR=/musl/lib \ + PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /build COPY . . -# Build static binary +# Build static binary based on architecture RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/build/target \ - SANDBOX_AGENT_SKIP_INSPECTOR=1 \ - RUSTFLAGS="-C target-feature=+crt-static" \ - cargo build -p sandbox-agent --release --target x86_64-unknown-linux-musl && \ - cp target/x86_64-unknown-linux-musl/release/sandbox-agent /sandbox-agent + if [ "$TARGETARCH" = "amd64" ]; then \ + export PATH="/opt/x86_64-unknown-linux-musl/bin:$PATH" && \ + export CC_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-gcc && \ + export CXX_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-g++ && \ + export AR_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-ar && \ + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-unknown-linux-musl-gcc && \ + export RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static-libgcc" && \ + SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo build -p sandbox-agent --release --target x86_64-unknown-linux-musl && \ + cp target/x86_64-unknown-linux-musl/release/sandbox-agent /sandbox-agent; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + export PATH="/opt/aarch64-unknown-linux-musl/bin:$PATH" && \ + export CC_aarch64_unknown_linux_musl=aarch64-unknown-linux-musl-gcc && \ + export CXX_aarch64_unknown_linux_musl=aarch64-unknown-linux-musl-g++ && \ + export AR_aarch64_unknown_linux_musl=aarch64-unknown-linux-musl-ar && \ + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-unknown-linux-musl-gcc && \ + export RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static-libgcc" && \ + SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo build -p sandbox-agent --release --target aarch64-unknown-linux-musl && \ + cp target/aarch64-unknown-linux-musl/release/sandbox-agent /sandbox-agent; \ + fi # Runtime stage - minimal image FROM debian:bookworm-slim diff --git a/examples/daytona/package.json b/examples/daytona/package.json index 6ba5ebd..8f36591 100644 --- a/examples/daytona/package.json +++ b/examples/daytona/package.json @@ -4,7 +4,6 @@ "type": "module", "scripts": { "start": "tsx src/daytona-fallback.ts", - "start:cli": "tsx src/daytona.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/examples/daytona/src/daytona-fallback.ts b/examples/daytona/src/daytona-fallback.ts index b2a1d98..7005981 100644 --- a/examples/daytona/src/daytona-fallback.ts +++ b/examples/daytona/src/daytona-fallback.ts @@ -12,7 +12,6 @@ if ( const SNAPSHOT = "sandbox-agent-ready"; const BINARY = "/usr/local/bin/sandbox-agent"; -const AGENT_BIN_DIR = "/root/.local/share/sandbox-agent/bin"; const daytona = new Daytona(); @@ -28,18 +27,11 @@ if (!hasSnapshot) { image: Image.base("ubuntu:22.04").runCommands( // Install dependencies "apt-get update && apt-get install -y curl ca-certificates", - // Download sandbox-agent - `curl -fsSL -o ${BINARY} https://releases.rivet.dev/sandbox-agent/latest/binaries/sandbox-agent-x86_64-unknown-linux-musl && chmod +x ${BINARY}`, - // Create agent bin directory - `mkdir -p ${AGENT_BIN_DIR}`, - // Install Claude: get latest version, download binary - `CLAUDE_VERSION=$(curl -fsSL https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest) && ` + - `curl -fsSL -o ${AGENT_BIN_DIR}/claude "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/$CLAUDE_VERSION/linux-x64/claude" && ` + - `chmod +x ${AGENT_BIN_DIR}/claude`, - // Install Codex: download tarball, extract binary - `curl -fsSL -L https://github.com/openai/codex/releases/latest/download/codex-x86_64-unknown-linux-musl.tar.gz | tar -xzf - -C /tmp && ` + - `find /tmp -name 'codex-x86_64-unknown-linux-musl' -exec mv {} ${AGENT_BIN_DIR}/codex \\; && ` + - `chmod +x ${AGENT_BIN_DIR}/codex`, + // Install sandbox-agent via install script + "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", + // Pre-install agents using sandbox-agent CLI + "sandbox-agent install-agent claude", + "sandbox-agent install-agent codex", ), }, { onLogs: (log) => console.log(` ${log}`) }, diff --git a/examples/daytona/src/daytona.ts b/examples/daytona/src/daytona.ts index a894576..7005981 100644 --- a/examples/daytona/src/daytona.ts +++ b/examples/daytona/src/daytona.ts @@ -27,11 +27,11 @@ if (!hasSnapshot) { image: Image.base("ubuntu:22.04").runCommands( // Install dependencies "apt-get update && apt-get install -y curl ca-certificates", - // Download sandbox-agent - `curl -fsSL -o ${BINARY} https://releases.rivet.dev/sandbox-agent/latest/binaries/sandbox-agent-x86_64-unknown-linux-musl && chmod +x ${BINARY}`, + // Install sandbox-agent via install script + "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", // Pre-install agents using sandbox-agent CLI - `${BINARY} install-agent claude`, - `${BINARY} install-agent codex`, + "sandbox-agent install-agent claude", + "sandbox-agent install-agent codex", ), }, { onLogs: (log) => console.log(` ${log}`) }, diff --git a/examples/docker/package.json b/examples/docker/package.json index 28d126d..82396cb 100644 --- a/examples/docker/package.json +++ b/examples/docker/package.json @@ -3,7 +3,8 @@ "private": true, "type": "module", "scripts": { - "start": "tsx src/docker.ts" + "start": "tsx src/docker.ts", + "typecheck": "tsc --noEmit" }, "dependencies": { "dockerode": "latest", diff --git a/examples/docker/src/docker.ts b/examples/docker/src/docker.ts index 50754f0..b79b007 100644 --- a/examples/docker/src/docker.ts +++ b/examples/docker/src/docker.ts @@ -1,132 +1,58 @@ import Docker from "dockerode"; -import { pathToFileURL } from "node:url"; -import { - ensureUrl, - logInspectorUrl, - runPrompt, - waitForHealth, -} from "@sandbox-agent/example-shared"; +import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -const INSTALL_SCRIPT = "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"; -const DEFAULT_IMAGE = "debian:bookworm-slim"; -const DEFAULT_PORT = 2468; +if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { + throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); +} -async function pullImage(docker: Docker, image: string): Promise { +const IMAGE = "debian:bookworm-slim"; +const PORT = 3000; + +const docker = new Docker({ socketPath: "/var/run/docker.sock" }); + +// Pull image if needed +try { + await docker.getImage(IMAGE).inspect(); +} catch { + console.log(`Pulling ${IMAGE}...`); await new Promise((resolve, reject) => { - docker.pull(image, (error, stream) => { - if (error) { - reject(error); - return; - } - docker.modem.followProgress(stream, (progressError) => { - if (progressError) { - reject(progressError); - } else { - resolve(); - } - }); + docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) return reject(err); + docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve()); }); }); } -async function ensureImage(docker: Docker, image: string): Promise { - try { - await docker.getImage(image).inspect(); - } catch { - await pullImage(docker, image); - } -} +console.log("Starting container..."); +const container = await docker.createContainer({ + Image: IMAGE, + Cmd: ["bash", "-lc", [ + "apt-get update && apt-get install -y curl ca-certificates", + "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", + "sandbox-agent install-agent claude", + "sandbox-agent install-agent codex", + `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, + ].join(" && ")], + ExposedPorts: { [`${PORT}/tcp`]: {} }, + HostConfig: { + AutoRemove: true, + PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] }, + }, +}); +await container.start(); -export async function setupDockerSandboxAgent(): Promise<{ - baseUrl: string; - token: string; - cleanup: () => Promise; -}> { - const token = process.env.SANDBOX_TOKEN || ""; - const port = Number.parseInt(process.env.SANDBOX_PORT || "", 10) || DEFAULT_PORT; - const hostPort = Number.parseInt(process.env.SANDBOX_HOST_PORT || "", 10) || port; - const image = process.env.DOCKER_IMAGE || DEFAULT_IMAGE; - const containerName = process.env.DOCKER_CONTAINER_NAME; - const socketPath = process.env.DOCKER_SOCKET || "/var/run/docker.sock"; +const baseUrl = `http://127.0.0.1:${PORT}`; +await waitForHealth({ baseUrl }); +logInspectorUrl({ baseUrl }); - const docker = new Docker({ socketPath }); - await ensureImage(docker, image); +const cleanup = async () => { + console.log("Cleaning up..."); + try { await container.stop({ t: 5 }); } catch {} + try { await container.remove({ force: true }); } catch {} + process.exit(0); +}; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); - const tokenFlag = token ? "--token $SANDBOX_TOKEN" : "--no-token"; - const command = [ - "bash", - "-lc", - [ - "apt-get update", - "apt-get install -y curl ca-certificates", - INSTALL_SCRIPT, - `sandbox-agent server ${tokenFlag} --host 0.0.0.0 --port ${port}`, - ].join(" && "), - ]; - - const container = await docker.createContainer({ - Image: image, - Cmd: command, - Env: token ? [`SANDBOX_TOKEN=${token}`] : [], - ExposedPorts: { - [`${port}/tcp`]: {}, - }, - HostConfig: { - AutoRemove: true, - PortBindings: { - [`${port}/tcp`]: [{ HostPort: `${hostPort}` }], - }, - }, - ...(containerName ? { name: containerName } : {}), - }); - - await container.start(); - - const baseUrl = ensureUrl(`http://127.0.0.1:${hostPort}`); - await waitForHealth({ baseUrl, token }); - logInspectorUrl({ baseUrl, token }); - - const cleanup = async () => { - try { - await container.stop({ t: 5 }); - } catch { - // ignore stop errors - } - try { - await container.remove({ force: true }); - } catch { - // ignore remove errors - } - }; - - return { - baseUrl, - token, - cleanup, - }; -} - -async function main(): Promise { - const { baseUrl, token, cleanup } = await setupDockerSandboxAgent(); - - const exitHandler = async () => { - await cleanup(); - process.exit(0); - }; - - process.on("SIGINT", () => { - void exitHandler(); - }); - process.on("SIGTERM", () => { - void exitHandler(); - }); - - await runPrompt({ baseUrl, token }); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} +await runPrompt({ baseUrl }); +await cleanup(); diff --git a/examples/docker/tsconfig.json b/examples/docker/tsconfig.json new file mode 100644 index 0000000..96ba2fd --- /dev/null +++ b/examples/docker/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/examples/e2b/package.json b/examples/e2b/package.json index 001851c..1d9f84e 100644 --- a/examples/e2b/package.json +++ b/examples/e2b/package.json @@ -3,7 +3,8 @@ "private": true, "type": "module", "scripts": { - "start": "tsx src/e2b.ts" + "start": "tsx src/e2b.ts", + "typecheck": "tsc --noEmit" }, "dependencies": { "@e2b/code-interpreter": "latest", diff --git a/examples/e2b/src/e2b.ts b/examples/e2b/src/e2b.ts index 3cff4cd..4e54767 100644 --- a/examples/e2b/src/e2b.ts +++ b/examples/e2b/src/e2b.ts @@ -1,89 +1,32 @@ import { Sandbox } from "@e2b/code-interpreter"; -import { pathToFileURL } from "node:url"; -import { - ensureUrl, - logInspectorUrl, - runPrompt, - waitForHealth, -} from "@sandbox-agent/example-shared"; +import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared"; -const INSTALL_SCRIPT = "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"; -const DEFAULT_PORT = 2468; - -type CommandRunner = (command: string, options?: Record) => Promise; - -function resolveCommandRunner(sandbox: Sandbox): CommandRunner { - if (sandbox.commands?.run) { - return sandbox.commands.run.bind(sandbox.commands); - } - if (sandbox.commands?.exec) { - return sandbox.commands.exec.bind(sandbox.commands); - } - throw new Error("E2B SDK does not expose commands.run or commands.exec"); +if (!process.env.E2B_API_KEY || (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY)) { + throw new Error("E2B_API_KEY and (OPENAI_API_KEY or ANTHROPIC_API_KEY) required"); } -export async function setupE2BSandboxAgent(): Promise<{ - baseUrl: string; - token: string; - cleanup: () => Promise; -}> { - const token = process.env.SANDBOX_TOKEN || ""; - const port = Number.parseInt(process.env.SANDBOX_PORT || "", 10) || DEFAULT_PORT; +const sandbox = await Sandbox.create({ allowInternetAccess: true }); - const sandbox = await Sandbox.create({ - allowInternetAccess: true, - envs: token ? { SANDBOX_TOKEN: token } : undefined, - }); +const run = (cmd: string) => sandbox.commands.run(cmd); - const runCommand = resolveCommandRunner(sandbox); +console.log("Installing sandbox-agent..."); +await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); +await run("sandbox-agent install-agent claude"); +await run("sandbox-agent install-agent codex"); - await runCommand(`bash -lc "${INSTALL_SCRIPT}"`); - const tokenFlag = token ? "--token $SANDBOX_TOKEN" : "--no-token"; - await runCommand(`bash -lc "sandbox-agent server ${tokenFlag} --host 0.0.0.0 --port ${port}"`, { - background: true, - envs: token ? { SANDBOX_TOKEN: token } : undefined, - }); +console.log("Starting server..."); +await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); - const baseUrl = ensureUrl(sandbox.getHost(port)); - await waitForHealth({ baseUrl, token }); - logInspectorUrl({ baseUrl, token }); +const baseUrl = `https://${sandbox.getHost(3000)}`; +logInspectorUrl({ baseUrl }); - const cleanup = async () => { - try { - await sandbox.kill(); - } catch { - // ignore cleanup errors - } - }; +const cleanup = async () => { + console.log("Cleaning up..."); + await sandbox.kill(); + process.exit(0); +}; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); - return { - baseUrl, - token, - cleanup, - }; -} - -async function main(): Promise { - const { baseUrl, token, cleanup } = await setupE2BSandboxAgent(); - - const exitHandler = async () => { - await cleanup(); - process.exit(0); - }; - - process.on("SIGINT", () => { - void exitHandler(); - }); - process.on("SIGTERM", () => { - void exitHandler(); - }); - - await runPrompt({ baseUrl, token }); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} +await runPrompt({ baseUrl }); +await cleanup(); diff --git a/examples/e2b/tsconfig.json b/examples/e2b/tsconfig.json new file mode 100644 index 0000000..96ba2fd --- /dev/null +++ b/examples/e2b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/examples/shared/src/sandbox-agent-client.ts b/examples/shared/src/sandbox-agent-client.ts index ddd3c40..d23a45d 100644 --- a/examples/shared/src/sandbox-agent-client.ts +++ b/examples/shared/src/sandbox-agent-client.ts @@ -1,4 +1,4 @@ -import { createInterface } from "node:readline"; +import { createInterface } from "node:readline/promises"; import { randomUUID } from "node:crypto"; import { setTimeout as delay } from "node:timers/promises"; @@ -277,25 +277,13 @@ export async function runPrompt({ agentId?: string; }): Promise { const sessionId = await createSession({ baseUrl, token, extraHeaders, agentId }); + console.log(`Session ${sessionId} ready. Press Ctrl+C to quit.`); - console.log(`Session ${sessionId} ready. Type /exit to quit.`); + const rl = createInterface({ input: process.stdin, output: process.stdout }); - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - prompt: "> ", - }); - - const handleLine = async (line: string) => { - const trimmed = line.trim(); - if (!trimmed) { - rl.prompt(); - return; - } - if (trimmed === "/exit") { - rl.close(); - return; - } + while (true) { + const line = await rl.question("> "); + if (!line.trim()) continue; try { await sendMessageStream({ @@ -303,24 +291,12 @@ export async function runPrompt({ token, extraHeaders, sessionId, - message: trimmed, + message: line.trim(), onText: (text) => process.stdout.write(text), }); process.stdout.write("\n"); } catch (error) { console.error(error instanceof Error ? error.message : error); } - - rl.prompt(); - }; - - rl.on("line", (line) => { - void handleLine(line); - }); - - rl.on("close", () => { - process.exit(0); - }); - - rl.prompt(); + } } diff --git a/examples/vercel/package.json b/examples/vercel/package.json index b89c3c8..924bc4e 100644 --- a/examples/vercel/package.json +++ b/examples/vercel/package.json @@ -3,7 +3,8 @@ "private": true, "type": "module", "scripts": { - "start": "tsx src/vercel-sandbox.ts" + "start": "tsx src/vercel-sandbox.ts", + "typecheck": "tsc --noEmit" }, "dependencies": { "@vercel/sandbox": "latest", diff --git a/examples/vercel/src/vercel-sandbox.ts b/examples/vercel/src/vercel-sandbox.ts index 34c3378..696dbd5 100644 --- a/examples/vercel/src/vercel-sandbox.ts +++ b/examples/vercel/src/vercel-sandbox.ts @@ -1,105 +1,46 @@ import { Sandbox } from "@vercel/sandbox"; -import { pathToFileURL } from "node:url"; -import { - ensureUrl, - logInspectorUrl, - runPrompt, - waitForHealth, -} from "@sandbox-agent/example-shared"; +import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -const INSTALL_SCRIPT = "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"; -const DEFAULT_PORT = 2468; +if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { + throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); +} -type VercelSandboxOptions = { - runtime: string; - ports: number[]; - token?: string; - teamId?: string; - projectId?: string; +const PORT = 3000; + +const sandbox = await Sandbox.create({ + runtime: process.env.VERCEL_RUNTIME || "node24", + ports: [PORT], + ...(process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID + ? { token: process.env.VERCEL_TOKEN, teamId: process.env.VERCEL_TEAM_ID, projectId: process.env.VERCEL_PROJECT_ID } + : {}), +}); + +const run = (cmd: string) => sandbox.runCommand({ cmd: "bash", args: ["-lc", cmd], sudo: true }); + +console.log("Installing sandbox-agent..."); +await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); +await run("sandbox-agent install-agent claude"); +await run("sandbox-agent install-agent codex"); + +console.log("Starting server..."); +await sandbox.runCommand({ + cmd: "bash", + args: ["-lc", `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`], + sudo: true, + detached: true, +}); + +const baseUrl = `https://${sandbox.domain(PORT)}`; +await waitForHealth({ baseUrl }); +logInspectorUrl({ baseUrl }); + +const cleanup = async () => { + console.log("Cleaning up..."); + await sandbox.stop(); + process.exit(0); }; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); -export async function setupVercelSandboxAgent(): Promise<{ - baseUrl: string; - token: string; - cleanup: () => Promise; -}> { - const token = process.env.SANDBOX_TOKEN || ""; - const port = Number.parseInt(process.env.SANDBOX_PORT || "", 10) || DEFAULT_PORT; - const runtime = process.env.VERCEL_RUNTIME || "node24"; - - const createOptions: VercelSandboxOptions = { - runtime, - ports: [port], - }; - - const accessToken = process.env.VERCEL_TOKEN; - const teamId = process.env.VERCEL_TEAM_ID; - const projectId = process.env.VERCEL_PROJECT_ID; - if (accessToken && teamId && projectId) { - createOptions.token = accessToken; - createOptions.teamId = teamId; - createOptions.projectId = projectId; - } - - const sandbox = await Sandbox.create(createOptions); - - await sandbox.runCommand({ - cmd: "bash", - args: ["-lc", INSTALL_SCRIPT], - sudo: true, - }); - - const tokenFlag = token ? "--token $SANDBOX_TOKEN" : "--no-token"; - await sandbox.runCommand({ - cmd: "bash", - args: [ - "-lc", - `SANDBOX_TOKEN=${token} sandbox-agent server ${tokenFlag} --host 0.0.0.0 --port ${port}`, - ], - sudo: true, - detached: true, - }); - - const baseUrl = ensureUrl(sandbox.domain(port)); - await waitForHealth({ baseUrl, token }); - logInspectorUrl({ baseUrl, token }); - - const cleanup = async () => { - try { - await sandbox.stop(); - } catch { - // ignore cleanup errors - } - }; - - return { - baseUrl, - token, - cleanup, - }; -} - -async function main(): Promise { - const { baseUrl, token, cleanup } = await setupVercelSandboxAgent(); - - const exitHandler = async () => { - await cleanup(); - process.exit(0); - }; - - process.on("SIGINT", () => { - void exitHandler(); - }); - process.on("SIGTERM", () => { - void exitHandler(); - }); - - await runPrompt({ baseUrl, token }); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} +await runPrompt({ baseUrl }); +await cleanup(); diff --git a/examples/vercel/tsconfig.json b/examples/vercel/tsconfig.json new file mode 100644 index 0000000..96ba2fd --- /dev/null +++ b/examples/vercel/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/todo.md b/todo.md deleted file mode 100644 index 3bdcd8e..0000000 --- a/todo.md +++ /dev/null @@ -1,14 +0,0 @@ -# Todo - -- [x] Replace server --mock flag with built-in mock agent and update UI approvals layout. -- [x] Add telemetry module with opt-out flag and sandbox provider detection. -- [x] Add turn-stream message endpoint with SSE response and tests. -- [x] Update CLI + TypeScript SDK/OpenAPI for turn streaming. -- [x] Add inspector UI mode for turn stream and wire send flow. -- [x] Refresh docs for new endpoint and UI mode. -- [x] Add Docker/Vercel/Daytona/E2B examples with basic prompt scripts and tests. -- [x] Add unified AgentServerManager for shared agent servers (Codex/OpenCode). -- [x] Expose server status details in agent list API (uptime/restarts/last error/base URL). -- [x] Add local agent install CLI command and document optional preinstall step. -- [x] Move API CLI commands under the api subcommand. -- [ ] Regenerate TypeScript SDK from updated OpenAPI (blocked: Node/pnpm not available in env).