Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions examples/computesdk/src/computesdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 });
Expand Down
4 changes: 2 additions & 2 deletions examples/daytona/src/daytona-with-snapshot.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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)...");
Expand Down
67 changes: 51 additions & 16 deletions examples/daytona/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {};
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 })}`);
Expand Down
4 changes: 2 additions & 2 deletions examples/docker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(" && "),
],
Expand Down
84 changes: 62 additions & 22 deletions examples/e2b/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {};
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<string> {
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<string> {
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);
Expand Down
86 changes: 41 additions & 45 deletions examples/shared/src/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<string> {
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<void> {
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<boolean>((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();
}
}

/**
Expand All @@ -166,8 +177,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
*/
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
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();
Expand Down Expand Up @@ -197,27 +207,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do

const docker = new Docker({ socketPath: "/var/run/docker.sock" });

if (useCustomImage) {
try {
await docker.getImage(image).inspect();
} catch {
console.log(` Pulling ${image}...`);
await new Promise<void>((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`]: {} },
Expand Down
Loading