E2B
Give each Rivet Actor its own E2B sandbox.
E2B runs cloud sandboxes from templates you define, and starts one in a few hundred milliseconds. Through @rivet-dev/sandbox-adapter, each Actor creates an E2B sandbox, runs commands and uses files in it, pauses it while the Actor sleeps, and deletes it when the Actor is destroyed.
Quickstart
Install
npm install rivetkit @rivet-dev/sandbox-adapter e2b
Configure E2B
Create an API key in the E2B dashboard and put it in .env. It stays on the node and never enters the sandbox.
E2B_API_KEY=
Define the Actor
Each build Actor owns one E2B sandbox. The first run, saveFile, or readFile creates it; later calls connect to the same sandbox.
import { posix } from "node:path";
import type { Sandbox, SandboxActorContext } from "@rivet-dev/sandbox-adapter";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { actor, event, setup } from "rivetkit";
// One provider object serves every Actor of this definition.
const provider = e2bProvider({ template: "base" });
interface BuildState {
sandboxId: string | null;
}
export const build = actor({
state: { sandboxId: null } as BuildState,
events: {
// Command output, as it arrives.
output: event<string>(),
},
actions: {
run: async (c, command: string) => {
const sandbox = await openSandbox(c, c.state);
const decoder = new TextDecoder();
return sandbox.exec(command, {
cwd: sandbox.cwd,
timeoutMs: 60_000,
// Stops the command if the Actor shuts down.
signal: c.abortSignal,
onData: (chunk) => c.broadcast("output", decoder.decode(chunk, { stream: true })),
});
},
saveFile: async (c, path: string, content: string) => {
const sandbox = await openSandbox(c, c.state);
const target = posix.resolve(sandbox.cwd, path);
await sandbox.mkdir(posix.dirname(target));
await sandbox.writeFile(target, content);
},
readFile: async (c, path: string) => {
const sandbox = await openSandbox(c, c.state);
const bytes = await sandbox.readFile(posix.resolve(sandbox.cwd, path));
return new TextDecoder().decode(bytes);
},
},
// The sandbox stops while the Actor sleeps and is removed along with the Actor.
onSleep: async (c) => {
if (c.state.sandboxId) await provider.suspend?.(c, c.state.sandboxId);
},
onDestroy: async (c) => {
if (c.state.sandboxId) await provider.destroy?.(c, c.state.sandboxId);
},
});
// Connects to this Actor's sandbox. The first call creates it.
async function openSandbox(c: SandboxActorContext, state: BuildState): Promise<Sandbox> {
const existing = state.sandboxId ? await provider.connect(c, state.sandboxId) : undefined;
if (existing) return existing;
state.sandboxId = await provider.create(c);
const sandbox = await provider.connect(c, state.sandboxId);
if (!sandbox) throw new Error(`Sandbox ${state.sandboxId} was not found right after it was created.`);
return sandbox;
}
export const registry = setup({ use: { build } });
registry.start();
Options
| Option | Description |
|---|---|
template | Template name or id. Defaults to base. |
create | Options for E2B’s Sandbox.create. The API key defaults to E2B_API_KEY. |
cwd | Working directory inside the sandbox. Defaults to /home/user. |
E2B ends a sandbox when its timeout runs out, so the provider creates sandboxes that pause on timeout and resume on the next call. suspend pauses the sandbox while the Actor sleeps, and destroy deletes it. After a timeout, exec returns empty stdout and stderr; onData has already received the output.
The Sandbox API, lifecycle, and the other providers are in the sandbox adapter guide.