diff --git a/CLAUDE.md b/CLAUDE.md
index d7e091b..c651fb1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -74,6 +74,7 @@
- `examples/docker/src/index.ts`
- `examples/e2b/src/index.ts`
- `examples/vercel/src/index.ts`
+ - `sdks/typescript/src/providers/shared.ts`
- `scripts/release/main.ts`
- `scripts/release/promote-artifacts.ts`
- `scripts/release/sdk.ts`
diff --git a/docs/agent-capabilities.mdx b/docs/agent-capabilities.mdx
index 13f2723..acb582c 100644
--- a/docs/agent-capabilities.mdx
+++ b/docs/agent-capabilities.mdx
@@ -37,7 +37,7 @@ await writeFile(
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468" });
await sdk.createSession({
agent: "claude",
- sessionInit: { cwd, mcpServers: [] },
+ cwd,
});
```
diff --git a/docs/agent-sessions.mdx b/docs/agent-sessions.mdx
index cf56e9c..0f9e2ab 100644
--- a/docs/agent-sessions.mdx
+++ b/docs/agent-sessions.mdx
@@ -21,10 +21,7 @@ const sdk = await SandboxAgent.connect({
const session = await sdk.createSession({
agent: "codex",
- sessionInit: {
- cwd: "/",
- mcpServers: [],
- },
+ cwd: "/",
});
console.log(session.id, session.agentSessionId);
diff --git a/docs/architecture.mdx b/docs/architecture.mdx
index 78585a2..36e0302 100644
--- a/docs/architecture.mdx
+++ b/docs/architecture.mdx
@@ -1,64 +1,59 @@
---
title: "Architecture"
-description: "How the client, sandbox, server, and agent fit together."
-icon: "microchip"
+description: "How the Sandbox Agent server, SDK, and agent processes fit together."
---
-Sandbox Agent runs as an HTTP server inside your sandbox. Your app talks to it remotely.
+Sandbox Agent is a lightweight HTTP server that runs **inside** a sandbox. It:
+
+- **Agent management**: Installs, spawns, and stops coding agent processes
+- **Sessions**: Routes prompts to agents and streams events back in real time
+- **Sandbox APIs**: Filesystem, process, and terminal access for the sandbox environment
## Components
-- `Your client`: your app code using the `sandbox-agent` SDK.
-- `Sandbox`: isolated runtime (E2B, Daytona, Docker, etc.).
-- `Sandbox Agent server`: process inside the sandbox exposing HTTP transport.
-- `Agent`: Claude/Codex/OpenCode/Amp process managed by Sandbox Agent.
-
-```mermaid placement="top-right"
- flowchart LR
- CLIENT["Sandbox Agent SDK"]
- SERVER["Sandbox Agent server"]
- AGENT["Agent process"]
+```mermaid
+flowchart LR
+ CLIENT["Your App"]
subgraph SANDBOX["Sandbox"]
- direction TB
- SERVER --> AGENT
+ direction TB
+ SERVER["Sandbox Agent Server"]
+ AGENT["Agent Process
(Claude, Codex, etc.)"]
+ SERVER --> AGENT
end
- CLIENT -->|HTTP| SERVER
+ CLIENT -->|"SDK (HTTP)"| SERVER
```
-## Suggested Topology
+- **Your app**: Uses the `sandbox-agent` TypeScript SDK to talk to the server over HTTP.
+- **Sandbox**: An isolated runtime (local process, Docker, E2B, Daytona, Vercel, Cloudflare).
+- **Sandbox Agent server**: A single binary inside the sandbox that manages agent lifecycles, routes prompts, streams events, and exposes filesystem/process/terminal APIs.
+- **Agent process**: A coding agent (Claude Code, Codex, etc.) spawned by the server. Each session maps to one agent process.
-Run the SDK on your backend, then call it from your frontend.
+## What `SandboxAgent.start()` does
-This extra hop is recommended because it keeps auth/token logic on the backend and makes persistence simpler.
+1. **Provision**: The provider creates a sandbox (starts a container, creates a VM, etc.)
+2. **Install**: The Sandbox Agent binary is installed inside the sandbox
+3. **Boot**: The server starts listening on an HTTP port
+4. **Health check**: The SDK waits for `/v1/health` to respond
+5. **Ready**: The SDK returns a connected client
-```mermaid placement="top-right"
- flowchart LR
- BROWSER["Browser"]
- subgraph BACKEND["Your backend"]
- direction TB
- SDK["Sandbox Agent SDK"]
- end
- subgraph SANDBOX_SIMPLE["Sandbox"]
- SERVER_SIMPLE["Sandbox Agent server"]
- end
+For the `local` provider, provisioning is a no-op and the server runs as a local subprocess.
- BROWSER --> BACKEND
- BACKEND --> SDK --> SERVER_SIMPLE
+## Server endpoints
+
+See the [HTTP API reference](/api-reference) for the full list of server endpoints.
+
+## Agent installation
+
+Agents are installed lazily on first use. To avoid the cold-start delay, pre-install them:
+
+```bash
+sandbox-agent install-agent --all
```
-### Backend requirements
+The `rivetdev/sandbox-agent:0.3.2-full` Docker image ships with all agents pre-installed.
-Your backend layer needs to handle:
+## Production topology
-- **Long-running connections**: prompts can take minutes.
-- **Session affinity**: follow-up messages must reach the same session.
-- **State between requests**: session metadata and event history must persist across requests.
-- **Graceful recovery**: sessions should resume after backend restarts.
-
-We recommend [Rivet](https://rivet.dev) over serverless because actors natively support the long-lived connections, session routing, and state persistence that agent workloads require.
-
-## Session persistence
-
-For storage driver options and replay behavior, see [Persisting Sessions](/session-persistence).
+For production deployments, see [Orchestration Architecture](/orchestration-architecture) for recommended topology, backend requirements, and session persistence patterns.
diff --git a/docs/custom-tools.mdx b/docs/custom-tools.mdx
index 727fb02..2fb3e15 100644
--- a/docs/custom-tools.mdx
+++ b/docs/custom-tools.mdx
@@ -80,9 +80,7 @@ await sdk.setMcpConfig(
const session = await sdk.createSession({
agent: "claude",
- sessionInit: {
- cwd: "/workspace",
- },
+ cwd: "/workspace",
});
await session.prompt([
@@ -145,9 +143,7 @@ await sdk.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skill);
```ts
const session = await sdk.createSession({
agent: "claude",
- sessionInit: {
- cwd: "/workspace",
- },
+ cwd: "/workspace",
});
await session.prompt([
diff --git a/docs/deploy/cloudflare.mdx b/docs/deploy/cloudflare.mdx
index deca490..1cecdd7 100644
--- a/docs/deploy/cloudflare.mdx
+++ b/docs/deploy/cloudflare.mdx
@@ -31,7 +31,38 @@ RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex
EXPOSE 8000
```
-## TypeScript example
+## TypeScript example (with provider)
+
+For standalone scripts, use the `cloudflare` provider:
+
+```bash
+npm install sandbox-agent@0.3.x @cloudflare/sandbox
+```
+
+```typescript
+import { SandboxAgent } from "sandbox-agent";
+import { cloudflare } from "sandbox-agent/cloudflare";
+
+const sdk = await SandboxAgent.start({
+ sandbox: cloudflare(),
+});
+
+try {
+ const session = await sdk.createSession({ agent: "codex" });
+ const response = await session.prompt([
+ { type: "text", text: "Summarize this repository" },
+ ]);
+ console.log(response.stopReason);
+} finally {
+ await sdk.destroySandbox();
+}
+```
+
+The `cloudflare` provider uses `containerFetch` under the hood, automatically stripping `AbortSignal` to avoid dropped streaming updates.
+
+## TypeScript example (Durable Objects)
+
+For Workers with Durable Objects, use `SandboxAgent.connect(...)` with a custom `fetch` backed by `sandbox.containerFetch(...)`:
```typescript
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
@@ -109,7 +140,6 @@ app.all("*", (c) => c.env.ASSETS.fetch(c.req.raw));
export default app;
```
-Create the SDK client inside the Worker using custom `fetch` backed by `sandbox.containerFetch(...)`.
This keeps all Sandbox Agent calls inside the Cloudflare sandbox routing path and does not require a `baseUrl`.
## Troubleshooting streaming updates
diff --git a/docs/deploy/daytona.mdx b/docs/deploy/daytona.mdx
index 5eb8f5d..b65aec9 100644
--- a/docs/deploy/daytona.mdx
+++ b/docs/deploy/daytona.mdx
@@ -15,40 +15,37 @@ See [Daytona network limits](https://www.daytona.io/docs/en/network-limits/).
## TypeScript example
-```typescript
-import { Daytona } from "@daytonaio/sdk";
-import { SandboxAgent } from "sandbox-agent";
+```bash
+npm install sandbox-agent@0.3.x @daytonaio/sdk
+```
-const daytona = new Daytona();
+```typescript
+import { SandboxAgent } from "sandbox-agent";
+import { daytona } from "sandbox-agent/daytona";
const envVars: Record = {};
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-const sandbox = await daytona.create({ envVars });
+const sdk = await SandboxAgent.start({
+ sandbox: daytona({
+ create: { envVars },
+ }),
+});
-await sandbox.process.executeCommand(
- "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"
-);
-
-await sandbox.process.executeCommand("sandbox-agent install-agent claude");
-await sandbox.process.executeCommand("sandbox-agent install-agent codex");
-
-await sandbox.process.executeCommand(
- "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"
-);
-
-await new Promise((r) => setTimeout(r, 2000));
-
-const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
-const sdk = await SandboxAgent.connect({ baseUrl });
-
-const session = await sdk.createSession({ agent: "claude" });
-await session.prompt([{ type: "text", text: "Summarize this repository" }]);
-
-await sandbox.delete();
+try {
+ const session = await sdk.createSession({ agent: "claude" });
+ const response = await session.prompt([
+ { type: "text", text: "Summarize this repository" },
+ ]);
+ console.log(response.stopReason);
+} finally {
+ await sdk.destroySandbox();
+}
```
+The `daytona` provider uses the `rivetdev/sandbox-agent:0.3.2-full` image by default and starts the server automatically.
+
## Using snapshots for faster startup
```typescript
diff --git a/docs/deploy/docker.mdx b/docs/deploy/docker.mdx
index 030ddc9..b674b7a 100644
--- a/docs/deploy/docker.mdx
+++ b/docs/deploy/docker.mdx
@@ -15,43 +15,43 @@ Run the published full image with all supported agents pre-installed:
docker run --rm -p 3000:3000 \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
- rivetdev/sandbox-agent:0.3.1-full \
+ rivetdev/sandbox-agent:0.3.2-full \
server --no-token --host 0.0.0.0 --port 3000
```
-The `0.3.1-full` tag pins the exact version. The moving `full` tag is also published for contributors who want the latest full image.
+The `0.3.2-full` tag pins the exact version. The moving `full` tag is also published for contributors who want the latest full image.
-## TypeScript with dockerode
+## TypeScript with the Docker provider
+
+```bash
+npm install sandbox-agent@0.3.x dockerode get-port
+```
```typescript
-import Docker from "dockerode";
import { SandboxAgent } from "sandbox-agent";
+import { docker } from "sandbox-agent/docker";
-const docker = new Docker();
-const PORT = 3000;
-
-const container = await docker.createContainer({
- Image: "rivetdev/sandbox-agent:0.3.1-full",
- Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`],
- Env: [
- `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
- `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`,
- `CODEX_API_KEY=${process.env.CODEX_API_KEY}`,
- ].filter(Boolean),
- ExposedPorts: { [`${PORT}/tcp`]: {} },
- HostConfig: {
- AutoRemove: true,
- PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] },
- },
+const sdk = await SandboxAgent.start({
+ sandbox: docker({
+ env: [
+ `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
+ `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`,
+ ].filter(Boolean),
+ }),
});
-await container.start();
+try {
+ const session = await sdk.createSession({ agent: "codex" });
+ await session.prompt([{ type: "text", text: "Summarize this repository." }]);
+} finally {
+ await sdk.destroySandbox();
+}
+```
-const baseUrl = `http://127.0.0.1:${PORT}`;
-const sdk = await SandboxAgent.connect({ baseUrl });
+The `docker` provider uses the `rivetdev/sandbox-agent:0.3.2-full` image by default. Override with `image`:
-const session = await sdk.createSession({ agent: "codex" });
-await session.prompt([{ type: "text", text: "Summarize this repository." }]);
+```typescript
+docker({ image: "my-custom-image:latest" })
```
## Building a custom image with everything preinstalled
diff --git a/docs/deploy/e2b.mdx b/docs/deploy/e2b.mdx
index 8ea4c74..4e056ee 100644
--- a/docs/deploy/e2b.mdx
+++ b/docs/deploy/e2b.mdx
@@ -10,42 +10,37 @@ description: "Deploy Sandbox Agent inside an E2B sandbox."
## TypeScript example
+```bash
+npm install sandbox-agent@0.3.x @e2b/code-interpreter
+```
+
```typescript
-import { Sandbox } from "@e2b/code-interpreter";
import { SandboxAgent } from "sandbox-agent";
+import { e2b } from "sandbox-agent/e2b";
const envs: Record = {};
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-const sandbox = await Sandbox.create({ allowInternetAccess: true, envs });
-
-await sandbox.commands.run(
- "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"
-);
-
-await sandbox.commands.run("sandbox-agent install-agent claude");
-await sandbox.commands.run("sandbox-agent install-agent codex");
-
-await sandbox.commands.run(
- "sandbox-agent server --no-token --host 0.0.0.0 --port 3000",
- { background: true, timeoutMs: 0 }
-);
-
-const baseUrl = `https://${sandbox.getHost(3000)}`;
-const sdk = await SandboxAgent.connect({ baseUrl });
-
-const session = await sdk.createSession({ agent: "claude" });
-const off = session.onEvent((event) => {
- console.log(event.sender, event.payload);
+const sdk = await SandboxAgent.start({
+ sandbox: e2b({
+ create: { envs },
+ }),
});
-await session.prompt([{ type: "text", text: "Summarize this repository" }]);
-off();
-
-await sandbox.kill();
+try {
+ const session = await sdk.createSession({ agent: "claude" });
+ const response = await session.prompt([
+ { type: "text", text: "Summarize this repository" },
+ ]);
+ console.log(response.stopReason);
+} finally {
+ await sdk.destroySandbox();
+}
```
+The `e2b` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically.
+
## Faster cold starts
For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed.
diff --git a/docs/deploy/local.mdx b/docs/deploy/local.mdx
index eab8f3f..90e2ba6 100644
--- a/docs/deploy/local.mdx
+++ b/docs/deploy/local.mdx
@@ -32,12 +32,15 @@ Or with npm/Bun:
## With the TypeScript SDK
-The SDK can spawn and manage the server as a subprocess:
+The SDK can spawn and manage the server as a subprocess using the `local` provider:
```typescript
import { SandboxAgent } from "sandbox-agent";
+import { local } from "sandbox-agent/local";
-const sdk = await SandboxAgent.start();
+const sdk = await SandboxAgent.start({
+ sandbox: local(),
+});
const session = await sdk.createSession({
agent: "claude",
@@ -47,7 +50,21 @@ await session.prompt([
{ type: "text", text: "Summarize this repository." },
]);
-await sdk.dispose();
+await sdk.destroySandbox();
```
This starts the server on an available local port and connects automatically.
+
+Pass options to customize the local provider:
+
+```typescript
+const sdk = await SandboxAgent.start({
+ sandbox: local({
+ port: 3000,
+ log: "inherit",
+ env: {
+ ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY,
+ },
+ }),
+});
+```
diff --git a/docs/deploy/vercel.mdx b/docs/deploy/vercel.mdx
index 2025d67..db97236 100644
--- a/docs/deploy/vercel.mdx
+++ b/docs/deploy/vercel.mdx
@@ -10,52 +10,40 @@ description: "Deploy Sandbox Agent inside a Vercel Sandbox."
## TypeScript example
-```typescript
-import { Sandbox } from "@vercel/sandbox";
-import { SandboxAgent } from "sandbox-agent";
-
-const envs: Record = {};
-if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
-if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-
-const sandbox = await Sandbox.create({
- runtime: "node24",
- ports: [3000],
-});
-
-const run = async (cmd: string, args: string[] = []) => {
- const result = await sandbox.runCommand({ cmd, args, env: envs });
- if (result.exitCode !== 0) {
- throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
- }
-};
-
-await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"]);
-await run("sandbox-agent", ["install-agent", "claude"]);
-await run("sandbox-agent", ["install-agent", "codex"]);
-
-await sandbox.runCommand({
- cmd: "sandbox-agent",
- args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"],
- env: envs,
- detached: true,
-});
-
-const baseUrl = sandbox.domain(3000);
-const sdk = await SandboxAgent.connect({ baseUrl });
-
-const session = await sdk.createSession({ agent: "claude" });
-
-const off = session.onEvent((event) => {
- console.log(event.sender, event.payload);
-});
-
-await session.prompt([{ type: "text", text: "Summarize this repository" }]);
-off();
-
-await sandbox.stop();
+```bash
+npm install sandbox-agent@0.3.x @vercel/sandbox
```
+```typescript
+import { SandboxAgent } from "sandbox-agent";
+import { vercel } from "sandbox-agent/vercel";
+
+const env: Record = {};
+if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
+if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+
+const sdk = await SandboxAgent.start({
+ sandbox: vercel({
+ create: {
+ runtime: "node24",
+ env,
+ },
+ }),
+});
+
+try {
+ const session = await sdk.createSession({ agent: "claude" });
+ const response = await session.prompt([
+ { type: "text", text: "Summarize this repository" },
+ ]);
+ console.log(response.stopReason);
+} finally {
+ await sdk.destroySandbox();
+}
+```
+
+The `vercel` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically.
+
## Authentication
Vercel Sandboxes support OIDC token auth (recommended) and access-token auth.
diff --git a/docs/docs.json b/docs/docs.json
index 9ba082c..3f2be1a 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -58,13 +58,13 @@
"icon": "server",
"pages": [
"deploy/local",
- "deploy/computesdk",
"deploy/e2b",
"deploy/daytona",
"deploy/vercel",
"deploy/cloudflare",
"deploy/docker",
- "deploy/boxlite"
+ "deploy/boxlite",
+ "deploy/computesdk"
]
}
]
@@ -79,11 +79,12 @@
},
{
"group": "Orchestration",
- "pages": ["architecture", "session-persistence", "observability", "multiplayer", "security"]
+ "pages": ["orchestration-architecture", "session-persistence", "observability", "multiplayer", "security"]
},
{
"group": "Reference",
"pages": [
+ "architecture",
"agent-capabilities",
"cli",
"inspector",
diff --git a/docs/mcp-config.mdx b/docs/mcp-config.mdx
index 71e8105..cc1c976 100644
--- a/docs/mcp-config.mdx
+++ b/docs/mcp-config.mdx
@@ -27,9 +27,7 @@ await sdk.setMcpConfig(
// Create a session using the configured MCP servers
const session = await sdk.createSession({
agent: "claude",
- sessionInit: {
- cwd: "/workspace",
- },
+ cwd: "/workspace",
});
await session.prompt([
diff --git a/docs/orchestration-architecture.mdx b/docs/orchestration-architecture.mdx
new file mode 100644
index 0000000..08c776c
--- /dev/null
+++ b/docs/orchestration-architecture.mdx
@@ -0,0 +1,43 @@
+---
+title: "Orchestration Architecture"
+description: "Production topology, backend requirements, and session persistence."
+icon: "sitemap"
+---
+
+This page covers production topology and backend requirements. Read [Architecture](/architecture) first for an overview of how the server, SDK, and agent processes fit together.
+
+## Suggested Topology
+
+Run the SDK on your backend, then call it from your frontend.
+
+This extra hop is recommended because it keeps auth/token logic on the backend and makes persistence simpler.
+
+```mermaid placement="top-right"
+ flowchart LR
+ BROWSER["Browser"]
+ subgraph BACKEND["Your backend"]
+ direction TB
+ SDK["Sandbox Agent SDK"]
+ end
+ subgraph SANDBOX_SIMPLE["Sandbox"]
+ SERVER_SIMPLE["Sandbox Agent server"]
+ end
+
+ BROWSER --> BACKEND
+ BACKEND --> SDK --> SERVER_SIMPLE
+```
+
+### Backend requirements
+
+Your backend layer needs to handle:
+
+- **Long-running connections**: prompts can take minutes.
+- **Session affinity**: follow-up messages must reach the same session.
+- **State between requests**: session metadata and event history must persist across requests.
+- **Graceful recovery**: sessions should resume after backend restarts.
+
+We recommend [Rivet](https://rivet.dev) over serverless because actors natively support the long-lived connections, session routing, and state persistence that agent workloads require.
+
+## Session persistence
+
+For storage driver options and replay behavior, see [Persisting Sessions](/session-persistence).
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index caf2c21..07bd55a 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -1,20 +1,22 @@
---
title: "Quickstart"
-description: "Start the server and send your first message."
+description: "Get a coding agent running in a sandbox in under a minute."
icon: "rocket"
---
-
+
-
+
```bash
- npx skills add rivet-dev/skills -s sandbox-agent
+ npm install sandbox-agent@0.3.x
```
-
+
```bash
- bunx skills add rivet-dev/skills -s sandbox-agent
+ bun add sandbox-agent@0.3.x
+ # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
+ bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
```
@@ -23,52 +25,10 @@ icon: "rocket"
Each coding agent requires API keys to connect to their respective LLM providers.
-
-
- ```bash
- export ANTHROPIC_API_KEY="sk-ant-..."
- export OPENAI_API_KEY="sk-..."
- ```
-
-
-
- ```typescript
- import { Sandbox } from "@e2b/code-interpreter";
-
- const envs: Record = {};
- if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
- if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-
- const sandbox = await Sandbox.create({ envs });
- ```
-
-
-
- ```typescript
- import { Daytona } from "@daytonaio/sdk";
-
- const envVars: Record = {};
- if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
- if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-
- const daytona = new Daytona();
- const sandbox = await daytona.create({
- snapshot: "sandbox-agent-ready",
- envVars,
- });
- ```
-
-
-
- ```bash
- docker run -p 2468:2468 \
- -e ANTHROPIC_API_KEY="sk-ant-..." \
- -e OPENAI_API_KEY="sk-..." \
- rivetdev/sandbox-agent:0.3.1-full \
- server --no-token --host 0.0.0.0 --port 2468
- ```
-
-
+ ```bash
+ export ANTHROPIC_API_KEY="sk-ant-..."
+ export OPENAI_API_KEY="sk-..."
+ ```
@@ -83,173 +43,146 @@ icon: "rocket"
-
-
-
- Install and run the binary directly.
+
+ `SandboxAgent.start()` provisions a sandbox, starts a lightweight [Sandbox Agent server](/architecture) inside it, and connects your SDK client.
- ```bash
- curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh
- sandbox-agent server --no-token --host 0.0.0.0 --port 2468
- ```
-
+
+ ```typescript Local
+ import { SandboxAgent } from "sandbox-agent";
+ import { local } from "sandbox-agent/local";
-
- Run without installing globally.
+ // Runs on your machine. Best for local development and testing.
+ const sdk = await SandboxAgent.start({
+ sandbox: local(),
+ });
+ ```
- ```bash
- npx @sandbox-agent/cli@0.3.x server --no-token --host 0.0.0.0 --port 2468
- ```
-
+ ```typescript E2B
+ import { SandboxAgent } from "sandbox-agent";
+ import { e2b } from "sandbox-agent/e2b";
-
- Run without installing globally.
+ const sdk = await SandboxAgent.start({
+ sandbox: e2b({ create: { envs } }),
+ });
+ ```
- ```bash
- bunx @sandbox-agent/cli@0.3.x server --no-token --host 0.0.0.0 --port 2468
- ```
-
+ ```typescript Daytona
+ import { SandboxAgent } from "sandbox-agent";
+ import { daytona } from "sandbox-agent/daytona";
-
- Install globally, then run.
+ const sdk = await SandboxAgent.start({
+ sandbox: daytona({ create: { envVars } }),
+ });
+ ```
- ```bash
- npm install -g @sandbox-agent/cli@0.3.x
- sandbox-agent server --no-token --host 0.0.0.0 --port 2468
- ```
-
+ ```typescript Vercel
+ import { SandboxAgent } from "sandbox-agent";
+ import { vercel } from "sandbox-agent/vercel";
-
- Install globally, then run.
+ const sdk = await SandboxAgent.start({
+ sandbox: vercel({ create: { runtime: "node24", env } }),
+ });
+ ```
- ```bash
- bun add -g @sandbox-agent/cli@0.3.x
- # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
- bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
- sandbox-agent server --no-token --host 0.0.0.0 --port 2468
- ```
-
+ ```typescript Cloudflare
+ import { SandboxAgent } from "sandbox-agent";
+ import { cloudflare } from "sandbox-agent/cloudflare";
-
- For local development, use `SandboxAgent.start()` to spawn and manage the server as a subprocess.
+ const sdk = await SandboxAgent.start({
+ sandbox: cloudflare({ sdk: cfSandboxClient }),
+ });
+ ```
- ```bash
- npm install sandbox-agent@0.3.x
- ```
+ ```typescript Docker
+ import { SandboxAgent } from "sandbox-agent";
+ import { docker } from "sandbox-agent/docker";
- ```typescript
- import { SandboxAgent } from "sandbox-agent";
+ // Good for testing. Not security-hardened like cloud sandboxes.
+ const sdk = await SandboxAgent.start({
+ sandbox: docker({
+ env: [`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`],
+ }),
+ });
+ ```
+
- const sdk = await SandboxAgent.start();
- ```
-
-
-
- For local development, use `SandboxAgent.start()` to spawn and manage the server as a subprocess.
-
- ```bash
- bun add sandbox-agent@0.3.x
- # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
- bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
- ```
-
- ```typescript
- import { SandboxAgent } from "sandbox-agent";
-
- const sdk = await SandboxAgent.start();
- ```
-
-
-
- If you're running from source instead of the installed CLI.
-
- ```bash
- cargo run -p sandbox-agent -- server --no-token --host 0.0.0.0 --port 2468
- ```
-
-
-
- Binding to `0.0.0.0` allows the server to accept connections from any network interface, which is required when running inside a sandbox where clients connect remotely.
+ Each provider handles provisioning, server installation, and networking. Install the provider's peer dependency (e.g. `@e2b/code-interpreter`, `dockerode`) in your project. See the [Deploy](/deploy/local) guides for full setup details.
-
- Tokens are usually not required. Most sandbox providers (E2B, Daytona, etc.) already secure networking at the infrastructure layer.
+
+ Implement the `SandboxProvider` interface to use any sandbox platform:
- If you expose the server publicly, use `--token "$SANDBOX_TOKEN"` to require authentication:
+ ```typescript
+ import { SandboxAgent, type SandboxProvider } from "sandbox-agent";
- ```bash
- sandbox-agent server --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468
+ const myProvider: SandboxProvider = {
+ name: "my-provider",
+ async create() {
+ // Provision a sandbox, install & start the server, return an ID
+ return "sandbox-123";
+ },
+ async destroy(sandboxId) {
+ // Tear down the sandbox
+ },
+ async getUrl(sandboxId) {
+ // Return the Sandbox Agent server URL
+ return `https://${sandboxId}.my-platform.dev:3000`;
+ },
+ };
+
+ const sdk = await SandboxAgent.start({
+ sandbox: myProvider,
+ });
```
+
- Then pass the token when connecting:
+
+ If you already have a Sandbox Agent server running, connect directly:
+ ```typescript
+ const sdk = await SandboxAgent.connect({
+ baseUrl: "http://127.0.0.1:2468",
+ });
+ ```
+
+
+
-
- ```typescript
- import { SandboxAgent } from "sandbox-agent";
-
- const sdk = await SandboxAgent.connect({
- baseUrl: "http://your-server:2468",
- token: process.env.SANDBOX_TOKEN,
- });
- ```
-
-
```bash
- curl "http://your-server:2468/v1/health" \
- -H "Authorization: Bearer $SANDBOX_TOKEN"
+ curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh
+ sandbox-agent server --no-token --host 0.0.0.0 --port 2468
```
-
-
+
```bash
- sandbox-agent --token "$SANDBOX_TOKEN" api agents list \
- --endpoint http://your-server:2468
+ npx @sandbox-agent/cli@0.3.x server --no-token --host 0.0.0.0 --port 2468
+ ```
+
+
+ ```bash
+ docker run -p 2468:2468 \
+ -e ANTHROPIC_API_KEY="sk-ant-..." \
+ -e OPENAI_API_KEY="sk-..." \
+ rivetdev/sandbox-agent:0.3.2-full \
+ server --no-token --host 0.0.0.0 --port 2468
```
-
- If you're calling the server from a browser, see the [CORS configuration guide](/cors).
-
-
- Supported agent IDs: `claude`, `codex`, `opencode`, `amp`, `pi`, `cursor`, `mock`.
-
- To preinstall agents:
-
- ```bash
- sandbox-agent install-agent --all
- ```
-
- If agents are not installed up front, they are lazily installed when creating a session.
-
-
-
+
```typescript
- import { SandboxAgent } from "sandbox-agent";
-
- const sdk = await SandboxAgent.connect({
- baseUrl: "http://127.0.0.1:2468",
- });
-
const session = await sdk.createSession({
agent: "claude",
- sessionInit: {
- cwd: "/",
- mcpServers: [],
- },
});
- console.log(session.id);
- ```
-
+ session.onEvent((event) => {
+ console.log(event.sender, event.payload);
+ });
-
- ```typescript
const result = await session.prompt([
{ type: "text", text: "Summarize the repository and suggest next steps." },
]);
@@ -258,24 +191,16 @@ icon: "rocket"
```
-
+
```typescript
- const off = session.onEvent((event) => {
- console.log(event.sender, event.payload);
- });
-
- const page = await sdk.getEvents({
- sessionId: session.id,
- limit: 50,
- });
-
- console.log(page.items.length);
- off();
+ await sdk.destroySandbox(); // tears down the sandbox and disconnects
```
+
+ Use `sdk.dispose()` instead to disconnect without destroying the sandbox (for reconnecting later).
-
- Open the Inspector UI at `/ui/` on your server (for example, `http://localhost:2468/ui/`) to inspect sessions and events in a GUI.
+
+ Open the Inspector at `/ui/` on your server (e.g. `http://localhost:2468/ui/`) to view sessions and events in a GUI.
@@ -283,16 +208,40 @@ icon: "rocket"
+## Full example
+
+```typescript
+import { SandboxAgent } from "sandbox-agent";
+import { local } from "sandbox-agent/local";
+
+const sdk = await SandboxAgent.start({
+ sandbox: local(),
+});
+
+try {
+ const session = await sdk.createSession({ agent: "claude" });
+
+ session.onEvent((event) => {
+ console.log(`[${event.sender}]`, JSON.stringify(event.payload));
+ });
+
+ const result = await session.prompt([
+ { type: "text", text: "Write a function that checks if a number is prime." },
+ ]);
+
+ console.log("Done:", result.stopReason);
+} finally {
+ await sdk.destroySandbox();
+}
+```
+
## Next steps
-
-
- Configure in-memory, Rivet Actor state, IndexedDB, SQLite, and Postgres persistence.
+
+
+ Full TypeScript SDK API surface.
- Deploy your agent to E2B, Daytona, Docker, Vercel, or Cloudflare.
-
-
- Use the latest TypeScript SDK API.
+ Deploy to E2B, Daytona, Docker, Vercel, or Cloudflare.
diff --git a/docs/sdk-overview.mdx b/docs/sdk-overview.mdx
index fc4aee1..1b8a707 100644
--- a/docs/sdk-overview.mdx
+++ b/docs/sdk-overview.mdx
@@ -84,25 +84,40 @@ const sdk = await SandboxAgent.connect({
});
```
-Local autospawn (Node.js only):
+Local spawn with a sandbox provider:
```ts
import { SandboxAgent } from "sandbox-agent";
+import { local } from "sandbox-agent/local";
-const localSdk = await SandboxAgent.start();
+const sdk = await SandboxAgent.start({
+ sandbox: local(),
+});
-await localSdk.dispose();
+// sdk.sandboxId — prefixed provider ID (e.g. "local/127.0.0.1:2468")
+
+await sdk.destroySandbox(); // tears down sandbox + disposes client
```
+`SandboxAgent.start(...)` requires a `sandbox` provider. Built-in providers:
+
+| Import | Provider |
+|--------|----------|
+| `sandbox-agent/local` | Local subprocess |
+| `sandbox-agent/docker` | Docker container |
+| `sandbox-agent/e2b` | E2B sandbox |
+| `sandbox-agent/daytona` | Daytona workspace |
+| `sandbox-agent/vercel` | Vercel Sandbox |
+| `sandbox-agent/cloudflare` | Cloudflare Sandbox |
+
+Use `sdk.dispose()` to disconnect without destroying the sandbox, or `sdk.destroySandbox()` to tear down both.
+
## Session flow
```ts
const session = await sdk.createSession({
agent: "mock",
- sessionInit: {
- cwd: "/",
- mcpServers: [],
- },
+ cwd: "/",
});
const prompt = await session.prompt([
@@ -223,6 +238,7 @@ Parameters:
- `token` (optional): Bearer token for authenticated servers
- `headers` (optional): Additional request headers
- `fetch` (optional): Custom fetch implementation used by SDK HTTP and session calls
+- `skipHealthCheck` (optional): set `true` to skip the startup `/v1/health` wait
- `waitForHealth` (optional, defaults to enabled): waits for `/v1/health` before HTTP helpers and session setup proceed; pass `false` to disable or `{ timeoutMs }` to bound the wait
- `signal` (optional): aborts the startup `/v1/health` wait used by `connect()`
diff --git a/docs/security.mdx b/docs/security.mdx
index ec00f49..c8b02ad 100644
--- a/docs/security.mdx
+++ b/docs/security.mdx
@@ -4,7 +4,7 @@ description: "Backend-first auth and access control patterns."
icon: "shield"
---
-As covered in [Architecture](/architecture), run the Sandbox Agent client on your backend, not in the browser.
+As covered in [Orchestration Architecture](/orchestration-architecture), run the Sandbox Agent client on your backend, not in the browser.
This keeps sandbox credentials private and gives you one place for authz, rate limiting, and audit logging.
@@ -92,7 +92,7 @@ export const workspace = actor({
const session = await sdk.createSession({
agent: "claude",
- sessionInit: { cwd: "/workspace" },
+ cwd: "/workspace",
});
session.onEvent((event) => {
diff --git a/docs/session-persistence.mdx b/docs/session-persistence.mdx
index eaa4de0..b8328ec 100644
--- a/docs/session-persistence.mdx
+++ b/docs/session-persistence.mdx
@@ -10,7 +10,7 @@ With persistence enabled, sessions can be restored after runtime/session loss. S
Each driver stores:
-- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sessionInit`)
+- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sandboxId`, optional `sessionInit`)
- `SessionEvent` (`id`, `eventIndex`, `sessionId`, `connectionId`, `sender`, `payload`, `createdAt`)
## Persistence drivers
@@ -160,11 +160,11 @@ Implement `SessionPersistDriver` for custom backends.
import type { SessionPersistDriver } from "sandbox-agent";
class MyDriver implements SessionPersistDriver {
- async getSession(id) { return null; }
+ async getSession(id) { return undefined; }
async listSessions(request) { return { items: [] }; }
async updateSession(session) {}
async listEvents(request) { return { items: [] }; }
- async insertEvent(event) {}
+ async insertEvent(sessionId, event) {}
}
```
diff --git a/docs/skills-config.mdx b/docs/skills-config.mdx
index c85bc2c..c3145c2 100644
--- a/docs/skills-config.mdx
+++ b/docs/skills-config.mdx
@@ -35,9 +35,7 @@ await sdk.setSkillsConfig(
// Create a session using the configured skills
const session = await sdk.createSession({
agent: "claude",
- sessionInit: {
- cwd: "/workspace",
- },
+ cwd: "/workspace",
});
await session.prompt([
diff --git a/examples/boxlite/src/index.ts b/examples/boxlite/src/index.ts
index bdcd53a..171166b 100644
--- a/examples/boxlite/src/index.ts
+++ b/examples/boxlite/src/index.ts
@@ -25,7 +25,7 @@ const baseUrl = "http://localhost:3000";
console.log("Connecting to server...");
const client = await SandboxAgent.connect({ baseUrl });
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/root", mcpServers: [] } });
+const session = await client.createSession({ agent: detectAgent(), cwd: "/root" });
const sessionId = session.id;
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
diff --git a/examples/computesdk/src/computesdk.ts b/examples/computesdk/src/computesdk.ts
index 46f43d6..8e32644 100644
--- a/examples/computesdk/src/computesdk.ts
+++ b/examples/computesdk/src/computesdk.ts
@@ -131,7 +131,7 @@ export async function runComputeSdkExample(): Promise {
process.once("SIGTERM", handleExit);
const client = await SandboxAgent.connect({ baseUrl });
- const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home", mcpServers: [] } });
+ const session = await client.createSession({ agent: detectAgent(), cwd: "/home" });
const sessionId = session.id;
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
diff --git a/examples/daytona/src/index.ts b/examples/daytona/src/index.ts
index 09f4cff..b881113 100644
--- a/examples/daytona/src/index.ts
+++ b/examples/daytona/src/index.ts
@@ -1,42 +1,31 @@
-import { Daytona } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
-import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
-
-const daytona = new Daytona();
+import { daytona } from "sandbox-agent/daytona";
+import { detectAgent } from "@sandbox-agent/example-shared";
const envVars: Record = {};
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build)
-console.log("Creating Daytona sandbox...");
-const sandbox = await daytona.create({ envVars, autoStopInterval: 0 });
+const client = await SandboxAgent.start({
+ sandbox: daytona({
+ create: { envVars },
+ }),
+});
-// Install sandbox-agent and start server
-console.log("Installing sandbox-agent...");
-await sandbox.process.executeCommand("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh");
+console.log(`UI: ${client.inspectorUrl}`);
-console.log("Installing agents...");
-await sandbox.process.executeCommand("sandbox-agent install-agent claude");
-await sandbox.process.executeCommand("sandbox-agent install-agent codex");
+const session = await client.createSession({
+ agent: detectAgent(),
+ cwd: "/home/daytona",
+});
-await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
+session.onEvent((event) => {
+ console.log(`[${event.sender}]`, JSON.stringify(event.payload));
+});
-const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
+session.prompt([{ type: "text", text: "Say hello from Daytona in one sentence." }]);
-console.log("Connecting to server...");
-const client = await SandboxAgent.connect({ baseUrl });
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/daytona", mcpServers: [] } });
-const sessionId = session.id;
-
-console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
-console.log(" Press Ctrl+C to stop.");
-
-const keepAlive = setInterval(() => {}, 60_000);
-const cleanup = async () => {
- clearInterval(keepAlive);
- await sandbox.delete(60);
+process.once("SIGINT", async () => {
+ await client.destroySandbox();
process.exit(0);
-};
-process.once("SIGINT", cleanup);
-process.once("SIGTERM", cleanup);
+});
diff --git a/examples/docker/package.json b/examples/docker/package.json
index 2c29cfe..7b796c9 100644
--- a/examples/docker/package.json
+++ b/examples/docker/package.json
@@ -9,10 +9,10 @@
"dependencies": {
"@sandbox-agent/example-shared": "workspace:*",
"dockerode": "latest",
+ "get-port": "latest",
"sandbox-agent": "workspace:*"
},
"devDependencies": {
- "@types/dockerode": "latest",
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest",
diff --git a/examples/docker/src/index.ts b/examples/docker/src/index.ts
index 74469f3..9f50859 100644
--- a/examples/docker/src/index.ts
+++ b/examples/docker/src/index.ts
@@ -1,68 +1,40 @@
-import Docker from "dockerode";
import fs from "node:fs";
import path from "node:path";
import { SandboxAgent } from "sandbox-agent";
-import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
+import { docker } from "sandbox-agent/docker";
+import { detectAgent } from "@sandbox-agent/example-shared";
import { FULL_IMAGE } from "@sandbox-agent/example-shared/docker";
-const IMAGE = FULL_IMAGE;
-const PORT = 3000;
-const agent = detectAgent();
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath) ? [`${codexAuthPath}:/home/sandbox/.codex/auth.json:ro`] : [];
+const env = [
+ process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
+ process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
+ process.env.CODEX_API_KEY ? `CODEX_API_KEY=${process.env.CODEX_API_KEY}` : "",
+].filter(Boolean);
-const docker = new Docker({ socketPath: "/var/run/docker.sock" });
-
-// Pull image if needed
-try {
- await docker.getImage(IMAGE).inspect();
-} catch {
- console.log(`Pulling ${IMAGE}...`);
- await new Promise((resolve, reject) => {
- docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => {
- if (err) return reject(err);
- docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
- });
- });
-}
-
-console.log("Starting container...");
-const container = await docker.createContainer({
- Image: IMAGE,
- Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`],
- Env: [
- process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
- process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
- process.env.CODEX_API_KEY ? `CODEX_API_KEY=${process.env.CODEX_API_KEY}` : "",
- ].filter(Boolean),
- ExposedPorts: { [`${PORT}/tcp`]: {} },
- HostConfig: {
- AutoRemove: true,
- PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] },
- Binds: bindMounts,
- },
+const client = await SandboxAgent.start({
+ sandbox: docker({
+ image: FULL_IMAGE,
+ env,
+ binds: bindMounts,
+ }),
});
-await container.start();
-const baseUrl = `http://127.0.0.1:${PORT}`;
+console.log(`UI: ${client.inspectorUrl}`);
-const client = await SandboxAgent.connect({ baseUrl });
-const session = await client.createSession({ agent, sessionInit: { cwd: "/home/sandbox", mcpServers: [] } });
-const sessionId = session.id;
+const session = await client.createSession({
+ agent: detectAgent(),
+ cwd: "/home/sandbox",
+});
-console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
-console.log(" Press Ctrl+C to stop.");
+session.onEvent((event) => {
+ console.log(`[${event.sender}]`, JSON.stringify(event.payload));
+});
-const keepAlive = setInterval(() => {}, 60_000);
-const cleanup = async () => {
- clearInterval(keepAlive);
- try {
- await container.stop({ t: 5 });
- } catch {}
- try {
- await container.remove({ force: true });
- } catch {}
+session.prompt([{ type: "text", text: "Say hello from Docker in one sentence." }]);
+
+process.once("SIGINT", async () => {
+ await client.destroySandbox();
process.exit(0);
-};
-process.once("SIGINT", cleanup);
-process.once("SIGTERM", cleanup);
+});
diff --git a/examples/e2b/src/index.ts b/examples/e2b/src/index.ts
index 7dd2882..996b99f 100644
--- a/examples/e2b/src/index.ts
+++ b/examples/e2b/src/index.ts
@@ -1,45 +1,31 @@
-import { Sandbox } from "@e2b/code-interpreter";
import { SandboxAgent } from "sandbox-agent";
-import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
+import { e2b } from "sandbox-agent/e2b";
+import { detectAgent } from "@sandbox-agent/example-shared";
const envs: Record = {};
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-console.log("Creating E2B sandbox...");
-const sandbox = await Sandbox.create({ allowInternetAccess: true, envs });
+const client = await SandboxAgent.start({
+ sandbox: e2b({
+ create: { envs },
+ }),
+});
-const run = async (cmd: string) => {
- const result = await sandbox.commands.run(cmd);
- if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`);
- return result;
-};
+console.log(`UI: ${client.inspectorUrl}`);
-console.log("Installing sandbox-agent...");
-await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh");
+const session = await client.createSession({
+ agent: detectAgent(),
+ cwd: "/home/user",
+});
-console.log("Installing agents...");
-await run("sandbox-agent install-agent claude");
-await run("sandbox-agent install-agent codex");
+session.onEvent((event) => {
+ console.log(`[${event.sender}]`, JSON.stringify(event.payload));
+});
-console.log("Starting server...");
-await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true, timeoutMs: 0 });
+session.prompt([{ type: "text", text: "Say hello from E2B in one sentence." }]);
-const baseUrl = `https://${sandbox.getHost(3000)}`;
-
-console.log("Connecting to server...");
-const client = await SandboxAgent.connect({ baseUrl });
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/user", mcpServers: [] } });
-const sessionId = session.id;
-
-console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
-console.log(" Press Ctrl+C to stop.");
-
-const keepAlive = setInterval(() => {}, 60_000);
-const cleanup = async () => {
- clearInterval(keepAlive);
- await sandbox.kill();
+process.once("SIGINT", async () => {
+ await client.destroySandbox();
process.exit(0);
-};
-process.once("SIGINT", cleanup);
-process.once("SIGTERM", cleanup);
+});
diff --git a/examples/file-system/src/index.ts b/examples/file-system/src/index.ts
index abe4e08..71d65c0 100644
--- a/examples/file-system/src/index.ts
+++ b/examples/file-system/src/index.ts
@@ -44,7 +44,7 @@ const readmeText = new TextDecoder().decode(readmeBytes);
console.log(` README.md content: ${readmeText.trim()}`);
console.log("Creating session...");
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/opt/my-project", mcpServers: [] } });
+const session = await client.createSession({ agent: detectAgent(), cwd: "/opt/my-project" });
const sessionId = session.id;
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
console.log(' Try: "read the README in /opt/my-project"');
diff --git a/examples/permissions/src/index.ts b/examples/permissions/src/index.ts
index 811f65c..e684e34 100644
--- a/examples/permissions/src/index.ts
+++ b/examples/permissions/src/index.ts
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { Command } from "commander";
import { SandboxAgent, type PermissionReply, type SessionPermissionRequest } from "sandbox-agent";
+import { local } from "sandbox-agent/local";
const options = parseOptions();
const agent = options.agent.trim().toLowerCase();
@@ -9,10 +10,7 @@ const autoReply = parsePermissionReply(options.reply);
const promptText = options.prompt?.trim() || `Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
const sdk = await SandboxAgent.start({
- spawn: {
- enabled: true,
- log: "inherit",
- },
+ sandbox: local({ log: "inherit" }),
});
try {
@@ -43,10 +41,7 @@ try {
const session = await sdk.createSession({
agent,
...(mode ? { mode } : {}),
- sessionInit: {
- cwd: process.cwd(),
- mcpServers: [],
- },
+ cwd: process.cwd(),
});
const rl = autoReply
diff --git a/examples/skills-custom-tool/src/index.ts b/examples/skills-custom-tool/src/index.ts
index 44b2161..490be64 100644
--- a/examples/skills-custom-tool/src/index.ts
+++ b/examples/skills-custom-tool/src/index.ts
@@ -36,7 +36,7 @@ await client.setSkillsConfig({ directory: "/", skillName: "random-number" }, { s
// Create a session.
console.log("Creating session with custom skill...");
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/root", mcpServers: [] } });
+const session = await client.createSession({ agent: detectAgent(), cwd: "/root" });
const sessionId = session.id;
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
console.log(' Try: "generate a random number between 1 and 100"');
diff --git a/examples/skills/src/index.ts b/examples/skills/src/index.ts
index c04815c..3087ecc 100644
--- a/examples/skills/src/index.ts
+++ b/examples/skills/src/index.ts
@@ -15,7 +15,7 @@ await client.setSkillsConfig(
);
console.log("Creating session...");
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/root", mcpServers: [] } });
+const session = await client.createSession({ agent: detectAgent(), cwd: "/root" });
const sessionId = session.id;
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
console.log(' Try: "How do I start sandbox-agent?"');
diff --git a/examples/vercel/src/index.ts b/examples/vercel/src/index.ts
index 4a63bfc..9839893 100644
--- a/examples/vercel/src/index.ts
+++ b/examples/vercel/src/index.ts
@@ -1,56 +1,34 @@
-import { Sandbox } from "@vercel/sandbox";
import { SandboxAgent } from "sandbox-agent";
-import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
+import { vercel } from "sandbox-agent/vercel";
+import { detectAgent } from "@sandbox-agent/example-shared";
-const envs: Record = {};
-if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
-if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+const env: Record = {};
+if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
+if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
-console.log("Creating Vercel sandbox...");
-const sandbox = await Sandbox.create({
- runtime: "node24",
- ports: [3000],
+const client = await SandboxAgent.start({
+ sandbox: vercel({
+ create: {
+ runtime: "node24",
+ env,
+ },
+ }),
});
-const run = async (cmd: string, args: string[] = []) => {
- const result = await sandbox.runCommand({ cmd, args, env: envs });
- if (result.exitCode !== 0) {
- const stderr = await result.stderr();
- throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${stderr}`);
- }
- return result;
-};
+console.log(`UI: ${client.inspectorUrl}`);
-console.log("Installing sandbox-agent...");
-await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh"]);
-
-console.log("Installing agents...");
-await run("sandbox-agent", ["install-agent", "claude"]);
-await run("sandbox-agent", ["install-agent", "codex"]);
-
-console.log("Starting server...");
-await sandbox.runCommand({
- cmd: "sandbox-agent",
- args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"],
- env: envs,
- detached: true,
+const session = await client.createSession({
+ agent: detectAgent(),
+ cwd: "/home/vercel-sandbox",
});
-const baseUrl = sandbox.domain(3000);
+session.onEvent((event) => {
+ console.log(`[${event.sender}]`, JSON.stringify(event.payload));
+});
-console.log("Connecting to server...");
-const client = await SandboxAgent.connect({ baseUrl });
-const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/vercel-sandbox", mcpServers: [] } });
-const sessionId = session.id;
+session.prompt([{ type: "text", text: "Say hello from Vercel in one sentence." }]);
-console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
-console.log(" Press Ctrl+C to stop.");
-
-const keepAlive = setInterval(() => {}, 60_000);
-const cleanup = async () => {
- clearInterval(keepAlive);
- await sandbox.stop();
+process.once("SIGINT", async () => {
+ await client.destroySandbox();
process.exit(0);
-};
-process.once("SIGINT", cleanup);
-process.once("SIGTERM", cleanup);
+});
diff --git a/frontend/packages/website/src/components/GetStarted.tsx b/frontend/packages/website/src/components/GetStarted.tsx
index 57cccef..8a03b34 100644
--- a/frontend/packages/website/src/components/GetStarted.tsx
+++ b/frontend/packages/website/src/components/GetStarted.tsx
@@ -5,8 +5,11 @@ import { Code, Server, GitBranch } from "lucide-react";
import { CopyButton } from "./ui/CopyButton";
const sdkCodeRaw = `import { SandboxAgent } from "sandbox-agent";
+import { local } from "sandbox-agent/local";
-const client = await SandboxAgent.start();
+const client = await SandboxAgent.start({
+ sandbox: local(),
+});
await client.createSession("my-session", {
agent: "claude-code",
@@ -32,13 +35,26 @@ function SdkCodeHighlighted() {
"sandbox-agent"
;
+ {"\n"}
+ import
+ {" { "}
+ local
+ {" } "}
+ from
+
+ "sandbox-agent/local"
+ ;
{"\n\n"}
const
client =
await
SandboxAgent.
start
- ();
+ {"({"}
+ {"\n"}
+ {" sandbox: local(),"}
+ {"\n"}
+ {"});"}
{"\n\n"}
await
client.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8396837..59a5a6b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -154,13 +154,13 @@ importers:
dockerode:
specifier: latest
version: 4.0.9
+ get-port:
+ specifier: latest
+ version: 7.1.0
sandbox-agent:
specifier: workspace:*
version: link:../../sdks/typescript
devDependencies:
- '@types/dockerode':
- specifier: latest
- version: 4.0.1
'@types/node':
specifier: latest
version: 25.5.0
@@ -1025,12 +1025,33 @@ importers:
specifier: workspace:*
version: link:../cli
devDependencies:
+ '@cloudflare/sandbox':
+ specifier: '>=0.1.0'
+ version: 0.7.17(@opencode-ai/sdk@1.2.24)
+ '@daytonaio/sdk':
+ specifier: '>=0.12.0'
+ version: 0.151.0(ws@8.19.0)
+ '@e2b/code-interpreter':
+ specifier: '>=1.0.0'
+ version: 2.3.3
+ '@types/dockerode':
+ specifier: ^4.0.0
+ version: 4.0.1
'@types/node':
specifier: ^22.0.0
version: 22.19.7
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
+ '@vercel/sandbox':
+ specifier: '>=0.1.0'
+ version: 1.8.1
+ dockerode:
+ specifier: '>=4.0.0'
+ version: 4.0.9
+ get-port:
+ specifier: '>=7.0.0'
+ version: 7.1.0
openapi-typescript:
specifier: ^6.7.0
version: 6.7.6
@@ -11273,7 +11294,7 @@ snapshots:
glob: 11.1.0
openapi-fetch: 0.14.1
platform: 1.3.6
- tar: 7.5.6
+ tar: 7.5.7
earcut@2.2.4: {}
diff --git a/sdks/persist-indexeddb/src/index.ts b/sdks/persist-indexeddb/src/index.ts
index 945e993..b6af1c8 100644
--- a/sdks/persist-indexeddb/src/index.ts
+++ b/sdks/persist-indexeddb/src/index.ts
@@ -31,11 +31,11 @@ export class IndexedDbSessionPersistDriver implements SessionPersistDriver {
this.dbPromise = this.openDatabase();
}
- async getSession(id: string): Promise {
+ async getSession(id: string): Promise {
const db = await this.dbPromise;
const row = await requestToPromise(db.transaction(SESSIONS_STORE, "readonly").objectStore(SESSIONS_STORE).get(id));
if (!row || typeof row !== "object") {
- return null;
+ return undefined;
}
return decodeSessionRow(row as SessionRow);
}
@@ -84,7 +84,7 @@ export class IndexedDbSessionPersistDriver implements SessionPersistDriver {
};
}
- async insertEvent(event: SessionEvent): Promise {
+ async insertEvent(_sessionId: string, event: SessionEvent): Promise {
const db = await this.dbPromise;
await transactionPromise(db, [EVENTS_STORE], "readwrite", (tx) => {
tx.objectStore(EVENTS_STORE).put(encodeEventRow(event));
@@ -139,6 +139,7 @@ type SessionRow = {
lastConnectionId: string;
createdAt: number;
destroyedAt?: number;
+ sandboxId?: string;
sessionInit?: SessionRecord["sessionInit"];
};
@@ -160,6 +161,7 @@ function encodeSessionRow(session: SessionRecord): SessionRow {
lastConnectionId: session.lastConnectionId,
createdAt: session.createdAt,
destroyedAt: session.destroyedAt,
+ sandboxId: session.sandboxId,
sessionInit: session.sessionInit,
};
}
@@ -172,6 +174,7 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
lastConnectionId: row.lastConnectionId,
createdAt: row.createdAt,
destroyedAt: row.destroyedAt,
+ sandboxId: row.sandboxId,
sessionInit: row.sessionInit,
};
}
diff --git a/sdks/persist-indexeddb/tests/driver.test.ts b/sdks/persist-indexeddb/tests/driver.test.ts
index 78acbe1..5c743be 100644
--- a/sdks/persist-indexeddb/tests/driver.test.ts
+++ b/sdks/persist-indexeddb/tests/driver.test.ts
@@ -28,7 +28,7 @@ describe("IndexedDbSessionPersistDriver", () => {
destroyedAt: 300,
});
- await driver.insertEvent({
+ await driver.insertEvent("s-1", {
id: "evt-1",
eventIndex: 1,
sessionId: "s-1",
@@ -38,7 +38,7 @@ describe("IndexedDbSessionPersistDriver", () => {
payload: { jsonrpc: "2.0", method: "session/prompt", params: { sessionId: "a-1" } },
});
- await driver.insertEvent({
+ await driver.insertEvent("s-1", {
id: "evt-2",
eventIndex: 2,
sessionId: "s-1",
diff --git a/sdks/persist-postgres/src/index.ts b/sdks/persist-postgres/src/index.ts
index 7c77827..8b79791 100644
--- a/sdks/persist-postgres/src/index.ts
+++ b/sdks/persist-postgres/src/index.ts
@@ -33,18 +33,18 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
this.initialized = this.initialize();
}
- async getSession(id: string): Promise {
+ async getSession(id: string): Promise {
await this.ready();
const result = await this.pool.query(
- `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json
+ `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
FROM ${this.table("sessions")}
WHERE id = $1`,
[id],
);
if (result.rows.length === 0) {
- return null;
+ return undefined;
}
return decodeSessionRow(result.rows[0]);
@@ -57,7 +57,7 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
const limit = normalizeLimit(request.limit);
const rowsResult = await this.pool.query(
- `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json
+ `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
FROM ${this.table("sessions")}
ORDER BY created_at ASC, id ASC
LIMIT $1 OFFSET $2`,
@@ -79,14 +79,15 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
await this.pool.query(
`INSERT INTO ${this.table("sessions")} (
- id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json
- ) VALUES ($1, $2, $3, $4, $5, $6, $7)
+ id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT(id) DO UPDATE SET
agent = EXCLUDED.agent,
agent_session_id = EXCLUDED.agent_session_id,
last_connection_id = EXCLUDED.last_connection_id,
created_at = EXCLUDED.created_at,
destroyed_at = EXCLUDED.destroyed_at,
+ sandbox_id = EXCLUDED.sandbox_id,
session_init_json = EXCLUDED.session_init_json`,
[
session.id,
@@ -95,6 +96,7 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
session.lastConnectionId,
session.createdAt,
session.destroyedAt ?? null,
+ session.sandboxId ?? null,
session.sessionInit ?? null,
],
);
@@ -127,7 +129,7 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
};
}
- async insertEvent(event: SessionEvent): Promise {
+ async insertEvent(_sessionId: string, event: SessionEvent): Promise {
await this.ready();
await this.pool.query(
@@ -171,10 +173,16 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
last_connection_id TEXT NOT NULL,
created_at BIGINT NOT NULL,
destroyed_at BIGINT,
+ sandbox_id TEXT,
session_init_json JSONB
)
`);
+ await this.pool.query(`
+ ALTER TABLE ${this.table("sessions")}
+ ADD COLUMN IF NOT EXISTS sandbox_id TEXT
+ `);
+
await this.pool.query(`
CREATE TABLE IF NOT EXISTS ${this.table("events")} (
id TEXT PRIMARY KEY,
@@ -228,6 +236,7 @@ type SessionRow = {
last_connection_id: string;
created_at: string | number;
destroyed_at: string | number | null;
+ sandbox_id: string | null;
session_init_json: unknown | null;
};
@@ -249,6 +258,7 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
lastConnectionId: row.last_connection_id,
createdAt: parseInteger(row.created_at),
destroyedAt: row.destroyed_at === null ? undefined : parseInteger(row.destroyed_at),
+ sandboxId: row.sandbox_id ?? undefined,
sessionInit: row.session_init_json ? (row.session_init_json as SessionRecord["sessionInit"]) : undefined,
};
}
diff --git a/sdks/persist-rivet/src/index.ts b/sdks/persist-rivet/src/index.ts
index d236040..b89b7df 100644
--- a/sdks/persist-rivet/src/index.ts
+++ b/sdks/persist-rivet/src/index.ts
@@ -50,9 +50,9 @@ export class RivetSessionPersistDriver implements SessionPersistDriver {
return this.ctx.state[this.stateKey] as RivetPersistData;
}
- async getSession(id: string): Promise {
+ async getSession(id: string): Promise {
const session = this.data.sessions[id];
- return session ? cloneSessionRecord(session) : null;
+ return session ? cloneSessionRecord(session) : undefined;
}
async listSessions(request: ListPageRequest = {}): Promise> {
@@ -112,15 +112,15 @@ export class RivetSessionPersistDriver implements SessionPersistDriver {
};
}
- async insertEvent(event: SessionEvent): Promise {
- const events = this.data.events[event.sessionId] ?? [];
+ async insertEvent(sessionId: string, event: SessionEvent): Promise {
+ const events = this.data.events[sessionId] ?? [];
events.push(cloneSessionEvent(event));
if (events.length > this.maxEventsPerSession) {
events.splice(0, events.length - this.maxEventsPerSession);
}
- this.data.events[event.sessionId] = events;
+ this.data.events[sessionId] = events;
}
}
diff --git a/sdks/persist-rivet/tests/driver.test.ts b/sdks/persist-rivet/tests/driver.test.ts
index c16e733..839cffe 100644
--- a/sdks/persist-rivet/tests/driver.test.ts
+++ b/sdks/persist-rivet/tests/driver.test.ts
@@ -59,7 +59,7 @@ describe("RivetSessionPersistDriver", () => {
expect(loaded?.destroyedAt).toBe(300);
const missing = await driver.getSession("s-nonexistent");
- expect(missing).toBeNull();
+ expect(missing).toBeUndefined();
});
it("pages sessions sorted by createdAt", async () => {
@@ -103,7 +103,7 @@ describe("RivetSessionPersistDriver", () => {
createdAt: 1,
});
- await driver.insertEvent({
+ await driver.insertEvent("s-1", {
id: "evt-1",
eventIndex: 1,
sessionId: "s-1",
@@ -113,7 +113,7 @@ describe("RivetSessionPersistDriver", () => {
payload: { jsonrpc: "2.0", method: "session/prompt", params: { sessionId: "a-1" } },
});
- await driver.insertEvent({
+ await driver.insertEvent("s-1", {
id: "evt-2",
eventIndex: 2,
sessionId: "s-1",
@@ -159,9 +159,9 @@ describe("RivetSessionPersistDriver", () => {
createdAt: 300,
});
- expect(await driver.getSession("s-1")).toBeNull();
- expect(await driver.getSession("s-2")).not.toBeNull();
- expect(await driver.getSession("s-3")).not.toBeNull();
+ expect(await driver.getSession("s-1")).toBeUndefined();
+ expect(await driver.getSession("s-2")).toBeDefined();
+ expect(await driver.getSession("s-3")).toBeDefined();
});
it("trims oldest events when maxEventsPerSession exceeded", async () => {
@@ -176,7 +176,7 @@ describe("RivetSessionPersistDriver", () => {
});
for (let i = 1; i <= 3; i++) {
- await driver.insertEvent({
+ await driver.insertEvent("s-1", {
id: `evt-${i}`,
eventIndex: i,
sessionId: "s-1",
diff --git a/sdks/persist-sqlite/src/index.ts b/sdks/persist-sqlite/src/index.ts
index 379c4ef..b04b0fc 100644
--- a/sdks/persist-sqlite/src/index.ts
+++ b/sdks/persist-sqlite/src/index.ts
@@ -15,16 +15,16 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
this.initialize();
}
- async getSession(id: string): Promise {
+ async getSession(id: string): Promise {
const row = this.db
.prepare(
- `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json
+ `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
FROM sessions WHERE id = ?`,
)
.get(id) as SessionRow | undefined;
if (!row) {
- return null;
+ return undefined;
}
return decodeSessionRow(row);
@@ -36,7 +36,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
const rows = this.db
.prepare(
- `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json
+ `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
FROM sessions
ORDER BY created_at ASC, id ASC
LIMIT ? OFFSET ?`,
@@ -56,14 +56,15 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
this.db
.prepare(
`INSERT INTO sessions (
- id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
agent = excluded.agent,
agent_session_id = excluded.agent_session_id,
last_connection_id = excluded.last_connection_id,
created_at = excluded.created_at,
destroyed_at = excluded.destroyed_at,
+ sandbox_id = excluded.sandbox_id,
session_init_json = excluded.session_init_json`,
)
.run(
@@ -73,6 +74,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
session.lastConnectionId,
session.createdAt,
session.destroyedAt ?? null,
+ session.sandboxId ?? null,
session.sessionInit ? JSON.stringify(session.sessionInit) : null,
);
}
@@ -101,7 +103,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
};
}
- async insertEvent(event: SessionEvent): Promise {
+ async insertEvent(_sessionId: string, event: SessionEvent): Promise {
this.db
.prepare(
`INSERT INTO events (
@@ -131,10 +133,16 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
last_connection_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
destroyed_at INTEGER,
+ sandbox_id TEXT,
session_init_json TEXT
)
`);
+ const sessionColumns = this.db.prepare(`PRAGMA table_info(sessions)`).all() as TableInfoRow[];
+ if (!sessionColumns.some((column) => column.name === "sandbox_id")) {
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN sandbox_id TEXT`);
+ }
+
this.ensureEventsTable();
}
@@ -223,6 +231,7 @@ type SessionRow = {
last_connection_id: string;
created_at: number;
destroyed_at: number | null;
+ sandbox_id: string | null;
session_init_json: string | null;
};
@@ -249,6 +258,7 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
lastConnectionId: row.last_connection_id,
createdAt: row.created_at,
destroyedAt: row.destroyed_at ?? undefined,
+ sandboxId: row.sandbox_id ?? undefined,
sessionInit: row.session_init_json ? (JSON.parse(row.session_init_json) as SessionRecord["sessionInit"]) : undefined,
};
}
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index 2c94592..64d42e6 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -14,6 +14,58 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
+ },
+ "./local": {
+ "types": "./dist/providers/local.d.ts",
+ "import": "./dist/providers/local.js"
+ },
+ "./e2b": {
+ "types": "./dist/providers/e2b.d.ts",
+ "import": "./dist/providers/e2b.js"
+ },
+ "./daytona": {
+ "types": "./dist/providers/daytona.d.ts",
+ "import": "./dist/providers/daytona.js"
+ },
+ "./docker": {
+ "types": "./dist/providers/docker.d.ts",
+ "import": "./dist/providers/docker.js"
+ },
+ "./vercel": {
+ "types": "./dist/providers/vercel.d.ts",
+ "import": "./dist/providers/vercel.js"
+ },
+ "./cloudflare": {
+ "types": "./dist/providers/cloudflare.d.ts",
+ "import": "./dist/providers/cloudflare.js"
+ }
+ },
+ "peerDependencies": {
+ "@cloudflare/sandbox": ">=0.1.0",
+ "@daytonaio/sdk": ">=0.12.0",
+ "@e2b/code-interpreter": ">=1.0.0",
+ "@vercel/sandbox": ">=0.1.0",
+ "dockerode": ">=4.0.0",
+ "get-port": ">=7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/sandbox": {
+ "optional": true
+ },
+ "@daytonaio/sdk": {
+ "optional": true
+ },
+ "@e2b/code-interpreter": {
+ "optional": true
+ },
+ "@vercel/sandbox": {
+ "optional": true
+ },
+ "dockerode": {
+ "optional": true
+ },
+ "get-port": {
+ "optional": true
}
},
"dependencies": {
@@ -33,8 +85,15 @@
"test:watch": "vitest"
},
"devDependencies": {
+ "@cloudflare/sandbox": ">=0.1.0",
+ "@daytonaio/sdk": ">=0.12.0",
+ "@e2b/code-interpreter": ">=1.0.0",
+ "@types/dockerode": "^4.0.0",
"@types/node": "^22.0.0",
"@types/ws": "^8.18.1",
+ "@vercel/sandbox": ">=0.1.0",
+ "dockerode": ">=4.0.0",
+ "get-port": ">=7.0.0",
"openapi-typescript": "^6.7.0",
"tsup": "^8.0.0",
"typescript": "^5.7.0",
diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts
index 9945c0a..94fbc3e 100644
--- a/sdks/typescript/src/client.ts
+++ b/sdks/typescript/src/client.ts
@@ -22,7 +22,7 @@ import {
type SetSessionModeResponse,
type SetSessionModeRequest,
} from "acp-http-client";
-import type { SandboxAgentSpawnHandle, SandboxAgentSpawnOptions } from "./spawn.ts";
+import type { SandboxProvider } from "./providers/types.ts";
import {
type AcpServerListResponse,
type AgentInfo,
@@ -101,6 +101,8 @@ interface SandboxAgentConnectCommonOptions {
replayMaxChars?: number;
signal?: AbortSignal;
token?: string;
+ skipHealthCheck?: boolean;
+ /** @deprecated Use skipHealthCheck instead. */
waitForHealth?: boolean | SandboxAgentHealthWaitOptions;
}
@@ -115,17 +117,24 @@ export type SandboxAgentConnectOptions =
});
export interface SandboxAgentStartOptions {
+ sandbox: SandboxProvider;
+ sandboxId?: string;
+ skipHealthCheck?: boolean;
fetch?: typeof fetch;
headers?: HeadersInit;
persist?: SessionPersistDriver;
replayMaxEvents?: number;
replayMaxChars?: number;
- spawn?: SandboxAgentSpawnOptions | boolean;
+ signal?: AbortSignal;
+ token?: string;
}
export interface SessionCreateRequest {
id?: string;
agent: string;
+ /** Shorthand for `sessionInit.cwd`. Ignored when `sessionInit` is provided. */
+ cwd?: string;
+ /** Full session init. When omitted, built from `cwd` (or default) with empty `mcpServers`. */
sessionInit?: Omit;
model?: string;
mode?: string;
@@ -135,6 +144,9 @@ export interface SessionCreateRequest {
export interface SessionResumeOrCreateRequest {
id: string;
agent: string;
+ /** Shorthand for `sessionInit.cwd`. Ignored when `sessionInit` is provided. */
+ cwd?: string;
+ /** Full session init. When omitted, built from `cwd` (or default) with empty `mcpServers`. */
sessionInit?: Omit;
model?: string;
mode?: string;
@@ -824,12 +836,14 @@ export class SandboxAgent {
private readonly defaultHeaders?: HeadersInit;
private readonly healthWait: NormalizedHealthWaitOptions;
private readonly healthWaitAbortController = new AbortController();
+ private sandboxProvider?: SandboxProvider;
+ private sandboxProviderId?: string;
+ private sandboxProviderRawId?: string;
private readonly persist: SessionPersistDriver;
private readonly replayMaxEvents: number;
private readonly replayMaxChars: number;
- private spawnHandle?: SandboxAgentSpawnHandle;
private healthPromise?: Promise;
private healthError?: Error;
private disposed = false;
@@ -857,7 +871,7 @@ export class SandboxAgent {
}
this.fetcher = resolvedFetch;
this.defaultHeaders = options.headers;
- this.healthWait = normalizeHealthWaitOptions(options.waitForHealth, options.signal);
+ this.healthWait = normalizeHealthWaitOptions(options.skipHealthCheck, options.waitForHealth, options.signal);
this.persist = options.persist ?? new InMemorySessionPersistDriver();
this.replayMaxEvents = normalizePositiveInt(options.replayMaxEvents, DEFAULT_REPLAY_MAX_EVENTS);
@@ -870,29 +884,79 @@ export class SandboxAgent {
return new SandboxAgent(options);
}
- static async start(options: SandboxAgentStartOptions = {}): Promise {
- const spawnOptions = normalizeSpawnOptions(options.spawn, true);
- if (!spawnOptions.enabled) {
- throw new Error("SandboxAgent.start requires spawn to be enabled.");
+ static async start(options: SandboxAgentStartOptions): Promise {
+ const provider = options.sandbox;
+ if (!provider.getUrl && !provider.getFetch) {
+ throw new Error(`Sandbox provider '${provider.name}' must implement getUrl() or getFetch().`);
}
- const { spawnSandboxAgent } = await import("./spawn.js");
- const resolvedFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
- const handle = await spawnSandboxAgent(spawnOptions, resolvedFetch);
+ const existingSandbox = options.sandboxId ? parseSandboxProviderId(options.sandboxId) : null;
- const client = new SandboxAgent({
- baseUrl: handle.baseUrl,
- token: handle.token,
- fetch: options.fetch,
- headers: options.headers,
- waitForHealth: false,
- persist: options.persist,
- replayMaxEvents: options.replayMaxEvents,
- replayMaxChars: options.replayMaxChars,
- });
+ if (existingSandbox && existingSandbox.provider !== provider.name) {
+ throw new Error(
+ `SandboxAgent.start received sandboxId '${options.sandboxId}' for provider '${existingSandbox.provider}', but the configured provider is '${provider.name}'.`,
+ );
+ }
- client.spawnHandle = handle;
- return client;
+ const rawSandboxId = existingSandbox?.rawId ?? (await provider.create());
+ const prefixedSandboxId = `${provider.name}/${rawSandboxId}`;
+ const createdSandbox = !existingSandbox;
+
+ if (existingSandbox) {
+ await provider.wake?.(rawSandboxId);
+ }
+
+ try {
+ const fetcher = await resolveProviderFetch(provider, rawSandboxId);
+ const baseUrl = provider.getUrl ? await provider.getUrl(rawSandboxId) : undefined;
+ const providerFetch = options.fetch ?? fetcher;
+ const commonConnectOptions = {
+ headers: options.headers,
+ persist: options.persist,
+ replayMaxEvents: options.replayMaxEvents,
+ replayMaxChars: options.replayMaxChars,
+ signal: options.signal,
+ skipHealthCheck: options.skipHealthCheck,
+ token: options.token ?? (await resolveProviderToken(provider, rawSandboxId)),
+ };
+
+ const client = providerFetch
+ ? new SandboxAgent({
+ ...commonConnectOptions,
+ baseUrl,
+ fetch: providerFetch,
+ })
+ : new SandboxAgent({
+ ...commonConnectOptions,
+ baseUrl: requireSandboxBaseUrl(baseUrl, provider.name),
+ });
+
+ client.sandboxProvider = provider;
+ client.sandboxProviderId = prefixedSandboxId;
+ client.sandboxProviderRawId = rawSandboxId;
+ return client;
+ } catch (error) {
+ if (createdSandbox) {
+ try {
+ await provider.destroy(rawSandboxId);
+ } catch {
+ // Best-effort cleanup if connect fails after provisioning.
+ }
+ }
+ throw error;
+ }
+ }
+
+ get sandboxId(): string | undefined {
+ return this.sandboxProviderId;
+ }
+
+ get sandbox(): SandboxProvider | undefined {
+ return this.sandboxProvider;
+ }
+
+ get inspectorUrl(): string {
+ return `${this.baseUrl.replace(/\/+$/, "")}/ui/`;
}
async dispose(): Promise {
@@ -922,10 +986,23 @@ export class SandboxAgent {
await connection.close();
}),
);
+ }
- if (this.spawnHandle) {
- await this.spawnHandle.dispose();
- this.spawnHandle = undefined;
+ async destroySandbox(): Promise {
+ const provider = this.sandboxProvider;
+ const rawSandboxId = this.sandboxProviderRawId;
+
+ try {
+ if (provider && rawSandboxId) {
+ await provider.destroy(rawSandboxId);
+ } else if (!provider || !rawSandboxId) {
+ throw new Error("SandboxAgent is not attached to a provisioned sandbox.");
+ }
+ } finally {
+ await this.dispose();
+ this.sandboxProvider = undefined;
+ this.sandboxProviderId = undefined;
+ this.sandboxProviderRawId = undefined;
}
}
@@ -956,7 +1033,7 @@ export class SandboxAgent {
const localSessionId = request.id?.trim() || randomId();
const live = await this.getLiveConnection(request.agent.trim());
- const sessionInit = normalizeSessionInit(request.sessionInit);
+ const sessionInit = normalizeSessionInit(request.sessionInit, request.cwd);
const response = await live.createRemoteSession(localSessionId, sessionInit);
@@ -966,6 +1043,7 @@ export class SandboxAgent {
agentSessionId: response.sessionId,
lastConnectionId: live.connectionId,
createdAt: nowMs(),
+ sandboxId: this.sandboxProviderId,
sessionInit,
configOptions: cloneConfigOptions(response.configOptions),
modes: cloneModes(response.modes),
@@ -2255,17 +2333,17 @@ function toAgentQuery(options: AgentQueryOptions | undefined): Record | undefined): Omit {
+function normalizeSessionInit(value: Omit | undefined, cwdShorthand?: string): Omit {
if (!value) {
return {
- cwd: defaultCwd(),
+ cwd: cwdShorthand ?? defaultCwd(),
mcpServers: [],
};
}
return {
...value,
- cwd: value.cwd ?? defaultCwd(),
+ cwd: value.cwd ?? cwdShorthand ?? defaultCwd(),
mcpServers: value.mcpServers ?? [],
};
}
@@ -2405,16 +2483,23 @@ function normalizePositiveInt(value: number | undefined, fallback: number): numb
return Math.floor(value as number);
}
-function normalizeHealthWaitOptions(value: boolean | SandboxAgentHealthWaitOptions | undefined, signal: AbortSignal | undefined): NormalizedHealthWaitOptions {
- if (value === false) {
+function normalizeHealthWaitOptions(
+ skipHealthCheck: boolean | undefined,
+ waitForHealth: boolean | SandboxAgentHealthWaitOptions | undefined,
+ signal: AbortSignal | undefined,
+): NormalizedHealthWaitOptions {
+ if (skipHealthCheck === true || waitForHealth === false) {
return { enabled: false };
}
- if (value === true || value === undefined) {
+ if (waitForHealth === true || waitForHealth === undefined) {
return { enabled: true, signal };
}
- const timeoutMs = typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0 ? Math.floor(value.timeoutMs) : undefined;
+ const timeoutMs =
+ typeof waitForHealth.timeoutMs === "number" && Number.isFinite(waitForHealth.timeoutMs) && waitForHealth.timeoutMs > 0
+ ? Math.floor(waitForHealth.timeoutMs)
+ : undefined;
return {
enabled: true,
@@ -2423,24 +2508,47 @@ function normalizeHealthWaitOptions(value: boolean | SandboxAgentHealthWaitOptio
};
}
-function normalizeSpawnOptions(
- spawn: SandboxAgentSpawnOptions | boolean | undefined,
- defaultEnabled: boolean,
-): SandboxAgentSpawnOptions & { enabled: boolean } {
- if (spawn === false) {
- return { enabled: false };
- }
-
- if (spawn === true || spawn === undefined) {
- return { enabled: defaultEnabled };
+function parseSandboxProviderId(sandboxId: string): { provider: string; rawId: string } {
+ const slashIndex = sandboxId.indexOf("/");
+ if (slashIndex < 1 || slashIndex === sandboxId.length - 1) {
+ throw new Error(`Sandbox IDs must be prefixed as "{provider}/{id}". Received '${sandboxId}'.`);
}
return {
- ...spawn,
- enabled: spawn.enabled ?? defaultEnabled,
+ provider: sandboxId.slice(0, slashIndex),
+ rawId: sandboxId.slice(slashIndex + 1),
};
}
+function requireSandboxBaseUrl(baseUrl: string | undefined, providerName: string): string {
+ if (!baseUrl) {
+ throw new Error(`Sandbox provider '${providerName}' did not return a base URL.`);
+ }
+ return baseUrl;
+}
+
+async function resolveProviderFetch(provider: SandboxProvider, rawSandboxId: string): Promise {
+ if (provider.getFetch) {
+ return await provider.getFetch(rawSandboxId);
+ }
+
+ return undefined;
+}
+
+async function resolveProviderToken(provider: SandboxProvider, rawSandboxId: string): Promise {
+ const maybeGetToken = (
+ provider as SandboxProvider & {
+ getToken?: (sandboxId: string) => string | undefined | Promise;
+ }
+ ).getToken;
+ if (typeof maybeGetToken !== "function") {
+ return undefined;
+ }
+
+ const token = await maybeGetToken.call(provider, rawSandboxId);
+ return typeof token === "string" && token ? token : undefined;
+}
+
async function readProblem(response: Response): Promise {
try {
const text = await response.clone().text();
diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts
index 99bc1b6..f0ebe2e 100644
--- a/sdks/typescript/src/index.ts
+++ b/sdks/typescript/src/index.ts
@@ -38,6 +38,7 @@ export type {
export type { InspectorUrlOptions } from "./inspector.ts";
export { InMemorySessionPersistDriver } from "./types.ts";
+export type { SandboxProvider } from "./providers/types.ts";
export type {
AcpEnvelope,
diff --git a/sdks/typescript/src/providers/cloudflare.ts b/sdks/typescript/src/providers/cloudflare.ts
new file mode 100644
index 0000000..c17adfc
--- /dev/null
+++ b/sdks/typescript/src/providers/cloudflare.ts
@@ -0,0 +1,79 @@
+import type { SandboxProvider } from "./types.ts";
+
+const DEFAULT_AGENT_PORT = 3000;
+
+export interface CloudflareSandboxClient {
+ create?(options?: Record): Promise<{ id?: string; sandboxId?: string }>;
+ connect?(
+ sandboxId: string,
+ options?: Record,
+ ): Promise<{
+ close?(): Promise;
+ stop?(): Promise;
+ containerFetch(input: RequestInfo | URL, init?: RequestInit, port?: number): Promise;
+ }>;
+}
+
+export interface CloudflareProviderOptions {
+ sdk: CloudflareSandboxClient;
+ create?: Record | (() => Record | Promise>);
+ agentPort?: number;
+}
+
+async function resolveCreateOptions(value: CloudflareProviderOptions["create"]): Promise> {
+ if (!value) {
+ return {};
+ }
+ if (typeof value === "function") {
+ return await value();
+ }
+ return value;
+}
+
+export function cloudflare(options: CloudflareProviderOptions): SandboxProvider {
+ const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
+ const sdk = options.sdk;
+
+ return {
+ name: "cloudflare",
+ async create(): Promise {
+ if (typeof sdk.create !== "function") {
+ throw new Error('sandbox provider "cloudflare" requires a sdk with a `create()` method.');
+ }
+ const sandbox = await sdk.create(await resolveCreateOptions(options.create));
+ const sandboxId = sandbox.sandboxId ?? sandbox.id;
+ if (!sandboxId) {
+ throw new Error("cloudflare sandbox did not return an id");
+ }
+ return sandboxId;
+ },
+ async destroy(sandboxId: string): Promise {
+ if (typeof sdk.connect !== "function") {
+ throw new Error('sandbox provider "cloudflare" requires a sdk with a `connect()` method.');
+ }
+ const sandbox = await sdk.connect(sandboxId);
+ if (typeof sandbox.close === "function") {
+ await sandbox.close();
+ return;
+ }
+ if (typeof sandbox.stop === "function") {
+ await sandbox.stop();
+ }
+ },
+ async getFetch(sandboxId: string): Promise {
+ if (typeof sdk.connect !== "function") {
+ throw new Error('sandbox provider "cloudflare" requires a sdk with a `connect()` method.');
+ }
+ const sandbox = await sdk.connect(sandboxId);
+ return async (input, init) =>
+ sandbox.containerFetch(
+ input,
+ {
+ ...(init ?? {}),
+ signal: undefined,
+ },
+ agentPort,
+ );
+ },
+ };
+}
diff --git a/sdks/typescript/src/providers/daytona.ts b/sdks/typescript/src/providers/daytona.ts
new file mode 100644
index 0000000..eb34b88
--- /dev/null
+++ b/sdks/typescript/src/providers/daytona.ts
@@ -0,0 +1,65 @@
+import { Daytona } from "@daytonaio/sdk";
+import type { SandboxProvider } from "./types.ts";
+import { DEFAULT_SANDBOX_AGENT_IMAGE, buildServerStartCommand } from "./shared.ts";
+
+const DEFAULT_AGENT_PORT = 3000;
+const DEFAULT_PREVIEW_TTL_SECONDS = 4 * 60 * 60;
+
+type DaytonaCreateParams = NonNullable[0]>;
+
+export interface DaytonaProviderOptions {
+ create?: DaytonaCreateParams | (() => DaytonaCreateParams | Promise);
+ image?: string;
+ agentPort?: number;
+ previewTtlSeconds?: number;
+ deleteTimeoutSeconds?: number;
+}
+
+async function resolveCreateOptions(value: DaytonaProviderOptions["create"]): Promise {
+ if (!value) return undefined;
+ if (typeof value === "function") return await value();
+ return value;
+}
+
+export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider {
+ const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
+ const image = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE;
+ const previewTtlSeconds = options.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS;
+ const client = new Daytona();
+
+ return {
+ name: "daytona",
+ async create(): Promise {
+ const createOpts = await resolveCreateOptions(options.create);
+ const sandbox = await client.create({
+ image,
+ autoStopInterval: 0,
+ ...createOpts,
+ } as DaytonaCreateParams);
+ await sandbox.process.executeCommand(buildServerStartCommand(agentPort));
+ return sandbox.id;
+ },
+ async destroy(sandboxId: string): Promise {
+ const sandbox = await client.get(sandboxId);
+ if (!sandbox) {
+ return;
+ }
+ await sandbox.delete(options.deleteTimeoutSeconds);
+ },
+ async getUrl(sandboxId: string): Promise {
+ const sandbox = await client.get(sandboxId);
+ if (!sandbox) {
+ throw new Error(`daytona sandbox not found: ${sandboxId}`);
+ }
+ const preview = await sandbox.getSignedPreviewUrl(agentPort, previewTtlSeconds);
+ return typeof preview === "string" ? preview : preview.url;
+ },
+ async wake(sandboxId: string): Promise {
+ const sandbox = await client.get(sandboxId);
+ if (!sandbox) {
+ throw new Error(`daytona sandbox not found: ${sandboxId}`);
+ }
+ await sandbox.process.executeCommand(buildServerStartCommand(agentPort));
+ },
+ };
+}
diff --git a/sdks/typescript/src/providers/docker.ts b/sdks/typescript/src/providers/docker.ts
new file mode 100644
index 0000000..9e49687
--- /dev/null
+++ b/sdks/typescript/src/providers/docker.ts
@@ -0,0 +1,85 @@
+import Docker from "dockerode";
+import getPort from "get-port";
+import type { SandboxProvider } from "./types.ts";
+import { DEFAULT_SANDBOX_AGENT_IMAGE } from "./shared.ts";
+
+const DEFAULT_HOST = "127.0.0.1";
+const DEFAULT_AGENT_PORT = 3000;
+
+export interface DockerProviderOptions {
+ image?: string;
+ host?: string;
+ agentPort?: number;
+ env?: string[] | (() => string[] | Promise);
+ binds?: string[] | (() => string[] | Promise);
+ createContainerOptions?: Record;
+}
+
+async function resolveValue(value: T | (() => T | Promise) | undefined, fallback: T): Promise {
+ if (value === undefined) {
+ return fallback;
+ }
+ if (typeof value === "function") {
+ return await (value as () => T | Promise)();
+ }
+ return value;
+}
+
+function extractMappedPort(
+ inspect: { NetworkSettings?: { Ports?: Record | null | undefined> } },
+ containerPort: number,
+): number {
+ const hostPort = inspect.NetworkSettings?.Ports?.[`${containerPort}/tcp`]?.[0]?.HostPort;
+ if (!hostPort) {
+ throw new Error(`docker sandbox-agent port ${containerPort} is not published`);
+ }
+ return Number(hostPort);
+}
+
+export function docker(options: DockerProviderOptions = {}): SandboxProvider {
+ const image = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE;
+ const host = options.host ?? DEFAULT_HOST;
+ const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
+ const client = new Docker({ socketPath: "/var/run/docker.sock" });
+
+ return {
+ name: "docker",
+ async create(): Promise {
+ const hostPort = await getPort();
+ const env = await resolveValue(options.env, []);
+ const binds = await resolveValue(options.binds, []);
+
+ const container = await client.createContainer({
+ Image: image,
+ Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", String(agentPort)],
+ Env: env,
+ ExposedPorts: { [`${agentPort}/tcp`]: {} },
+ HostConfig: {
+ AutoRemove: true,
+ Binds: binds,
+ PortBindings: {
+ [`${agentPort}/tcp`]: [{ HostPort: String(hostPort) }],
+ },
+ },
+ ...(options.createContainerOptions ?? {}),
+ });
+
+ await container.start();
+ return container.id;
+ },
+ async destroy(sandboxId: string): Promise {
+ const container = client.getContainer(sandboxId);
+ try {
+ await container.stop({ t: 5 });
+ } catch {}
+ try {
+ await container.remove({ force: true });
+ } catch {}
+ },
+ async getUrl(sandboxId: string): Promise {
+ const container = client.getContainer(sandboxId);
+ const hostPort = extractMappedPort(await container.inspect(), agentPort);
+ return `http://${host}:${hostPort}`;
+ },
+ };
+}
diff --git a/sdks/typescript/src/providers/e2b.ts b/sdks/typescript/src/providers/e2b.ts
new file mode 100644
index 0000000..833fe9d
--- /dev/null
+++ b/sdks/typescript/src/providers/e2b.ts
@@ -0,0 +1,57 @@
+import { Sandbox } from "@e2b/code-interpreter";
+import type { SandboxProvider } from "./types.ts";
+import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
+
+const DEFAULT_AGENT_PORT = 3000;
+
+export interface E2BProviderOptions {
+ create?: Record | (() => Record | Promise>);
+ connect?: Record | ((sandboxId: string) => Record | Promise>);
+ agentPort?: number;
+}
+
+async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderOptions["connect"], sandboxId?: string): Promise> {
+ if (!value) return {};
+ if (typeof value === "function") {
+ if (sandboxId) {
+ return await (value as (id: string) => Record | Promise>)(sandboxId);
+ }
+ return await (value as () => Record | Promise>)();
+ }
+ return value;
+}
+
+export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
+ const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
+
+ return {
+ name: "e2b",
+ async create(): Promise {
+ const createOpts = await resolveOptions(options.create);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const sandbox = await Sandbox.create({ allowInternetAccess: true, ...createOpts } as any);
+
+ await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => {
+ if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);
+ });
+ for (const agent of DEFAULT_AGENTS) {
+ await sandbox.commands.run(`sandbox-agent install-agent ${agent}`).then((r) => {
+ if (r.exitCode !== 0) throw new Error(`e2b agent install failed: ${agent}\n${r.stderr}`);
+ });
+ }
+ await sandbox.commands.run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, { background: true, timeoutMs: 0 });
+
+ return sandbox.sandboxId;
+ },
+ async destroy(sandboxId: string): Promise {
+ const connectOpts = await resolveOptions(options.connect, sandboxId);
+ const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
+ await sandbox.kill();
+ },
+ async getUrl(sandboxId: string): Promise {
+ const connectOpts = await resolveOptions(options.connect, sandboxId);
+ const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
+ return `https://${sandbox.getHost(agentPort)}`;
+ },
+ };
+}
diff --git a/sdks/typescript/src/providers/local.ts b/sdks/typescript/src/providers/local.ts
new file mode 100644
index 0000000..18fc3d4
--- /dev/null
+++ b/sdks/typescript/src/providers/local.ts
@@ -0,0 +1,84 @@
+import { spawnSandboxAgent, type SandboxAgentSpawnHandle, type SandboxAgentSpawnLogMode, type SandboxAgentSpawnOptions } from "../spawn.ts";
+import type { SandboxProvider } from "./types.ts";
+
+export interface LocalProviderOptions {
+ host?: string;
+ port?: number;
+ token?: string;
+ binaryPath?: string;
+ log?: SandboxAgentSpawnLogMode;
+ env?: Record;
+}
+
+const localSandboxes = new Map();
+
+type LocalSandboxProvider = SandboxProvider & {
+ getToken(sandboxId: string): Promise;
+};
+
+export function local(options: LocalProviderOptions = {}): SandboxProvider {
+ const provider: LocalSandboxProvider = {
+ name: "local",
+ async create(): Promise {
+ const handle = await spawnSandboxAgent(
+ {
+ host: options.host,
+ port: options.port,
+ token: options.token,
+ binaryPath: options.binaryPath,
+ log: options.log,
+ env: options.env,
+ } satisfies SandboxAgentSpawnOptions,
+ globalThis.fetch?.bind(globalThis),
+ );
+
+ const rawSandboxId = baseUrlToSandboxId(handle.baseUrl);
+ localSandboxes.set(rawSandboxId, handle);
+ return rawSandboxId;
+ },
+ async destroy(sandboxId: string): Promise {
+ const handle = localSandboxes.get(sandboxId);
+ if (!handle) {
+ return;
+ }
+ localSandboxes.delete(sandboxId);
+ await handle.dispose();
+ },
+ async getUrl(sandboxId: string): Promise {
+ return `http://${sandboxId}`;
+ },
+ async getFetch(sandboxId: string): Promise {
+ const handle = localSandboxes.get(sandboxId);
+ const token = options.token ?? handle?.token;
+ const fetcher = globalThis.fetch?.bind(globalThis);
+ if (!fetcher) {
+ throw new Error("Fetch API is not available; provide a fetch implementation.");
+ }
+
+ if (!token) {
+ return fetcher;
+ }
+
+ return async (input, init) => {
+ const request = new Request(input, init);
+ const targetUrl = new URL(request.url);
+ targetUrl.protocol = "http:";
+ targetUrl.host = sandboxId;
+ const headers = new Headers(request.headers);
+ if (!headers.has("authorization")) {
+ headers.set("authorization", `Bearer ${token}`);
+ }
+ const forwarded = new Request(targetUrl.toString(), request);
+ return fetcher(new Request(forwarded, { headers }));
+ };
+ },
+ async getToken(sandboxId: string): Promise {
+ return options.token ?? localSandboxes.get(sandboxId)?.token;
+ },
+ };
+ return provider;
+}
+
+function baseUrlToSandboxId(baseUrl: string): string {
+ return new URL(baseUrl).host;
+}
diff --git a/sdks/typescript/src/providers/shared.ts b/sdks/typescript/src/providers/shared.ts
new file mode 100644
index 0000000..d838a0a
--- /dev/null
+++ b/sdks/typescript/src/providers/shared.ts
@@ -0,0 +1,7 @@
+export const DEFAULT_SANDBOX_AGENT_IMAGE = "rivetdev/sandbox-agent:0.3.2-full";
+export const SANDBOX_AGENT_INSTALL_SCRIPT = "https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh";
+export const DEFAULT_AGENTS = ["claude", "codex"] as const;
+
+export function buildServerStartCommand(port: number): string {
+ return `nohup sandbox-agent server --no-token --host 0.0.0.0 --port ${port} >/tmp/sandbox-agent.log 2>&1 &`;
+}
diff --git a/sdks/typescript/src/providers/types.ts b/sdks/typescript/src/providers/types.ts
new file mode 100644
index 0000000..e51163f
--- /dev/null
+++ b/sdks/typescript/src/providers/types.ts
@@ -0,0 +1,28 @@
+export interface SandboxProvider {
+ /** Provider name. Must match the prefix in sandbox IDs (for example "e2b"). */
+ name: string;
+
+ /** Provision a new sandbox and return the provider-specific ID. */
+ create(): Promise;
+
+ /** Permanently tear down a sandbox. */
+ destroy(sandboxId: string): Promise;
+
+ /**
+ * Return the sandbox-agent base URL for this sandbox.
+ * Providers that cannot expose a URL should implement `getFetch()` instead.
+ */
+ getUrl?(sandboxId: string): Promise;
+
+ /**
+ * Return a fetch implementation that routes requests to the sandbox.
+ * Providers that expose a URL can implement `getUrl()` instead.
+ */
+ getFetch?(sandboxId: string): Promise;
+
+ /**
+ * Optional hook invoked before reconnecting to an existing sandbox.
+ * Useful for providers where the sandbox-agent process may need to be restarted.
+ */
+ wake?(sandboxId: string): Promise;
+}
diff --git a/sdks/typescript/src/providers/vercel.ts b/sdks/typescript/src/providers/vercel.ts
new file mode 100644
index 0000000..98d21c1
--- /dev/null
+++ b/sdks/typescript/src/providers/vercel.ts
@@ -0,0 +1,57 @@
+import { Sandbox } from "@vercel/sandbox";
+import type { SandboxProvider } from "./types.ts";
+import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
+
+const DEFAULT_AGENT_PORT = 3000;
+
+export interface VercelProviderOptions {
+ create?: Record | (() => Record | Promise>);
+ agentPort?: number;
+}
+
+async function resolveCreateOptions(value: VercelProviderOptions["create"], agentPort: number): Promise> {
+ const resolved = typeof value === "function" ? await value() : (value ?? {});
+ return {
+ ports: [agentPort],
+ ...resolved,
+ };
+}
+
+async function runVercelCommand(sandbox: InstanceType, cmd: string, args: string[] = []): Promise {
+ const result = await sandbox.runCommand({ cmd, args });
+ if (result.exitCode !== 0) {
+ const stderr = await result.stderr();
+ throw new Error(`vercel command failed: ${cmd} ${args.join(" ")}\n${stderr}`);
+ }
+}
+
+export function vercel(options: VercelProviderOptions = {}): SandboxProvider {
+ const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
+
+ return {
+ name: "vercel",
+ async create(): Promise {
+ const sandbox = await Sandbox.create((await resolveCreateOptions(options.create, agentPort)) as Parameters[0]);
+
+ await runVercelCommand(sandbox, "sh", ["-c", `curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`]);
+ for (const agent of DEFAULT_AGENTS) {
+ await runVercelCommand(sandbox, "sandbox-agent", ["install-agent", agent]);
+ }
+ await sandbox.runCommand({
+ cmd: "sandbox-agent",
+ args: ["server", "--no-token", "--host", "0.0.0.0", "--port", String(agentPort)],
+ detached: true,
+ });
+
+ return sandbox.sandboxId;
+ },
+ async destroy(sandboxId: string): Promise {
+ const sandbox = await Sandbox.get({ sandboxId });
+ await sandbox.stop();
+ },
+ async getUrl(sandboxId: string): Promise {
+ const sandbox = await Sandbox.get({ sandboxId });
+ return sandbox.domain(agentPort);
+ },
+ };
+}
diff --git a/sdks/typescript/src/types.ts b/sdks/typescript/src/types.ts
index 6865690..f2a7af3 100644
--- a/sdks/typescript/src/types.ts
+++ b/sdks/typescript/src/types.ts
@@ -98,6 +98,7 @@ export interface SessionRecord {
lastConnectionId: string;
createdAt: number;
destroyedAt?: number;
+ sandboxId?: string;
sessionInit?: Omit;
configOptions?: SessionConfigOption[];
modes?: SessionModeState | null;
@@ -131,11 +132,11 @@ export interface ListEventsRequest extends ListPageRequest {
}
export interface SessionPersistDriver {
- getSession(id: string): Promise;
+ getSession(id: string): Promise;
listSessions(request?: ListPageRequest): Promise>;
updateSession(session: SessionRecord): Promise;
listEvents(request: ListEventsRequest): Promise>;
- insertEvent(event: SessionEvent): Promise;
+ insertEvent(sessionId: string, event: SessionEvent): Promise;
}
export interface InMemorySessionPersistDriverOptions {
@@ -158,9 +159,9 @@ export class InMemorySessionPersistDriver implements SessionPersistDriver {
this.maxEventsPerSession = normalizeCap(options.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION);
}
- async getSession(id: string): Promise {
+ async getSession(id: string): Promise {
const session = this.sessions.get(id);
- return session ? cloneSessionRecord(session) : null;
+ return session ? cloneSessionRecord(session) : undefined;
}
async listSessions(request: ListPageRequest = {}): Promise> {
@@ -219,15 +220,15 @@ export class InMemorySessionPersistDriver implements SessionPersistDriver {
};
}
- async insertEvent(event: SessionEvent): Promise {
- const events = this.eventsBySession.get(event.sessionId) ?? [];
+ async insertEvent(sessionId: string, event: SessionEvent): Promise {
+ const events = this.eventsBySession.get(sessionId) ?? [];
events.push(cloneSessionEvent(event));
if (events.length > this.maxEventsPerSession) {
events.splice(0, events.length - this.maxEventsPerSession);
}
- this.eventsBySession.set(event.sessionId, events);
+ this.eventsBySession.set(sessionId, events);
}
}
diff --git a/sdks/typescript/tests/providers.test.ts b/sdks/typescript/tests/providers.test.ts
new file mode 100644
index 0000000..4b874b0
--- /dev/null
+++ b/sdks/typescript/tests/providers.test.ts
@@ -0,0 +1,379 @@
+import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
+import { createRequire } from "node:module";
+import { existsSync, mkdtempSync, rmSync } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { tmpdir } from "node:os";
+import { execSync } from "node:child_process";
+
+const _require = createRequire(import.meta.url);
+import { InMemorySessionPersistDriver, SandboxAgent, type SandboxProvider } from "../src/index.ts";
+import { local } from "../src/providers/local.ts";
+import { docker } from "../src/providers/docker.ts";
+import { e2b } from "../src/providers/e2b.ts";
+import { daytona } from "../src/providers/daytona.ts";
+import { vercel } from "../src/providers/vercel.ts";
+import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+function findBinary(): string | null {
+ if (process.env.SANDBOX_AGENT_BIN) {
+ return process.env.SANDBOX_AGENT_BIN;
+ }
+
+ const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
+ for (const candidate of cargoPaths) {
+ if (existsSync(candidate)) {
+ return candidate;
+ }
+ }
+
+ return null;
+}
+
+const BINARY_PATH = findBinary();
+if (!BINARY_PATH) {
+ throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
+}
+if (!process.env.SANDBOX_AGENT_BIN) {
+ process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
+}
+
+function isModuleAvailable(name: string): boolean {
+ try {
+ _require.resolve(name);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function isDockerAvailable(): boolean {
+ try {
+ execSync("docker info", { stdio: "ignore", timeout: 5_000 });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Provider registry — each entry defines how to create a provider and
+// what preconditions are required for it to run.
+// ---------------------------------------------------------------------------
+
+interface ProviderEntry {
+ name: string;
+ /** Human-readable reasons this provider can't run, or empty if ready. */
+ skipReasons: string[];
+ /** Return a fresh provider instance for a single test. */
+ createProvider: () => SandboxProvider;
+ /** Optional per-provider setup (e.g. create temp dirs). Returns cleanup fn. */
+ setup?: () => { cleanup: () => void };
+ /** Agent to use for session tests. */
+ agent: string;
+ /** Timeout for start() — remote providers need longer. */
+ startTimeoutMs?: number;
+ /** Some providers (e.g. local) can verify the sandbox is gone after destroy. */
+ canVerifyDestroyedSandbox?: boolean;
+ /**
+ * Whether session tests (createSession, prompt) should run.
+ * The mock agent only works with local provider (requires mock-acp process binary).
+ * Remote providers need a real agent (claude) which requires compatible server version + API keys.
+ */
+ sessionTestsEnabled: boolean;
+}
+
+function missingEnvVars(...vars: string[]): string[] {
+ const missing = vars.filter((v) => !process.env[v]);
+ return missing.length > 0 ? [`missing env: ${missing.join(", ")}`] : [];
+}
+
+function missingModules(...modules: string[]): string[] {
+ const missing = modules.filter((m) => !isModuleAvailable(m));
+ return missing.length > 0 ? [`missing npm packages: ${missing.join(", ")}`] : [];
+}
+
+function collectApiKeys(): Record {
+ const keys: Record = {};
+ if (process.env.ANTHROPIC_API_KEY) keys.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
+ if (process.env.OPENAI_API_KEY) keys.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+ return keys;
+}
+
+function buildProviders(): ProviderEntry[] {
+ const entries: ProviderEntry[] = [];
+
+ // --- local ---
+ // Uses the mock-acp process binary created by prepareMockAgentDataHome.
+ {
+ let dataHome: string | undefined;
+ entries.push({
+ name: "local",
+ skipReasons: [],
+ agent: "mock",
+ canVerifyDestroyedSandbox: true,
+ sessionTestsEnabled: true,
+ setup() {
+ dataHome = mkdtempSync(join(tmpdir(), "sdk-provider-local-"));
+ return {
+ cleanup: () => {
+ if (dataHome) rmSync(dataHome, { recursive: true, force: true });
+ },
+ };
+ },
+ createProvider() {
+ return local({
+ log: "silent",
+ env: prepareMockAgentDataHome(dataHome!),
+ });
+ },
+ });
+ }
+
+ // --- docker ---
+ // Requires SANDBOX_AGENT_DOCKER_IMAGE (e.g. "sandbox-agent-dev:local").
+ // Session tests disabled: released server images use a different ACP protocol
+ // version than the current SDK branch, causing "Query closed before response
+ // received" errors on session creation.
+ {
+ entries.push({
+ name: "docker",
+ skipReasons: [
+ ...missingEnvVars("SANDBOX_AGENT_DOCKER_IMAGE"),
+ ...missingModules("dockerode", "get-port"),
+ ...(isDockerAvailable() ? [] : ["Docker daemon not available"]),
+ ],
+ agent: "claude",
+ startTimeoutMs: 180_000,
+ canVerifyDestroyedSandbox: false,
+ sessionTestsEnabled: false,
+ createProvider() {
+ const apiKeys = [
+ process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
+ process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
+ ].filter(Boolean);
+ return docker({
+ image: process.env.SANDBOX_AGENT_DOCKER_IMAGE,
+ env: apiKeys,
+ });
+ },
+ });
+ }
+
+ // --- e2b ---
+ // Session tests disabled: see docker comment above (ACP protocol mismatch).
+ {
+ entries.push({
+ name: "e2b",
+ skipReasons: [...missingEnvVars("E2B_API_KEY"), ...missingModules("@e2b/code-interpreter")],
+ agent: "claude",
+ startTimeoutMs: 300_000,
+ canVerifyDestroyedSandbox: false,
+ sessionTestsEnabled: false,
+ createProvider() {
+ return e2b({
+ create: { envs: collectApiKeys() },
+ });
+ },
+ });
+ }
+
+ // --- daytona ---
+ // Session tests disabled: see docker comment above (ACP protocol mismatch).
+ {
+ entries.push({
+ name: "daytona",
+ skipReasons: [...missingEnvVars("DAYTONA_API_KEY"), ...missingModules("@daytonaio/sdk")],
+ agent: "claude",
+ startTimeoutMs: 300_000,
+ canVerifyDestroyedSandbox: false,
+ sessionTestsEnabled: false,
+ createProvider() {
+ return daytona({
+ create: { envVars: collectApiKeys() },
+ });
+ },
+ });
+ }
+
+ // --- vercel ---
+ // Session tests disabled: see docker comment above (ACP protocol mismatch).
+ {
+ entries.push({
+ name: "vercel",
+ skipReasons: [...missingEnvVars("VERCEL_ACCESS_TOKEN"), ...missingModules("@vercel/sandbox")],
+ agent: "claude",
+ startTimeoutMs: 300_000,
+ canVerifyDestroyedSandbox: false,
+ sessionTestsEnabled: false,
+ createProvider() {
+ return vercel({
+ create: { env: collectApiKeys() },
+ });
+ },
+ });
+ }
+
+ return entries;
+}
+
+// ---------------------------------------------------------------------------
+// Shared test suite — runs the same assertions against every provider.
+//
+// Provider lifecycle tests (start, sandboxId, reconnect, destroy) use only
+// listAgents() and never create sessions — these work regardless of which
+// agents are installed or whether API keys are present.
+//
+// Session tests (createSession, prompt) are only enabled for providers where
+// the agent is known to work. For local, the mock-acp process binary is
+// created by test setup. For remote providers, a real agent (claude) is used
+// which requires ANTHROPIC_API_KEY and a compatible server version.
+// ---------------------------------------------------------------------------
+
+function providerSuite(entry: ProviderEntry) {
+ const skip = entry.skipReasons.length > 0;
+
+ const descFn = skip ? describe.skip : describe;
+
+ descFn(`SandboxProvider: ${entry.name}`, () => {
+ let sdk: SandboxAgent | undefined;
+ let cleanupFn: (() => void) | undefined;
+
+ if (skip) {
+ it.skip(`skipped — ${entry.skipReasons.join("; ")}`, () => {});
+ return;
+ }
+
+ beforeAll(() => {
+ const result = entry.setup?.();
+ cleanupFn = result?.cleanup;
+ });
+
+ afterEach(async () => {
+ if (!sdk) return;
+ await sdk.destroySandbox().catch(async () => {
+ await sdk?.dispose().catch(() => {});
+ });
+ sdk = undefined;
+ }, 30_000);
+
+ afterAll(() => {
+ cleanupFn?.();
+ });
+
+ // -- lifecycle tests (no session creation) --
+
+ it(
+ "starts with a prefixed sandboxId and passes health",
+ async () => {
+ sdk = await SandboxAgent.start({ sandbox: entry.createProvider() });
+ expect(sdk.sandboxId).toMatch(new RegExp(`^${entry.name}/`));
+
+ // listAgents() awaits the internal health gate, confirming the server is ready.
+ const agents = await sdk.listAgents();
+ expect(agents.agents.length).toBeGreaterThan(0);
+ },
+ entry.startTimeoutMs,
+ );
+
+ it("rejects mismatched sandboxId prefixes", async () => {
+ await expect(
+ SandboxAgent.start({
+ sandbox: entry.createProvider(),
+ sandboxId: "wrong-provider/example",
+ }),
+ ).rejects.toThrow(/provider/i);
+ });
+
+ it(
+ "reconnects after dispose without destroying the sandbox",
+ async () => {
+ sdk = await SandboxAgent.start({ sandbox: entry.createProvider() });
+ const sandboxId = sdk.sandboxId;
+ expect(sandboxId).toBeTruthy();
+
+ await sdk.dispose();
+
+ const reconnected = await SandboxAgent.start({
+ sandbox: entry.createProvider(),
+ sandboxId,
+ });
+
+ const agents = await reconnected.listAgents();
+ expect(agents.agents.length).toBeGreaterThan(0);
+ sdk = reconnected;
+ },
+ entry.startTimeoutMs ? entry.startTimeoutMs * 2 : undefined,
+ );
+
+ it(
+ "destroySandbox tears the sandbox down",
+ async () => {
+ sdk = await SandboxAgent.start({ sandbox: entry.createProvider() });
+ const sandboxId = sdk.sandboxId;
+ expect(sandboxId).toBeTruthy();
+
+ await sdk.destroySandbox();
+ sdk = undefined;
+
+ if (entry.canVerifyDestroyedSandbox) {
+ const reconnected = await SandboxAgent.start({
+ sandbox: entry.createProvider(),
+ sandboxId,
+ skipHealthCheck: true,
+ });
+ await expect(reconnected.listAgents()).rejects.toThrow();
+ }
+ },
+ entry.startTimeoutMs,
+ );
+
+ // -- session tests (require working agent) --
+
+ const sessionIt = entry.sessionTestsEnabled ? it : it.skip;
+
+ sessionIt(
+ "creates sessions with persisted sandboxId",
+ async () => {
+ const persist = new InMemorySessionPersistDriver();
+ sdk = await SandboxAgent.start({ sandbox: entry.createProvider(), persist });
+
+ const session = await sdk.createSession({ agent: entry.agent });
+ const record = await persist.getSession(session.id);
+
+ expect(record?.sandboxId).toBe(sdk.sandboxId);
+ },
+ entry.startTimeoutMs,
+ );
+
+ sessionIt(
+ "sends a prompt and receives a response",
+ async () => {
+ sdk = await SandboxAgent.start({ sandbox: entry.createProvider() });
+
+ const session = await sdk.createSession({ agent: entry.agent });
+ const events: unknown[] = [];
+ const off = session.onEvent((event) => {
+ events.push(event);
+ });
+
+ const result = await session.prompt([{ type: "text", text: "Say hello in one word." }]);
+ off();
+
+ expect(result.stopReason).toBe("end_turn");
+ expect(events.length).toBeGreaterThan(0);
+ },
+ entry.startTimeoutMs ? entry.startTimeoutMs * 2 : 30_000,
+ );
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Register all providers
+// ---------------------------------------------------------------------------
+
+for (const entry of buildProviders()) {
+ providerSuite(entry);
+}
diff --git a/sdks/typescript/tsup.config.ts b/sdks/typescript/tsup.config.ts
index faf3167..fe84102 100644
--- a/sdks/typescript/tsup.config.ts
+++ b/sdks/typescript/tsup.config.ts
@@ -1,9 +1,18 @@
import { defineConfig } from "tsup";
export default defineConfig({
- entry: ["src/index.ts"],
+ entry: [
+ "src/index.ts",
+ "src/providers/local.ts",
+ "src/providers/e2b.ts",
+ "src/providers/daytona.ts",
+ "src/providers/docker.ts",
+ "src/providers/vercel.ts",
+ "src/providers/cloudflare.ts",
+ ],
format: ["esm"],
dts: true,
clean: true,
sourcemap: true,
+ external: ["@cloudflare/sandbox", "@daytonaio/sdk", "@e2b/code-interpreter", "@vercel/sandbox", "dockerode", "get-port"],
});
diff --git a/sdks/typescript/vitest.config.ts b/sdks/typescript/vitest.config.ts
index 8676010..e83d10a 100644
--- a/sdks/typescript/vitest.config.ts
+++ b/sdks/typescript/vitest.config.ts
@@ -4,5 +4,7 @@ export default defineConfig({
test: {
include: ["tests/**/*.test.ts"],
testTimeout: 30000,
+ teardownTimeout: 10000,
+ pool: "forks",
},
});