diff --git a/examples/computesdk/src/computesdk.ts b/examples/computesdk/src/computesdk.ts index 46f43d6e..21ca596d 100644 --- a/examples/computesdk/src/computesdk.ts +++ b/examples/computesdk/src/computesdk.ts @@ -10,7 +10,7 @@ import { type ProviderName, } from "computesdk"; import { SandboxAgent } from "sandbox-agent"; -import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { detectAgent, buildInspectorUrl, generateInstallCommand, type SandboxAgentComponent } from "@sandbox-agent/example-shared"; import { fileURLToPath } from "node:url"; import { resolve } from "node:path"; @@ -90,18 +90,12 @@ export async function setupComputeSdkSandboxAgent(): Promise<{ return result; }; - console.log("Installing sandbox-agent..."); - await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); - - if (env.ANTHROPIC_API_KEY) { - console.log("Installing Claude agent..."); - await run("sandbox-agent install-agent claude"); - } + const components: SandboxAgentComponent[] = []; + if (env.ANTHROPIC_API_KEY) components.push("claude"); + if (env.OPENAI_API_KEY) components.push("codex"); - if (env.OPENAI_API_KEY) { - console.log("Installing Codex agent..."); - await run("sandbox-agent install-agent codex"); - } + console.log("Installing sandbox-agent..."); + await run(generateInstallCommand({ components })); console.log("Starting server..."); await run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, { background: true }); diff --git a/examples/daytona/src/daytona-with-snapshot.ts b/examples/daytona/src/daytona-with-snapshot.ts index 661d303b..117cd0eb 100644 --- a/examples/daytona/src/daytona-with-snapshot.ts +++ b/examples/daytona/src/daytona-with-snapshot.ts @@ -1,6 +1,6 @@ import { Daytona, Image } from "@daytonaio/sdk"; import { SandboxAgent } from "sandbox-agent"; -import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { detectAgent, buildInspectorUrl, generateInstallCommand } from "@sandbox-agent/example-shared"; const daytona = new Daytona(); @@ -11,7 +11,7 @@ if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_ // Build a custom image with sandbox-agent pre-installed (slower first run, faster subsequent runs) const image = Image.base("ubuntu:22.04").runCommands( "apt-get update && apt-get install -y curl ca-certificates", - "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh", + generateInstallCommand(), ); console.log("Creating Daytona sandbox (first run builds the base image and may take a few minutes, subsequent runs are fast)..."); diff --git a/examples/daytona/src/index.ts b/examples/daytona/src/index.ts index 09f4cffc..5b67587c 100644 --- a/examples/daytona/src/index.ts +++ b/examples/daytona/src/index.ts @@ -1,32 +1,67 @@ -import { Daytona } from "@daytonaio/sdk"; +import { Daytona, Image } from "@daytonaio/sdk"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { SandboxAgent } from "sandbox-agent"; -import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { + SANDBOX_AGENT_IMAGE, + SANDBOX_AGENT_INSTALL_VERSION, + buildCredentialEnv, + buildInspectorUrl, + detectAgent, + generateBaseImageDockerfile, + getPreinstallComponents, +} from "@sandbox-agent/example-shared"; const daytona = new Daytona(); -const envVars: Record = {}; -if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; -if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const envVars = buildCredentialEnv(); +const agent = detectAgent(); +const components = getPreinstallComponents(agent); +const componentSuffix = components.length > 0 ? components.join("-") : "base"; +const baseImage = process.env.SANDBOX_AGENT_DAYTONA_IMAGE ?? SANDBOX_AGENT_IMAGE; +const snapshotName = process.env.SANDBOX_AGENT_DAYTONA_SNAPSHOT ?? `sandbox-agent-${SANDBOX_AGENT_INSTALL_VERSION.replaceAll(".", "-")}-${componentSuffix}`; -// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build) -console.log("Creating Daytona sandbox..."); -const sandbox = await daytona.create({ envVars, autoStopInterval: 0 }); +async function ensureSnapshot(name: string) { + try { + return await daytona.snapshot.get(name); + } catch { + console.log(`Building Daytona snapshot ${name} from ${baseImage}...`); + const dockerfileDir = fs.mkdtempSync(path.join(os.tmpdir(), "sandbox-agent-daytona-")); + const dockerfilePath = path.join(dockerfileDir, "Dockerfile"); + fs.writeFileSync( + dockerfilePath, + generateBaseImageDockerfile({ + image: baseImage, + components, + }), + "utf8", + ); -// Install sandbox-agent and start server -console.log("Installing sandbox-agent..."); -await sandbox.process.executeCommand("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"); + const image = Image.fromDockerfile(dockerfilePath) + .workdir("/home/sandbox") + .entrypoint(["sandbox-agent", "server", "--no-token", "--host", "0.0.0.0", "--port", "3000"]); -console.log("Installing agents..."); -await sandbox.process.executeCommand("sandbox-agent install-agent claude"); -await sandbox.process.executeCommand("sandbox-agent install-agent codex"); + try { + const snapshot = await daytona.snapshot.create({ name, image }, { timeout: 180, onLogs: (line) => console.log(line) }); -await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"); + return await daytona.snapshot.activate(snapshot).catch(() => snapshot); + } finally { + fs.rmSync(dockerfileDir, { recursive: true, force: true }); + } + } +} + +const snapshot = await ensureSnapshot(snapshotName); + +console.log(`Creating Daytona sandbox from snapshot ${snapshot.name}...`); +const sandbox = await daytona.create({ envVars, snapshot: snapshot.name, autoStopInterval: 0 }, { timeout: 180 }); const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url; console.log("Connecting to server..."); const client = await SandboxAgent.connect({ baseUrl }); -const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/daytona", mcpServers: [] } }); +const session = await client.createSession({ agent, sessionInit: { cwd: "/home/sandbox", mcpServers: [] } }); const sessionId = session.id; console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`); diff --git a/examples/docker/src/index.ts b/examples/docker/src/index.ts index c6f29c2c..115ceda0 100644 --- a/examples/docker/src/index.ts +++ b/examples/docker/src/index.ts @@ -2,7 +2,7 @@ import Docker from "dockerode"; import fs from "node:fs"; import path from "node:path"; import { SandboxAgent } from "sandbox-agent"; -import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { detectAgent, buildInspectorUrl, generateInstallCommand } from "@sandbox-agent/example-shared"; const IMAGE = "node:22-bookworm-slim"; const PORT = 3000; @@ -35,7 +35,7 @@ const container = await docker.createContainer({ "apt-get update", "DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6", "rm -rf /var/lib/apt/lists/*", - "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh", + generateInstallCommand(), `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, ].join(" && "), ], diff --git a/examples/e2b/src/index.ts b/examples/e2b/src/index.ts index 7dd28820..ff625f1f 100644 --- a/examples/e2b/src/index.ts +++ b/examples/e2b/src/index.ts @@ -1,38 +1,78 @@ -import { Sandbox } from "@e2b/code-interpreter"; +import { Sandbox, Template, defaultBuildLogger } from "@e2b/code-interpreter"; import { SandboxAgent } from "sandbox-agent"; -import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { + SANDBOX_AGENT_IMAGE, + SANDBOX_AGENT_INSTALL_VERSION, + buildCredentialEnv, + buildInspectorUrl, + detectAgent, + getPreinstallComponents, +} from "@sandbox-agent/example-shared"; -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 envs = buildCredentialEnv(); +const agent = detectAgent(); +const components = getPreinstallComponents(agent); +const componentSuffix = components.length > 0 ? components.join("-") : "base"; +const baseImage = process.env.SANDBOX_AGENT_E2B_IMAGE ?? SANDBOX_AGENT_IMAGE; +const templateName = process.env.SANDBOX_AGENT_E2B_TEMPLATE ?? `sandbox-agent-${SANDBOX_AGENT_INSTALL_VERSION.replaceAll(".", "-")}-${componentSuffix}`; -console.log("Creating E2B sandbox..."); -const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); +async function ensureTemplate(name: string): Promise { + if (await Template.exists(name)) { + return name; + } + return buildTemplate(name); +} -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; -}; +async function buildTemplate(name: string): Promise { + console.log(`Building E2B template ${name} from ${baseImage}...`); + + let templateBuilder = Template().fromImage(baseImage); + if (components.includes("codex")) { + templateBuilder = templateBuilder.setUser("root").aptInstall("npm").setUser("user"); + } + for (const component of components) { + templateBuilder = templateBuilder.runCmd(`sandbox-agent install-agent ${component}`); + } + const template = templateBuilder; + + await Template.build(template, name, { + onBuildLogs: defaultBuildLogger(), + }); -console.log("Installing sandbox-agent..."); -await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"); + return name; +} -console.log("Installing agents..."); -await run("sandbox-agent install-agent claude"); -await run("sandbox-agent install-agent codex"); +function isMissingTemplateError(error: unknown): boolean { + return error instanceof Error && /template '.*' not found/.test(error.message); +} -console.log("Starting server..."); -await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true, timeoutMs: 0 }); +const resolvedTemplate = await ensureTemplate(templateName); + +console.log(`Creating E2B sandbox from template ${resolvedTemplate}...`); +let sandbox; +try { + sandbox = await Sandbox.create(resolvedTemplate, { allowInternetAccess: true, envs }); +} catch (error) { + if (!process.env.SANDBOX_AGENT_E2B_TEMPLATE && isMissingTemplateError(error)) { + const fallbackTemplate = `${templateName}-${Date.now()}`; + console.log(`Template ${resolvedTemplate} is stale; rebuilding as ${fallbackTemplate}...`); + sandbox = await Sandbox.create(await buildTemplate(fallbackTemplate), { allowInternetAccess: true, envs }); + } else { + throw error; + } +} const baseUrl = `https://${sandbox.getHost(3000)}`; +const token = sandbox.trafficAccessToken; + +await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); console.log("Connecting to server..."); -const client = await SandboxAgent.connect({ baseUrl }); -const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/user", mcpServers: [] } }); +const client = await SandboxAgent.connect({ baseUrl, token }); +const session = await client.createSession({ agent, sessionInit: { cwd: "/home/user", mcpServers: [] } }); const sessionId = session.id; -console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`); +console.log(` UI: ${buildInspectorUrl({ baseUrl, token, sessionId })}`); console.log(" Press Ctrl+C to stop."); const keepAlive = setInterval(() => {}, 60_000); diff --git a/examples/shared/src/docker.ts b/examples/shared/src/docker.ts index 80c39162..15716986 100644 --- a/examples/shared/src/docker.ts +++ b/examples/shared/src/docker.ts @@ -6,10 +6,10 @@ import { PassThrough } from "node:stream"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const EXAMPLE_IMAGE = "sandbox-agent-examples:latest"; -const EXAMPLE_IMAGE_DEV = "sandbox-agent-examples-dev:latest"; -const DOCKERFILE_DIR = path.resolve(__dirname, ".."); -const REPO_ROOT = path.resolve(DOCKERFILE_DIR, "../.."); +const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); + +/** Pre-built Docker image with all agents installed. */ +export const FULL_IMAGE = "rivetdev/sandbox-agent:0.3.1-full"; export interface DockerSandboxOptions { /** Container port used by sandbox-agent inside Docker. */ @@ -18,7 +18,7 @@ export interface DockerSandboxOptions { hostPort?: number; /** Additional shell commands to run before starting sandbox-agent. */ setupCommands?: string[]; - /** Docker image to use. Defaults to the pre-built sandbox-agent-examples image. */ + /** Docker image to use. Defaults to the pre-built full image. */ image?: string; } @@ -131,33 +131,44 @@ function stripAnsi(value: string): string { return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, ""); } -async function ensureExampleImage(_docker: Docker): Promise { - const dev = !!process.env.SANDBOX_AGENT_DEV; - const imageName = dev ? EXAMPLE_IMAGE_DEV : EXAMPLE_IMAGE; - - if (dev) { - console.log(" Building sandbox image from source (may take a while, only runs once)..."); - try { - execFileSync("docker", ["build", "-t", imageName, "-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"), REPO_ROOT], { - stdio: ["ignore", "ignore", "pipe"], - }); - } catch (err: unknown) { - const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : ""; - throw new Error(`Failed to build sandbox image: ${stderr}`); - } - } else { - console.log(" Building sandbox image (may take a while, only runs once)..."); +async function ensureImage(docker: Docker, image: string): Promise { + const buildFromSource = () => { + console.log(" Building sandbox image from source (may take a while)..."); try { - execFileSync("docker", ["build", "-t", imageName, DOCKERFILE_DIR], { - stdio: ["ignore", "ignore", "pipe"], + execFileSync("docker", ["build", "-t", image, "-f", path.join(REPO_ROOT, "docker/runtime/Dockerfile.full"), REPO_ROOT], { + stdio: "inherit", }); - } catch (err: unknown) { - const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : ""; - throw new Error(`Failed to build sandbox image: ${stderr}`); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to build sandbox image: ${message}`); } + }; + + if (process.env.SANDBOX_AGENT_DEV) { + buildFromSource(); + return; } - return imageName; + try { + await docker.getImage(image).inspect(); + return; + } catch {} + + console.log(` Pulling ${image}...`); + const pulled = await new Promise((resolve) => { + docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) { + resolve(false); + return; + } + docker.modem.followProgress(stream, (progressErr: Error | null) => resolve(!progressErr)); + }); + }); + + if (!pulled) { + console.log(` Could not pull ${image}; falling back to a local full-image build.`); + buildFromSource(); + } } /** @@ -166,8 +177,7 @@ async function ensureExampleImage(_docker: Docker): Promise { */ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise { const { port, hostPort } = opts; - const useCustomImage = !!opts.image; - let image = opts.image ?? EXAMPLE_IMAGE; + const image = opts.image ?? FULL_IMAGE; // TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available. const setupCommands = [...(opts.setupCommands ?? [])]; const credentialEnv = collectCredentialEnv(); @@ -197,27 +207,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): 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())); - }); - }); - } - } else { - image = await ensureExampleImage(docker); - } + await ensureImage(docker, image); const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`]; const container = await docker.createContainer({ Image: image, - WorkingDir: "/root", + WorkingDir: "/home/sandbox", Cmd: ["sh", "-c", bootCommands.join(" && ")], Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)], ExposedPorts: { [`${port}/tcp`]: {} }, diff --git a/examples/shared/src/sandbox-agent-client.ts b/examples/shared/src/sandbox-agent-client.ts index 8efec195..bcee2954 100644 --- a/examples/shared/src/sandbox-agent-client.ts +++ b/examples/shared/src/sandbox-agent-client.ts @@ -3,6 +3,76 @@ * Provides minimal helpers for connecting to and interacting with sandbox-agent servers. */ +export const SANDBOX_AGENT_INSTALL_VERSION = "0.3.1"; +export const SANDBOX_AGENT_IMAGE = `rivetdev/sandbox-agent:${SANDBOX_AGENT_INSTALL_VERSION}`; + +export type SandboxAgentComponent = "claude" | "codex" | "opencode" | "amp"; + +const DIRECT_CREDENTIAL_KEYS = [ + "ANTHROPIC_API_KEY", + "CLAUDE_API_KEY", + "CLAUDE_CODE_OAUTH_TOKEN", + "ANTHROPIC_AUTH_TOKEN", + "OPENAI_API_KEY", + "CODEX_API_KEY", + "CEREBRAS_API_KEY", + "OPENCODE_API_KEY", +] as const; + +export function generateInstallCommand({ + version = SANDBOX_AGENT_INSTALL_VERSION, + components = [], +}: { + version?: string; + components?: SandboxAgentComponent[]; +} = {}): string { + const uniqueComponents = [...new Set(components)]; + + return [ + `curl -fsSL https://releases.rivet.dev/sandbox-agent/${version}/install.sh | sh`, + ...uniqueComponents.map((component) => `sandbox-agent install-agent ${component}`), + ].join(" && "); +} + +export function getPreinstallComponents(agent: string): SandboxAgentComponent[] { + return ["claude", "codex", "opencode", "amp"].includes(agent) ? [agent as SandboxAgentComponent] : []; +} + +export function generateBaseImageDockerfile({ + image = SANDBOX_AGENT_IMAGE, + components = [], +}: { + image?: string; + components?: SandboxAgentComponent[]; +} = {}): string { + const uniqueComponents = [...new Set(components)]; + const lines = [`FROM ${image}`]; + + if (uniqueComponents.includes("codex")) { + lines.push("USER root"); + lines.push("RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y npm && rm -rf /var/lib/apt/lists/*"); + lines.push("USER sandbox"); + } + + lines.push("WORKDIR /home/sandbox"); + for (const component of uniqueComponents) { + lines.push(`RUN sandbox-agent install-agent ${component}`); + } + + return `${lines.join("\n")}\n`; +} + +export function buildCredentialEnv(): Record { + const env: Record = {}; + for (const key of DIRECT_CREDENTIAL_KEYS) { + const value = process.env[key]; + if (value) { + env[key] = value; + } + } + return env; +} + function normalizeBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/, ""); } diff --git a/examples/vercel/src/index.ts b/examples/vercel/src/index.ts index 4a63bfcc..336b3450 100644 --- a/examples/vercel/src/index.ts +++ b/examples/vercel/src/index.ts @@ -1,6 +1,6 @@ import { Sandbox } from "@vercel/sandbox"; import { SandboxAgent } from "sandbox-agent"; -import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { detectAgent, buildInspectorUrl, generateInstallCommand } from "@sandbox-agent/example-shared"; const envs: Record = {}; if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; @@ -22,11 +22,7 @@ const run = async (cmd: string, args: string[] = []) => { }; console.log("Installing sandbox-agent..."); -await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"]); - -console.log("Installing agents..."); -await run("sandbox-agent", ["install-agent", "claude"]); -await run("sandbox-agent", ["install-agent", "codex"]); +await run("sh", ["-c", generateInstallCommand({ components: ["claude", "codex"] })]); console.log("Starting server..."); await sandbox.runCommand({