# AWS Lambda MicroVMs (/adapters/aws-lambda)
Firecracker-based serverless sandboxes (GA June 2026) with suspend/resume up to 8h. Control plane via @aws-sdk/client-lambda-microvms; you bake a small runner into the image.
## Installation [#installation]
`@aws-sdk/client-lambda-microvms` is an optional peer dependency, used for the control plane (launch / suspend / resume / terminate). The data plane (exec, files) is plain `fetch` to the microVM's endpoint.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @aws-sdk/client-lambda-microvms
```
```bash
bun add sbox-sdk @aws-sdk/client-lambda-microvms
```
```bash
npm install sbox-sdk @aws-sdk/client-lambda-microvms
```
```bash
yarn add sbox-sdk @aws-sdk/client-lambda-microvms
```
## How it works [#how-it-works]
AWS Lambda MicroVMs give you isolation, lifecycle, and a dedicated HTTPS endpoint — but **the in-VM exec/filesystem protocol is defined by your image**. So this adapter speaks a tiny "runner" protocol over the endpoint, and you bake a reference runner into your MicroVM image once.
1. Build a MicroVM image from a `Dockerfile` + the reference runner, upload to S3, and call `CreateMicrovmImage` → note the **image ARN**.
2. The adapter calls `RunMicrovm` (launch from that ARN), `CreateMicrovmAuthToken` (JWE), then `Suspend`/`Resume`/`Terminate`.
3. Each op is an HTTPS request to the microVM endpoint with `X-aws-proxy-auth` + `X-aws-proxy-port` headers, routed to the runner's `POST /sbox/exec`, `/sbox/fs/read`, `/sbox/fs/write`.
The reference runner (a dependency-free Node server) ships in the package at
`sbox-sdk/src/aws-lambda/runner/` — `server.mjs`, `Dockerfile`, and a README
with the exact build & `create-microvm-image` commands. The runner runs
**inside the isolated MicroVM**; executing arbitrary commands is its purpose.
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { awsLambda } from "sbox-sdk/aws-lambda";
const client = createSandboxClient({
provider: awsLambda({
imageIdentifier:
"arn:aws:lambda:us-east-1:123456789012:microvm-image:sbox-runner",
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
});
const sandbox = await client.create();
const res = await sandbox.commands.run("echo hello");
await sandbox.files.write("/tmp/data.txt", "hi");
await sandbox.pause(); // SuspendMicrovm — memory + disk preserved (up to 8h)
await sandbox.resume(); // ResumeMicrovm
await sandbox.destroy(); // TerminateMicrovm
```
`credentials` and `region` resolve through the standard AWS SDK chain (environment variables, shared config, or an instance/task role) when those options are omitted.
[`template`](/templates) overrides the image ARN per `create()`; otherwise the `imageIdentifier` option is used.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | ------------------------------------ |
| `list` / `region` | native | |
| `pause` | native | `Suspend` preserves memory + disk |
| `stop` | unsupported | only run / suspend / terminate |
| `streaming` | emulated | runner returns buffered output |
| `filesUpload` | native | via the runner |
| `exposePort` | native | endpoint + `X-aws-proxy-port` + JWE |
| `codeInterpreter` | unsupported | |
| `snapshot` / `fork` | unsupported | the image is the build-time snapshot |
`preservesMemoryOnPause` is `true`; `previewModel` is `tunnel`.
---
# Beam Cloud (/adapters/beam)
A sandbox per Beam Cloud container. Fast, GPU-capable sandboxes with snapshots, via @beamcloud/beam-js.
## Installation [#installation]
`@beamcloud/beam-js` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @beamcloud/beam-js
```
```bash
bun add sbox-sdk @beamcloud/beam-js
```
```bash
npm install sbox-sdk @beamcloud/beam-js
```
```bash
yarn add sbox-sdk @beamcloud/beam-js
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { beam } from "sbox-sdk/beam";
const client = createSandboxClient({
provider: beam({
token: process.env.BEAM_TOKEN!,
workspaceId: process.env.BEAM_WORKSPACE_ID!,
}),
});
const sandbox = await client.create({
template: "python:3.12",
resources: { vcpus: 2, memoryMB: 2048, gpu: "A10G" }, // GPU is first-class
});
const res = await sandbox.commands.run("nvidia-smi");
console.log(res.stdout);
const snap = await sandbox.snapshots!.create();
const forks = await sandbox.snapshots!.fork(2); // boot 2 from the snapshot
```
[`template`](/templates) is a base image. Sandboxes are real Linux containers with coreutils.
exec takes `{ cwd, env }` natively (`perCommandEnvCwd: true`) and the adapter
buffers the process streams (`streaming` emulated). The SDK's file helpers are
local-path based, so `files.read`/`files.write` go through `exec` + `base64`
(binary-safe). Ports are public SSL endpoints (`exposePort`). `snapshots.create()`
is `instance.snapshot()` and `fork()` snapshots then boots N sandboxes via
`createFromSnapshot` — in-place `restore` is not supported. `setTimeout()` maps
to `updateTtl`; `list` isn't exposed by the SDK.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | ----------------------------------- |
| `gpu` | native | `resources.gpu` |
| `snapshot` / `fork` | native | `snapshot()` + `createFromSnapshot` |
| `setTimeout` | native | `updateTtl` |
| `exposePort` | native | public SSL endpoint |
| `filesUpload` | native | via `exec` + base64 |
| `streaming` | emulated | buffered process streams |
| `list` | unsupported | not exposed by the SDK |
| `pause` / `stop` | unsupported | `destroy()` = terminate |
`perCommandEnvCwd` is `true`; `previewModel` is `subdomain`. In-place snapshot `restore` reports `NotSupported`.
---
# Blaxel (/adapters/blaxel)
A sandbox per Blaxel instance. The "perpetual sandbox" — stays in standby indefinitely and resumes in sub-25ms, via @blaxel/core.
## Installation [#installation]
`@blaxel/core` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @blaxel/core
```
```bash
bun add sbox-sdk @blaxel/core
```
```bash
npm install sbox-sdk @blaxel/core
```
```bash
yarn add sbox-sdk @blaxel/core
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { blaxel } from "sbox-sdk/blaxel";
const client = createSandboxClient({
provider: blaxel({
apiKey: process.env.BL_API_KEY!,
workspace: process.env.BL_WORKSPACE!,
}),
});
const sandbox = await client.create({ template: "blaxel/prod-base:latest" });
const res = await sandbox.commands.run("node --version");
console.log(res.stdout);
// Public preview URL for a port (pass { private: true } for a signed one).
const preview = await sandbox.ports!.expose(3000);
console.log(preview.url);
```
[`template`](/templates) is the sandbox image. There's no explicit pause/resume — Blaxel keeps the sandbox in standby and resumes it in sub-25ms on the next request; `destroy()` removes it.
exec is the buffered `process.exec({ waitForCompletion: true })` (`streaming`
emulated); it has no per-command env (only a working dir), so the core folds
cwd+env into a `cd … && KEY=v …` wrapper (`perCommandEnvCwd: false`).
Filesystem read/write use the binary-safe `fs.readBinary` / `fs.writeBinary`;
`rm`/`mv`/`stat` aren't in the SDK surface, so the core polyfills them via
exec. Preview URLs can be public or private (`privatePreview`). Credentials
fall back to Blaxel's standard environment (`BL_API_KEY` / `BL_WORKSPACE`).
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | --------------------------- |
| `list` | native | |
| `exposePort` | native | preview URLs |
| `privatePreview` | native | signed (`public: false`) |
| `region` | native | chosen at create |
| `volumes` | native | mounted at create |
| `filesUpload` | native | `fs.writeBinary` |
| `streaming` | emulated | exec waits for completion |
| `pause` / `stop` | unsupported | standby/resume is automatic |
| `snapshot` / `fork` | unsupported | |
`perCommandEnvCwd` is `false` (core wraps); `preservesMemoryOnPause` is `true` (standby); `previewModel` is `subdomain`.
---
# Cloudflare Sandbox (/adapters/cloudflare)
Cloudflare Sandbox via a Worker / Durable Object binding — a code interpreter, native ports with proxied fetch, and get-or-create semantics. Worker-only.
## Installation [#installation]
`@cloudflare/sandbox` is an optional peer dependency, used from inside a Cloudflare Worker. The adapter needs the **Durable Object namespace binding** for your exported Sandbox class.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @cloudflare/sandbox
```
```bash
bun add sbox-sdk @cloudflare/sandbox
```
```bash
npm install sbox-sdk @cloudflare/sandbox
```
```bash
yarn add sbox-sdk @cloudflare/sandbox
```
## Usage [#usage]
Cloudflare is **Worker-only** — it runs against a live Durable Object binding, not a plain Node process. Pass the binding from your Worker `env`:
```ts
import { createSandboxClient } from "sbox-sdk";
import { cloudflare } from "sbox-sdk/cloudflare";
export default {
async fetch(request: Request, env: Env) {
const client = createSandboxClient({
provider: cloudflare({
binding: env.Sandbox, // your Durable Object namespace
hostname: "my-worker.example.com", // for preview URLs
}),
});
const sandbox = await client.create({ name: "session-42" });
const exec = await sandbox.code.runCode("print('hello from the edge')");
return Response.json({ logs: exec.logs });
},
};
```
The base image is defined by your Worker's container config at deploy time, so [`template`](/templates) is ignored. `create()` uses `name` (or the idempotency key) as the Durable Object id, giving **get-or-create** semantics. `list` is unsupported.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ---------------------------------------- | :---------: | ------------------------------------------ |
| `setTimeout` | native | |
| `streaming` | native | |
| `filesUpload` | native | |
| `codeInterpreter` / `statefulKernel` | native | `sandbox.code` is defined |
| `exposePort` | native | preview via wildcard DNS |
| `proxiedFetch` | native | `ports.fetch()` proxies through the Worker |
| `list` / `stop` / `pause` / `background` | unsupported | |
| `snapshot` / `fork` | unsupported | |
`previewModel` is `wildcardDNS`.
---
# CodeSandbox (/adapters/codesandbox)
A sandbox per CodeSandbox microVM. Native filesystem, fast hibernate/resume, and public preview hosts, via @codesandbox/sdk.
## Installation [#installation]
`@codesandbox/sdk` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @codesandbox/sdk
```
```bash
bun add sbox-sdk @codesandbox/sdk
```
```bash
npm install sbox-sdk @codesandbox/sdk
```
```bash
yarn add sbox-sdk @codesandbox/sdk
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { codesandbox } from "sbox-sdk/codesandbox";
const client = createSandboxClient({
provider: codesandbox({ apiKey: process.env.CSB_API_KEY! }),
});
const sandbox = await client.create();
const res = await sandbox.commands.run("node --version");
console.log(res.stdout);
await sandbox.files.write("/project/sandbox/state.json", '{"v":1}');
await sandbox.pause(); // hibernate — VM state snapshotted, resumes in ~2–3s
```
[`template`](/templates) is a sandbox/template id to fork from (defaults to CodeSandbox's universal template). The SDK splits into a control object (`sdk.sandboxes.*`) and a per-sandbox `client` from `connect()` — the adapter manages both.
Filesystem is native (`client.fs.*`). `commands.run` is buffered and
**throws** a `CommandError` on a non-zero exit — the adapter catches it and
emits a normal exit event, so `sandbox.commands.run` never throws on non-zero
(it returns an `ExecResult` with the real `exitCode`). `pause()` hibernates
(memory snapshotted), `resume()` wakes it, `setTimeout()` sets the hibernation
timeout, and `destroy()` shuts it down.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | -------------------------------- |
| `list` | native | |
| `pause` | native | hibernate (memory snapshotted) |
| `setTimeout` | native | hibernation timeout |
| `filesUpload` | native | native `client.fs` |
| `exposePort` | native | preview at `-.csb.app` |
| `privatePreview` | native | signed preview tokens |
| `codeInterpreter` | unsupported | |
| `snapshot` / `fork` | unsupported | hibernate is the memory snapshot |
`perCommandEnvCwd` is `true` (`commands.run` takes `cwd`/`env`); `preservesMemoryOnPause` is `true`; `previewModel` is `subdomain`.
---
# Daytona (/adapters/daytona)
A full-featured sandbox — code interpreter with charts, stop/pause/resume, preview links, and regions. Backed by @daytonaio/sdk.
## Installation [#installation]
`@daytonaio/sdk` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @daytonaio/sdk
```
```bash
bun add sbox-sdk @daytonaio/sdk
```
```bash
npm install sbox-sdk @daytonaio/sdk
```
```bash
yarn add sbox-sdk @daytonaio/sdk
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { daytona } from "sbox-sdk/daytona";
const client = createSandboxClient({
provider: daytona({ apiKey: process.env.DAYTONA_API_KEY!, target: "us" }),
});
const sandbox = await client.create({ template: "debian:12" });
// Daytona has a code interpreter, so `sandbox.code` is defined:
const exec = await sandbox.code.runCode("print(2 + 2)");
console.log(exec.logs.stdout);
await sandbox.pause(); // preserves memory
await sandbox.resume();
```
[`template`](/templates) is treated as a Daytona **snapshot** name, or — when it contains a `/` or `:` — as a container **image** (e.g. `"debian:12"`). Daytona's API works in **seconds**; the adapter converts from the SDK's milliseconds for you.
For v0.1, `snapshot` and `background` are scoped to `unsupported` to keep the
surface honest — reach for [`raw()`](/escape-hatch) for Daytona-only features
in the meantime.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ---------------------------------- | :---------: | -------------------------------- |
| `list` / `setTimeout` | native | `setTimeout` = autostop interval |
| `stop` / `pause` | native | resume re-starts the sandbox |
| `streaming` | emulated | `executeCommand` is buffered |
| `filesUpload` | native | |
| `codeInterpreter` | native | `sandbox.code` is defined |
| `statefulKernel` | unsupported | |
| `exposePort` | native | tokenized preview links |
| `region` | native | `target: "us" \| "eu"` |
| `snapshot` / `fork` / `background` | unsupported | |
`preservesMemoryOnPause` and `preservesDiskOnStop` are both `true`; `previewModel` is `subdomain`.
---
# E2B (/adapters/e2b)
The canonical reference adapter — richest capability set, with a code interpreter, snapshots, native ports, and pause/resume. Backed by @e2b/code-interpreter.
## Installation [#installation]
`@e2b/code-interpreter` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use — install it alongside the SDK so the adapter's import resolves at runtime.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @e2b/code-interpreter
```
```bash
bun add sbox-sdk @e2b/code-interpreter
```
```bash
npm install sbox-sdk @e2b/code-interpreter
```
```bash
yarn add sbox-sdk @e2b/code-interpreter
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
});
const sandbox = await client.create({ template: "python-3.12", ttlMs: 60_000 });
// E2B has a code interpreter, so `sandbox.code` is defined:
const exec = await sandbox.code.runCode("import matplotlib; print('ok')");
console.log(exec.logs.stdout);
```
E2B is the richest adapter and the reference implementation other adapters are measured against. [`template`](/templates) maps to an E2B template id / alias (e.g. `"python-3.12"` or a custom-built template).
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------------------------------ | :---------: | -------------------------------------------------- |
| `list` / `pause` / `setTimeout` | native | |
| `stop` | unsupported | E2B has no stop-without-destroy |
| `background` / `streaming` / `killProcess` | native | |
| `filesWatch` / `filesUpload` | native | |
| `codeInterpreter` / `statefulKernel` | native | `sandbox.code` is defined |
| `exposePort` | native | preview via subdomain |
| `snapshot` | native | |
| `fork` | unsupported | |
| `metrics` | native | |
| `egressControl` / `ssh` / `proxiedFetch` | unsupported | use [`raw()`](/escape-hatch) for E2B-only features |
`preservesMemoryOnPause` is `true`; `previewModel` is `subdomain`.
---
# Fly Machines (/adapters/fly)
A sandbox per Fly Machine over the Machines REST API. Pure fetch — no SDK — so it runs anywhere the core does, including the edge.
## Installation [#installation]
No provider SDK required — the adapter talks to the Fly Machines REST API with `fetch`, so it's edge-portable.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk
```
```bash
bun add sbox-sdk
```
```bash
npm install sbox-sdk
```
```bash
yarn add sbox-sdk
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { fly } from "sbox-sdk/fly";
const client = createSandboxClient({
provider: fly({
apiToken: process.env.FLY_API_TOKEN!,
appName: "my-fly-app", // must already exist
region: "iad",
}),
});
const sandbox = await client.create({ template: "ubuntu:22.04" });
const res = await sandbox.commands.run("uname -a");
console.log(res.stdout);
```
The Fly app must exist; each sandbox is a **Machine** inside it. [`template`](/templates) is the machine's container image.
Fly's `/exec` is **buffered** and has no per-command `cwd`/`env`, so the core
folds those into a `cd … && KEY=v …` wrapper (`perCommandEnvCwd: false`) and
`streaming` is emulated. `stop()` releases compute but the **rootfs resets**
unless a volume is attached (`preservesDiskOnStop: false`). Files are read and
written via `exec` + `base64`.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | ------------------------------- |
| `list` / `region` | native | |
| `stop` / `pause` | native | `pause` = suspend (memory kept) |
| `streaming` | emulated | `/exec` is buffered |
| `filesUpload` | native | via `exec` + base64 |
| `exposePort` | native | preview at `.fly.dev` |
| `codeInterpreter` | unsupported | |
| `snapshot` / `fork` | unsupported | |
`perCommandEnvCwd` is `false` (core wraps); `preservesMemoryOnPause` is `true`; `preservesDiskOnStop` is `false`; `previewModel` is `subdomain`.
---
# In-Memory (/adapters/memory)
An in-memory sandbox backed by Maps — the zero-config default and the conformance fixture. No dependencies, no network, non-persistent. Ideal for tests.
## Installation [#installation]
The in-memory adapter has no peer dependencies and no runtime requirements — it's pure JavaScript backed by `Map`s, so it runs unchanged in Node, Bun, Deno, the browser, and edge runtimes. It's built in and is the zero-config default for `createSandboxClient()`.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk
```
```bash
bun add sbox-sdk
```
```bash
npm install sbox-sdk
```
```bash
yarn add sbox-sdk
```
## Usage [#usage]
The in-memory provider implements the same adapter contract as every cloud provider, but stores files and runs a tiny built-in shell in process instead of touching a network. That makes it the near-universal choice for testing code that uses sbox SDK without standing up a real sandbox — and it doubles as the reference adapter for the [conformance suite](/capabilities).
```ts
import { createSandboxClient } from "sbox-sdk";
import { memory } from "sbox-sdk/memory";
// A fresh, empty sandbox. Same surface as the cloud adapters,
// so you can swap it in for tests without changing any call sites.
const client = createSandboxClient({ provider: memory() });
const sandbox = await client.create();
await sandbox.files.write("/app/hello.txt", "hi");
const text = await (await sandbox.files.read("/app/hello.txt")).text(); // "hi"
const res = await sandbox.commands.run("echo hi", { cwd: "/app" });
res.exitCode; // 0
```
### Exercising the filesystem polyfills [#exercising-the-filesystem-polyfills]
Pass `bareFs: true` to hide the native filesystem methods, forcing the core's exec-based polyfills (`ls`, `mkdir`, `rm`, `mv`, `stat`) to run instead — useful for testing how the core behaves against providers that have no native fs primitives.
```ts
import { memory } from "sbox-sdk/memory";
const client = createSandboxClient({ provider: memory({ bareFs: true }) });
```
## Authentication [#authentication]
## Options [#options]
## Behavior notes [#behavior-notes]
* **Non-persistent.** State lives in the process. Two clients built from separate `memory()` calls don't share data. Not for production.
* **Built-in mini-shell.** `commands.run` interprets a small set of POSIX commands (`echo`, `cat`, `ls`, `mkdir`, `rm`, `mv`, `stat`, `env`, …). It is not a full shell — there's no `&&`, pipes, or subshells.
* **The `raw()` escape hatch** is the backing in-memory store, so a test can read or reset it directly.
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------------------------------ | :---------: | ------------------------------------------------------- |
| `list` / `stop` / `pause` / `setTimeout` | native | `setTimeout` is a no-op (memory sandboxes never expire) |
| `background` / `streaming` / `killProcess` | native | |
| `filesUpload` | native | string / `Uint8Array` only — no stream upload |
| `exposePort` | emulated | returns a `127.0.0.1` preview URL |
| `snapshot` / `fork` | native | in-process copies of the file map |
| `codeInterpreter` / `statefulKernel` | unsupported | `sandbox.code` is `undefined` |
| `egressControl` / `ssh` / `proxiedFetch` | unsupported | |
---
# Modal (/adapters/modal)
Modal sandboxes with streaming exec and first-class GPU. Backed by the modal JS SDK; an App + Image are created lazily.
## Installation [#installation]
`modal` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk modal
```
```bash
bun add sbox-sdk modal
```
```bash
npm install sbox-sdk modal
```
```bash
yarn add sbox-sdk modal
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { modal } from "sbox-sdk/modal";
const client = createSandboxClient({
provider: modal({
tokenId: process.env.MODAL_TOKEN_ID,
tokenSecret: process.env.MODAL_TOKEN_SECRET,
appName: "my-agent", // App is created if missing
}),
});
const sandbox = await client.create({
template: "python:3.13", // container image
resources: { vcpus: 2, memoryMB: 2048, gpu: "A100" }, // GPU is first-class
});
for await (const ev of sandbox.commands.run("nvidia-smi")) {
if (ev.type === "stdout") process.stdout.write(ev.data);
}
```
Credentials fall back to Modal's standard environment (`MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`) when the options are omitted. [`template`](/templates) is a container image passed to `images.fromRegistry`.
Modal's JS SDK has no high-level file API, so `files.read`/`files.write` are
implemented via `exec` + `base64` — the image needs coreutils `base64`
(present on `python:*`, `debian`, `ubuntu`). Ports must be declared in
`spec.ports` at create time before `ports.expose()` can resolve a tunnel.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | -------------------------- |
| `list` | native | |
| `streaming` | native | `ContainerProcess` streams |
| `filesUpload` | native | via `exec` + base64 |
| `gpu` | native | `resources.gpu: "A100"` |
| `exposePort` | native | declare ports at create |
| `codeInterpreter` | unsupported | |
| `stop` / `pause` | unsupported | `destroy()` = terminate |
| `snapshot` / `fork` | unsupported | |
`previewModel` is `tunnel`.
---
# Morph (/adapters/morph)
A sandbox per MorphCloud instance. Snapshot-first VMs with instant branching, via the morphcloud SDK.
## Installation [#installation]
`morphcloud` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk morphcloud
```
```bash
bun add sbox-sdk morphcloud
```
```bash
npm install sbox-sdk morphcloud
```
```bash
yarn add sbox-sdk morphcloud
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { morph } from "sbox-sdk/morph";
const client = createSandboxClient({
provider: morph({ apiKey: process.env.MORPH_API_KEY! }),
});
// `template` is a snapshot id (booted directly) or an image to snapshot first.
const sandbox = await client.create({ template: "morphvm-minimal" });
const res = await sandbox.commands.run("uname -a");
console.log(res.stdout);
// Morph's signature move: branch the live instance into N forks.
const forks = await sandbox.snapshots!.fork(3);
```
Morph is **snapshot-first** — there's no boot-from-image, so `create()` either boots a `snapshot…` [`template`](/templates) directly or lazily builds a snapshot from an image (`imageId`, `resources`) and boots that.
Exec is the buffered `instance.exec(cmd)` (`streaming` emulated); it has no
per-command `cwd`/`env`, so the core folds those into a `cd … && KEY=v …`
wrapper (`perCommandEnvCwd: false`). Files are read and written via `exec` +
`base64`. Ports are public HTTP services (`exposeHttpService`).
`snapshots.create()` is `instance.snapshot()` and `fork()` is
`instance.branch()` — both native; in-place `restore` is not supported
(branching makes new instances). `destroy()` stops the instance; there is no
separate `pause`.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | ---------------------------------- |
| `list` | native | |
| `snapshot` / `fork` | native | `instance.snapshot()` / `branch()` |
| `streaming` | emulated | `instance.exec` is buffered |
| `filesUpload` | native | via `exec` + base64 |
| `exposePort` | native | `exposeHttpService` |
| `codeInterpreter` | unsupported | |
| `pause` / `stop` | unsupported | `destroy()` = stop instance |
`perCommandEnvCwd` is `false` (core wraps); `previewModel` is `subdomain`. In-place snapshot `restore` reports `NotSupported`.
---
# Northflank (/adapters/northflank)
A sandbox per Northflank deployment service. The services platform, driven through @northflank/js-client — a sleep-infinity container you exec into.
## Installation [#installation]
`@northflank/js-client` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @northflank/js-client
```
```bash
bun add sbox-sdk @northflank/js-client
```
```bash
npm install sbox-sdk @northflank/js-client
```
```bash
yarn add sbox-sdk @northflank/js-client
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { northflank } from "sbox-sdk/northflank";
const client = createSandboxClient({
provider: northflank({
token: process.env.NORTHFLANK_TOKEN!,
projectId: "my-project", // must already exist
}),
});
const sandbox = await client.create({ template: "ubuntu:22.04" });
const res = await sandbox.commands.run("echo hi && ls /");
console.log(res.stdout);
await sandbox.pause(); // scale to zero — volume kept, compute billing stops
```
A sandbox is a **deployment service** that runs the base [`template`](/templates) image with a `sleep infinity` keep-alive command, so you can `exec` into it. `create()` waits (best-effort) for the service to reach `running` before returning.
Node-only: the SDK streams exec output over Node `EventEmitter`s, which the
adapter buffers. `exec` has no per-command `cwd`/`env`, so the core folds
those into a `cd … && KEY=v …` wrapper (`perCommandEnvCwd: false`) and
`streaming` is emulated. Files are read and written via `exec` + `base64`.
`pause()` scales the service to zero (volume retained, **memory lost**);
`destroy()` deletes the service and its child objects.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| --------------------- | :---------: | ---------------------------------- |
| `list` | native | |
| `pause` | native | scale-to-zero (volume kept) |
| `streaming` | emulated | exec session is buffered |
| `filesUpload` | native | via `exec` + base64 |
| `exposePort` | native | public service port → DNS |
| `codeInterpreter` | unsupported | |
| `stop` / `setTimeout` | unsupported | `pause` is the keep-disk state |
| `snapshot` / `fork` | unsupported | services platform, no VM snapshots |
`perCommandEnvCwd` is `false` (core wraps); `preservesMemoryOnPause` is `false`; `previewModel` is `subdomain`.
---
# Railway (/adapters/railway)
A sandbox per Railway sandbox. Ephemeral, isolated Debian Linux VMs with native streaming exec, byte-accurate files, checkpoints and forking.
**Beta.** Railway Sandboxes are a beta product; the SDK surface and behavior
may change. Pin the `railway` version and expect occasional breaking changes.
## Installation [#installation]
`railway` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk railway
```
```bash
bun add sbox-sdk railway
```
```bash
npm install sbox-sdk railway
```
```bash
yarn add sbox-sdk railway
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { railway } from "sbox-sdk/railway";
const client = createSandboxClient({
provider: railway({
token: process.env.RAILWAY_API_TOKEN!,
environmentId: process.env.RAILWAY_ENVIRONMENT_ID!,
}),
});
const sandbox = await client.create({ template: "debian:bookworm" });
// exec streams live via the SDK's onStdout/onStderr.
for await (const ev of sandbox.commands.run("npm install")) {
if (ev.type === "stdout") process.stdout.write(ev.data);
}
await sandbox.files.write("/work/data.bin", new Uint8Array([1, 2, 3]));
const forks = await sandbox.snapshots!.fork(2); // clone the filesystem
```
[`template`](/templates) is a base image (or a built template). Each sandbox is an isolated Debian Linux VM provisioned on demand.
This is one of the richest adapters: exec streams natively through the SDK's
`onStdout`/`onStderr` (`streaming: native`) and returns a real exit code;
filesystem is native and byte-accurate (`files.read(path, { format: "bytes" })`
/ `files.write`). `snapshots.create()` maps to `checkpoint()` and `fork()` to
the native `fork()`. Sandboxes are **network-isolated**, so there are no public
preview ports (`exposePort` unsupported). Credentials fall back to
`RAILWAY_API_TOKEN` / `RAILWAY_ENVIRONMENT_ID`.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ----------------- | :---------: | ----------------------- |
| `list` | native | |
| `streaming` | native | `onStdout` / `onStderr` |
| `filesUpload` | native | byte-accurate `files.*` |
| `snapshot` | native | `checkpoint()` |
| `fork` | native | clone the filesystem |
| `exposePort` | unsupported | network-isolated VMs |
| `codeInterpreter` | unsupported | |
| `pause` / `stop` | unsupported | `destroy()` = teardown |
`perCommandEnvCwd` is `true`; `previewModel` is `none` (no public ports). In-place snapshot `restore` reports `NotSupported`.
---
# Runloop (/adapters/runloop)
A sandbox per Runloop devbox. AI-focused devboxes with suspend/resume and disk snapshots, via @runloop/api-client.
## Installation [#installation]
`@runloop/api-client` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @runloop/api-client
```
```bash
bun add sbox-sdk @runloop/api-client
```
```bash
npm install sbox-sdk @runloop/api-client
```
```bash
yarn add sbox-sdk @runloop/api-client
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { runloop } from "sbox-sdk/runloop";
const client = createSandboxClient({
provider: runloop({ apiKey: process.env.RUNLOOP_API_KEY! }),
});
const sandbox = await client.create({ template: "my-blueprint" });
const res = await sandbox.commands.run("python --version");
console.log(res.stdout);
// Snapshot the disk, then fork two fresh devboxes from it.
const forks = await sandbox.snapshots!.fork(2);
await sandbox.pause(); // suspend — memory preserved
```
[`template`](/templates) is a **blueprint id**, or a **snapshot id** (`snp…`) to restore disk state into a new devbox. `create()` polls until the devbox is `running`.
Exec is the buffered `executeSync` (`streaming` emulated); the devbox runs it
through a shell, so the core folds `cwd`/`env` into a `cd … && KEY=v …`
wrapper (`perCommandEnvCwd: false`). Files use the binary-safe `downloadFile`
/ `uploadFile` endpoints. `suspend`/`resume` map to `pause()`/`resume()`
(memory preserved); `shutdown` is `destroy()`. `snapshots.create()` is
`snapshotDisk` and `fork()` snapshots then boots N devboxes — in-place
`restore` is not supported (snapshots create new devboxes).
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ------------------- | :---------: | ----------------------------- |
| `list` | native | |
| `pause` | native | suspend (memory preserved) |
| `streaming` | emulated | `executeSync` is buffered |
| `filesUpload` | native | binary `uploadFile` |
| `exposePort` | native | tunnel via `enableTunnel` |
| `snapshot` / `fork` | native | `snapshotDisk`; fork = boot N |
| `codeInterpreter` | unsupported | |
| `stop` | unsupported | `pause` (suspend) / `destroy` |
`perCommandEnvCwd` is `false` (core wraps); `preservesMemoryOnPause` is `true`; `previewModel` is `subdomain`. In-place snapshot `restore` reports `NotSupported`.
---
# Vercel Sandbox (/adapters/vercel)
Vercel Sandbox via @vercel/sandbox — exec-emulation, native ports, and a filesystem that is auto-snapshotted on stop. No code interpreter.
## Installation [#installation]
`@vercel/sandbox` is an optional peer dependency of `sbox-sdk`, loaded lazily on first use.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @vercel/sandbox
```
```bash
bun add sbox-sdk @vercel/sandbox
```
```bash
npm install sbox-sdk @vercel/sandbox
```
```bash
yarn add sbox-sdk @vercel/sandbox
```
## Usage [#usage]
```ts
import { createSandboxClient } from "sbox-sdk";
import { vercel } from "sbox-sdk/vercel";
const client = createSandboxClient({
provider: vercel({
token: process.env.VERCEL_TOKEN!,
teamId: process.env.VERCEL_TEAM_ID!,
projectId: process.env.VERCEL_PROJECT_ID!,
}),
});
const sandbox = await client.create({ template: "node22", ports: [3000] });
const preview = await sandbox.ports.expose(3000);
console.log(preview.url);
```
[`template`](/templates) maps to Vercel's `runtime` selector (e.g. `"node22"`), not an arbitrary image. Vercel has no code interpreter, so `sandbox.code` is `undefined`.
## Authentication [#authentication]
## Options [#options]
## Capabilities [#capabilities]
| Capability | Level | Notes |
| ---------------------------------------- | :---------: | ----------------------------- |
| `background` / `streaming` | native | |
| `filesUpload` | native | |
| `exposePort` | native | preview via declared ports |
| `list` / `stop` / `pause` / `setTimeout` | unsupported | |
| `killProcess` | unsupported | |
| `codeInterpreter` / `statefulKernel` | unsupported | `sandbox.code` is `undefined` |
| `snapshot` / `fork` | unsupported | |
`preservesDiskOnStop` is `true` — `stop()` auto-snapshots the filesystem. `previewModel` is `declaredPorts`.
---
# Vercel AI SDK (/ai/ai-sdk)
Project sandbox tools into the AI SDK's tool() shape as a map for generateText / streamText. Use the ai() plugin with the aiSdk() framework, or the standalone toAISDKTools().
## Installation [#installation]
`ai` is an optional peer dependency, loaded only when you use this adapter.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk ai
```
```bash
bun add sbox-sdk ai
```
```bash
npm install sbox-sdk ai
```
```bash
yarn add sbox-sdk ai
```
## Usage [#usage]
Pass the `aiSdk()` framework to the `ai()` plugin and `sandbox.tools` is a tool map you pass straight to `generateText` or `streamText`:
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { aiSdk } from "sbox-sdk/ai-sdk";
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: aiSdk() })],
});
const sandbox = await client.create({ template: "node" });
const result = await generateText({
model: openai("gpt-4o"),
prompt: "Write app.js that prints the current date, then run it.",
tools: sandbox.tools, // Record, keyed by tool name
stopWhen: ({ steps }) => steps.length > 8,
});
```
`sandbox.tools` is a `Record`, capability-gated to the provider. Each tool's `execute` runs the sandbox operation and returns the result as text the model reads.
## Standalone [#standalone]
Build the same map explicitly — useful for filtering or merging with your own tools:
```ts
import { toAISDKTools } from "sbox-sdk/ai-sdk";
const tools = {
...toAISDKTools(sandbox, {
only: ["sbox_exec", "sbox_fs_read", "sbox_fs_write"],
}),
...myOtherTools,
};
```
`toAISDKTools` accepts a live `Sandbox` (capability-gated for you) or a pre-built `ToolSpec[]`.
## Approval [#approval]
The stable AI SDK (`ai@5`) has no `needsApproval` field on a tool, so this adapter enforces the [policy](/ai/approval) inside `execute`: a `deny` decision returns a message the model reads, and `ask` awaits `policy.onApprovalRequest` when you provide one.
```ts
plugins: [
ai({
framework: aiSdk(),
policy: {
defaults: { destructive: "ask" },
onApprovalRequest: async (req) => askUser(req.title),
},
}),
];
```
For AI SDK v7's call-site approval, `toolApproval(sandbox)` builds the matching `{ [toolName]: (input) => "user-approval" | undefined }` map from the same policy.
## Notes [#notes]
* Peer range: `ai@^5 || ^6`.
* Returns each tool's result as text. Mastra agents accept AI SDK tools too — see [Mastra](/ai/mastra).
---
# Anthropic / Claude (/ai/anthropic)
Project sandbox tools into the Anthropic SDK's runnable-tool shape. One array works with both the toolRunner auto-loop and a manual messages.create loop. Use the ai() plugin with the anthropic() framework, or toAnthropicTools().
## Installation [#installation]
`@anthropic-ai/sdk` is an optional peer dependency.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @anthropic-ai/sdk
```
```bash
bun add sbox-sdk @anthropic-ai/sdk
```
```bash
npm install sbox-sdk @anthropic-ai/sdk
```
```bash
yarn add sbox-sdk @anthropic-ai/sdk
```
## Usage [#usage]
Pass the `anthropic()` framework to the `ai()` plugin and `sandbox.tools` is an array of runnable Claude tools. The simplest path is the beta `toolRunner`, which runs the whole tool loop for you:
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { anthropic } from "sbox-sdk/anthropic";
import Anthropic from "@anthropic-ai/sdk";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: anthropic() })],
});
const sandbox = await client.create({ template: "node" });
const claude = new Anthropic();
const message = await claude.beta.messages.toolRunner({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [
{ role: "user", content: "Create app.js that prints hello, then run it." },
],
tools: sandbox.tools,
});
```
## One array, both loops [#one-array-both-loops]
Each tool is a *runnable tool* — it is both a tool definition and an executor — so the same `sandbox.tools` array also works with a manual `messages.create` loop:
```ts
const res = await claude.beta.messages.create({
model,
max_tokens,
messages,
tools: sandbox.tools,
});
for (const block of res.content) {
if (block.type === "tool_use") {
const tool = sandbox.tools.find((t) => t.name === block.name)!;
const output = await tool.run(tool.parse(block.input));
// append a tool_result block with `output`, then continue the conversation
}
}
```
## Standalone [#standalone]
```ts
import { toAnthropicTools } from "sbox-sdk/anthropic";
const tools = toAnthropicTools(sandbox, {
only: ["sbox_exec", "sbox_fs_read"],
});
```
## Approval [#approval]
Claude has no native human-in-the-loop, so this adapter enforces the [policy](/ai/approval) inside each tool's `run`: a `deny` decision short-circuits, and `ask` awaits `policy.onApprovalRequest` when configured. Both return a message Claude reads.
```ts
plugins: [
ai({
framework: anthropic(),
policy: {
defaults: { destructive: "ask" },
onApprovalRequest: async (req) => askUser(req.title),
},
}),
];
```
## Notes [#notes]
* Peer range: `@anthropic-ai/sdk@>=0.40`. The runnable tools use the SDK's beta tool helpers.
---
# Approval (/ai/approval)
One SandboxPolicy decides allow / ask / deny per tool call. Each framework maps "ask" to its native human-in-the-loop. Destructive actions pause by default.
## Why approval [#why-approval]
Sandboxes exist to run untrusted code, and an agent calling `sbox_fs_remove` or `sbox_lifecycle` with `action: "destroy"` is exactly the kind of action you may want a human to confirm. A single `SandboxPolicy` governs every tool, on every framework.
## The default posture [#the-default-posture]
Without any configuration, the policy is **destructive-asks**:
| Risk | Default | Examples |
| ------------- | ------- | ----------------------------------------------------------------------------------------------- |
| `safe` | `allow` | `sbox_fs_read`, `sbox_fs_list`, `sbox_lifecycle` → `getInfo` |
| `mutating` | `allow` | `sbox_exec`, `sbox_fs_write`, `sbox_run_code`, `sbox_expose_port` |
| `destructive` | `ask` | `sbox_fs_remove`, `sbox_lifecycle` → `stop` / `destroy`, `sbox_snapshot` → `restore` / `delete` |
So reads, writes, and command execution run freely, while irreversible actions pause for approval.
## Configuring the policy [#configuring-the-policy]
```ts
import type { SandboxPolicy } from "sbox-sdk/agent-tools";
const policy: SandboxPolicy = {
// Per-risk defaults (merged over the built-ins above).
defaults: { mutating: "ask", destructive: "deny" },
// Per-tool overrides — can inspect the actual input. Wins over `defaults`.
rules: {
sbox_exec: (input) =>
String((input as { command: string }).command).startsWith("rm ")
? "ask"
: "allow",
},
// Hard removal — the tool never appears in the produced set.
forbid: ["sbox_set_egress"],
// Called for "ask" by frameworks without native HITL (see below).
onApprovalRequest: async (req) => {
return confirmWithHuman(`${req.title} — allow?`); // your UI / queue
},
// Observe every decision (logging, metrics, audit trails).
audit: (rec) => log.info(rec),
};
```
Pass it to a plugin, a standalone function, or a single `create` call:
```ts
plugins: [ai({ framework: aiSdk(), policy })]; // client-wide default
toAISDKTools(sandbox, { policy }); // standalone
client.create(spec, { policy }); // this sandbox only
```
## How each framework handles "ask" [#how-each-framework-handles-ask]
The policy is authored once; each adapter maps a decision into that framework's native mechanism.
| Decision | OpenAI Agents | AI SDK · Mastra · Anthropic · LangChain |
| -------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `allow` | runs | runs |
| `ask` | native `needsApproval` → a `RunResult` interruption your host approves/rejects | awaits `policy.onApprovalRequest`; runs if it resolves `true`, otherwise returns a denial the model reads |
| `deny` | denial message | denial message |
[OpenAI Agents](/ai/openai) has built-in human-in-the-loop, so `ask` surfaces as an interruption on the run result. The other frameworks have no native gate in their current releases, so `ask` calls your `onApprovalRequest` callback. If you don't provide one, `ask` proceeds — human-in-the-loop is opt-in, so the tools never silently block.
## Risk classes [#risk-classes]
Risk is a property of the tool (and, for multi-verb tools, the chosen action), not of the provider:
* **`safe`** — read-only, no side effects.
* **`mutating`** — changes state but is recoverable.
* **`destructive`** — irreversible (deletes data, tears down or restores the sandbox).
Override the decision for any risk via `defaults`, or for any specific tool via `rules`.
---
# LangChain (/ai/langchain)
Project sandbox tools into @langchain/core tool() objects as an array for any LangChain agent or LangGraph ToolNode. Use the ai() plugin with the langchain() framework, or toLangChainTools().
## Installation [#installation]
`@langchain/core` is an optional peer dependency.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @langchain/core
```
```bash
bun add sbox-sdk @langchain/core
```
```bash
npm install sbox-sdk @langchain/core
```
```bash
yarn add sbox-sdk @langchain/core
```
## Usage [#usage]
Pass the `langchain()` framework to the `ai()` plugin and `sandbox.tools` is an array of structured tools for any LangChain agent:
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { langchain } from "sbox-sdk/langchain";
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: langchain() })],
});
const sandbox = await client.create({ template: "node" });
const agent = createReactAgent({
llm: new ChatOpenAI({ model: "gpt-4o" }),
tools: sandbox.tools, // StructuredToolInterface[]
});
const result = await agent.invoke({
messages: [
{ role: "user", content: "Create app.js that prints hello, then run it." },
],
});
```
You can also bind the tools to a model directly with `model.bindTools(sandbox.tools)`.
## Standalone [#standalone]
```ts
import { toLangChainTools } from "sbox-sdk/langchain";
const tools = toLangChainTools(sandbox, {
only: ["sbox_exec", "sbox_fs_read"],
});
```
`toLangChainTools` accepts a live `Sandbox` or a pre-built `ToolSpec[]`.
## Approval [#approval]
This adapter enforces the [policy](/ai/approval) inside the tool function via `policy.onApprovalRequest` — `deny` short-circuits, `ask` awaits your callback:
```ts
plugins: [
ai({
framework: langchain(),
policy: {
defaults: { destructive: "ask" },
onApprovalRequest: async (req) => askUser(req.title),
},
}),
];
```
For graph-native human-in-the-loop, wrap the call in a LangGraph `interrupt()`.
## Notes [#notes]
* Peer range: `@langchain/core@>=0.3` (Zod 4 supported).
* The only required peer is `@langchain/core`; agent runtimes like `@langchain/langgraph` and model packages are yours to choose.
---
# Mastra (/ai/mastra)
Project sandbox tools into Mastra's createTool() shape, keyed by id for an Agent. Use the ai() plugin with the mastra() framework, or the standalone toMastraTools().
## Installation [#installation]
`@mastra/core` is an optional peer dependency. Use **version 1 or later** — older `0.10.x` releases pin Zod 3 and AI SDK 4 and are incompatible.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @mastra/core
```
```bash
bun add sbox-sdk @mastra/core
```
```bash
npm install sbox-sdk @mastra/core
```
```bash
yarn add sbox-sdk @mastra/core
```
## Usage [#usage]
Pass the `mastra()` framework to the `ai()` plugin and `sandbox.tools` is a tool map you register on an `Agent`:
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { mastra } from "sbox-sdk/mastra";
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: mastra() })],
});
const sandbox = await client.create({ template: "node" });
const agent = new Agent({
name: "coder",
instructions: "You write and run code in a sandbox.",
model: openai("gpt-4o"),
tools: sandbox.tools, // Record, keyed by tool id
});
const res = await agent.generate(
"Create app.js that prints hello, then run it."
);
```
## Standalone [#standalone]
```ts
import { toMastraTools } from "sbox-sdk/mastra";
const tools = toMastraTools(sandbox, { only: ["sbox_exec", "sbox_fs_read"] });
```
`toMastraTools` accepts a live `Sandbox` or a pre-built `ToolSpec[]`.
## Approval [#approval]
Mastra has native suspend/resume, but for v1 this adapter enforces the [policy](/ai/approval) inside `execute` via `policy.onApprovalRequest`, consistent with the other adapters — `deny` short-circuits, `ask` awaits your callback:
```ts
plugins: [
ai({
framework: mastra(),
policy: {
defaults: { destructive: "ask" },
onApprovalRequest: async (req) => askUser(req.title),
},
}),
];
```
## Notes [#notes]
* Peer range: `@mastra/core@^1`.
* Mastra is built on the AI SDK and also accepts [AI SDK tools](/ai/ai-sdk) directly, so `ai({ framework: aiSdk() })` output works in a Mastra agent too. The native adapter is preferred for Mastra-native registration.
---
# OpenAI Agents (/ai/openai)
Project sandbox tools into the OpenAI Agents SDK tool() shape as an array, with native needsApproval human-in-the-loop. Use the ai() plugin with the openaiAgents() framework, or toOpenAITools().
## Installation [#installation]
`@openai/agents` is an optional peer dependency.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @openai/agents
```
```bash
bun add sbox-sdk @openai/agents
```
```bash
npm install sbox-sdk @openai/agents
```
```bash
yarn add sbox-sdk @openai/agents
```
## Usage [#usage]
Pass the `openaiAgents()` framework to the `ai()` plugin and `sandbox.tools` is a tool array you pass to `new Agent`:
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { openaiAgents } from "sbox-sdk/openai";
import { Agent, run } from "@openai/agents";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: openaiAgents() })],
});
const sandbox = await client.create({ template: "node" });
const agent = new Agent({
name: "coder",
instructions: "You write and run code in a sandbox.",
tools: sandbox.tools, // Tool[]
});
const result = await run(
agent,
"Create app.js that prints hello, then run it."
);
```
`sandbox.tools` is a `Tool[]`. Schemas are passed with `strict: true`; the SDK converts our optional fields to the nullable-and-required form OpenAI strict mode expects.
## Standalone [#standalone]
```ts
import { toOpenAITools } from "sbox-sdk/openai";
const tools = toOpenAITools(sandbox, { only: ["sbox_exec", "sbox_fs_read"] });
```
## Approval [#approval]
The OpenAI Agents SDK has **native human-in-the-loop**, so this adapter wires the [policy](/ai/approval) into `needsApproval`. An `ask` decision surfaces as a `tool_approval_requested` interruption on the `RunResult` — your host approves or rejects it, then re-runs:
```ts
let result = await run(agent, prompt);
while (result.interruptions?.length) {
for (const interruption of result.interruptions) {
if (await humanApproves(interruption)) result.state.approve(interruption);
else result.state.reject(interruption);
}
result = await run(agent, result.state);
}
```
A `deny` decision is enforced inside the tool and returns a message the model reads.
## Notes [#notes]
* Peer range: `@openai/agents@>=0.12`.
---
# Overview (/ai/overview)
Turn any sandbox into a set of tools your AI agent can call — one plugin shapes sandbox.tools for your framework, capability-gated to the provider and gated by an approval policy.
## Give your agent a sandbox [#give-your-agent-a-sandbox]
The AI layer turns a `Sandbox` into tools an LLM can call — run commands, read and write files, execute code, expose ports — shaped for whichever agent framework you use, with a single approval policy you control.
You write the integration once. Pick an **AI-provider plugin**, and the sandbox grows a correctly-typed `sandbox.tools` you hand straight to your framework:
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { aiSdk } from "sbox-sdk/ai-sdk";
import { generateText } from "ai";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: aiSdk() })],
});
const sandbox = await client.create({ template: "node" });
// `sandbox.tools` is typed for the AI SDK and already filtered to what E2B supports.
const result = await generateText({
model,
prompt: "Create app.js that prints hello, then run it.",
tools: sandbox.tools,
});
```
The framework you pass to `ai()` **decides the shape** of `sandbox.tools` — a tool map for the AI SDK, an array for OpenAI Agents, runnable tools for Claude. Swap `e2b()` for `vercel()` and the toolset re-shapes itself to the new provider's capabilities, with zero changes to your agent code.
## How it fits together [#how-it-fits-together]
One provider-neutral tool registry is projected into every framework. You never hand-write the same tools per framework:
```
Sandbox ──▶ agent-tools (10 capability-gated tools + policy)
│ projection (shape only)
┌────────────┼─────────────┬──────────────┬───────────────┐
▼ ▼ ▼ ▼ ▼
ai-sdk mastra openai anthropic langchain
tool() map createTool map tool() array runnable[] tool() array
```
* **[AI providers](/ai/providers)** — install an AI-provider [plugin](/plugins/overview) on the client and `sandbox.tools` is ready to use, typed for your framework.
* **[Tools](/ai/tools)** — the 10 canonical tools, each mapped to the core `Sandbox` API and gated by the provider's capabilities.
* **[Approval](/ai/approval)** — one `SandboxPolicy` decides `allow` / `ask` / `deny` per call; each framework maps `ask` to its native human-in-the-loop.
## Two ways to use it [#two-ways-to-use-it]
Every framework subpath ships both layers:
1. **Plugin (batteries included)** — `plugins: [ai({ framework: aiSdk() })]` → `sandbox.tools`. The shape follows the framework you pass.
2. **Standalone function (composable)** — `toAISDKTools(sandbox)` returns the same tools explicitly, for when you want to filter, merge, or build them yourself.
```ts
import { toAISDKTools } from "sbox-sdk/ai-sdk";
const tools = toAISDKTools(sandbox, {
policy: { defaults: { destructive: "ask" } },
});
```
## Supported frameworks [#supported-frameworks]
All used as `ai({ framework: … })`:
| Framework | Subpath | Adapter | `sandbox.tools` |
| ----------------------------------- | -------------------- | ---------------- | --------------- |
| [Vercel AI SDK](/ai/ai-sdk) | `sbox-sdk/ai-sdk` | `aiSdk()` | tool map |
| [Mastra](/ai/mastra) | `sbox-sdk/mastra` | `mastra()` | tool map |
| [OpenAI Agents](/ai/openai) | `sbox-sdk/openai` | `openaiAgents()` | tool array |
| [Anthropic / Claude](/ai/anthropic) | `sbox-sdk/anthropic` | `anthropic()` | runnable tools |
| [LangChain](/ai/langchain) | `sbox-sdk/langchain` | `langchain()` | tool array |
Each framework SDK is an optional peer dependency — install only the one you use.
---
# AI providers (/ai/providers)
AI-provider plugins shape sandbox.tools for your framework — capability-gated, policy-aware, and typed. One per client, configurable per sandbox.
An **AI-provider plugin** is the first [plugin](/plugins/overview) kind. It grafts a framework-shaped `sandbox.tools` onto every sandbox the client builds. This page covers what's specific to AI providers; for the general plugin mechanism, see [Plugins](/plugins/overview).
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { aiSdk } from "sbox-sdk/ai-sdk";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: aiSdk() })],
});
const sandbox = await client.create();
sandbox.tools; // typed for the AI SDK, ready for generateText({ tools })
```
## The shape follows the provider *and* the framework [#the-shape-follows-the-provider-and-the-framework]
`sandbox.tools` is resolved on two axes at once:
* **Capability-gated to the provider** — a tool only appears if the provider supports it. On a provider without a code interpreter, `sbox_run_code` simply isn't there. See [Tools](/ai/tools).
* **Typed for the framework** — the framework you pass to `ai()` decides the type: a tool map for the AI SDK, an array for OpenAI Agents, runnable tools for Claude.
Swap `e2b()` for `vercel()` and `sandbox.tools` re-shapes to the new provider's capabilities with no change to your agent code.
## One AI-provider per client [#one-ai-provider-per-client]
An agent runs in exactly one framework — you hand `sandbox.tools` to a single `generateText(...)` or `new Agent({ tools })` call — so a single shape is correct. The client **throws** if you pass two AI-provider plugins:
```ts
// ❌ throws: a client supports one AI-provider plugin
createSandboxClient({
provider: e2b(),
plugins: [ai({ framework: aiSdk() }), ai({ framework: openaiAgents() })],
});
```
Need two framework shapes from one sandbox (libraries, benchmarks)? Use the standalone `toXTools(sandbox)` functions, or read the provider-neutral registry with `createSandboxTools(sandbox)`. Other [plugin kinds](/plugins/overview#plugin-kinds) stack freely alongside an AI provider.
## Policy per sandbox [#policy-per-sandbox]
The framework is fixed on the client, but a sandbox's trust posture often isn't — you may run trusted internal work in one sandbox and untrusted user code in another. Pass per-sandbox overrides as the second argument to `create`:
```ts
const client = createSandboxClient({
provider: e2b(),
plugins: [ai({ framework: aiSdk(), policy: trustedDefaults })], // default policy
});
// Tighten approval for this sandbox only:
const box = await client.create(spec, {
policy: { defaults: { mutating: "ask", destructive: "deny" } },
});
```
Overrides apply to `policy`, `only`, and `forbid` — never the framework. See [Approval](/ai/approval).
## Configuring a plugin [#configuring-a-plugin]
Every AI-provider plugin accepts the same options as its standalone function — a `policy`, an `only` allow-list, and a `forbid` list:
```ts
plugins: [
ai({
framework: aiSdk(),
only: ["sbox_exec", "sbox_fs_read", "sbox_fs_write"],
policy: { forbid: ["sbox_set_egress"] },
}),
];
```
See the per-framework pages for each plugin's output shape and approval behavior: [Vercel AI SDK](/ai/ai-sdk), [Mastra](/ai/mastra), [OpenAI Agents](/ai/openai), [Anthropic](/ai/anthropic), [LangChain](/ai/langchain).
---
# Tools (/ai/tools)
The 10 canonical sandbox tools, each mapped to the core Sandbox API and gated by the provider's capabilities. Authored once, projected into every framework.
## The canonical tools [#the-canonical-tools]
Every adapter projects from the same provider-neutral registry. There are ten tools, each mapped to a real `Sandbox` operation:
| Tool | Maps to | Always present? | Risk |
| ------------------ | ------------------------------------------------------------------ | :---------------------: | ----------- |
| `sbox_exec` | `commands.run` | yes | mutating |
| `sbox_run_code` | `code.runCode` | needs `codeInterpreter` | mutating |
| `sbox_fs_read` | `files.read` | yes | safe |
| `sbox_fs_write` | `files.write` | yes | mutating |
| `sbox_fs_list` | `files.list` | yes | safe |
| `sbox_fs_remove` | `files.remove` | yes | destructive |
| `sbox_expose_port` | `ports.expose` | needs `exposePort` | mutating |
| `sbox_snapshot` | `snapshots.*` | needs `snapshot` | per-action |
| `sbox_lifecycle` | `getInfo` / `setTimeout` / `stop` / `pause` / `resume` / `destroy` | yes | per-action |
| `sbox_set_egress` | `network.setEgressPolicy` | needs `egressControl` | mutating |
`sbox_snapshot` and `sbox_lifecycle` fold several verbs behind one `action` argument to keep the tool count low (too many tools degrades model selection). Their risk is refined per action — `sbox_lifecycle` with `action: "getInfo"` is `safe`, with `action: "destroy"` is `destructive`.
The [`risk`](/ai/approval) class drives the default approval posture.
## Capability gating [#capability-gating]
A tool only exists if the provider can do it. `createSandboxTools` filters the set against the sandbox's [capabilities](/general/capabilities):
```ts
import { createSandboxClient } from "sbox-sdk";
import { memory } from "sbox-sdk/testing";
import { createSandboxTools } from "sbox-sdk/agent-tools";
const sandbox = await createSandboxClient({ provider: memory() }).create();
createSandboxTools(sandbox).map((t) => t.name);
// in-memory provider has no code interpreter or egress control, so:
// → sbox_exec, sbox_fs_read, sbox_fs_write, sbox_fs_list, sbox_fs_remove,
// sbox_expose_port, sbox_snapshot, sbox_lifecycle
// (sbox_run_code and sbox_set_egress are absent)
```
This is why `sandbox.tools` from an [AI-provider plugin](/ai/providers) automatically reflects the provider: a richer provider exposes more tools, with no change to your code.
## The neutral registry [#the-neutral-registry]
`createSandboxTools` returns a provider- and framework-agnostic `ToolSpec[]` — the input every framework adapter projects from. You rarely need it directly (the plugins and `toXTools` functions call it for you), but it's there when you want to filter, inspect, or build a custom integration:
```ts
import { createSandboxTools } from "sbox-sdk/agent-tools";
const specs = createSandboxTools(sandbox, {
only: ["sbox_exec", "sbox_fs_read", "sbox_fs_write"],
policy: { forbid: ["sbox_fs_remove"] },
});
```
Each `ToolSpec` carries a `name`, a model-facing `description`, a Zod `inputSchema`, a `risk` class, and an `execute` that validates its input and **never throws** for a tool-level failure — it returns a result the model can read and react to.
## Results [#results]
Tools return their output to the model as text. A failure (a non-zero command, a missing file, a denied call) comes back as a readable message rather than an exception, so the agent loop keeps going and the model can recover.
## Excluded operations [#excluded-operations]
A few `Sandbox` operations are deliberately **not** tools because their results can't be serialized to a model: `snapshots.fork` (returns new `Sandbox` objects), `ports.fetch` (returns a `Response`), and raw streaming process handles. Reach for those through the typed [escape hatch](/general/escape-hatch) or the core API directly.
---
# Client (/api/client)
createSandboxClient is the single entry point — it owns provider selection, idempotency-aware retry and fallback, lifecycle hooks, and disposal.
## `createSandboxClient(options?)` [#createsandboxclientoptions]
The single entry point. Pass a provider adapter, or call with no arguments for the zero-config in-memory provider.
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
});
```
### Options [#options]
| Option | Type | Default | Purpose |
| ----------------- | ----------------------------- | ------------------ | -------------------------------------------------------- |
| `provider` | `SandboxProvider` | in-memory | The adapter to route to |
| `fallback` | `SandboxProvider[]` | `[]` | Backup providers — engages only with an `idempotencyKey` |
| `retry` | `RetryPolicy` | `{ retries: 2 }` | `retries`, `delayMs`, `shouldRetry` |
| `hooks` | `Hooks` | — | `beforeCreate`, `afterCreate`, `onError` |
| `fetch` | `typeof fetch` | `globalThis.fetch` | Injectable transport (test seam) |
| `emulate` | `(keyof CapabilityMap)[]` | — | Opt-in lossy emulations |
| `defaultMetadata` | `Record` | — | Merged into every `create` |
| `plugins` | `SandboxPlugin[]` | `[]` | Augment every sandbox (e.g. the `ai(...)` plugin) |
| `telemetry` | `boolean \| TelemetryOptions` | enabled | Anonymous usage data. Pass `false` to opt out |
See [Retries & fallback](/retries) for `retry`, `fallback`, and `hooks` in depth.
See [Anonymous usage data](/telemetry) for what is collected and how to opt out.
## `SandboxClient` [#sandboxclient]
The object returned by `createSandboxClient`.
| Member | Signature | Notes |
| -------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `create` | `(spec?: SandboxSpec, options?: SandboxCreateOptions) => Promise` | Idempotency-aware; retries + fallback. `options` carries per-sandbox plugin overrides (e.g. AI `policy`) |
| `connect` | `(id: string) => Promise` | Reattach to an existing sandbox |
| `list` | `(filter?: ListFilter) => AsyncIterable` | Throws `NotSupported` if the provider has no `list` |
| `provider` | `string` | Active provider name (readonly) |
| `capabilities` | `Capabilities` | `{ map, flags }` read-model (readonly) |
| `dispose` | `() => Promise` | Release provider resources |
## `SandboxSpec` [#sandboxspec]
The shape passed to `create()`. `template` is mapped per-adapter — see [Templates](/templates).
---
# code (/api/code)
The code interpreter namespace — run code in a stateful kernel and collect rich results. Gated on the codeInterpreter capability.
`sandbox.code` is **capability-gated** — it's typed `undefined` on providers without a code interpreter (E2B and Cloudflare have it; Vercel and memory don't). Guard before use:
```ts
if (sandbox.code) {
const exec = await sandbox.code.runCode("import numpy as np; print(np.pi)");
console.log(exec.logs.stdout);
}
```
## Methods [#methods]
| Method | Signature |
| --------------- | ----------------------------------------- |
| `runCode` | `(code, opts?) => Promise` |
| `createContext` | `(opts?) => Promise` |
`runCode` options: `{ context?, language?, onStdout?, onStderr? }`. Pass a `KernelContext` from `createContext` to keep state across calls (a stateful kernel).
## `CodeExecution` [#codeexecution]
```ts
interface CodeExecution {
results: RichResult[]; // rich MIME outputs (plots, html, ...)
logs: { stdout: string[]; stderr: string[] };
error?: { name: string; value: string; traceback?: string };
}
interface RichResult {
mime: Record; // e.g. { "image/png": "" }
text?: string;
}
```
```ts
const exec = await sandbox.code.runCode(
"import matplotlib.pyplot as plt; plt.plot([1,2,3]); plt.show()"
);
for (const r of exec.results) {
if (r.mime["image/png"]) save(r.mime["image/png"]);
}
```
---
# commands (/api/commands)
Run, spawn, stream, and kill processes. commands.run returns a handle that is both a Promise and an AsyncIterable, and never throws on a non-zero exit.
`sandbox.commands` is always present. It covers process execution across every adapter.
## `run(cmd, opts?)` [#runcmd-opts]
Returns an `ExecHandle` — both a `Promise` and an `AsyncIterable`. `await` it to buffer, or `for await` the **same handle** to stream live. It **never throws on a non-zero exit** — the exit code is data.
```ts
// Buffered:
const res = await sandbox.commands.run("npm test", { cwd: "/app" });
res.exitCode; // number — non-zero is NOT an error
res.stdout;
res.stderr;
// Streamed (same handle):
for await (const ev of sandbox.commands.run(["python", "train.py"])) {
if (ev.type === "stdout") process.stdout.write(ev.data);
if (ev.type === "exit") console.log(ev.exitCode);
}
```
`cmd` is a `string` or `string[]`. The handle also exposes `pid: Promise`, `stdout` / `stderr` as `ReadableStream`s, `kill(signal?)`, and `writeStdin(data)`.
### `ExecOptions` [#execoptions]
### `ExecResult` [#execresult]
```ts
interface ExecResult {
readonly stdout: string;
readonly stderr: string;
readonly exitCode: number;
readonly exitCodeSynthesized?: boolean; // true when parsed from a $? echo
}
```
### `OutputEvent` [#outputevent]
```ts
type OutputEvent =
| { type: "stdout"; data: string }
| { type: "stderr"; data: string }
| { type: "exit"; exitCode: number; signal?: string };
```
## Other methods [#other-methods]
| Method | Signature | Notes |
| --------- | --------------------------------------- | ----------------------------- |
| `spawn` | `(cmd, opts?) => Promise` | Long-lived background process |
| `connect` | `(processId) => Promise` | Reattach to a running process |
| `kill` | `(processId, signal?) => Promise` | |
| `list` | `() => Promise` | |
`spawn` requires the `background` capability and `kill` requires `killProcess`; `connect` and `list` depend on the adapter implementing them. Each throws `NotSupportedError` on providers that don't support it.
---
# files (/api/files)
The filesystem namespace — read, write, list, mkdir, remove, rename, stat, exists, upload, download, and watch. Web-standard bodies in, StoredFile out.
`sandbox.files` is always present — native where the provider has filesystem primitives, and polyfilled over `exec` where it doesn't.
## Bodies and `StoredFile` [#bodies-and-storedfile]
Write bodies are web-standard: `string | Uint8Array | ReadableStream`. Reads return a `StoredFile`:
```ts
interface StoredFile {
path: string;
text(): Promise;
bytes(): Promise;
stream(): ReadableStream;
}
```
```ts
await sandbox.files.write("/app/data.json", JSON.stringify({ ok: true }));
const file = await sandbox.files.read("/app/data.json");
const text = await file.text();
```
## Methods [#methods]
| Method | Signature |
| ------------------- | ------------------------------------------------ |
| `read` / `download` | `(path) => Promise` |
| `write` / `upload` | `(path, data: FileBody) => Promise` |
| `list` | `(path) => Promise` |
| `mkdir` | `(path, { recursive? }) => Promise` |
| `remove` | `(path, { recursive? }) => Promise` |
| `rename` | `(from, to) => Promise` |
| `stat` | `(path) => Promise` |
| `exists` | `(path) => Promise` |
| `watch` | `(path, cb, { recursive? }) => Promise` |
## Notes [#notes]
* `exists()` returns `false` **only** on `NotFound` — auth and timeout errors still throw. See [Errors](/errors).
* `watch()` requires the `filesWatch` capability and throws `NotSupportedError` where it's unsupported.
* On providers without native filesystem methods, `list` / `mkdir` / `remove` / `rename` / `stat` are polyfilled by the core via `exec`.
### `FileInfo` & `DirEntry` [#fileinfo--direntry]
```ts
interface FileInfo {
path: string;
type: "file" | "dir" | "symlink";
size: number;
mtime?: Date;
}
interface DirEntry {
name: string;
path: string;
type: "file" | "dir" | "symlink";
}
```
---
# lifecycle (/api/lifecycle)
Top-level sandbox lifecycle — getInfo, setTimeout, pause, resume, stop, and destroy. Always present, runtime-gated per provider.
Lifecycle methods live directly on the `Sandbox` (not in a namespace). They are **always present** so the swap-one-import promise holds, but **runtime-gated**: calling one a provider doesn't support throws `NotSupportedError`.
## Methods [#methods]
| Method | Signature | Capability |
| ------------ | ---------------------------------- | -------------------- |
| `getInfo` | `() => Promise` | always |
| `setTimeout` | `(ttlMs: number) => Promise` | `setTimeout` |
| `pause` | `() => Promise` | `pause` |
| `resume` | `() => Promise` | (pairs with `pause`) |
| `stop` | `() => Promise` | `stop` |
| `destroy` | `() => Promise` | always |
```ts
const info = await sandbox.getInfo();
info.state; // "running" | "paused" | "stopped" | ...
if (sandbox.can("pause")) {
await sandbox.pause();
await sandbox.resume();
}
await sandbox.setTimeout(120_000);
await sandbox.destroy();
```
## Pause vs stop semantics [#pause-vs-stop-semantics]
What survives depends on the provider's behavioral flags (see [Capabilities](/capabilities)):
* `preservesMemoryOnPause` — whether RAM survives a `pause()`.
* `preservesDiskOnStop` — whether disk survives a `stop()`.
For example, E2B preserves memory on pause; Vercel auto-snapshots the filesystem on stop. Check `sandbox.capabilities.flags` to branch on this rather than assuming.
## Cleanup [#cleanup]
Always `destroy()` sandboxes you create, and `dispose()` the client when you're done:
```ts
await sandbox.destroy();
await client.dispose();
```
---
# ports (/api/ports)
Expose a port and get a preview URL, list exposures, and proxy a fetch through the sandbox. Gated on the exposePort capability.
`sandbox.ports` is **capability-gated** on `exposePort` — native on E2B, Vercel, and Cloudflare; emulated on the in-memory provider. Guard before use:
```ts
if (sandbox.ports) {
const preview = await sandbox.ports.expose(3000);
console.log(preview.url);
}
```
## Methods [#methods]
| Method | Signature | Notes |
| ---------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `expose` | `(port, { private? }) => Promise` | Returns the preview URL |
| `unexpose` | `(port) => Promise` | |
| `list` | `() => Promise` | |
| `fetch` | `(port, path?, init?) => Promise` | Proxy a request through the sandbox — native proxy where `proxiedFetch` is supported, else via an exposed preview URL |
## `Preview` [#preview]
```ts
interface Preview {
url: string;
port: number;
token?: string;
}
```
How the `url` is formed depends on the provider's `previewModel` flag — `subdomain` (E2B), `declaredPorts` (Vercel), `wildcardDNS` (Cloudflare), or `ip` (memory). See [Capabilities](/capabilities).
---
# Sandbox (/api/sandbox)
The object you hold after create() — always-present commands and files namespaces, capability-gated sub-APIs, lifecycle methods, and the raw() escape hatch.
A `Sandbox` is what `client.create()` and `client.connect()` return. It groups everything into namespaces, with capability-gated sub-APIs typed `undefined` when the provider can't support them.
## Members [#members]
| Member | Type | Presence |
| ------------------------------------------ | ----------------------------------- | ----------------------------------------------------------------- |
| `id` / `name?` / `provider` | `string` | always |
| `capabilities` | `Capabilities` | always |
| `commands` | `CommandsAPI` | **always** (universal) |
| `files` | `FilesAPI` | **always** (native or polyfilled) |
| `ports` | `PortsAPI \| undefined` | gated on `exposePort` |
| `code` | `CodeAPI \| undefined` | gated on `codeInterpreter` |
| `snapshots` | `SnapshotsAPI \| undefined` | gated on `snapshot` |
| `network` | `NetworkAPI \| undefined` | gated on `egressControl` |
| `getInfo` / `destroy` | `() => Promise<…>` | always |
| `setTimeout` / `stop` / `pause` / `resume` | `(…) => Promise` | present, but throw `NotSupported` where the capability is missing |
| `can` | `(cap: K) => this is Sandbox<…>` | runtime check + type guard |
| `raw` | `() => Raw` | the native client, typed per adapter |
## Capability gating [#capability-gating]
`ports`, `code`, `snapshots`, and `network` are typed `undefined` on providers that don't support them, so a guard is required — and the absence is caught at compile time:
```ts
if (sandbox.code) {
await sandbox.code.runCode("print(1)");
}
```
For dynamically-chosen providers, `can()` narrows at runtime and the type level:
```ts
if (sandbox.can("snapshot")) {
await sandbox.snapshots.create({ name: "ckpt" });
}
```
See [Capabilities](/capabilities) for the model, and [Escape hatch](/escape-hatch) for `raw()`.
## `SandboxInfo` [#sandboxinfo]
Returned by `getInfo()`:
```ts
interface SandboxInfo {
readonly id: string;
readonly name?: string;
readonly state: SandboxState; // "creating" | "running" | "paused" | "stopped" | "destroyed" | "error" | "unknown"
readonly provider: string;
readonly metadata: Readonly>;
readonly createdAt?: Date;
readonly raw: unknown; // the original provider payload
}
```
---
# snapshots (/api/snapshots)
Checkpoint and restore sandbox state, fork a sandbox, and manage snapshot refs. Gated on the snapshot capability.
`sandbox.snapshots` is **capability-gated** on `snapshot` — native on E2B and the in-memory provider; unsupported on Vercel and Cloudflare. Guard before use:
```ts
if (sandbox.snapshots) {
const ref = await sandbox.snapshots.create({ name: "ckpt" });
// ... mutate ...
await sandbox.snapshots.restore(ref);
}
```
Snapshots are *runtime* checkpoints, distinct from the build-time [`template`](/templates).
## Methods [#methods]
| Method | Signature | Notes |
| --------- | ----------------------------------------------- | ------------------------------ |
| `create` | `({ name? }) => Promise` | |
| `restore` | `(ref: SnapshotRef \| string) => Promise` | |
| `fork` | `(count?) => Promise` | Gated on the `fork` capability |
| `list` | `() => Promise` | |
| `delete` | `(ref: SnapshotRef \| string) => Promise` | |
## `SnapshotRef` [#snapshotref]
```ts
interface SnapshotRef {
id: string;
name?: string;
provider: string;
createdAt?: Date;
raw: unknown; // the original provider payload
}
```
---
# Errors (/general/errors)
Every provider normalizes to one SandboxError taxonomy — a small set of codes, a retryable flag, and the native error preserved on cause.
Providers never leak their native SDK errors past the adapter boundary. Everything routes through one normalized taxonomy, so your error handling is written once and works across every provider.
## `SandboxError` [#sandboxerror]
The base class. Key fields:
| Field | Type | Meaning |
| ----------- | ------------------ | --------------------------------------- |
| `code` | `SandboxErrorCode` | The normalized category (see below) |
| `provider` | `string?` | Which adapter raised it |
| `status` | `number?` | Underlying HTTP status, if any |
| `retryable` | `boolean` | Whether a retry could plausibly succeed |
| `timedOut` | `boolean` | True for timeout/abort |
| `aborted` | `boolean` | True when an `AbortSignal` fired |
The original error is preserved on `cause`. Use the static `SandboxError.wrap(err, provider?)` to coerce any thrown value into a `SandboxError` (pass-through if it already is one).
## Error codes [#error-codes]
```ts
type SandboxErrorCode =
| "NotFound" // sandbox / process / file / snapshot id unknown
| "Unauthorized" // bad or expired credentials
| "Timeout" // op exceeded timeoutMs / TTL
| "QuotaExceeded" // provider resource / billing limit
| "NotSupported" // capability gap — thrown BEFORE any network call
| "Conflict" // idempotency / state conflict
| "ProviderNotFound" // unknown provider id
| "Validation" // bad options
| "AllProvidersFailed" // every provider in the fallback chain failed
| "Provider"; // catch-all normalized provider / network failure
```
`Timeout`, `QuotaExceeded`, and `Provider` are **retryable by default**.
## Specialized classes [#specialized-classes]
| Class | When |
| ------------------------- | ------------------------------------------------------------------------------------- |
| `NotSupportedError` | A capability gap — thrown **synchronously, before any network call**. Adds `feature`. |
| `ProviderNotFoundError` | Unknown provider id. |
| `AllProvidersFailedError` | Every provider in a fallback chain failed; carries `attempts[]`. |
## Rules worth knowing [#rules-worth-knowing]
* `commands.run(...)` **never throws on a non-zero exit** — the exit code is data (`result.exitCode`), not an error.
* `files.exists()` returns `false` only on `NotFound`; auth and timeout errors still throw.
* `NotSupportedError` is thrown up front, before any network call, so an unsupported feature fails fast and cheap.
## Helpers [#helpers]
```ts
import { isRetryableError, isRetryableStatus } from "sbox-sdk";
try {
await sandbox.commands.run("...");
} catch (err) {
if (isRetryableError(err)) {
// err is a retryable SandboxError
}
}
isRetryableStatus(429); // true (408 / 409 / 425 / 429 / >= 500)
```
See [Retries & fallback](/retries) for how the client uses `retryable` to drive automatic retry and provider fallback.
---
# Escape hatch (/general/escape-hatch)
Drop down to the native, per-adapter client for any provider feature outside the unified surface — fully typed, no casting.
The unified surface is deliberately small — the namespaces every adapter can implement. When you need a provider feature that isn't part of it, drop down to the native client instead of waiting for it to be wrapped.
## `sandbox.raw()` [#sandboxraw]
`raw()` returns the underlying provider client, **typed per adapter** — the native `Sandbox` from `@e2b/code-interpreter` for `e2b()`, from `@vercel/sandbox` for `vercel()`, from `@cloudflare/sandbox` for `cloudflare()`, and the in-memory store for `memory()`. The type flows through from the adapter, so you keep full autocomplete.
```ts
import { e2b } from "sbox-sdk/e2b";
const client = createSandboxClient({ provider: e2b({ apiKey }) });
const sandbox = await client.create();
// Unified path:
await sandbox.commands.run("echo hi");
// Native E2B client for something we don't model:
const native = sandbox.raw(); // typed as E2B's Sandbox
```
## When to reach for it [#when-to-reach-for-it]
There are three layers, in order of preference:
1. **Capability-gated sub-APIs** (`sandbox.code`, `.ports`, `.snapshots`, `.network`) — for features that *are* in the unified model but not on every provider.
2. **`sandbox.raw()`** — for anything the unified surface doesn't model at all.
3. **`.raw` on payloads** — `SandboxInfo.raw` and `SnapshotRef.raw` carry the original provider response for those calls.
## The tradeoff [#the-tradeoff]
`raw()` is the **intentional exit from the portability promise**. Every `commands` / `files` / gated call site survives an adapter swap unchanged; the moment you call `raw()`, that line of code is pinned to one provider. That's the right tool for a genuinely provider-unique feature — but it's a one-way door for that call site, which is why it's an explicit, named method rather than something you reach by accident.
If you constructed the client with the default dynamically-typed provider, `raw()` is typed `unknown` and you'd cast it yourself. Import a concrete adapter to get the precise native type.
---
# Installation (/general/installation)
Install sbox-sdk and the optional peer dependency for the adapter you're using — nothing else is bundled.
`sbox-sdk` is small and dependency-free at its core — a fetch-only, edge-portable runtime behind a tree of subpath exports. Install the package, then add the peer dependency for the adapter you're wiring up. Anything you don't import is never bundled.
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk
```
```bash
bun add sbox-sdk
```
```bash
npm install sbox-sdk
```
```bash
yarn add sbox-sdk
```
The package ships its own TypeScript types and is published as ESM (`"type": "module"`), targeting modern runtimes — Node 20+, Bun, Deno, and edge/Workers. The core does no `node:` imports, so it runs unchanged across runtimes.
## Adapter peer dependencies [#adapter-peer-dependencies]
Adapters are subpath exports — `sbox-sdk/e2b`, `sbox-sdk/vercel`, `sbox-sdk/cloudflare` — and each provider's native SDK is an **optional peer dependency**, loaded lazily on first `create()` / `connect()`. The SDK you don't import is never bundled, so install size and cold-start cost stay proportional to the providers you actually use.
Install the peer dep alongside the SDK. For **E2B**:
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @e2b/code-interpreter
```
```bash
bun add sbox-sdk @e2b/code-interpreter
```
```bash
npm install sbox-sdk @e2b/code-interpreter
```
```bash
yarn add sbox-sdk @e2b/code-interpreter
```
For **Vercel Sandbox**:
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @vercel/sandbox
```
```bash
bun add sbox-sdk @vercel/sandbox
```
```bash
npm install sbox-sdk @vercel/sandbox
```
```bash
yarn add sbox-sdk @vercel/sandbox
```
For **Cloudflare Sandbox** (inside a Worker):
pnpm
bun
npm
yarn
```bash
pnpm add sbox-sdk @cloudflare/sandbox
```
```bash
bun add sbox-sdk @cloudflare/sandbox
```
```bash
npm install sbox-sdk @cloudflare/sandbox
```
```bash
yarn add sbox-sdk @cloudflare/sandbox
```
The [in-memory provider](/adapters/memory) needs no peer dependency at all — it's built in and is the zero-config default, ideal for tests.
## Missing a peer dependency? [#missing-a-peer-dependency]
Import an adapter without its peer installed and the lazy import throws `ERR_MODULE_NOT_FOUND` naming the missing package — the SDK doesn't vendor or shim provider clients, so the failure is loud and specific rather than a silent fallback. Install the named package and the import resolves.
Once your adapter's package is in place, head to [Usage](/usage) to construct a client and run the core namespaces.
---
# llms.txt (/general/llms-txt)
The docs are available as plain markdown for LLMs — an llms.txt index, a full-text bundle, and any page as markdown. Feed them to ChatGPT, Claude, Cursor, or your own agent.
Every page of these docs is available as clean markdown, plus two aggregate files following the [llms.txt](https://llmstxt.org) convention — so you can give an LLM accurate, current context about sbox SDK instead of letting it guess.
## Endpoints [#endpoints]
| URL | What it is |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [`/llms.txt`](/llms.txt) | An index of every doc page, with links and one-line descriptions. |
| [`/llms-full.txt`](/llms-full.txt) | The entire documentation concatenated into one markdown file. |
| `/llms.mdx/` | Any single page as markdown — append the page's path, e.g. [`/llms.mdx/general/usage`](/llms.mdx/general/usage). |
## Using it [#using-it]
* **In a chat (ChatGPT, Claude):** paste `https://sbox-sdk.vercel.app/llms.txt` and ask the model to read the index, then fetch the pages it needs — or paste `https://sbox-sdk.vercel.app/llms-full.txt` to load everything at once.
* **In an editor agent (Cursor, Copilot):** add `https://sbox-sdk.vercel.app/llms.txt` as a documentation source.
* **From any page:** fetch its markdown by prefixing the path with `/llms.mdx` (this page is at `/llms.mdx/general/llms-txt`).
For a guided, hands-off setup, the [sbox-sdk skill](/general/skills) packages these instructions for coding agents.
---
# Overview (/general/overview)
sbox SDK is a unified TypeScript API for agent sandbox providers — one namespaced surface, capability gating, a single error taxonomy, and a typed escape hatch.
## What is sbox SDK? [#what-is-sbox-sdk]
A single TypeScript API for running code in ephemeral **sandboxes** that works the same way across [E2B](/adapters/e2b), [Vercel Sandbox](/adapters/vercel), [Cloudflare Sandbox](/adapters/cloudflare), [Daytona](/adapters/daytona), [Modal](/adapters/modal), [Fly Machines](/adapters/fly), [AWS Lambda MicroVMs](/adapters/aws-lambda), [Northflank](/adapters/northflank), [Runloop](/adapters/runloop), [CodeSandbox](/adapters/codesandbox), [Morph](/adapters/morph), [Blaxel](/adapters/blaxel), [Beam](/adapters/beam), and [Railway](/adapters/railway) — plus an in-memory provider for tests and local dev.
You hold a `Sandbox` and call into a handful of namespaces that cover the surface you actually use:
* `sandbox.commands` — run, spawn, stream, and kill processes.
* `sandbox.files` — read, write, list, and manage the filesystem.
* `sandbox.code` — run code in a stateful interpreter (where supported).
* `sandbox.ports` — expose a port and get a preview URL.
* `sandbox.snapshots` — checkpoint and restore filesystem state.
* `sandbox.network` — egress policy and SSH (where supported).
Lifecycle (`getInfo`, `pause`, `resume`, `stop`, `destroy`, `setTimeout`) lives on the sandbox itself. When you need a provider-specific feature outside this surface, drop down to the native client via [`sandbox.raw()`](/escape-hatch), which stays typed per adapter.
## Why sbox SDK? [#why-sbox-sdk]
Every sandbox provider ships its own shape: different process APIs, different filesystem primitives, different error envelopes, different preview-URL models. Switching providers — or supporting more than one — means rewriting the call sites and re-learning the failure model.
sbox SDK collapses that into one client and one error type:
* **One API** — the same `commands` / `files` / `code` calls across every adapter. The code that runs a command on E2B is the code that runs it on Vercel.
* **Capability gating** — each provider declares a static [capability table](/capabilities). Sub-APIs a provider can't do are typed `undefined` (a compile error), and unsupported features throw `NotSupportedError` *before* any network call.
* **Honest exec** — `commands.run()` **never throws on a non-zero exit**. The exit code is data. `await` the handle for a buffered result, or `for await` the same handle to stream output live.
* **One error taxonomy** — every provider normalizes to [`SandboxError`](/errors) with a small set of codes (`NotFound`, `Unauthorized`, `Timeout`, `NotSupported`, …) and a `retryable` flag.
* **Lazy SDK loading** — adapters are subpath exports (`sbox-sdk/e2b`, `sbox-sdk/vercel`, …) and each provider SDK is an optional peer dependency, imported lazily on first use. The SDK you don't import is never bundled.
* **Typed escape hatch** — `sandbox.raw()` is the underlying provider client, typed per adapter, so the unified surface never traps you.
## Next steps [#next-steps]
* [Installation](/installation) — install the SDK and your adapter's peer dependency.
* [Usage](/usage) — construct a client and run the core namespaces.
* [API reference](/api/client) — the full client, sandbox, and namespace surface.
* [Adapters](/adapters/memory) — per-provider setup, options, and capability tables.
---
# Retries & fallback (/general/retries)
The client retries retryable failures with backoff and can fall back across providers — guarded by an idempotency key so a retry never orphans a second sandbox.
`createSandboxClient` owns retry and provider fallback. Both are driven by the [`retryable`](/errors) flag on `SandboxError`.
## Retry policy [#retry-policy]
By default the client retries up to twice (3 attempts total) on retryable errors, with exponential backoff. Override per-client:
```ts
const client = createSandboxClient({
provider: e2b({ apiKey }),
retry: {
retries: 4,
delayMs: (attempt) => attempt * 250,
shouldRetry: (err) => isRetryableError(err),
},
});
```
## Provider fallback [#provider-fallback]
Supply a `fallback` chain and the client will try the next provider when one fails:
```ts
const client = createSandboxClient({
provider: e2b({ apiKey }),
fallback: [vercel({ token, teamId, projectId })],
});
```
Fallback engages **only when you pass an `idempotencyKey`** on the create spec. This is deliberate: without a stable key, a create that failed *after* spawning a VM could leave an orphan when the client moves to the next provider. The key makes the operation safe to re-attempt.
```ts
const sandbox = await client.create({
idempotencyKey: "build-job-42",
});
```
If every provider in the chain fails, the client throws `AllProvidersFailedError` with the per-provider `attempts[]`.
## Hooks [#hooks]
Observe the lifecycle with hooks — handy for logging which provider served a request, or recording retry attempts:
```ts
const client = createSandboxClient({
provider: e2b({ apiKey }),
hooks: {
beforeCreate: (spec) => {
/* ... */
},
afterCreate: ({ id, provider }) => {
/* ... */
},
onError: (err, attempt) => {
/* ... */
},
},
});
```
---
# Skills (/general/skills)
Install the sbox-sdk agent skill so coding agents (Claude Code, Cursor, Codex, Copilot) can set up and use sbox correctly — or paste the setup prompt directly.
## The sbox-sdk skill [#the-sbox-sdk-skill]
`sbox-sdk` ships an **agent skill** — a `SKILL.md` that teaches a coding agent how to install, configure, and use the SDK correctly: picking an adapter, creating a sandbox client, running commands and files, capability gating, the common gotchas, and exposing sandbox tools to an agent framework. It follows the open [skills.sh](https://skills.sh) format and works with Claude Code, Cursor, GitHub Copilot, Gemini, and more.
The skill lives in the repo at [`skills/sbox-sdk/SKILL.md`](https://github.com/vivek-k3/sbox-sdk/blob/main/skills/sbox-sdk/SKILL.md).
## Install [#install]
The quickest path is the [skills.sh](https://skills.sh) CLI, which installs into whichever agent you use:
```bash
npx skills add vivek-k3/sbox-sdk
```
Or wire it up manually for your agent:
Drop the skill into your project (or `~/.claude/skills` for global use):
```bash
mkdir -p .claude/skills/sbox-sdk
curl -o .claude/skills/sbox-sdk/SKILL.md \
https://raw.githubusercontent.com/vivek-k3/sbox-sdk/main/skills/sbox-sdk/SKILL.md
```
Claude Code loads it automatically and activates it when a task involves sandboxes or running code.
Save the `SKILL.md` body as a project rule at `.cursor/rules/sbox-sdk.md` (or paste it into **Settings → Rules**), so the setup and usage instructions are in the agent's context.
Any agent with skills or custom-instruction support works — install via `npx skills add vivek-k3/sbox-sdk`, or paste the `SKILL.md` contents into the agent's rules / system prompt.
## Or just paste a prompt [#or-just-paste-a-prompt]
No skill support? Paste this into your agent to set sbox up in the current project:
```text
Set up sbox-sdk (https://sbox-sdk.vercel.app) in this project.
1. Install it with the E2B adapter: npm install sbox-sdk @e2b/code-interpreter
2. Create a client and a sandbox:
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
const client = createSandboxClient({ provider: e2b({ apiKey: process.env.E2B_API_KEY! }) });
const sandbox = await client.create({ template: "python-3.12", ttlMs: 60_000 });
3. Use sandbox.commands.run(cmd) (it never throws on a non-zero exit — read exitCode),
sandbox.files.{read,write,list}, and sandbox.code?.runCode(...) where supported.
Capability-gated namespaces (code, ports, snapshots, network) are `undefined` when
unsupported — guard with `?.` or sandbox.can(cap). Always set a TTL and call
sandbox.destroy() when finished.
For exact APIs, fetch https://sbox-sdk.vercel.app/llms.txt and the relevant /llms.mdx/... pages.
```
To give an agent framework sandbox tools instead of writing glue code, see [AI agent tools](/ai/overview).
## What the skill covers [#what-the-skill-covers]
* Installing the SDK and the right adapter peer dependency.
* Creating a client and sandbox; the core `commands` / `files` / `code` / `ports` API.
* The gotchas agents get wrong: [capability gating](/general/capabilities), `commands.run` not throwing on a non-zero exit, optional peer deps, TTLs, and cleanup.
* Exposing sandbox tools to an agent via the [`ai()` plugin](/ai/providers).
* Where to fetch exact signatures — [`/llms.txt`](/general/llms-txt) and per-page markdown.
---
# Anonymous usage data (/general/telemetry)
sbox SDK collects limited anonymous usage data to understand adoption and improve the SDK.
## What we collect [#what-we-collect]
sbox SDK collects limited anonymous usage data by default. We use it to understand adoption, see which parts of the SDK are used, and prioritize improvements.
The data is intentionally coarse:
* SDK version
* runtime and platform
* selected provider
* operation type, such as creating a sandbox or running a command
* success or failure
* duration bucket
* error category
* command exit code
Usage data may be sent from any environment where the SDK runs, including local development, CI, tests, previews, and production.
This is separate from any analytics you use in your own app. sbox SDK does not read, modify, identify, or share state with your app's analytics client.
## What we never collect [#what-we-never-collect]
sbox SDK does not collect:
* API keys or credentials
* prompts or model inputs
* command strings
* file paths or file contents
* environment variables
* stdout or stderr
* sandbox ids
* project names
* metadata values
* provider-specific request payloads
## How to opt out [#how-to-opt-out]
Set any of these environment variables:
```bash
SBOX_TELEMETRY_DISABLED=1
SBOX_DISABLE_TELEMETRY=1
SBOX_TELEMETRY=0
DO_NOT_TRACK=1
```
Or disable usage data for one client:
```ts
import { createSandboxClient } from "sbox-sdk";
const client = createSandboxClient({
telemetry: false,
});
```
Usage data is not automatically disabled in CI, tests, local development, previews, or production. Use one of the opt-out controls above when you do not want it sent.
---
# Templates (/general/templates)
The build-time base image for a sandbox. A single opaque string on SandboxSpec, mapped per-adapter to that provider's nearest concept.
`template` is the one field on `SandboxSpec` whose meaning is **deliberately provider-specific** — the build-time base your sandbox boots from.
```ts
const sandbox = await client.create({ template: "python-3.12" });
```
It's an optional, opaque `string`. The core never inspects it — it forwards the spec to the adapter, which maps `template` to that provider's nearest concept.
## How each adapter maps it [#how-each-adapter-maps-it]
| Adapter | `template` maps to | If omitted |
| -------------- | -------------------------------------------------------------------------------------------------- | ------------------------ |
| **E2B** | The E2B template id / alias (e.g. `"python-3.12"`, or a custom-built template) | E2B's default base |
| **Vercel** | The `runtime` selector (e.g. `"node22"`) — not an arbitrary image | Vercel's default runtime |
| **Cloudflare** | **Ignored** — the image is baked into the Worker's container config at deploy time, not per-create | n/a |
| **memory** | **Ignored** — there are no images in-memory | n/a |
## Not portable across providers [#not-portable-across-providers]
Because the value means different things to different providers, a `template` string is **not portable**. `{ template: "python-3.12" }` is valid on E2B, would be the wrong value on Vercel (which wants a runtime id), and is silently ignored on Cloudflare and memory.
This is the one documented break in the swap-one-import promise for `create()` — picking a base image is inherently provider-specific, so the SDK surfaces it honestly rather than pretending a universal catalog exists.
## Templates vs snapshots [#templates-vs-snapshots]
`template` is the *build-time* base set once at create. [Snapshots](/api/snapshots) are *runtime* checkpoints you take and restore on a live sandbox. They're separate surfaces, even where a provider implements them with the same underlying mechanism.
---
# Usage (/general/usage)
Construct a client, create a sandbox, and run the core namespaces — commands, files, code, ports, and lifecycle.
## Construct a client [#construct-a-client]
`createSandboxClient()` is the single entry point. Pass a provider adapter, or call it with no arguments for the zero-config in-memory provider — great for tests.
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
});
const sandbox = await client.create({ template: "python-3.12", ttlMs: 60_000 });
```
```ts
// Zero config — defaults to the in-memory provider:
import { createSandboxClient } from "sbox-sdk";
const sandbox = await createSandboxClient().create();
await sandbox.commands.run("echo works offline");
```
## Run commands [#run-commands]
`commands.run()` returns an `ExecHandle` that is both a `Promise` and an `AsyncIterable`. `await` it to buffer, or `for await` the same handle to stream.
```ts
// Buffered — never throws on a non-zero exit; the exit code is data:
const res = await sandbox.commands.run("npm test", { cwd: "/app" });
console.log(res.exitCode, res.stdout, res.stderr);
// Streamed — the SAME handle, iterated live:
for await (const ev of sandbox.commands.run(["python", "train.py"])) {
if (ev.type === "stdout") process.stdout.write(ev.data);
if (ev.type === "exit") console.log("done", ev.exitCode);
}
```
See the [commands reference](/api/commands) for `spawn`, `connect`, `kill`, and `list`.
## Read and write files [#read-and-write-files]
Filesystem bodies are web-standard (`string | Uint8Array | ReadableStream`) in, and a `StoredFile` out.
```ts
await sandbox.files.write("/app/data.json", JSON.stringify({ ok: true }));
const file = await sandbox.files.read("/app/data.json");
const text = await file.text();
await sandbox.files.mkdir("/app/out", { recursive: true });
const entries = await sandbox.files.list("/app");
const there = await sandbox.files.exists("/app/data.json");
```
See the [files reference](/api/files) for the full method surface.
## Capability-gated namespaces [#capability-gated-namespaces]
`code`, `ports`, `snapshots`, and `network` only exist on providers that support them — they're typed `undefined` otherwise, so guard before use.
```ts
if (sandbox.code) {
const exec = await sandbox.code.runCode("print(1 + 1)");
console.log(exec.logs.stdout);
}
if (sandbox.ports) {
const preview = await sandbox.ports.expose(3000);
console.log(preview.url);
}
```
For dynamically-chosen providers, `sandbox.can("snapshot")` narrows at both runtime and the type level:
```ts
if (sandbox.can("snapshot")) {
await sandbox.snapshots.create({ name: "ckpt" });
}
```
## Lifecycle and cleanup [#lifecycle-and-cleanup]
```ts
await sandbox.pause();
await sandbox.resume();
await sandbox.setTimeout(120_000);
await sandbox.destroy();
await client.dispose();
```
Next: read the [capability model](/capabilities), the [error taxonomy](/errors), or the full [API reference](/api/client).
---
# Writing a plugin (/plugins/authoring)
Build a custom plugin with the SandboxPlugin interface — graft typed properties onto the sandbox, run setup on create, and clean up on destroy.
## The interface [#the-interface]
A plugin is a plain object implementing `SandboxPlugin`, imported from `sbox-sdk`. Give it a `name`, then implement any of `extend`, `onCreate`, and `onDestroy`.
```ts
import type { SandboxPlugin } from "sbox-sdk";
```
## A lifecycle plugin: seed every sandbox [#a-lifecycle-plugin-seed-every-sandbox]
`onCreate` runs once the sandbox is built and is awaited before `create()` resolves — ideal for bootstrapping. This plugin writes a set of files into every sandbox:
```ts
import type { SandboxPlugin } from "sbox-sdk";
export function seedFiles(files: Record): SandboxPlugin {
return {
name: "seed-files",
kind: "lifecycle",
onCreate: async (sandbox) => {
for (const [path, content] of Object.entries(files)) {
await sandbox.files.write(path, content);
}
},
};
}
```
```ts
const client = createSandboxClient({
provider: e2b(),
plugins: [
seedFiles({
"/app/package.json": "{}",
"/app/.env": "NODE_ENV=development",
}),
],
});
const sandbox = await client.create(); // both files already exist
```
## An `extend` plugin: graft a typed property [#an-extend-plugin-graft-a-typed-property]
`extend` returns properties merged onto the sandbox — synchronously, so they're available immediately. Type the contribution with the `Ext` parameter and it shows up on the sandbox type. This is exactly how the [AI-provider plugins](/ai/providers) add `sandbox.tools`:
```ts
import type { SandboxPlugin } from "sbox-sdk";
function withMeta(meta: {
project: string;
}): SandboxPlugin<{ meta: typeof meta }> {
return {
name: "with-meta",
extend: () => ({ meta }),
};
}
const client = createSandboxClient({
provider: e2b(),
plugins: [withMeta({ project: "demo" })],
});
const sandbox = await client.create();
sandbox.meta.project; // typed as string
```
## Cleanup with `onDestroy` [#cleanup-with-ondestroy]
`onDestroy` runs before the sandbox is torn down — flush logs, close connections, stop anything `onCreate` started:
```ts
function auditLog(sink: (line: string) => void): SandboxPlugin {
return {
name: "audit-log",
kind: "middleware",
onCreate: (sandbox) => sink(`created ${sandbox.id}`),
onDestroy: (sandbox) => sink(`destroyed ${sandbox.id}`),
};
}
```
## Notes [#notes]
* **Hooks are optional** — a plugin can implement just one. `extend` is synchronous; `onCreate` / `onDestroy` may be async and are awaited.
* **`kind` is a hint** — set it to the category your plugin fits. The client allows at most one `ai-provider` plugin; every other kind stacks freely.
* **Plugins apply everywhere** — every sandbox from `create()`, `connect()`, and `fork()` runs your plugin's `extend`.
* **Per-`create` options** — the second argument to `client.create(spec, options)` reaches your hooks as `ctx.createOptions`, so a plugin can read per-sandbox configuration.
---
# Overview (/plugins/overview)
Plugins extend every sandbox a client builds — graft properties onto the sandbox, hook its lifecycle, and add cross-cutting behavior. AI tool providers are the first plugin kind.
## What is a plugin? [#what-is-a-plugin]
A plugin augments every sandbox a client builds. You pass plugins to `createSandboxClient`, and they apply to every sandbox from `create()` and `connect()` — including [forked](/api/snapshots) children.
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { aiSdk } from "sbox-sdk/ai-sdk";
const client = createSandboxClient({
provider: e2b({ apiKey: process.env.E2B_API_KEY! }),
plugins: [ai({ framework: aiSdk() })], // an AI-provider plugin
});
```
The first plugin most people reach for is an **[AI-provider plugin](/ai/providers)**, which grafts a framework-shaped `sandbox.tools` onto the sandbox. But the plugin system is general — middleware, bootstrap, and integration plugins all use the same interface.
## The plugin interface [#the-plugin-interface]
A plugin is a small object. Every hook is optional — implement only what you need:
```ts
import type { SandboxPlugin } from "sbox-sdk";
interface SandboxPlugin {
readonly name: string;
/** Discriminant; the client allows at most one "ai-provider" plugin. */
readonly kind?: "ai-provider" | "middleware" | "lifecycle" | "mcp";
/** Synchronous: returns properties merged onto the sandbox object. */
extend?(sandbox: Sandbox, ctx: PluginSetupContext): Ext;
/** Async side-effects, awaited after the sandbox is built. */
onCreate?(
sandbox: Sandbox & Ext,
ctx: PluginSetupContext
): void | Promise;
/** Runs before the sandbox is destroyed. */
onDestroy?(sandbox: Sandbox): void | Promise;
}
```
* **`extend`** grafts typed properties onto the sandbox — this is how the [`ai()`](/ai/providers) plugin adds `sandbox.tools`. It is synchronous, so the sandbox is ready to use the moment `create()` resolves.
* **`onCreate`** runs async side-effects once the sandbox exists — seed files, install dependencies, start a dev server.
* **`onDestroy`** runs cleanup before the sandbox is torn down — flush an audit log, stop a server.
The properties a plugin contributes via `extend` are **typed**: pass `SandboxPlugin<{ tools: ... }>` and `sandbox.tools` shows up on the sandbox type, with no `any`.
## Plugin kinds [#plugin-kinds]
The `kind` field groups plugins by what they do. Today the AI-provider kind ships; the others are reserved and on the roadmap — see the [catalog](/plugins/catalog).
| Kind | Purpose | Status |
| ------------- | ------------------------------------------------------ | --------- |
| `ai-provider` | Shape `sandbox.tools` for an agent framework | Available |
| `middleware` | Wrap tool / command calls — audit, redaction, metrics | Planned |
| `lifecycle` | Bootstrap a sandbox on create — seed, install, warm up | Planned |
| `mcp` | Expose the sandbox over the Model Context Protocol | Planned |
The client enforces **at most one `ai-provider` plugin** (an agent runs in one framework, so `sandbox.tools` has one shape). Plugins of other kinds stack freely.
## Next steps [#next-steps]
* [Catalog](/plugins/catalog) — every available plugin, and what's planned.
* [Writing a plugin](/plugins/authoring) — build your own with the `SandboxPlugin` interface.
* [AI providers](/ai/providers) — the first plugin kind, in depth.
---
# Capability graph (/general/capabilities/graph)
One static table per adapter drives compile-time gating, a runtime read-model, and fail-fast errors. Every capability, every provider — declared.
The artifact
One static table per adapter drives compile-time gating, a runtime read-model, and fail-fast errors. It's queryable in your code — and here it is in full.
---
# Capabilities (/general/capabilities)
Each provider declares a static capability table. It drives compile-time type gating, a runtime read-model, and fail-fast errors before any network call.
The unified surface is the common shape every adapter implements, but providers differ at the edges: some have a code interpreter, some can pause, some expose ports natively, some only emulate it. sbox SDK makes that difference **declarative and queryable** instead of something you discover by catching an error.
Each provider declares a static `CapabilityMap` — one of three levels per feature:
* `native` — first-class support.
* `emulated` — supported, but polyfilled by the core (e.g. exec-based filesystem ops).
* `unsupported` — not available; the sub-API is typed `undefined` and throws before any network call.
## Three layers from one table [#three-layers-from-one-table]
A single declaration drives all three of these:
### 1. Compile-time gating [#1-compile-time-gating]
Sub-APIs gated on an `unsupported` capability are typed `undefined`. Calling them is a **compile error**, so whole classes of "this provider can't do that" never reach runtime.
```ts
// `code` only exists on providers with a code interpreter (e.g. E2B):
if (sandbox.code) {
const exec = await sandbox.code.runCode("import numpy");
}
```
### 2. Runtime read-model [#2-runtime-read-model]
Every sandbox and client exposes a frozen `capabilities` read-model:
```ts
sandbox.capabilities.map.snapshot; // "native" | "emulated" | "unsupported"
sandbox.capabilities.flags.previewModel; // "subdomain" | "declaredPorts" | ...
```
### 3. Fail-fast enforcement [#3-fail-fast-enforcement]
Unsupported features throw `NotSupportedError` **synchronously, before any network call** — no wasted round trip, no ambiguous provider error.
## Dynamic narrowing with `can()` [#dynamic-narrowing-with-can]
For code that picks a provider at runtime, `sandbox.can(cap)` is a type guard that narrows at both the runtime and type level:
```ts
if (sandbox.can("snapshot")) {
// `sandbox.snapshots` is now non-undefined here.
await sandbox.snapshots.create({ name: "ckpt" });
}
```
## The capability map [#the-capability-map]
The static table covers 25 capabilities across lifecycle, exec, filesystem, code, network, and snapshots:
| Group | Capabilities |
| -------------- | ---------------------------------------------------------------------- |
| **lifecycle** | `list`, `stop`, `pause`, `setTimeout` |
| **exec** | `background`, `streaming`, `killProcess`, `pty`, `stdin` |
| **filesystem** | `filesWatch`, `filesUpload` |
| **code** | `codeInterpreter`, `statefulKernel` |
| **network** | `exposePort`, `privatePreview`, `egressControl`, `ssh`, `proxiedFetch` |
| **snapshot** | `snapshot`, `fork`, `volumes` |
| **meta** | `gpu`, `region`, `secretsVault`, `metrics` |
## Behavioral flags [#behavioral-flags]
Some differences aren't present/absent — they change semantics. These are `capabilities.flags`:
| Flag | Meaning |
| ------------------------ | --------------------------------------------------------------------------------------- |
| `preservesMemoryOnPause` | RAM survives a pause |
| `preservesDiskOnStop` | Disk survives a stop |
| `perCommandEnvCwd` | If `false`, the core wraps exec in `sh -c 'cd … && KEY=v …'` |
| `exitCodeNative` | If `false`, the core synthesizes the exit code via a `$?` echo |
| `previewModel` | How exposed-port URLs are formed (`subdomain`, `declaredPorts`, `wildcardDNS`, `ip`, …) |
## Per-provider matrix [#per-provider-matrix]
For the full `native` / `emulated` / `unsupported` breakdown across every adapter, see the interactive [capability graph](/general/capabilities/graph) — or each adapter's **Capabilities** section: [memory](/adapters/memory), [E2B](/adapters/e2b), [Vercel](/adapters/vercel), [Cloudflare](/adapters/cloudflare), [Daytona](/adapters/daytona), [Modal](/adapters/modal), [Fly](/adapters/fly), [AWS Lambda](/adapters/aws-lambda), [Northflank](/adapters/northflank), [Runloop](/adapters/runloop), [CodeSandbox](/adapters/codesandbox), [Morph](/adapters/morph), [Blaxel](/adapters/blaxel), [Beam](/adapters/beam), [Railway](/adapters/railway).
---
# Catalog (/plugins/catalog)
Every plugin that ships with sbox SDK, and what's on the roadmap. AI tool providers are available today; middleware, lifecycle, and MCP plugins are next.
## Available [#available]
### AI providers [#ai-providers]
The first plugin kind. The single [`ai()`](/ai/providers) plugin grafts a framework-shaped `sandbox.tools` onto the sandbox; you pick the framework by passing its adapter. See the **AI** group in the sidebar for each framework, or [AI providers](/ai/providers) for the shared behavior.
| Framework | Adapter | Import | `sandbox.tools` |
| ----------------------------------- | ---------------- | -------------------- | --------------- |
| [Vercel AI SDK](/ai/ai-sdk) | `aiSdk()` | `sbox-sdk/ai-sdk` | tool map |
| [Mastra](/ai/mastra) | `mastra()` | `sbox-sdk/mastra` | tool map |
| [OpenAI Agents](/ai/openai) | `openaiAgents()` | `sbox-sdk/openai` | tool array |
| [Anthropic / Claude](/ai/anthropic) | `anthropic()` | `sbox-sdk/anthropic` | runnable tools |
| [LangChain](/ai/langchain) | `langchain()` | `sbox-sdk/langchain` | tool array |
```ts
import { createSandboxClient } from "sbox-sdk";
import { e2b } from "sbox-sdk/e2b";
import { ai } from "sbox-sdk/ai";
import { aiSdk } from "sbox-sdk/ai-sdk";
const client = createSandboxClient({
provider: e2b(),
plugins: [ai({ framework: aiSdk() })],
});
```
## Coming soon [#coming-soon]
The plugin interface reserves these kinds. They aren't shipped yet — they're where the system is headed, and you can build any of them today with [a custom plugin](/plugins/authoring).
| Plugin | Kind | What it will do |
| ----------------------------------------- | ------------ | ---------------------------------------------------------------------- |
| [Middleware](/plugins/catalog/middleware) | `middleware` | Wrap tool and command calls — audit, redaction, metrics. |
| [Lifecycle](/plugins/catalog/lifecycle) | `lifecycle` | Bootstrap a sandbox on `create()` — seed files, install deps, warm up. |
| [MCP server](/plugins/catalog/mcp) | `mcp` | Expose the sandbox's tools over the Model Context Protocol. |
Have a plugin idea? [Open an issue](https://github.com/vivek-k3/sbox-sdk/issues), or write it yourself — the interface is small and stable.
---
# Lifecycle (coming soon) (/plugins/catalog/lifecycle)
Lifecycle plugins will bootstrap a sandbox on create — seed files, install dependencies, warm a dev server. Planned — build your own today.
Planned — not shipped yet.
Lifecycle plugins (`kind: "lifecycle"`) will bootstrap each sandbox when it's created — seed files, install dependencies, or warm up a dev server, so the sandbox is ready the moment `create()` resolves.
The plugin interface already supports this via the `onCreate` hook, so you can build one today — see [Writing a plugin](/plugins/authoring), which includes a `seedFiles` example.
---
# MCP server (coming soon) (/plugins/catalog/mcp)
An MCP plugin will expose the sandbox's tools over the Model Context Protocol for any MCP client. Planned.
Planned — not shipped yet.
An MCP plugin (`kind: "mcp"`) will expose the sandbox's [tools](/ai/tools) over the [Model Context Protocol](https://modelcontextprotocol.io), so any MCP client — Claude Desktop, Cursor, VS Code — can drive the sandbox.
Because the tools come from the same provider-neutral registry the AI providers use, the MCP server projects from exactly the same source. Until it ships, you can reach the tools directly with [`createSandboxTools(sandbox)`](/ai/tools).
---
# Middleware (coming soon) (/plugins/catalog/middleware)
Middleware plugins will wrap tool and command calls for audit, redaction, and metrics. Planned — build your own today.
Planned — not shipped yet.
Middleware plugins (`kind: "middleware"`) will wrap tool and command calls to add cross-cutting behavior without touching the tools themselves:
* **Audit** — record every call, with its [approval decision](/ai/approval), to your logger or store.
* **Redaction** — strip secrets from tool inputs and outputs before they reach the model.
* **Metrics** — emit timing and usage counters per tool.
You can build any of these today — see [Writing a plugin](/plugins/authoring).