website/src/content/docs/actors/sandbox.mdx
The Sandbox Actor wraps the sandbox-agent TypeScript SDK in a Rivet Actor.
onBeforeConnect is supported for auth and connection validation.sandbox-agent instance methods are exposed as actor actions.onSessionEvent and onPermissionRequest.destroy and getSandboxUrl helper actions.It is not a drop-in replacement for the full actor() API. Sandbox actors are
purpose-built around sandbox lifecycle and session management, so they do not
currently expose custom actor events, queues, onConnect, onDisconnect,
onRequest, onWebSocket, createState, createVars, or custom database
configuration.
Sandbox actors support these configuration options:
| Option | Description |
|---|---|
provider | Use one provider instance for every actor. |
createProvider | Resolve the provider dynamically from actor context such as c.key or environment. |
onBeforeConnect | Validate or reject client connections before they attach to the actor. |
onSessionEvent | Observe sandbox-agent session events. |
onPermissionRequest | Observe permission requests and keep the actor awake while they are pending. |
persistRawEvents | Store raw event payload JSON in SQLite in addition to the normalized transcript records. |
destroyActor | Destroy the actor after the custom destroy() action tears down the backing sandbox. |
options.warningAfterMs / options.staleAfterMs | Control active-turn warning and stale-session cleanup timers. |
The action surface includes:
SandboxAgent instance methodgetSandboxUrl() for direct helper access when the provider exposes getUrldestroy() for tearing down the backing sandbox while keeping transcript data readableUse provider when every actor instance should use the same sandbox backend.
export const codingSandbox = sandboxActor({ provider: docker({ image: "node:22-bookworm-slim", }), onSessionEvent: async (_c, sessionId, event) => { console.log("session event", sessionId, event.payload); }, onPermissionRequest: async (_c, sessionId, request) => { console.log("permission request", sessionId, request.id); }, });
export const registry = setup({ use: { codingSandbox }, }); registry.start();
```ts client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./index";
const client = createClient<typeof registry>("http://localhost:6420");
const sandbox = client.codingSandbox.getOrCreate(["task-123"]);
const session = await sandbox.resumeOrCreateSession({
id: "main",
agent: "codex",
sessionInit: {
cwd: "/root",
},
});
await sandbox.rawSendSessionMethod(session.id, "session/prompt", {
sessionId: session.id,
prompt: [{ type: "text", text: "Explain the current project structure." }],
});
const events = await sandbox.getEvents({
sessionId: session.id,
limit: 50,
});
console.log(events.items);
Use createProvider when provider selection depends on actor context, such as
the actor key or environment.
createProvider receives the sandbox actor context. Sandbox actors do not take
custom actor creation input.
export const codingSandbox = sandboxActor({ createProvider: async (c) => { switch (c.key[0]) { case "daytona": return daytona(); case "e2b": return e2b(); default: return docker(); } }, });
export const registry = setup({ use: { codingSandbox }, }); registry.start();
```ts client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./index";
const client = createClient<typeof registry>("http://localhost:6420");
const sandbox = client.codingSandbox.getOrCreate(["daytona", "task-456"]);
await sandbox.listAgents();
The sandbox actor pins the resolved provider name in actor state. If a later wake or reconnect resolves a different provider for the same actor, the actor throws instead of silently switching backends.
The sandbox actor always keeps itself awake while a subscribed session still looks like it is in the middle of a turn.
import { sandboxActor } from "rivetkit/sandbox";
import { docker } from "rivetkit/sandbox/docker";
const codingSandbox = sandboxActor({
provider: docker(),
options: {
warningAfterMs: 30_000,
staleAfterMs: 5 * 60_000,
},
});
This tracks active sessions from observed session/prompt envelopes and
permission requests. RivetKit sets preventSleep while any session still looks
active, logs if the stream goes quiet, and eventually clears stale state if no
terminal response arrives.
The sandbox actor adds a few behaviors on top of plain SDK parity:
destroy() tears down the backing sandbox without deleting the actor by defaultdestroy(), listSessions, getSession, and getEvents continue to read from persisted SQLite datadestroyActor: true makes destroy() also destroy the actor itselfpersistRawEvents: true stores raw event payload JSON for each persisted session eventProviders are re-exported from the sandbox-agent package. Each provider is available as a separate subpackage import to keep your bundle lean. Install the provider's peer dependency to use it.
Requires the dockerode and get-port packages.
pnpm add dockerode get-port
import { docker } from "rivetkit/sandbox/docker";
const provider = docker({
image: "node:22-bookworm-slim",
host: "127.0.0.1",
env: ["MY_VAR=value"],
binds: ["/host/path:/container/path"],
createContainerOptions: { User: "node" },
});
| Option | Default | Description |
|---|---|---|
image | node:22-bookworm-slim | Docker image to use. |
host | 127.0.0.1 | Host address for connecting to the container. |
agentPort | Provider default | Port the sandbox-agent server listens on. |
env | [] | Environment variables. Can be a static array or an async function. |
binds | [] | Volume binds. Can be a static array or an async function. |
createContainerOptions | {} | Additional options passed to dockerode's createContainer. |
Requires the @daytonaio/sdk package.
pnpm add @daytonaio/sdk
import { daytona } from "rivetkit/sandbox/daytona";
const provider = daytona({
create: { image: "node:22" },
previewTtlSeconds: 4 * 60 * 60,
deleteTimeoutSeconds: 10,
});
| Option | Default | Description |
|---|---|---|
create | {} | Options passed to client.create(). Can be a static object or an async function. |
image | Provider default | Docker image for the Daytona workspace. |
agentPort | Provider default | Port the sandbox-agent server listens on. |
previewTtlSeconds | 14400 (4 hours) | TTL for the signed preview URL used to connect. |
deleteTimeoutSeconds | undefined | Timeout passed to sandbox.delete() on destroy. |
Requires the @e2b/code-interpreter package.
pnpm add @e2b/code-interpreter
import { e2b } from "rivetkit/sandbox/e2b";
const provider = e2b({
template: "base",
});
| Option | Default | Description |
|---|---|---|
template | undefined | E2B sandbox template to use. Can be a string or an async function. |
create | {} | Options passed to Sandbox.create(). Can be a static object or an async function. |
connect | {} | Options passed to Sandbox.connect() when reconnecting. Can be a static object or an async function receiving the sandbox ID. |
agentPort | Provider default | Port the sandbox-agent server listens on. |
Requires the @vercel/sandbox package.
pnpm add @vercel/sandbox
import { vercel } from "rivetkit/sandbox/vercel";
const provider = vercel({
create: { template: "nextjs" },
});
Requires the modal package.
pnpm add modal
import { modal } from "rivetkit/sandbox/modal";
const provider = modal({
create: { secrets: { MY_SECRET: "value" } },
});
Runs sandbox-agent locally on the host machine. No additional dependencies required.
import { local } from "rivetkit/sandbox/local";
const provider = local({
port: 2468,
});
Requires the computesdk package.
pnpm add computesdk
import { computesdk } from "rivetkit/sandbox/computesdk";
const provider = computesdk({
create: {},
});
Requires the @fly/sprites package.
pnpm add @fly/sprites
import { sprites } from "rivetkit/sandbox/sprites";
const provider = sprites({});
Cloudflare Sandbox is available in sandbox-agent but is not re-exported from RivetKit.
Providers that only expose getFetch can still back the proxied sandbox actor
actions, but they cannot use getSandboxUrl() or the direct helper APIs in
rivetkit/sandbox/client, because those helpers require a reachable sandbox URL.
If you need Cloudflare sandboxes, use sandbox-agent/cloudflare directly and
do not rely on the direct URL helper flow.
Implement the SandboxProvider interface from sandbox-agent to use any sandbox backend.
import { type SandboxProvider } from "rivetkit/sandbox";
const provisionSandbox = async (): Promise<string> => "sandbox-123";
const teardownSandbox = async (_sandboxId: string): Promise<void> => {};
const lookupSandboxUrl = async (_sandboxId: string): Promise<string> =>
"http://127.0.0.1:3000";
const restartAgentIfNeeded = async (_sandboxId: string): Promise<void> => {};
const myProvider: SandboxProvider = {
name: "my-provider",
async create() {
// Provision a sandbox and return a string ID.
const sandboxId = await provisionSandbox();
return sandboxId;
},
async destroy(sandboxId) {
// Tear down the sandbox identified by `sandboxId`.
await teardownSandbox(sandboxId);
},
async getUrl(sandboxId) {
// Return the sandbox-agent base URL.
return await lookupSandboxUrl(sandboxId);
},
async ensureServer(sandboxId) {
// Restart the sandbox-agent process if it stopped.
// Called automatically before connecting. Must be idempotent.
await restartAgentIfNeeded(sandboxId);
},
};
Use it like any built-in provider:
import { sandboxActor, type SandboxProvider } from "rivetkit/sandbox";
declare const myProvider: SandboxProvider;
const mySandbox = sandboxActor({
provider: myProvider,
});
The provider methods map to the sandbox lifecycle:
create is called once when the actor first needs a sandbox. Return a stable string ID.getUrl returns the sandbox-agent base URL for direct filesystem, terminal, and log-stream helpers. Alternatively, implement getFetch for providers that cannot expose a URL.ensureServer (optional) is called before connecting to ensure the sandbox-agent server process is running. Must be idempotent.destroy is called when the actor is destroyed. Clean up all external resources.When a provider implements only getFetch, the sandbox actor can still proxy
structured SDK actions, but getSandboxUrl() and the direct helper APIs are not
available.
Some sandbox-agent operations involve raw binary data, WebSocket streams, or SSE
event streams that cannot be efficiently proxied through JSON-based actor actions.
For these, rivetkit/sandbox/client provides helper functions that talk directly
to the sandbox-agent HTTP API, bypassing the actor.
Use the getSandboxUrl action to obtain the sandbox's base URL, then pass it to
the helpers.
export const codingSandbox = sandboxActor({ provider: docker({ image: "node:22-bookworm-slim" }), });
export const registry = setup({ use: { codingSandbox }, });
```ts client.ts
import { createClient } from "rivetkit/client";
import {
uploadFile,
downloadFile,
uploadBatch,
listFiles,
statFile,
deleteFile,
mkdirFs,
moveFile,
} from "rivetkit/sandbox/client";
import type { registry } from "./index";
const client = createClient<typeof registry>("http://localhost:6420");
const sandbox = client.codingSandbox.getOrCreate(["task-789"]);
const tarBuffer = new Uint8Array([0x75, 0x73, 0x74, 0x61, 0x72]);
// Get the direct URL to the sandbox-agent server.
const { url } = await sandbox.getSandboxUrl();
// Upload a file (raw binary, no base64 encoding).
const csvFile = new Blob(["id,name\n1,Alice"], { type: "text/csv" });
await uploadFile(url, "/workspace/data.csv", csvFile);
// Download a file.
const contents = await downloadFile(url, "/workspace/data.csv");
// Batch upload a tar archive.
await uploadBatch(url, "/workspace", tarBuffer);
// List, stat, delete, mkdir, move.
const entries = await listFiles(url, "/workspace");
const info = await statFile(url, "/workspace/data.csv");
await mkdirFs(url, "/workspace/output");
await moveFile(url, "/workspace/data.csv", "/workspace/output/data.csv");
await deleteFile(url, "/workspace/output/data.csv");
import {
connectTerminal,
buildTerminalWebSocketUrl,
} from "rivetkit/sandbox/client";
const url = "http://127.0.0.1:3000";
const processId = "proc-123";
// Connect to a process terminal via WebSocket.
const terminal = await connectTerminal(url, processId);
terminal.onData((data) => console.log("output:", data));
terminal.sendInput("ls\n");
terminal.close();
// Or get the raw WebSocket URL for use with xterm.js or another client.
const wsUrl = buildTerminalWebSocketUrl(url, processId);
import { followProcessLogs } from "rivetkit/sandbox/client";
const url = "http://127.0.0.1:3000";
const processId = "proc-123";
// Stream process logs via SSE.
const subscription = await followProcessLogs(url, processId, (entry) => {
console.log(`[${entry.stream}] ${entry.data}`);
});
// Stop streaming.
subscription.close();
The sandbox actor proxies all structured sandbox-agent methods as actor
actions. However, three categories of operations do not fit JSON-based RPC:
readFsFile, writeFsFile, uploadFsBatch): base64 encoding adds ~33% overhead.connectProcessTerminal): bidirectional binary streams.followProcessLogs): continuous event streams with callbacks.These helpers bypass the actor for the data plane while the actor remains the control plane for sessions, permissions, and lifecycle management.
The public action surface intentionally mirrors sandbox-agent.
onSessionEvent, onPermissionRequestSandboxAgent instance methodrivetkit/sandbox/clientThis is enforced by a parity test in RivetKit so SDK upgrades fail fast if the sandbox actor falls out of sync.