Skip to main content
Sign In
Features

Sandbox Actor

Run sandbox-agent sessions behind a Rivet Actor with provider-backed sandbox creation.

The Sandbox Actor wraps the sandbox-agent TypeScript SDK in a Rivet Actor.

  • One sandbox actor key maps to one backing sandbox.
  • All non-hook sandbox-agent instance methods are exposed as actor actions.
  • The hook surface matches the SDK callback methods: onSessionEvent and onPermissionRequest.
  • Transcript data is persisted automatically in the actor’s built-in SQLite database.

Basic setup

Use provider when every actor instance should use the same sandbox backend.

Dynamic providers

Use createProvider when provider selection depends on the actor input.

createProvider receives the same actor creation input shape as createState.

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.

Active turn sleep behavior

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.

Providers

Providers 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.

Docker

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" },
});
OptionDefaultDescription
imagenode:22-bookworm-slimDocker image to use.
host127.0.0.1Host address for connecting to the container.
agentPortProvider defaultPort 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.

Daytona

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,
});
OptionDefaultDescription
create{}Options passed to client.create(). Can be a static object or an async function.
imageProvider defaultDocker image for the Daytona workspace.
agentPortProvider defaultPort the sandbox-agent server listens on.
previewTtlSeconds14400 (4 hours)TTL for the signed preview URL used to connect.
deleteTimeoutSecondsundefinedTimeout passed to sandbox.delete() on destroy.

E2B

Requires the @e2b/code-interpreter package.

pnpm add @e2b/code-interpreter
import { e2b } from "rivetkit/sandbox/e2b";

const provider = e2b({
  create: { template: "base" },
  connect: async (sandboxId) => ({ sandboxId }),
});
OptionDefaultDescription
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.
agentPortProvider defaultPort the sandbox-agent server listens on.

Vercel

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" } },
});

Local

Runs sandbox-agent locally on the host machine. No additional dependencies required.

import { local } from "rivetkit/sandbox/local";

const provider = local({
  port: 2468,
});

Unsupported providers

Cloudflare Sandbox is available in sandbox-agent but is not re-exported from RivetKit. Cloudflare sandboxes do not expose a URL, instead routing requests through a custom fetch implementation. This architecture is incompatible with Rivet’s direct sandbox access helpers (rivetkit/sandbox/client) and the getSandboxUrl action, which both require a publicly reachable URL. If you need Cloudflare sandboxes, use sandbox-agent/cloudflare directly without the sandbox actor wrapper.

Custom providers

Implement the SandboxProvider interface from sandbox-agent to use any sandbox backend.

import type { SandboxProvider } from "sandbox-agent";

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 } from "rivetkit/sandbox";

const mySandbox = sandboxActor({
  provider: myProvider,
});

The provider methods map to the sandbox lifecycle:

  1. create is called once when the actor first needs a sandbox. Return a stable string ID.
  2. 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.
  3. ensureServer (optional) is called before connecting to ensure the sandbox-agent server process is running. Must be idempotent.
  4. destroy is called when the actor is destroyed. Clean up all external resources.

Direct sandbox access

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.

Filesystem helpers

import { createClient } from "rivetkit/client";
import {
  uploadFile,
  downloadFile,
  uploadBatch,
  listFiles,
  statFile,
  deleteFile,
  mkdirFs,
  moveFile,
} from "rivetkit/sandbox/client";
import type { registry } from "./actors";

const client = createClient<typeof registry>();
const sandbox = client.codingSandbox.getOrCreate(["task-789"]);

// 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");

Process terminal

import { connectTerminal, buildTerminalWebSocketUrl } from "rivetkit/sandbox/client";

// 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);

Log streaming

import { followProcessLogs } from "rivetkit/sandbox/client";

// Stream process logs via SSE.
const subscription = await followProcessLogs(url, processId, (entry) => {
  console.log(`[${entry.stream}] ${entry.data}`);
});

// Stop streaming.
subscription.close();

Why direct access?

The sandbox actor proxies all structured sandbox-agent methods as actor actions. However, three categories of operations do not fit JSON-based RPC:

  • Binary filesystem I/O (readFsFile, writeFsFile, uploadFsBatch): base64 encoding adds ~33% overhead.
  • WebSocket terminals (connectProcessTerminal): bidirectional binary streams.
  • SSE log streaming (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.

SDK parity

The public action surface intentionally mirrors sandbox-agent.

  • Hooks: onSessionEvent, onPermissionRequest
  • Actions: every other public SandboxAgent instance method
  • Direct access: filesystem, terminal, and log streaming helpers in rivetkit/sandbox/client

This is enforced by a parity test in RivetKit so SDK upgrades fail fast if the sandbox actor falls out of sync.

Notes

  • Use input parameters when you need per-actor provider selection.
  • Use actions to call sandbox-agent methods from clients or other actors.
  • The transcript store is internal to the sandbox actor. You do not need to configure a transcript adapter yourself.
  • The sandbox actor automatically provisions and migrates its SQLite tables. You do not need to pass a database config.