Sandboxes
Run an agent without a sandbox, or give it a shell in E2B, Daytona, agentOS, or a sandbox provider you write.
An agent doesn’t need a sandbox. Without one, its file tools work on files in the Actor’s own database. Add a sandbox only when the agent needs a shell.
| Files | Shell | Setup | |
|---|---|---|---|
| No sandbox | In the Actor’s SQLite database | None. bash returns an error. | None |
| Sandbox | In the sandbox | bash runs in the sandbox | A provider account or agentOS |
Without a sandbox
Leave out the sandbox option. Pi’s read, write, and edit tools then work on files stored in the Actor’s own SQLite database:
import { createRegistry, defineExtension } from "@earendil-works/pi-durable";
import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { setup } from "rivetkit";
// read, write, and edit. With no sandbox, the files live in the Actor's own database.
const extensions = createRegistry();
extensions.install(defineExtension({ name: "files", tools: [createReadTool(), createWriteTool(), createEditTool()] }));
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
});
export const registry = setup({ use: { agent } });
registry.start();
- Nothing to provision. There is no sandbox to create, pay for, pause, or delete.
- Files are durable. They are saved with the conversation and survive sleep, crashes, and upgrades.
- No shell.
bashreturns an error, so this agent installs only the file tools.
See Built-in Tools for what each tool does.
With a sandbox
When the agent needs to run commands, such as tests or a build, pass a sandbox provider. @rivet-dev/sandbox-adapter gives each Actor its own sandbox, and CodingTools run there.
Install the adapter
npm add @rivet-dev/sandbox-adapter e2b
Pass a provider
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";
// read, write, edit, and bash, running in the sandbox
const extensions = createRegistry();
extensions.install(CodingTools);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: e2bProvider({ template: "base" }),
});
export const registry = setup({ use: { agent } });
registry.start();
Providers
| Provider | SDK | While the Actor sleeps | When the Actor is destroyed |
|---|---|---|---|
| E2B | e2b | Paused | Deleted |
| Daytona | @daytonaio/sdk | Stopped | Deleted |
| agentOS | None | Sleeps when idle, and keeps its files | Left in place |
Provider credentials stay on your worker and never enter the sandbox.
E2B
npm add @rivet-dev/sandbox-adapter e2b
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";
// read, write, edit, and bash, running in the sandbox
const extensions = createRegistry();
extensions.install(CodingTools);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: e2bProvider({ template: "base" }),
});
export const registry = setup({ use: { agent } });
registry.start();
| 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.
Daytona
npm add @rivet-dev/sandbox-adapter @daytonaio/sdk
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { daytonaProvider } from "@rivet-dev/sandbox-adapter/daytona";
import { setup } from "rivetkit";
// read, write, edit, and bash, running in the sandbox
const extensions = createRegistry();
extensions.install(CodingTools);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: daytonaProvider(),
});
export const registry = setup({ use: { agent } });
registry.start();
| Option | Description |
|---|---|
client | A Daytona client. Defaults to new Daytona(), which reads DAYTONA_API_KEY. |
create | Options for Daytona’s create. |
The working directory is the sandbox’s own working directory. A stopped sandbox is started again the next time the Actor uses it.
agentOS
npm add @rivet-dev/sandbox-adapter
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { agentOSProvider } from "@rivet-dev/sandbox-adapter/agentos";
import { setup } from "rivetkit";
// read, write, edit, and bash, running in the sandbox
const extensions = createRegistry();
extensions.install(CodingTools);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: agentOSProvider(),
});
export const registry = setup({ use: { agent } });
registry.start();
| Option | Description |
|---|---|
cwd | Working directory inside the VM. Defaults to /workspace. |
software | Packages installed when the VM is created, such as coreutils for sh. |
The VM is an agentOS Actor in the services pool, with the key ["sandbox", <agent Actor id>]. It keeps its files in its own SQLite database, so they persist while the agent sleeps. The VM sleeps on its own when idle. Destroying the agent leaves the VM and its files in place.
How the sandbox is managed
- The sandbox is created the first time the agent needs it.
- After a wake, the Actor reconnects to the same sandbox the first time a tool needs it. If it no longer exists, a new one is created and the old files are lost.
- Actions that don’t touch the sandbox keep working while the provider is down.
- An existing Actor cannot switch to a different provider.
- Commands run with the sandbox’s environment, not your worker’s. File tools reject paths outside the working directory, but shell commands can reach anything inside the sandbox.
- Binary file writes aren’t supported yet.
Write your own provider
Any sandbox can back an agent. Implement SandboxProvider and pass it as sandbox. One provider object is shared by every Actor of a definition, so keep no per-Actor state in it. The Actor stores the sandbox id.
| Member | Description |
|---|---|
name | Stored next to the sandbox id, so a changed provider is detected on wake. |
create(c) | Provisions a new sandbox and returns its id. |
connect(c, id) | Connects to an existing sandbox and returns a Sandbox. Returns undefined when the sandbox no longer exists. |
suspend(c, id) | Optional. Releases resources while the Actor sleeps. When omitted, the sandbox stops itself when idle or keeps running, depending on the provider. |
destroy(c, id) | Optional. Deletes the sandbox when the Actor is destroyed. When omitted, the sandbox is left in place. |
The context c carries the owning Actor’s actorId and a client() for your registry. The Sandbox that connect returns has a cwd and the operations the tools run: exec, readFile, writeFile, mkdir, stat, readdir, and exists. All paths are absolute paths inside the sandbox. See the E2B provider for a complete example.
Next: Custom Tools, tools that run in your backend.