diff --git a/examples/docker/package.json b/examples/docker/package.json index 82396cb..289b0c3 100644 --- a/examples/docker/package.json +++ b/examples/docker/package.json @@ -7,13 +7,14 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "dockerode": "latest", - "@sandbox-agent/example-shared": "workspace:*" + "@sandbox-agent/example-shared": "workspace:*", + "dockerode": "latest" }, "devDependencies": { "@types/dockerode": "latest", "@types/node": "latest", "tsx": "latest", - "typescript": "latest" + "typescript": "latest", + "vitest": "^3.0.0" } } diff --git a/examples/docker/src/docker.test.ts b/examples/docker/src/docker.test.ts index 8c4036b..9b8df32 100644 --- a/examples/docker/src/docker.test.ts +++ b/examples/docker/src/docker.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { buildHeaders } from "../shared/sandbox-agent-client.ts"; +import { buildHeaders } from "@sandbox-agent/example-shared"; import { setupDockerSandboxAgent } from "./docker.ts"; const shouldRun = process.env.RUN_DOCKER_EXAMPLES === "1"; diff --git a/examples/docker/src/docker.ts b/examples/docker/src/docker.ts index 02535c6..4753bfd 100644 --- a/examples/docker/src/docker.ts +++ b/examples/docker/src/docker.ts @@ -1,64 +1,84 @@ import Docker from "dockerode"; import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { - throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); -} - // Alpine is required because Claude Code binary is built for musl libc const IMAGE = "alpine:latest"; const PORT = 3000; -const docker = new Docker({ socketPath: "/var/run/docker.sock" }); +export async function setupDockerSandboxAgent(): Promise<{ + baseUrl: string; + token?: string; + cleanup: () => Promise; +}> { + 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, (err: Error | null, stream: NodeJS.ReadableStream) => { - if (err) return reject(err); - docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve()); + // Pull image if needed + try { + await docker.getImage(IMAGE).inspect(); + } catch { + console.log(`Pulling ${IMAGE}...`); + await new Promise((resolve, reject) => { + 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()); + }); }); + } + + console.log("Starting container..."); + const container = await docker.createContainer({ + Image: IMAGE, + Cmd: ["sh", "-c", [ + // Install dependencies (Alpine uses apk, not apt-get) + "apk add --no-cache curl ca-certificates libstdc++ libgcc bash", + "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(" && ")], + Env: [ + process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "", + process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "", + ].filter(Boolean), + ExposedPorts: { [`${PORT}/tcp`]: {} }, + HostConfig: { + AutoRemove: true, + PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] }, + }, }); + await container.start(); + + const baseUrl = `http://127.0.0.1:${PORT}`; + await waitForHealth({ baseUrl }); + + const cleanup = async () => { + console.log("Cleaning up..."); + try { await container.stop({ t: 5 }); } catch {} + try { await container.remove({ force: true }); } catch {} + }; + + return { baseUrl, cleanup }; } -console.log("Starting container..."); -const container = await docker.createContainer({ - Image: IMAGE, - Cmd: ["sh", "-c", [ - // Install dependencies (Alpine uses apk, not apt-get) - "apk add --no-cache curl ca-certificates libstdc++ libgcc bash", - "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(" && ")], - Env: [ - process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "", - process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "", - ].filter(Boolean), - ExposedPorts: { [`${PORT}/tcp`]: {} }, - HostConfig: { - AutoRemove: true, - PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] }, - }, -}); -await container.start(); +// Run interactively if executed directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { + throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); + } -const baseUrl = `http://127.0.0.1:${PORT}`; -await waitForHealth({ baseUrl }); -logInspectorUrl({ baseUrl }); + const { baseUrl, cleanup } = await setupDockerSandboxAgent(); + logInspectorUrl({ baseUrl }); -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); + process.once("SIGINT", async () => { + await cleanup(); + process.exit(0); + }); + process.once("SIGTERM", async () => { + await cleanup(); + process.exit(0); + }); -await runPrompt({ baseUrl }); -await cleanup(); + await runPrompt({ baseUrl }); + await cleanup(); +} diff --git a/examples/e2b/package.json b/examples/e2b/package.json index be413c6..f44574c 100644 --- a/examples/e2b/package.json +++ b/examples/e2b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@types/node": "latest", "tsx": "latest", - "typescript": "latest" + "typescript": "latest", + "vitest": "^3.0.0" } } diff --git a/examples/e2b/src/e2b.test.ts b/examples/e2b/src/e2b.test.ts index d2fbb45..6ba0c43 100644 --- a/examples/e2b/src/e2b.test.ts +++ b/examples/e2b/src/e2b.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { buildHeaders } from "../shared/sandbox-agent-client.ts"; +import { buildHeaders } from "@sandbox-agent/example-shared"; import { setupE2BSandboxAgent } from "./e2b.ts"; const shouldRun = Boolean(process.env.E2B_API_KEY); diff --git a/examples/e2b/src/e2b.ts b/examples/e2b/src/e2b.ts index dc64ea8..692c15b 100644 --- a/examples/e2b/src/e2b.ts +++ b/examples/e2b/src/e2b.ts @@ -1,52 +1,65 @@ import { Sandbox } from "@e2b/code-interpreter"; -import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared"; +import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -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 envs: Record = {}; + if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; + if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; + + const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); + const run = async (cmd: string) => { + const result = await sandbox.commands.run(cmd); + if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`); + return result; + }; + + console.log("Installing sandbox-agent..."); + await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); + + console.log("Installing agents..."); + await run("sandbox-agent install-agent claude"); + await run("sandbox-agent install-agent codex"); + + console.log("Starting server..."); + await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); + + const baseUrl = `https://${sandbox.getHost(3000)}`; + + // Wait for server to be ready + console.log("Waiting for server..."); + await waitForHealth({ baseUrl }); + + const cleanup = async () => { + console.log("Cleaning up..."); + await sandbox.kill(); + }; + + return { baseUrl, cleanup }; } -const envs: Record = {}; -if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; -if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; - -const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); -const run = async (cmd: string) => { - const result = await sandbox.commands.run(cmd); - if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`); - return result; -}; - -console.log("Installing sandbox-agent..."); -await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); - -console.log("Installing agents..."); -await run("sandbox-agent install-agent claude"); -await run("sandbox-agent install-agent codex"); - -console.log("Starting server..."); -await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); - -const baseUrl = `https://${sandbox.getHost(3000)}`; -logInspectorUrl({ baseUrl }); - -// Wait for server to be ready -console.log("Waiting for server..."); -for (let i = 0; i < 30; i++) { - try { - const res = await fetch(`${baseUrl}/v1/health`); - if (res.ok) break; - } catch { - await new Promise((r) => setTimeout(r, 1000)); +// Run interactively if executed directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + if (!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"); } + + const { baseUrl, cleanup } = await setupE2BSandboxAgent(); + logInspectorUrl({ baseUrl }); + + process.once("SIGINT", async () => { + await cleanup(); + process.exit(0); + }); + process.once("SIGTERM", async () => { + await cleanup(); + process.exit(0); + }); + + await runPrompt({ baseUrl }); + await cleanup(); } - -const cleanup = async () => { - console.log("Cleaning up..."); - await sandbox.kill(); - process.exit(0); -}; -process.once("SIGINT", cleanup); -process.once("SIGTERM", cleanup); - -await runPrompt({ baseUrl }); -await cleanup(); diff --git a/package.json b/package.json index 80b6115..66b5e57 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev:docs": "cd docs/ && pnpm dlx mintlify dev" }, "devDependencies": { - "turbo": "^2.4.0" + "turbo": "^2.4.0", + "vitest": "^3.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7c5b4a..8637a29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: turbo: specifier: ^2.4.0 version: 2.7.6 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.10) examples/daytona: dependencies: @@ -52,6 +55,9 @@ importers: typescript: specifier: latest version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.10) examples/e2b: dependencies: @@ -74,6 +80,9 @@ importers: typescript: specifier: latest version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.10) examples/shared: dependencies: