mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-19 06:01:21 +00:00
SDK sandbox provisioning: built-in providers, docs restructure, and quickstart overhaul
- Add built-in sandbox providers (local, docker, e2b, daytona, vercel, cloudflare) to the TypeScript SDK so users import directly instead of passing client instances - Restructure docs: rename architecture to orchestration-architecture, add new architecture page for server overview, improve getting started flow - Rewrite quickstart to be TypeScript-first with provider CodeGroup and custom provider accordion - Update all examples to use new provider APIs - Update persist drivers and foundry for new SDK surface Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3426cbc6ec
commit
6a42f06342
53 changed files with 1689 additions and 667 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<br/>(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.
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
43
docs/orchestration-architecture.mdx
Normal file
43
docs/orchestration-architecture.mdx
Normal file
|
|
@ -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).
|
||||
|
|
@ -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"
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step title="Install skill (optional)">
|
||||
<Step title="Install">
|
||||
<Tabs>
|
||||
<Tab title="npx">
|
||||
<Tab title="npm">
|
||||
```bash
|
||||
npx skills add rivet-dev/skills -s sandbox-agent
|
||||
npm install sandbox-agent@0.3.x
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="bunx">
|
||||
<Tab title="bun">
|
||||
```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
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
|
@ -23,52 +25,10 @@ icon: "rocket"
|
|||
<Step title="Set environment variables">
|
||||
Each coding agent requires API keys to connect to their respective LLM providers.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Local shell">
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="E2B">
|
||||
```typescript
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
|
||||
const envs: Record<string, string> = {};
|
||||
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 });
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Daytona">
|
||||
```typescript
|
||||
import { Daytona } from "@daytonaio/sdk";
|
||||
|
||||
const envVars: Record<string, string> = {};
|
||||
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,
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Docker">
|
||||
```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
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Extracting API keys from current machine">
|
||||
|
|
@ -83,173 +43,146 @@ icon: "rocket"
|
|||
</AccordionGroup>
|
||||
</Step>
|
||||
|
||||
<Step title="Run the server">
|
||||
<Tabs>
|
||||
<Tab title="curl">
|
||||
Install and run the binary directly.
|
||||
<Step title="Start the sandbox">
|
||||
`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
|
||||
```
|
||||
</Tab>
|
||||
<CodeGroup>
|
||||
```typescript Local
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { local } from "sandbox-agent/local";
|
||||
|
||||
<Tab title="npx">
|
||||
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
|
||||
```
|
||||
</Tab>
|
||||
```typescript E2B
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { e2b } from "sandbox-agent/e2b";
|
||||
|
||||
<Tab title="bunx">
|
||||
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
|
||||
```
|
||||
</Tab>
|
||||
```typescript Daytona
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { daytona } from "sandbox-agent/daytona";
|
||||
|
||||
<Tab title="npm i -g">
|
||||
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
|
||||
```
|
||||
</Tab>
|
||||
```typescript Vercel
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { vercel } from "sandbox-agent/vercel";
|
||||
|
||||
<Tab title="bun add -g">
|
||||
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
|
||||
```
|
||||
</Tab>
|
||||
```typescript Cloudflare
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { cloudflare } from "sandbox-agent/cloudflare";
|
||||
|
||||
<Tab title="Node.js (local)">
|
||||
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}`],
|
||||
}),
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
const sdk = await SandboxAgent.start();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Bun (local)">
|
||||
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();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Build from source">
|
||||
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
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
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.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Configuring token">
|
||||
Tokens are usually not required. Most sandbox providers (E2B, Daytona, etc.) already secure networking at the infrastructure layer.
|
||||
<Accordion title="Implementing a custom provider">
|
||||
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,
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
Then pass the token when connecting:
|
||||
<Accordion title="Connecting to an existing server">
|
||||
If you already have a Sandbox Agent server running, connect directly:
|
||||
|
||||
```typescript
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Starting the server manually">
|
||||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl: "http://your-server:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="curl">
|
||||
```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
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="CLI">
|
||||
<Tab title="npx">
|
||||
```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
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Docker">
|
||||
```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
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="CORS">
|
||||
If you're calling the server from a browser, see the [CORS configuration guide](/cors).
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Step>
|
||||
|
||||
<Step title="Install agents (optional)">
|
||||
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.
|
||||
</Step>
|
||||
|
||||
<Step title="Create a session">
|
||||
<Step title="Create a session and send a prompt">
|
||||
```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);
|
||||
```
|
||||
</Step>
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
<Step title="Send a message">
|
||||
```typescript
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
|
@ -258,24 +191,16 @@ icon: "rocket"
|
|||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Read events">
|
||||
<Step title="Clean up">
|
||||
```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).
|
||||
</Step>
|
||||
|
||||
<Step title="Test with Inspector">
|
||||
Open the Inspector UI at `/ui/` on your server (for example, `http://localhost:2468/ui/`) to inspect sessions and events in a GUI.
|
||||
<Step title="Inspect with the UI">
|
||||
Open the Inspector at `/ui/` on your server (e.g. `http://localhost:2468/ui/`) to view sessions and events in a GUI.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" />
|
||||
|
|
@ -283,16 +208,40 @@ icon: "rocket"
|
|||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 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
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Session Persistence" icon="database" href="/session-persistence">
|
||||
Configure in-memory, Rivet Actor state, IndexedDB, SQLite, and Postgres persistence.
|
||||
<CardGroup cols={2}>
|
||||
<Card title="SDK Overview" icon="compass" href="/sdk-overview">
|
||||
Full TypeScript SDK API surface.
|
||||
</Card>
|
||||
<Card title="Deploy to a Sandbox" icon="box" href="/deploy/local">
|
||||
Deploy your agent to E2B, Daytona, Docker, Vercel, or Cloudflare.
|
||||
</Card>
|
||||
<Card title="SDK Overview" icon="compass" href="/sdk-overview">
|
||||
Use the latest TypeScript SDK API.
|
||||
Deploy to E2B, Daytona, Docker, Vercel, or Cloudflare.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
|
|||
|
|
@ -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()`
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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 })}`);
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export async function runComputeSdkExample(): Promise<void> {
|
|||
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 })}`);
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<void>((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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
|
|
|
|||
|
|
@ -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?"');
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<span className="text-zinc-300"> </span>
|
||||
<span className="text-green-400">"sandbox-agent"</span>
|
||||
<span className="text-zinc-300">;</span>
|
||||
{"\n"}
|
||||
<span className="text-purple-400">import</span>
|
||||
<span className="text-zinc-300">{" { "}</span>
|
||||
<span className="text-white">local</span>
|
||||
<span className="text-zinc-300">{" } "}</span>
|
||||
<span className="text-purple-400">from</span>
|
||||
<span className="text-zinc-300"> </span>
|
||||
<span className="text-green-400">"sandbox-agent/local"</span>
|
||||
<span className="text-zinc-300">;</span>
|
||||
{"\n\n"}
|
||||
<span className="text-purple-400">const</span>
|
||||
<span className="text-zinc-300"> client = </span>
|
||||
<span className="text-purple-400">await</span>
|
||||
<span className="text-zinc-300"> SandboxAgent.</span>
|
||||
<span className="text-blue-400">start</span>
|
||||
<span className="text-zinc-300">();</span>
|
||||
<span className="text-zinc-300">{"({"}</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" sandbox: local(),"}</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{"});"}</span>
|
||||
{"\n\n"}
|
||||
<span className="text-purple-400">await</span>
|
||||
<span className="text-zinc-300"> client.</span>
|
||||
|
|
|
|||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ export class IndexedDbSessionPersistDriver implements SessionPersistDriver {
|
|||
this.dbPromise = this.openDatabase();
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<SessionRecord | null> {
|
||||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
const db = await this.dbPromise;
|
||||
const row = await requestToPromise<IDBValidKey | SessionRow | undefined>(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<void> {
|
||||
async insertEvent(_sessionId: string, event: SessionEvent): Promise<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -33,18 +33,18 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
this.initialized = this.initialize();
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<SessionRecord | null> {
|
||||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
await this.ready();
|
||||
|
||||
const result = await this.pool.query<SessionRow>(
|
||||
`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<SessionRow>(
|
||||
`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<void> {
|
||||
async insertEvent(_sessionId: string, event: SessionEvent): Promise<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ export class RivetSessionPersistDriver implements SessionPersistDriver {
|
|||
return this.ctx.state[this.stateKey] as RivetPersistData;
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<SessionRecord | null> {
|
||||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
const session = this.data.sessions[id];
|
||||
return session ? cloneSessionRecord(session) : null;
|
||||
return session ? cloneSessionRecord(session) : undefined;
|
||||
}
|
||||
|
||||
async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {
|
||||
|
|
@ -112,15 +112,15 @@ export class RivetSessionPersistDriver implements SessionPersistDriver {
|
|||
};
|
||||
}
|
||||
|
||||
async insertEvent(event: SessionEvent): Promise<void> {
|
||||
const events = this.data.events[event.sessionId] ?? [];
|
||||
async insertEvent(sessionId: string, event: SessionEvent): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
this.initialize();
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<SessionRecord | null> {
|
||||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
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<void> {
|
||||
async insertEvent(_sessionId: string, event: SessionEvent): Promise<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<NewSessionRequest, "_meta">;
|
||||
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<NewSessionRequest, "_meta">;
|
||||
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<void>;
|
||||
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<SandboxAgent> {
|
||||
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<SandboxAgent> {
|
||||
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<void> {
|
||||
|
|
@ -922,10 +986,23 @@ export class SandboxAgent {
|
|||
await connection.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.spawnHandle) {
|
||||
await this.spawnHandle.dispose();
|
||||
this.spawnHandle = undefined;
|
||||
async destroySandbox(): Promise<void> {
|
||||
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<string, Qu
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeSessionInit(value: Omit<NewSessionRequest, "_meta"> | undefined): Omit<NewSessionRequest, "_meta"> {
|
||||
function normalizeSessionInit(value: Omit<NewSessionRequest, "_meta"> | undefined, cwdShorthand?: string): Omit<NewSessionRequest, "_meta"> {
|
||||
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<typeof globalThis.fetch | undefined> {
|
||||
if (provider.getFetch) {
|
||||
return await provider.getFetch(rawSandboxId);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function resolveProviderToken(provider: SandboxProvider, rawSandboxId: string): Promise<string | undefined> {
|
||||
const maybeGetToken = (
|
||||
provider as SandboxProvider & {
|
||||
getToken?: (sandboxId: string) => string | undefined | Promise<string | undefined>;
|
||||
}
|
||||
).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<ProblemDetails | undefined> {
|
||||
try {
|
||||
const text = await response.clone().text();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
79
sdks/typescript/src/providers/cloudflare.ts
Normal file
79
sdks/typescript/src/providers/cloudflare.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import type { SandboxProvider } from "./types.ts";
|
||||
|
||||
const DEFAULT_AGENT_PORT = 3000;
|
||||
|
||||
export interface CloudflareSandboxClient {
|
||||
create?(options?: Record<string, unknown>): Promise<{ id?: string; sandboxId?: string }>;
|
||||
connect?(
|
||||
sandboxId: string,
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<{
|
||||
close?(): Promise<void>;
|
||||
stop?(): Promise<void>;
|
||||
containerFetch(input: RequestInfo | URL, init?: RequestInit, port?: number): Promise<Response>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CloudflareProviderOptions {
|
||||
sdk: CloudflareSandboxClient;
|
||||
create?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
|
||||
agentPort?: number;
|
||||
}
|
||||
|
||||
async function resolveCreateOptions(value: CloudflareProviderOptions["create"]): Promise<Record<string, unknown>> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<typeof globalThis.fetch> {
|
||||
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,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
65
sdks/typescript/src/providers/daytona.ts
Normal file
65
sdks/typescript/src/providers/daytona.ts
Normal file
|
|
@ -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<Parameters<Daytona["create"]>[0]>;
|
||||
|
||||
export interface DaytonaProviderOptions {
|
||||
create?: DaytonaCreateParams | (() => DaytonaCreateParams | Promise<DaytonaCreateParams>);
|
||||
image?: string;
|
||||
agentPort?: number;
|
||||
previewTtlSeconds?: number;
|
||||
deleteTimeoutSeconds?: number;
|
||||
}
|
||||
|
||||
async function resolveCreateOptions(value: DaytonaProviderOptions["create"]): Promise<DaytonaCreateParams | undefined> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
const sandbox = await client.get(sandboxId);
|
||||
if (!sandbox) {
|
||||
return;
|
||||
}
|
||||
await sandbox.delete(options.deleteTimeoutSeconds);
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
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<void> {
|
||||
const sandbox = await client.get(sandboxId);
|
||||
if (!sandbox) {
|
||||
throw new Error(`daytona sandbox not found: ${sandboxId}`);
|
||||
}
|
||||
await sandbox.process.executeCommand(buildServerStartCommand(agentPort));
|
||||
},
|
||||
};
|
||||
}
|
||||
85
sdks/typescript/src/providers/docker.ts
Normal file
85
sdks/typescript/src/providers/docker.ts
Normal file
|
|
@ -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<string[]>);
|
||||
binds?: string[] | (() => string[] | Promise<string[]>);
|
||||
createContainerOptions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function resolveValue<T>(value: T | (() => T | Promise<T>) | undefined, fallback: T): Promise<T> {
|
||||
if (value === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return await (value as () => T | Promise<T>)();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function extractMappedPort(
|
||||
inspect: { NetworkSettings?: { Ports?: Record<string, Array<{ HostPort?: string }> | 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<string> {
|
||||
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<void> {
|
||||
const container = client.getContainer(sandboxId);
|
||||
try {
|
||||
await container.stop({ t: 5 });
|
||||
} catch {}
|
||||
try {
|
||||
await container.remove({ force: true });
|
||||
} catch {}
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
const container = client.getContainer(sandboxId);
|
||||
const hostPort = extractMappedPort(await container.inspect(), agentPort);
|
||||
return `http://${host}:${hostPort}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
57
sdks/typescript/src/providers/e2b.ts
Normal file
57
sdks/typescript/src/providers/e2b.ts
Normal file
|
|
@ -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<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
|
||||
connect?: Record<string, unknown> | ((sandboxId: string) => Record<string, unknown> | Promise<Record<string, unknown>>);
|
||||
agentPort?: number;
|
||||
}
|
||||
|
||||
async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderOptions["connect"], sandboxId?: string): Promise<Record<string, unknown>> {
|
||||
if (!value) return {};
|
||||
if (typeof value === "function") {
|
||||
if (sandboxId) {
|
||||
return await (value as (id: string) => Record<string, unknown> | Promise<Record<string, unknown>>)(sandboxId);
|
||||
}
|
||||
return await (value as () => Record<string, unknown> | Promise<Record<string, unknown>>)();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||
|
||||
return {
|
||||
name: "e2b",
|
||||
async create(): Promise<string> {
|
||||
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<void> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
await sandbox.kill();
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
return `https://${sandbox.getHost(agentPort)}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
84
sdks/typescript/src/providers/local.ts
Normal file
84
sdks/typescript/src/providers/local.ts
Normal file
|
|
@ -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<string, string>;
|
||||
}
|
||||
|
||||
const localSandboxes = new Map<string, SandboxAgentSpawnHandle>();
|
||||
|
||||
type LocalSandboxProvider = SandboxProvider & {
|
||||
getToken(sandboxId: string): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
export function local(options: LocalProviderOptions = {}): SandboxProvider {
|
||||
const provider: LocalSandboxProvider = {
|
||||
name: "local",
|
||||
async create(): Promise<string> {
|
||||
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<void> {
|
||||
const handle = localSandboxes.get(sandboxId);
|
||||
if (!handle) {
|
||||
return;
|
||||
}
|
||||
localSandboxes.delete(sandboxId);
|
||||
await handle.dispose();
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
return `http://${sandboxId}`;
|
||||
},
|
||||
async getFetch(sandboxId: string): Promise<typeof globalThis.fetch> {
|
||||
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<string | undefined> {
|
||||
return options.token ?? localSandboxes.get(sandboxId)?.token;
|
||||
},
|
||||
};
|
||||
return provider;
|
||||
}
|
||||
|
||||
function baseUrlToSandboxId(baseUrl: string): string {
|
||||
return new URL(baseUrl).host;
|
||||
}
|
||||
7
sdks/typescript/src/providers/shared.ts
Normal file
7
sdks/typescript/src/providers/shared.ts
Normal file
|
|
@ -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 &`;
|
||||
}
|
||||
28
sdks/typescript/src/providers/types.ts
Normal file
28
sdks/typescript/src/providers/types.ts
Normal file
|
|
@ -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<string>;
|
||||
|
||||
/** Permanently tear down a sandbox. */
|
||||
destroy(sandboxId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the sandbox-agent base URL for this sandbox.
|
||||
* Providers that cannot expose a URL should implement `getFetch()` instead.
|
||||
*/
|
||||
getUrl?(sandboxId: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Return a fetch implementation that routes requests to the sandbox.
|
||||
* Providers that expose a URL can implement `getUrl()` instead.
|
||||
*/
|
||||
getFetch?(sandboxId: string): Promise<typeof globalThis.fetch>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
}
|
||||
57
sdks/typescript/src/providers/vercel.ts
Normal file
57
sdks/typescript/src/providers/vercel.ts
Normal file
|
|
@ -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<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
|
||||
agentPort?: number;
|
||||
}
|
||||
|
||||
async function resolveCreateOptions(value: VercelProviderOptions["create"], agentPort: number): Promise<Record<string, unknown>> {
|
||||
const resolved = typeof value === "function" ? await value() : (value ?? {});
|
||||
return {
|
||||
ports: [agentPort],
|
||||
...resolved,
|
||||
};
|
||||
}
|
||||
|
||||
async function runVercelCommand(sandbox: InstanceType<typeof Sandbox>, cmd: string, args: string[] = []): Promise<void> {
|
||||
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<string> {
|
||||
const sandbox = await Sandbox.create((await resolveCreateOptions(options.create, agentPort)) as Parameters<typeof Sandbox.create>[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<void> {
|
||||
const sandbox = await Sandbox.get({ sandboxId });
|
||||
await sandbox.stop();
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
const sandbox = await Sandbox.get({ sandboxId });
|
||||
return sandbox.domain(agentPort);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -98,6 +98,7 @@ export interface SessionRecord {
|
|||
lastConnectionId: string;
|
||||
createdAt: number;
|
||||
destroyedAt?: number;
|
||||
sandboxId?: string;
|
||||
sessionInit?: Omit<NewSessionRequest, "_meta">;
|
||||
configOptions?: SessionConfigOption[];
|
||||
modes?: SessionModeState | null;
|
||||
|
|
@ -131,11 +132,11 @@ export interface ListEventsRequest extends ListPageRequest {
|
|||
}
|
||||
|
||||
export interface SessionPersistDriver {
|
||||
getSession(id: string): Promise<SessionRecord | null>;
|
||||
getSession(id: string): Promise<SessionRecord | undefined>;
|
||||
listSessions(request?: ListPageRequest): Promise<ListPage<SessionRecord>>;
|
||||
updateSession(session: SessionRecord): Promise<void>;
|
||||
listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>>;
|
||||
insertEvent(event: SessionEvent): Promise<void>;
|
||||
insertEvent(sessionId: string, event: SessionEvent): Promise<void>;
|
||||
}
|
||||
|
||||
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<SessionRecord | null> {
|
||||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
const session = this.sessions.get(id);
|
||||
return session ? cloneSessionRecord(session) : null;
|
||||
return session ? cloneSessionRecord(session) : undefined;
|
||||
}
|
||||
|
||||
async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {
|
||||
|
|
@ -219,15 +220,15 @@ export class InMemorySessionPersistDriver implements SessionPersistDriver {
|
|||
};
|
||||
}
|
||||
|
||||
async insertEvent(event: SessionEvent): Promise<void> {
|
||||
const events = this.eventsBySession.get(event.sessionId) ?? [];
|
||||
async insertEvent(sessionId: string, event: SessionEvent): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
379
sdks/typescript/tests/providers.test.ts
Normal file
379
sdks/typescript/tests/providers.test.ts
Normal file
|
|
@ -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<string, string> {
|
||||
const keys: Record<string, string> = {};
|
||||
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);
|
||||
}
|
||||
|
|
@ -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"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@ export default defineConfig({
|
|||
test: {
|
||||
include: ["tests/**/*.test.ts"],
|
||||
testTimeout: 30000,
|
||||
teardownTimeout: 10000,
|
||||
pool: "forks",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue