Skip to main content
Sign In
Extensions

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.
  • onBeforeConnect is supported for auth and connection validation.
  • All non-hook sandbox-agent instance methods are exposed as actor actions.
  • The hook surface matches the SDK callback methods: onSessionEvent and onPermissionRequest.
  • The actor also adds destroy and getSandboxUrl helper actions.
  • Transcript data is persisted automatically in the actor’s built-in SQLite database.

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.

Feature surface

Sandbox actors support these configuration options:

OptionDescription
providerUse one provider instance for every actor.
createProviderResolve the provider dynamically from actor context such as c.key or environment.
onBeforeConnectValidate or reject client connections before they attach to the actor.
onSessionEventObserve sandbox-agent session events.
onPermissionRequestObserve permission requests and keep the actor awake while they are pending.
persistRawEventsStore raw event payload JSON in SQLite in addition to the normalized transcript records.
destroyActorDestroy the actor after the custom destroy() action tears down the backing sandbox.
options.warningAfterMs / options.staleAfterMsControl active-turn warning and stale-session cleanup timers.

The action surface includes:

  • every public non-hook SandboxAgent instance method
  • getSandboxUrl() for direct helper access when the provider exposes getUrl
  • destroy() for tearing down the backing sandbox while keeping transcript data readable

Basic setup

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

Dynamic providers

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.

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.

Lifecycle and persistence behavior

The sandbox actor adds a few behaviors on top of plain SDK parity:

  • destroy() tears down the backing sandbox without deleting the actor by default
  • after destroy(), listSessions, getSession, and getEvents continue to read from persisted SQLite data
  • destroyActor: true makes destroy() also destroy the actor itself
  • persistRawEvents: true stores raw event payload JSON for each persisted session event

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

ComputeSDK

Requires the computesdk package.

pnpm add computesdk
import { computesdk } from "rivetkit/sandbox/computesdk";

const provider = computesdk({
	create: {},
});

Sprites

Requires the @fly/sprites package.

pnpm add @fly/sprites
import { sprites } from "rivetkit/sandbox/sprites";

const provider = sprites({});

Unsupported providers

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.

Custom providers

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:

  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.

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.

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

Process terminal

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

Log streaming

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

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 actor keys or runtime context 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.