
Sandbox Agent
- 9.8k installs
- 18 repo stars
- Updated July 31, 2026
- rivet-dev/skills
sandbox-agent is an agent skill that Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environment.
About
Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environments. Use when setting up sandbox-agent server locally or in cloud sandboxes (E2B, Daytona, Docker), creating and managing agent sessions via SDK or API, streaming agent events and handling human-in-the-loop interactions, building chat UIs for --- name: "sandbox-agent" description: "Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environments. Use when setting up sandbox-agent server locally or in cloud sandboxes (E2B, Daytona, Docker), creating and managing agent sessions via SDK or API, streaming agent events and handling human-in-the-loop interactions, building chat UIs for coding agents, or understanding the universal schema for agent responses." --- # Sandbox Agent Sandbox Agent provides a universal API for orchestrating AI coding agents in sandboxed environments.
- If this is happening in local dev, deployed, or both
- The error you're seeing
- Relevant source code related to this
- What you've tried to solve it
- Sandbox Agent version
Sandbox Agent by the numbers
- 9,842 all-time installs (skills.sh)
- +286 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #40 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 3, 2026 (Skillselion catalog sync)
sandbox-agent capabilities & compatibility
- Capabilities
- if this is happening in local dev, deployed, or · the error you're seeing · relevant source code related to this · what you've tried to solve it · sandbox agent version
- Use cases
- documentation
What sandbox-agent says it does
#### Testing without API keys Use the `mock` agent for SDK and integration testing without provider credentials.
### Run the server #### curl Install and run the binary directly.
```bash curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh sandbox-agent server --no-token --host 0.0.0.0 --port 2468 ``` #### npx Run without installing globally.
```bash npx @sandbox-agent/cli@0.4.x server --no-token --host 0.0.0.0 --port 2468 ``` #### bunx Run without installing globally.
npx skills add https://github.com/rivet-dev/skills --skill sandbox-agentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9.8k |
|---|---|
| repo stars | ★ 18 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 31, 2026 |
| Repository | rivet-dev/skills ↗ |
What problem does sandbox-agent solve for developers using this skill?
Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environments. Use when setting up sandbox-agent ser
Who is it for?
Developers who need sandbox-agent patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environments. Use when setting up sandbox-agent ser
What you get
Actionable workflows and conventions from SKILL.md for sandbox-agent.
- Agent session code
- Event history inspection
- Restored session handles
By the numbers
- Supports Claude, Codex, and Cursor-style agent types in createSession
Files
skill.md
Source: docs/ai/skill.mdxCanonical URL: https://sandboxagent.dev/docs/ai/skill
Description: Agent skill manifest for this documentation.
--- Mintlify hosts a skill.md file for this documentation site.
Access it at:
https://sandboxagent.dev/docs/skill.mdTo add it to an agent using the Skills CLI:
npx
npx skills add rivet-dev/skills -s sandbox-agentbunx
bunx skills add rivet-dev/skills -s sandbox-agentIf you run a reverse proxy in front of the docs, make sure /skill.md and /.well-known/skills/* are forwarded to Mintlify.
Agent Sessions
Source: docs/agent-sessions.mdxCanonical URL: https://sandboxagent.dev/docs/agent-sessions
Description: Create sessions, prompt agents, and inspect event history.
--- Sessions are the unit of interaction with an agent. Create one session per task, send prompts, and consume event history.
For SDK-based flows, sessions can be restored after runtime/session loss when persistence is enabled. See Session Restoration.
Create a session
import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
});
const session = await sdk.createSession({
agent: "codex",
cwd: "/",
});
console.log(session.id, session.agentSessionId);Send a prompt
const response = await session.prompt([
{ type: "text", text: "Summarize the repository structure." },
]);
console.log(response.stopReason);Subscribe to live events
const unsubscribe = session.onEvent((event) => {
console.log(event.eventIndex, event.sender, event.payload);
});
await session.prompt([
{ type: "text", text: "Explain the main entrypoints." },
]);
unsubscribe();Event types
Each event's payload contains a session update. The sessionUpdate field identifies the type.
agent_message_chunk
Streamed text or content from the agent's response.
{
"sessionUpdate": "agent_message_chunk",
"content": { "type": "text", "text": "Here's how the repository is structured..." }
}agent_thought_chunk
Internal reasoning from the agent (chain-of-thought / extended thinking).
{
"sessionUpdate": "agent_thought_chunk",
"content": { "type": "text", "text": "I should start by looking at the project structure..." }
}user_message_chunk
Echo of the user's prompt being processed.
{
"sessionUpdate": "user_message_chunk",
"content": { "type": "text", "text": "Summarize the repository structure." }
}tool_call
The agent invoked a tool (file edit, terminal command, etc.).
{
"sessionUpdate": "tool_call",
"toolCallId": "tc_abc123",
"title": "Read file",
"status": "in_progress",
"rawInput": { "path": "/src/index.ts" }
}tool_call_update
Progress or result update for an in-progress tool call.
{
"sessionUpdate": "tool_call_update",
"toolCallId": "tc_abc123",
"status": "completed",
"content": [{ "type": "text", "text": "import express from 'express';\n..." }]
}plan
The agent's execution plan for the current task.
{
"sessionUpdate": "plan",
"entries": [
{ "content": "Read the project structure", "status": "completed" },
{ "content": "Identify main entrypoints", "status": "in_progress" },
{ "content": "Write summary", "status": "pending" }
]
}usage_update
Token usage metrics for the current turn.
{
"sessionUpdate": "usage_update"
}session_info_update
Session metadata changed (e.g. agent-generated title).
{
"sessionUpdate": "session_info_update",
"title": "Repository structure analysis"
}Fetch persisted event history
const page = await sdk.getEvents({
sessionId: session.id,
limit: 50,
});
for (const event of page.items) {
console.log(event.id, event.createdAt, event.sender);
}List and load sessions
const sessions = await sdk.listSessions({ limit: 20 });
for (const item of sessions.items) {
console.log(item.id, item.agent, item.createdAt);
}
if (sessions.items.length > 0) {
const loaded = await sdk.resumeSession(sessions.items[0]!.id);
await loaded.prompt([{ type: "text", text: "Continue." }]);
}Configure model, mode, and thought level
Set the model, mode, or thought level on a session at creation time or after:
// At creation time
const session = await sdk.createSession({
agent: "codex",
model: "gpt-5.3-codex",
mode: "auto",
thoughtLevel: "high",
});// After creation
await session.setModel("gpt-5.2-codex");
await session.setMode("full-access");
await session.setThoughtLevel("medium");Query available modes:
const modes = await session.getModes();
console.log(modes?.currentModeId, modes?.availableModes);Advanced config options
For config options beyond model, mode, and thought level, use getConfigOptions to discover what the agent supports and setConfigOption to set any option by ID:
const options = await session.getConfigOptions();
for (const opt of options) {
console.log(opt.id, opt.category, opt.type);
}await session.setConfigOption("some-agent-option", "value");Handle permission requests
For agents that request tool-use permissions, register a permission listener and reply with once, always, or reject:
const session = await sdk.createSession({
agent: "claude",
mode: "default",
});
session.onPermissionRequest((request) => {
console.log(request.toolCall.title, request.availableReplies);
void session.respondPermission(request.id, "once");
});
await session.prompt([
{ type: "text", text: "Create ./permission-example.txt with the text hello." },
]);Auto-approving permissions
To auto-approve all permission requests, respond with "once" or "always" in your listener:
session.onPermissionRequest((request) => {
void session.respondPermission(request.id, "always");
});See examples/permissions/src/index.ts for a complete permissions example that works with Claude and Codex.
Some agents like Claude allow configuring permission behavior through modes (e.g. bypassPermissions, acceptEdits). We recommend leaving the mode as default and handling permission decisions explicitly in onPermissionRequest instead.
Destroy a session
await sdk.destroySession(session.id);Amp
Source: docs/agents/amp.mdxCanonical URL: https://sandboxagent.dev/docs/agents/amp
Description: Use Amp as a sandbox agent.
---
Usage
const session = await client.createSession({
agent: "amp",
});Capabilities
| Category | Values |
|---|---|
| Models | amp-default |
| Modes | default, bypass |
| Thought levels | Unsupported |
Claude
Source: docs/agents/claude.mdxCanonical URL: https://sandboxagent.dev/docs/agents/claude
Description: Use Claude Code as a sandbox agent.
---
Usage
const session = await client.createSession({
agent: "claude",
});Capabilities
| Category | Values |
|---|---|
| Models | default, sonnet, opus, haiku |
| Modes | default, acceptEdits, plan, dontAsk, bypassPermissions |
| Thought levels | Unsupported |
Configuring effort level
Claude does not support changing effort level after a session starts. Configure it in the filesystem before creating the session.
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
const cwd = "/path/to/workspace";
await mkdir(path.join(cwd, ".claude"), { recursive: true });
await writeFile(
path.join(cwd, ".claude", "settings.json"),
JSON.stringify({ effortLevel: "high" }, null, 2),
);
const session = await client.createSession({
agent: "claude",
cwd,
});Supported settings file locations (highest precedence last)
1. ~/.claude/settings.json 2. <session cwd>/.claude/settings.json 3. <session cwd>/.claude/settings.local.json
Codex
Source: docs/agents/codex.mdxCanonical URL: https://sandboxagent.dev/docs/agents/codex
Description: Use OpenAI Codex as a sandbox agent.
---
Usage
const session = await client.createSession({
agent: "codex",
});Capabilities
| Category | Values |
|---|---|
| Models | gpt-5.3-codex (default), gpt-5.3-codex-spark, gpt-5.2-codex, gpt-5.1-codex-max, gpt-5.2, gpt-5.1-codex-mini |
| Modes | read-only (default), auto, full-access |
| Thought levels | low, medium, high (default), xhigh |
Cursor
Source: docs/agents/cursor.mdxCanonical URL: https://sandboxagent.dev/docs/agents/cursor
Description: Use Cursor as a sandbox agent.
---
Usage
const session = await client.createSession({
agent: "cursor",
});Capabilities
| Category | Values |
|---|---|
| Models | See below |
| Modes | Unsupported |
| Thought levels | Unsupported |
All models
| Group | Models |
|---|---|
| Auto | auto |
| Composer | composer-1.5, composer-1 |
| GPT-5.3 Codex | gpt-5.3-codex, gpt-5.3-codex-low, gpt-5.3-codex-high, gpt-5.3-codex-xhigh, gpt-5.3-codex-fast, gpt-5.3-codex-low-fast, gpt-5.3-codex-high-fast, gpt-5.3-codex-xhigh-fast |
| GPT-5.2 | gpt-5.2, gpt-5.2-high, gpt-5.2-codex, gpt-5.2-codex-low, gpt-5.2-codex-high, gpt-5.2-codex-xhigh, gpt-5.2-codex-fast, gpt-5.2-codex-low-fast, gpt-5.2-codex-high-fast, gpt-5.2-codex-xhigh-fast |
| GPT-5.1 | gpt-5.1-high, gpt-5.1-codex-max, gpt-5.1-codex-max-high |
| Claude | opus-4.6-thinking (default), opus-4.6, opus-4.5, opus-4.5-thinking, sonnet-4.5, sonnet-4.5-thinking |
| Other | gemini-3-pro, gemini-3-flash, grok |
OpenCode
Source: docs/agents/opencode.mdxCanonical URL: https://sandboxagent.dev/docs/agents/opencode
Description: Use OpenCode as a sandbox agent.
---
Usage
const session = await client.createSession({
agent: "opencode",
});Capabilities
| Category | Values |
|---|---|
| Models | See below |
| Modes | build (default), plan |
| Thought levels | Unsupported |
All models
| Provider | Models |
|---|---|
| Anthropic | anthropic/claude-3-5-haiku-20241022, anthropic/claude-3-5-haiku-latest, anthropic/claude-3-5-sonnet-20240620, anthropic/claude-3-5-sonnet-20241022, anthropic/claude-3-7-sonnet-20250219, anthropic/claude-3-7-sonnet-latest, anthropic/claude-3-haiku-20240307, anthropic/claude-3-opus-20240229, anthropic/claude-3-sonnet-20240229, anthropic/claude-haiku-4-5, anthropic/claude-haiku-4-5-20251001, anthropic/claude-opus-4-0, anthropic/claude-opus-4-1, anthropic/claude-opus-4-1-20250805, anthropic/claude-opus-4-20250514, anthropic/claude-opus-4-5, anthropic/claude-opus-4-5-20251101, anthropic/claude-opus-4-6, anthropic/claude-sonnet-4-0, anthropic/claude-sonnet-4-20250514, anthropic/claude-sonnet-4-5, anthropic/claude-sonnet-4-5-20250929 |
| OpenAI | openai/gpt-5.1-codex, openai/gpt-5.1-codex-max, openai/gpt-5.1-codex-mini, openai/gpt-5.2, openai/gpt-5.2-codex, openai/gpt-5.3-codex |
| Cerebras | cerebras/gpt-oss-120b, cerebras/qwen-3-235b-a22b-instruct-2507, cerebras/zai-glm-4.7 |
| OpenCode Zen | opencode/big-pickle, opencode/claude-3-5-haiku, opencode/claude-haiku-4-5, opencode/claude-opus-4-1, opencode/claude-opus-4-5, opencode/claude-opus-4-6, opencode/claude-sonnet-4, opencode/claude-sonnet-4-5, opencode/gemini-3-flash, opencode/gemini-3-pro (default), opencode/glm-4.6, opencode/glm-4.7, opencode/gpt-5, opencode/gpt-5-codex, opencode/gpt-5-nano, opencode/gpt-5.1, opencode/gpt-5.1-codex, opencode/gpt-5.1-codex-max, opencode/gpt-5.1-codex-mini, opencode/gpt-5.2, opencode/gpt-5.2-codex, opencode/kimi-k2, opencode/kimi-k2-thinking, opencode/kimi-k2.5, opencode/kimi-k2.5-free, opencode/minimax-m2.1, opencode/minimax-m2.1-free, opencode/trinity-large-preview-free |
Pi
Source: docs/agents/pi.mdxCanonical URL: https://sandboxagent.dev/docs/agents/pi
Description: Use Pi as a sandbox agent.
---
Usage
const session = await client.createSession({
agent: "pi",
});Capabilities
| Category | Values |
|---|---|
| Models | default |
| Modes | Unsupported |
| Thought levels | Unsupported |
llms.txt
Source: docs/ai/llms-txt.mdxCanonical URL: https://sandboxagent.dev/docs/ai/llms-txt
Description: LLM-friendly documentation manifests.
--- Mintlify publishes llms.txt and llms-full.txt for this documentation site.
Access them at:
https://sandboxagent.dev/docs/llms.txt
https://sandboxagent.dev/docs/llms-full.txtIf you run a reverse proxy in front of the docs, forward /llms.txt and /llms-full.txt to Mintlify.
Architecture
Source: docs/architecture.mdxCanonical URL: https://sandboxagent.dev/docs/architecture
Description: How the Sandbox Agent server, SDK, and agent processes fit together.
--- 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
flowchart LR
CLIENT["Your App"]
subgraph SANDBOX["Sandbox"]
direction TB
SERVER["Sandbox Agent Server"]
AGENT["Agent Process<br/>(Claude, Codex, etc.)"]
SERVER --> AGENT
end
CLIENT -->|"SDK (HTTP)"| SERVER- Your app: Uses the
sandbox-agentTypeScript 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.
What SandboxAgent.start() does
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
For the local provider, provisioning is a no-op and the server runs as a local subprocess.
Server recovery
If the server process stops, the SDK automatically calls the provider's ensureServer() after 3 consecutive health-check failures. Most built-in providers implement this. Custom providers can add ensureServer(sandboxId) to their SandboxProvider object.
Server HTTP API
See the HTTP 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:
sandbox-agent install-agent --allThe rivetdev/sandbox-agent:0.4.2-full Docker image ships with all agents pre-installed.
Production-ready agent orchestration
For production deployments, see Orchestration Architecture for recommended topology, backend requirements, and session persistence patterns.
Attachments
Source: docs/attachments.mdxCanonical URL: https://sandboxagent.dev/docs/attachments
Description: Upload files into the sandbox and reference them in prompts.
--- Use the filesystem API to upload files, then include file references in prompt content.
Upload a file
```ts TypeScript import { SandboxAgent } from "sandbox-agent"; import fs from "node:fs";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
const buffer = await fs.promises.readFile("./data.csv");
const upload = await sdk.writeFsFile( { path: "./uploads/data.csv" }, buffer, );
console.log(upload.path);
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./uploads/data.csv" \ --data-binary @./data.csv
The upload response returns the absolute path.
### Reference the file in a prompt
const session = await sdk.createSession({ agent: "mock" });
await session.prompt([ { type: "text", text: "Please analyze the attached CSV." }, { type: "resource_link", name: "data.csv", uri: "file:///home/sandbox/uploads/data.csv", mimeType: "text/csv", }, ]);
## Notes
- Use absolute file URIs in `resource_link` blocks.
- If `mimeType` is omitted, the agent/runtime may infer a default.
- Support for non-text resources depends on each agent's prompt capabilities.
CLI Reference
Source: docs/cli.mdxCanonical URL: https://sandboxagent.dev/docs/cli
Description: CLI reference for sandbox-agent.
--- Global flags (available on all commands):
-t, --token: require/use bearer auth-n, --no-token: disable auth
server
Run the HTTP server.
sandbox-agent server [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host to bind |
-p, --port | 2468 | Port to bind |
-O, --cors-allow-origin | - | Allowed CORS origin (repeatable) |
-M, --cors-allow-method | all | Allowed CORS method (repeatable) |
-A, --cors-allow-header | all | Allowed CORS header (repeatable) |
-C, --cors-allow-credentials | false | Enable CORS credentials |
--no-telemetry | false | Disable anonymous telemetry |
sandbox-agent server --port 3000Notes:
- Server logs are redirected to files by default.
- Set
SANDBOX_AGENT_LOG_STDOUT=1to force stdout/stderr logging. - Use
SANDBOX_AGENT_LOG_DIRto override log directory.
install
Install first-party runtime dependencies.
install desktop
Install the Linux desktop runtime packages required by /v1/desktop/*.
sandbox-agent install desktop [OPTIONS]| Option | Description |
|---|---|
--yes | Skip the confirmation prompt |
--print-only | Print the package-manager command without executing it |
| `--package-manager <apt\ | dnf\ |
--no-fonts | Skip the default DejaVu font package |
sandbox-agent install desktop --yes
sandbox-agent install desktop --print-onlyNotes:
- Supported on Linux only.
- The command detects
apt,dnf, orapk. - If the host is not already running as root, the command requires
sudo.
install-agent
Install or reinstall a single agent, or every supported agent with --all.
sandbox-agent install-agent [<AGENT>] [OPTIONS]| Option | Description |
|---|---|
--all | Install every supported agent |
-r, --reinstall | Force reinstall |
--agent-version | Override agent package version (conflicts with --all) |
--agent-process-version | Override agent process version (conflicts with --all) |
Examples:
sandbox-agent install-agent claude --reinstall
sandbox-agent install-agent --allCustom Pi implementation path
If you use a forked/custom pi binary with pi-acp, you can override what executable gets launched.
Option 1: explicit command override (recommended)
Set PI_ACP_PI_COMMAND in the environment where sandbox-agent runs:
PI_ACP_PI_COMMAND=/absolute/path/to/your/pi-fork sandbox-agent serverThis is forwarded to pi-acp, which uses it instead of looking up pi on PATH.
Option 2: PATH override
Put your custom pi first on PATH before starting sandbox-agent:
export PATH="/path/to/custom-pi-dir:$PATH"
sandbox-agent serverOption 3: symlink override
Point pi to your custom binary via symlink in a directory that is early on PATH:
ln -sf /absolute/path/to/your/pi-fork /usr/local/bin/piThen start sandbox-agent normally.
opencode (experimental)
Start/reuse daemon and run opencode attach against /opencode.
sandbox-agent opencode [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Daemon host |
-p, --port | 2468 | Daemon port |
--session-title | - | Reserved option (currently no-op) |
--yolo | false | OpenCode attach mode flag |
sandbox-agent opencodedaemon
Manage the background daemon.
daemon start
sandbox-agent daemon start [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host |
-p, --port | 2468 | Port |
--upgrade | false | Use ensure-running + upgrade behavior |
sandbox-agent daemon start
sandbox-agent daemon start --upgradedaemon stop
sandbox-agent daemon stop [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host |
-p, --port | 2468 | Port |
daemon status
sandbox-agent daemon status [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host |
-p, --port | 2468 | Port |
credentials
credentials extract
sandbox-agent credentials extract [OPTIONS]| Option | Description |
|---|---|
-a, --agent | Filter by claude, codex, opencode, or amp |
-p, --provider | Filter by provider |
-d, --home-dir | Override home dir |
--no-oauth | Skip OAuth sources |
-r, --reveal | Show full credential values |
sandbox-agent credentials extract --agent claude --revealcredentials extract-env
sandbox-agent credentials extract-env [OPTIONS]| Option | Description |
|---|---|
-e, --export | Prefix output with export |
-d, --home-dir | Override home dir |
--no-oauth | Skip OAuth sources |
eval "$(sandbox-agent credentials extract-env --export)"api
API subcommands for scripting.
Shared option:
| Option | Default | Description |
|---|---|---|
-e, --endpoint | http://127.0.0.1:2468 | Target server |
api agents
sandbox-agent api agents list [--endpoint <URL>]
sandbox-agent api agents report [--endpoint <URL>]
sandbox-agent api agents install <AGENT> [--reinstall] [--endpoint <URL>]api agents list
List all agents and their install status.
sandbox-agent api agents listapi agents report
Emit a JSON report of available models, modes, and thought levels for every agent, grouped by category.
sandbox-agent api agents report --endpoint http://127.0.0.1:2468 | jq .Example output:
{
"generatedAtMs": 1740000000000,
"endpoint": "http://127.0.0.1:2468",
"agents": [
{
"id": "claude",
"installed": true,
"models": {
"currentValue": "default",
"values": [
{ "value": "default", "name": "Default" },
{ "value": "sonnet", "name": "Sonnet" },
{ "value": "opus", "name": "Opus" },
{ "value": "haiku", "name": "Haiku" }
]
},
"modes": {
"currentValue": "default",
"values": [
{ "value": "default", "name": "Default" },
{ "value": "acceptEdits", "name": "Accept Edits" },
{ "value": "plan", "name": "Plan" },
{ "value": "dontAsk", "name": "Don't Ask" },
{ "value": "bypassPermissions", "name": "Bypass Permissions" }
]
},
"thoughtLevels": { "values": [] }
}
]
}See individual agent pages (e.g. Claude, Codex) for supported models, modes, and thought levels.
api agents install
sandbox-agent api agents install codex --reinstallCommon Software
Source: docs/common-software.mdxCanonical URL: https://sandboxagent.dev/docs/common-software
Description: Install browsers, languages, databases, and other tools inside the sandbox.
--- The sandbox runs a Debian/Ubuntu base image. You can install software with apt-get via the Process API or by customizing your Docker image. This page covers commonly needed packages and how to install them.
Browsers
Chromium
```ts TypeScript await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "chromium", "chromium-sandbox"], });
// Launch headless await sdk.runProcess({ command: "chromium", args: ["--headless", "--no-sandbox", "--disable-gpu", "https://example.com"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","chromium","chromium-sandbox"]}'
Use `--no-sandbox` when running Chromium inside a container. The container itself provides isolation.
### Firefox
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "firefox-esr"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","firefox-esr"]}'
### Playwright browsers
Playwright bundles its own browser binaries. Install the Playwright CLI and let it download browsers for you.
await sdk.runProcess({ command: "npx", args: ["playwright", "install", "--with-deps", "chromium"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"npx","args":["playwright","install","--with-deps","chromium"]}'
---
## Languages and runtimes
### Node.js
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "nodejs", "npm"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","nodejs","npm"]}'
For a specific version, use [nvm](https://github.com/nvm-sh/nvm):
await sdk.runProcess({ command: "bash", args: ["-c", "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && . ~/.nvm/nvm.sh && nvm install 22"], });
### Python
Python 3 is typically pre-installed. To add pip and common packages:
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "python3", "python3-pip", "python3-venv"], });
await sdk.runProcess({ command: "pip3", args: ["install", "numpy", "pandas", "matplotlib"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","python3","python3-pip","python3-venv"]}'
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"pip3","args":["install","numpy","pandas","matplotlib"]}'
### Go
await sdk.runProcess({ command: "bash", args: ["-c", "curl -fsSL https://go.dev/dl/go1.23.6.linux-amd64.tar.gz | tar -C /usr/local -xz"], });
// Add to PATH for subsequent commands await sdk.runProcess({ command: "bash", args: ["-c", "export PATH=$PATH:/usr/local/go/bin && go version"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"bash","args":["-c","curl -fsSL https://go.dev/dl/go1.23.6.linux-amd64.tar.gz | tar -C /usr/local -xz"]}'
### Rust
await sdk.runProcess({ command: "bash", args: ["-c", "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"bash","args":["-c","curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"]}'
### Java (OpenJDK)
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "default-jdk"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","default-jdk"]}'
### Ruby
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "ruby-full"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","ruby-full"]}'
---
## Databases
### PostgreSQL
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "postgresql", "postgresql-client"], });
// Start the service const proc = await sdk.createProcess({ command: "bash", args: ["-c", "su - postgres -c 'pg_ctlcluster 15 main start'"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","postgresql","postgresql-client"]}'
### SQLite
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "sqlite3"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","sqlite3"]}'
### Redis
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "redis-server"], });
const proc = await sdk.createProcess({ command: "redis-server", args: ["--daemonize", "no"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","redis-server"]}'
curl -X POST "http://127.0.0.1:2468/v1/processes" \ -H "Content-Type: application/json" \ -d '{"command":"redis-server","args":["--daemonize","no"]}'
### MySQL / MariaDB
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "mariadb-server", "mariadb-client"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","mariadb-server","mariadb-client"]}'
---
## Build tools
### Essential build toolchain
Most compiled software needs the standard build toolchain:
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "build-essential", "cmake", "pkg-config"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","build-essential","cmake","pkg-config"]}'
This installs `gcc`, `g++`, `make`, `cmake`, and related tools.
---
## Desktop applications
These require the [Computer Use](/computer-use) desktop to be started first.
### LibreOffice
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "libreoffice"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","libreoffice"]}'
### GIMP
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "gimp"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","gimp"]}'
### VLC
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "vlc"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","vlc"]}'
### VS Code (code-server)
await sdk.runProcess({ command: "bash", args: ["-c", "curl -fsSL https://code-server.dev/install.sh | sh"], });
const proc = await sdk.createProcess({ command: "code-server", args: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"bash","args":["-c","curl -fsSL https://code-server.dev/install.sh | sh"]}'
curl -X POST "http://127.0.0.1:2468/v1/processes" \ -H "Content-Type: application/json" \ -d '{"command":"code-server","args":["--bind-addr","0.0.0.0:8080","--auth","none"]}'
---
## CLI tools
### Git
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "git"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","git"]}'
### Docker
await sdk.runProcess({ command: "bash", args: ["-c", "curl -fsSL https://get.docker.com | sh"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"bash","args":["-c","curl -fsSL https://get.docker.com | sh"]}'
### jq
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "jq"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","jq"]}'
### tmux
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "tmux"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","tmux"]}'
---
## Media and graphics
### FFmpeg
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "ffmpeg"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","ffmpeg"]}'
### ImageMagick
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "imagemagick"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","imagemagick"]}'
### Poppler (PDF utilities)
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "poppler-utils"], });
// Convert PDF to images await sdk.runProcess({ command: "pdftoppm", args: ["-png", "document.pdf", "output"], });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","poppler-utils"]}'
---
## Pre-installing in a Docker image
For production use, install software in your Dockerfile instead of at runtime. This avoids repeated downloads and makes startup faster.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \ chromium \ firefox-esr \ nodejs npm \ python3 python3-pip \ git curl wget \ build-essential \ sqlite3 \ ffmpeg \ imagemagick \ jq \ && rm -rf /var/lib/apt/lists/*
RUN pip3 install numpy pandas matplotlib
See [Docker deployment](/deploy/docker) for how to use custom images with Sandbox Agent.
Computer Use
Source: docs/computer-use.mdxCanonical URL: https://sandboxagent.dev/docs/computer-use
Description: Control a virtual desktop inside the sandbox with mouse, keyboard, screenshots, recordings, and live streaming.
--- Sandbox Agent provides a managed virtual desktop (Xvfb + openbox) that you can control programmatically. This is useful for browser automation, GUI testing, and AI computer-use workflows.
Start and stop
```ts TypeScript import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
const status = await sdk.startDesktop({ width: 1920, height: 1080, dpi: 96, });
console.log(status.state); // "active" console.log(status.display); // ":99"
// When done await sdk.stopDesktop();
curl -X POST "http://127.0.0.1:2468/v1/desktop/start" \ -H "Content-Type: application/json" \ -d '{"width":1920,"height":1080,"dpi":96}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/stop"
All fields in the start request are optional. Defaults are 1440x900 at 96 DPI.
### Start request options
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `width` | number | 1440 | Desktop width in pixels |
| `height` | number | 900 | Desktop height in pixels |
| `dpi` | number | 96 | Display DPI |
| `displayNum` | number | 99 | Starting X display number. The runtime probes from this number upward to find an available display. |
| `stateDir` | string | (auto) | Desktop state directory for home, logs, recordings |
| `streamVideoCodec` | string | `"vp8"` | WebRTC video codec (`vp8`, `vp9`, `h264`) |
| `streamAudioCodec` | string | `"opus"` | WebRTC audio codec (`opus`, `g722`) |
| `streamFrameRate` | number | 30 | Streaming frame rate (1-60) |
| `webrtcPortRange` | string | `"59050-59070"` | UDP port range for WebRTC media |
| `recordingFps` | number | 30 | Default recording FPS when not specified in `startDesktopRecording` (1-60) |
The streaming and recording options configure defaults for the desktop session. They take effect when streaming or recording is started later.
const status = await sdk.startDesktop({ width: 1920, height: 1080, streamVideoCodec: "h264", streamFrameRate: 60, webrtcPortRange: "59100-59120", recordingFps: 15, });
curl -X POST "http://127.0.0.1:2468/v1/desktop/start" \ -H "Content-Type: application/json" \ -d '{ "width": 1920, "height": 1080, "streamVideoCodec": "h264", "streamFrameRate": 60, "webrtcPortRange": "59100-59120", "recordingFps": 15 }'
## Status
const status = await sdk.getDesktopStatus(); console.log(status.state); // "inactive" | "active" | "failed" | ...
curl "http://127.0.0.1:2468/v1/desktop/status"
## Screenshots
Capture the full desktop or a specific region. Optionally include the cursor position.
// Full screenshot (PNG by default) const png = await sdk.takeDesktopScreenshot();
// JPEG at 70% quality, half scale const jpeg = await sdk.takeDesktopScreenshot({ format: "jpeg", quality: 70, scale: 0.5, });
// Include cursor overlay const withCursor = await sdk.takeDesktopScreenshot({ showCursor: true, });
// Region screenshot const region = await sdk.takeDesktopRegionScreenshot({ x: 100, y: 100, width: 400, height: 300, });
curl "http://127.0.0.1:2468/v1/desktop/screenshot" --output screenshot.png
curl "http://127.0.0.1:2468/v1/desktop/screenshot?format=jpeg&quality=70&scale=0.5" \ --output screenshot.jpg
Include cursor overlay
curl "http://127.0.0.1:2468/v1/desktop/screenshot?show_cursor=true" \ --output with_cursor.png
curl "http://127.0.0.1:2468/v1/desktop/screenshot/region?x=100&y=100&width=400&height=300" \ --output region.png
### Screenshot options
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `format` | string | `"png"` | Output format: `png`, `jpeg`, or `webp` |
| `quality` | number | 85 | Compression quality (1-100, JPEG/WebP only) |
| `scale` | number | 1.0 | Scale factor (0.1-1.0) |
| `showCursor` | boolean | `false` | Composite a crosshair at the cursor position |
When `showCursor` is enabled, the cursor position is captured at the moment of the screenshot and a red crosshair is drawn at that location. This is useful for AI agents that need to see where the cursor is in the screenshot.
## Mouse
// Get current position const pos = await sdk.getDesktopMousePosition(); console.log(pos.x, pos.y);
// Move await sdk.moveDesktopMouse({ x: 500, y: 300 });
// Click (left by default) await sdk.clickDesktop({ x: 500, y: 300 });
// Right click await sdk.clickDesktop({ x: 500, y: 300, button: "right" });
// Double click await sdk.clickDesktop({ x: 500, y: 300, clickCount: 2 });
// Drag await sdk.dragDesktopMouse({ startX: 100, startY: 100, endX: 400, endY: 400, });
// Scroll await sdk.scrollDesktop({ x: 500, y: 300, deltaY: -3 });
curl "http://127.0.0.1:2468/v1/desktop/mouse/position"
curl -X POST "http://127.0.0.1:2468/v1/desktop/mouse/click" \ -H "Content-Type: application/json" \ -d '{"x":500,"y":300}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/mouse/drag" \ -H "Content-Type: application/json" \ -d '{"startX":100,"startY":100,"endX":400,"endY":400}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/mouse/scroll" \ -H "Content-Type: application/json" \ -d '{"x":500,"y":300,"deltaY":-3}'
## Keyboard
// Type text await sdk.typeDesktopText({ text: "Hello, world!" });
// Press a key with modifiers await sdk.pressDesktopKey({ key: "c", modifiers: { ctrl: true }, });
// Low-level key down/up await sdk.keyDownDesktop({ key: "Shift_L" }); await sdk.keyUpDesktop({ key: "Shift_L" });
curl -X POST "http://127.0.0.1:2468/v1/desktop/keyboard/type" \ -H "Content-Type: application/json" \ -d '{"text":"Hello, world!"}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/keyboard/press" \ -H "Content-Type: application/json" \ -d '{"key":"c","modifiers":{"ctrl":true}}'
## Clipboard
Read and write the X11 clipboard programmatically.
// Read clipboard const clipboard = await sdk.getDesktopClipboard(); console.log(clipboard.text);
// Read primary selection (mouse-selected text) const primary = await sdk.getDesktopClipboard({ selection: "primary" });
// Write to clipboard await sdk.setDesktopClipboard({ text: "Pasted via API" });
// Write to both clipboard and primary selection await sdk.setDesktopClipboard({ text: "Synced text", selection: "both", });
curl "http://127.0.0.1:2468/v1/desktop/clipboard"
curl "http://127.0.0.1:2468/v1/desktop/clipboard?selection=primary"
curl -X POST "http://127.0.0.1:2468/v1/desktop/clipboard" \ -H "Content-Type: application/json" \ -d '{"text":"Pasted via API"}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/clipboard" \ -H "Content-Type: application/json" \ -d '{"text":"Synced text","selection":"both"}'
The `selection` parameter controls which X11 selection to read or write:
| Value | Description |
|-------|-------------|
| `clipboard` (default) | The standard clipboard (Ctrl+C / Ctrl+V) |
| `primary` | The primary selection (text selected with the mouse) |
| `both` | Write to both clipboard and primary selection (write only) |
## Display and windows
const display = await sdk.getDesktopDisplayInfo(); console.log(display.resolution); // { width: 1920, height: 1080, dpi: 96 }
const { windows } = await sdk.listDesktopWindows(); for (const win of windows) { console.log(win.title, win.x, win.y, win.width, win.height); }
curl "http://127.0.0.1:2468/v1/desktop/display/info"
curl "http://127.0.0.1:2468/v1/desktop/windows"
The windows endpoint filters out noise automatically: window manager internals (Openbox), windows with empty titles, and tiny helper windows (under 120x80) are excluded. The currently active/focused window is always included regardless of filters.
### Focused window
Get the currently focused window without listing all windows.
const focused = await sdk.getDesktopFocusedWindow(); console.log(focused.title, focused.id);
curl "http://127.0.0.1:2468/v1/desktop/windows/focused"
Returns 404 if no window currently has focus.
### Window management
Focus, move, and resize windows by their X11 window ID.
const { windows } = await sdk.listDesktopWindows(); const win = windows[0];
// Bring window to foreground await sdk.focusDesktopWindow(win.id);
// Move window await sdk.moveDesktopWindow(win.id, { x: 100, y: 50 });
// Resize window await sdk.resizeDesktopWindow(win.id, { width: 1280, height: 720 });
Focus a window
curl -X POST "http://127.0.0.1:2468/v1/desktop/windows/12345/focus"
Move a window
curl -X POST "http://127.0.0.1:2468/v1/desktop/windows/12345/move" \ -H "Content-Type: application/json" \ -d '{"x":100,"y":50}'
Resize a window
curl -X POST "http://127.0.0.1:2468/v1/desktop/windows/12345/resize" \ -H "Content-Type: application/json" \ -d '{"width":1280,"height":720}'
All three endpoints return the updated window info so you can verify the operation took effect. The window manager may adjust the requested position or size.
## App launching
Launch applications or open files/URLs on the desktop without needing to shell out.
// Launch an app by name const result = await sdk.launchDesktopApp({ app: "firefox", args: ["--private"], }); console.log(result.processId); // "proc_7"
// Launch and wait for the window to appear const withWindow = await sdk.launchDesktopApp({ app: "xterm", wait: true, }); console.log(withWindow.windowId); // "12345" or null if timed out
// Open a URL with the default handler const opened = await sdk.openDesktopTarget({ target: "https://example.com", }); console.log(opened.processId);
curl -X POST "http://127.0.0.1:2468/v1/desktop/launch" \ -H "Content-Type: application/json" \ -d '{"app":"firefox","args":["--private"]}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/launch" \ -H "Content-Type: application/json" \ -d '{"app":"xterm","wait":true}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/open" \ -H "Content-Type: application/json" \ -d '{"target":"https://example.com"}'
The returned `processId` can be used with the [Process API](/processes) to read logs (`GET /v1/processes/{id}/logs`) or stop the application (`POST /v1/processes/{id}/stop`).
When `wait` is `true`, the API polls for up to 5 seconds for a window to appear. If the window appears, its ID is returned in `windowId`. If it times out, `windowId` is `null` but the process is still running.
**Launch/Open vs the Process API:** Both `launch` and `open` are convenience wrappers around the [Process API](/processes). They create managed processes (with `owner: "desktop"`) that you can inspect, log, and stop through the same Process endpoints. The difference is that `launch` validates the binary exists in PATH first and can optionally wait for a window to appear, while `open` delegates to the system default handler (`xdg-open`). Use the Process API directly when you need full control over command, environment, working directory, or restart policies.
## Recording
Record the desktop to MP4.
const recording = await sdk.startDesktopRecording({ fps: 30 }); console.log(recording.id);
// ... do things ...
const stopped = await sdk.stopDesktopRecording();
// List all recordings const { recordings } = await sdk.listDesktopRecordings();
// Download const mp4 = await sdk.downloadDesktopRecording(recording.id);
// Clean up await sdk.deleteDesktopRecording(recording.id);
curl -X POST "http://127.0.0.1:2468/v1/desktop/recording/start" \ -H "Content-Type: application/json" \ -d '{"fps":30}'
curl -X POST "http://127.0.0.1:2468/v1/desktop/recording/stop"
curl "http://127.0.0.1:2468/v1/desktop/recordings"
curl "http://127.0.0.1:2468/v1/desktop/recordings/rec_1/download" --output recording.mp4
curl -X DELETE "http://127.0.0.1:2468/v1/desktop/recordings/rec_1"
## Desktop processes
The desktop runtime manages several background processes (Xvfb, openbox, neko, ffmpeg). These are all registered with the general [Process API](/processes) under the `desktop` owner, so you can inspect logs, check status, and troubleshoot using the same tools you use for any other managed process.
// List all processes, including desktop-owned ones const { processes } = await sdk.listProcesses();
const desktopProcs = processes.filter((p) => p.owner === "desktop"); for (const p of desktopProcs) { console.log(p.id, p.command, p.status); }
// Read logs from a specific desktop process const logs = await sdk.getProcessLogs(desktopProcs[0].id, { tail: 50 }); for (const entry of logs.entries) { console.log(entry.stream, atob(entry.data)); }
List all processes (desktop processes have owner: "desktop")
curl "http://127.0.0.1:2468/v1/processes"
Get logs from a specific desktop process
curl "http://127.0.0.1:2468/v1/processes/proc_1/logs?tail=50"
The desktop status endpoint also includes a summary of running processes:
const status = await sdk.getDesktopStatus(); for (const proc of status.processes) { console.log(proc.name, proc.pid, proc.running); }
curl "http://127.0.0.1:2468/v1/desktop/status"
Response includes: processes: [{ name: "Xvfb", pid: 123, running: true }, ...]
| Process | Role | Restart policy |
|---------|------|---------------|
| Xvfb | Virtual X11 framebuffer | Auto-restart while desktop is active |
| openbox | Window manager | Auto-restart while desktop is active |
| neko | WebRTC streaming server (started by `startDesktopStream`) | No auto-restart |
| ffmpeg | Screen recorder (started by `startDesktopRecording`) | No auto-restart |
## Live streaming
Start a WebRTC stream for real-time desktop viewing in a browser.
await sdk.startDesktopStream();
// Check stream status const status = await sdk.getDesktopStreamStatus(); console.log(status.active); // true console.log(status.processId); // "proc_5"
// Connect via the React DesktopViewer component or // use the WebSocket signaling endpoint directly // at ws://127.0.0.1:2468/v1/desktop/stream/signaling
await sdk.stopDesktopStream();
curl -X POST "http://127.0.0.1:2468/v1/desktop/stream/start"
Check stream status
curl "http://127.0.0.1:2468/v1/desktop/stream/status"
Connect to ws://127.0.0.1:2468/v1/desktop/stream/signaling for WebRTC signaling
curl -X POST "http://127.0.0.1:2468/v1/desktop/stream/stop"
For a drop-in React component, see [React Components](/react-components).
## API reference
### Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/v1/desktop/start` | Start the desktop runtime |
| `POST` | `/v1/desktop/stop` | Stop the desktop runtime |
| `GET` | `/v1/desktop/status` | Get desktop runtime status |
| `GET` | `/v1/desktop/screenshot` | Capture full desktop screenshot |
| `GET` | `/v1/desktop/screenshot/region` | Capture a region screenshot |
| `GET` | `/v1/desktop/mouse/position` | Get current mouse position |
| `POST` | `/v1/desktop/mouse/move` | Move the mouse |
| `POST` | `/v1/desktop/mouse/click` | Click the mouse |
| `POST` | `/v1/desktop/mouse/down` | Press mouse button down |
| `POST` | `/v1/desktop/mouse/up` | Release mouse button |
| `POST` | `/v1/desktop/mouse/drag` | Drag from one point to another |
| `POST` | `/v1/desktop/mouse/scroll` | Scroll at a position |
| `POST` | `/v1/desktop/keyboard/type` | Type text |
| `POST` | `/v1/desktop/keyboard/press` | Press a key with optional modifiers |
| `POST` | `/v1/desktop/keyboard/down` | Press a key down (hold) |
| `POST` | `/v1/desktop/keyboard/up` | Release a key |
| `GET` | `/v1/desktop/display/info` | Get display info |
| `GET` | `/v1/desktop/windows` | List visible windows |
| `GET` | `/v1/desktop/windows/focused` | Get focused window info |
| `POST` | `/v1/desktop/windows/{id}/focus` | Focus a window |
| `POST` | `/v1/desktop/windows/{id}/move` | Move a window |
| `POST` | `/v1/desktop/windows/{id}/resize` | Resize a window |
| `GET` | `/v1/desktop/clipboard` | Read clipboard contents |
| `POST` | `/v1/desktop/clipboard` | Write to clipboard |
| `POST` | `/v1/desktop/launch` | Launch an application |
| `POST` | `/v1/desktop/open` | Open a file or URL |
| `POST` | `/v1/desktop/recording/start` | Start recording |
| `POST` | `/v1/desktop/recording/stop` | Stop recording |
| `GET` | `/v1/desktop/recordings` | List recordings |
| `GET` | `/v1/desktop/recordings/{id}` | Get recording metadata |
| `GET` | `/v1/desktop/recordings/{id}/download` | Download recording |
| `DELETE` | `/v1/desktop/recordings/{id}` | Delete recording |
| `POST` | `/v1/desktop/stream/start` | Start WebRTC streaming |
| `POST` | `/v1/desktop/stream/stop` | Stop WebRTC streaming |
| `GET` | `/v1/desktop/stream/status` | Get stream status |
| `GET` | `/v1/desktop/stream/signaling` | WebSocket for WebRTC signaling |
### TypeScript SDK methods
| Method | Returns | Description |
|--------|---------|-------------|
| `startDesktop(request?)` | `DesktopStatusResponse` | Start the desktop |
| `stopDesktop()` | `DesktopStatusResponse` | Stop the desktop |
| `getDesktopStatus()` | `DesktopStatusResponse` | Get desktop status |
| `takeDesktopScreenshot(query?)` | `Uint8Array` | Capture screenshot |
| `takeDesktopRegionScreenshot(query)` | `Uint8Array` | Capture region screenshot |
| `getDesktopMousePosition()` | `DesktopMousePositionResponse` | Get mouse position |
| `moveDesktopMouse(request)` | `DesktopMousePositionResponse` | Move mouse |
| `clickDesktop(request)` | `DesktopMousePositionResponse` | Click mouse |
| `mouseDownDesktop(request)` | `DesktopMousePositionResponse` | Mouse button down |
| `mouseUpDesktop(request)` | `DesktopMousePositionResponse` | Mouse button up |
| `dragDesktopMouse(request)` | `DesktopMousePositionResponse` | Drag mouse |
| `scrollDesktop(request)` | `DesktopMousePositionResponse` | Scroll |
| `typeDesktopText(request)` | `DesktopActionResponse` | Type text |
| `pressDesktopKey(request)` | `DesktopActionResponse` | Press key |
| `keyDownDesktop(request)` | `DesktopActionResponse` | Key down |
| `keyUpDesktop(request)` | `DesktopActionResponse` | Key up |
| `getDesktopDisplayInfo()` | `DesktopDisplayInfoResponse` | Get display info |
| `listDesktopWindows()` | `DesktopWindowListResponse` | List windows |
| `getDesktopFocusedWindow()` | `DesktopWindowInfo` | Get focused window |
| `focusDesktopWindow(id)` | `DesktopWindowInfo` | Focus a window |
| `moveDesktopWindow(id, request)` | `DesktopWindowInfo` | Move a window |
| `resizeDesktopWindow(id, request)` | `DesktopWindowInfo` | Resize a window |
| `getDesktopClipboard(query?)` | `DesktopClipboardResponse` | Read clipboard |
| `setDesktopClipboard(request)` | `DesktopActionResponse` | Write clipboard |
| `launchDesktopApp(request)` | `DesktopLaunchResponse` | Launch an app |
| `openDesktopTarget(request)` | `DesktopOpenResponse` | Open file/URL |
| `startDesktopRecording(request?)` | `DesktopRecordingInfo` | Start recording |
| `stopDesktopRecording()` | `DesktopRecordingInfo` | Stop recording |
| `listDesktopRecordings()` | `DesktopRecordingListResponse` | List recordings |
| `getDesktopRecording(id)` | `DesktopRecordingInfo` | Get recording |
| `downloadDesktopRecording(id)` | `Uint8Array` | Download recording |
| `deleteDesktopRecording(id)` | `void` | Delete recording |
| `startDesktopStream()` | `DesktopStreamStatusResponse` | Start streaming |
| `stopDesktopStream()` | `DesktopStreamStatusResponse` | Stop streaming |
| `getDesktopStreamStatus()` | `DesktopStreamStatusResponse` | Stream status |
## Customizing the desktop environment
The desktop runs inside the sandbox filesystem, so you can customize it using the [File System](/file-system) API before or after starting the desktop. The desktop HOME directory is located at `~/.local/state/sandbox-agent/desktop/home` (or `$XDG_STATE_HOME/sandbox-agent/desktop/home` if `XDG_STATE_HOME` is set).
All configuration files below are written to paths relative to this HOME directory.
### Window manager (openbox)
The desktop uses [openbox](http://openbox.org/) as its window manager. You can customize its behavior, theme, and keyboard shortcuts by writing an `rc.xml` config file.
const openboxConfig = <?xml version="1.0" encoding="UTF-8"?> <openbox_config xmlns="http://openbox.org/3.4/rc"> <theme> <name>Clearlooks</name> <titleLayout>NLIMC</titleLayout> <font place="ActiveWindow"><name>DejaVu Sans</name><size>10</size></font> </theme> <desktops><number>1</number></desktops> <keyboard> <keybind key="A-F4"><action name="Close"/></keybind> <keybind key="A-Tab"><action name="NextWindow"/></keybind> </keyboard> </openbox_config>;
await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox" }); await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox/rc.xml" }, openboxConfig, );
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox"
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox/rc.xml" \ -H "Content-Type: application/octet-stream" \ --data-binary @rc.xml
### Autostart programs
Openbox runs scripts in `~/.config/openbox/autostart` on startup. Use this to launch applications, set the background, or configure the environment.
const autostart = `#!/bin/sh
Set a solid background color
xsetroot -solid "#1e1e2e" &
Launch a terminal
xterm -geometry 120x40+50+50 &
Launch a browser
firefox --no-remote & `;
await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox" }); await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" }, autostart, );
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox"
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" \ -H "Content-Type: application/octet-stream" \ --data-binary @autostart.sh
The autostart script runs when openbox starts, which happens during `startDesktop()`. Write the autostart file before calling `startDesktop()` for it to take effect.
### Background
There is no wallpaper set by default (the background is the X root window default). You can set it using `xsetroot` in the autostart script (as shown above), or use `feh` if you need an image:
// Upload a wallpaper image import fs from "node:fs";
const wallpaper = await fs.promises.readFile("./wallpaper.png"); await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/wallpaper.png" }, wallpaper, );
// Set the autostart to apply it const autostart = #!/bin/sh feh --bg-fill ~/wallpaper.png & ;
await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox" }); await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" }, autostart, );
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/wallpaper.png" \ -H "Content-Type: application/octet-stream" \ --data-binary @wallpaper.png
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" \ -H "Content-Type: application/octet-stream" \ --data-binary @autostart.sh
`feh` is not installed by default. Install it via the [Process API](/processes) before starting the desktop: `await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "feh"] })`.
### Fonts
Only `fonts-dejavu-core` is installed by default. To add more fonts, install them with your system package manager or copy font files into the sandbox:
// Install a font package await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "fonts-noto", "fonts-liberation"], });
// Or copy a custom font file import fs from "node:fs";
const font = await fs.promises.readFile("./CustomFont.ttf"); await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.local/share/fonts" }); await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/.local/share/fonts/CustomFont.ttf" }, font, );
// Rebuild the font cache await sdk.runProcess({ command: "fc-cache", args: ["-fv"] });
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","fonts-noto","fonts-liberation"]}'
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.local/share/fonts"
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.local/share/fonts/CustomFont.ttf" \ -H "Content-Type: application/octet-stream" \ --data-binary @CustomFont.ttf
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"fc-cache","args":["-fv"]}'
### Cursor theme
await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "dmz-cursor-theme"], });
const xresources = Xcursor.theme: DMZ-White\nXcursor.size: 24\n; await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/.Xresources" }, xresources, );
curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ -H "Content-Type: application/json" \ -d '{"command":"apt-get","args":["install","-y","dmz-cursor-theme"]}'
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.Xresources" \ -H "Content-Type: application/octet-stream" \ --data-binary 'Xcursor.theme: DMZ-White\nXcursor.size: 24'
Run `xrdb -merge ~/.Xresources` (via the autostart or process API) after writing the file for changes to take effect.
### Shell and terminal
No terminal emulator or shell is launched by default. Add one to the openbox autostart:
In ~/.config/openbox/autostart
xterm -geometry 120x40+50+50 &
To use a different shell, set the `SHELL` environment variable in your Dockerfile or install your preferred shell and configure the terminal to use it.
### GTK theme
Applications using GTK will pick up settings from `~/.config/gtk-3.0/settings.ini`:
const gtkSettings = [Settings] gtk-theme-name=Adwaita gtk-icon-theme-name=Adwaita gtk-font-name=DejaVu Sans 10 gtk-cursor-theme-name=DMZ-White gtk-cursor-theme-size=24 ;
await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0" }); await sdk.writeFsFile( { path: "~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0/settings.ini" }, gtkSettings, );
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0"
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0/settings.ini" \ -H "Content-Type: application/octet-stream" \ --data-binary @settings.ini
### Summary of configuration paths
All paths are relative to the desktop HOME directory (`~/.local/state/sandbox-agent/desktop/home`).
| What | Path | Notes |
|------|------|-------|
| Openbox config | `.config/openbox/rc.xml` | Window manager theme, keybindings, behavior |
| Autostart | `.config/openbox/autostart` | Shell script run on desktop start |
| Custom fonts | `.local/share/fonts/` | TTF/OTF files, run `fc-cache -fv` after |
| Cursor theme | `.Xresources` | Requires `xrdb -merge` to apply |
| GTK 3 settings | `.config/gtk-3.0/settings.ini` | Theme, icons, fonts for GTK apps |
| Wallpaper | Any path, referenced from autostart | Requires `feh` or similar tool |
CORS Configuration
Source: docs/cors.mdxCanonical URL: https://sandboxagent.dev/docs/cors
Description: Configure CORS for browser-based applications.
--- When calling the Sandbox Agent server from a browser, CORS (Cross-Origin Resource Sharing) controls which origins can make requests.
Default Behavior
By default, no CORS origins are allowed. You must explicitly specify origins for browser-based applications:
sandbox-agent server \
--cors-allow-origin "http://localhost:5173"The built-in Inspector UI at /ui/ is served from the same origin as the server, so it does not require CORS configuration.
Options
| Flag | Description |
|---|---|
--cors-allow-origin | Origins to allow |
--cors-allow-method | HTTP methods to allow (defaults to all if not specified) |
--cors-allow-header | Headers to allow (defaults to all if not specified) |
--cors-allow-credentials | Allow credentials (cookies, authorization headers) |
Multiple Origins
Specify the flag multiple times to allow multiple origins:
sandbox-agent server \
--cors-allow-origin "http://localhost:5173" \
--cors-allow-origin "http://localhost:3000"Restricting Methods and Headers
By default, all methods and headers are allowed. To restrict them:
sandbox-agent server \
--cors-allow-origin "https://your-app.com" \
--cors-allow-method "GET" \
--cors-allow-method "POST" \
--cors-allow-header "Authorization" \
--cors-allow-header "Content-Type" \
--cors-allow-credentialsCustom Tools
Source: docs/custom-tools.mdxCanonical URL: https://sandboxagent.dev/docs/custom-tools
Description: Give agents custom tools inside the sandbox using MCP servers or skills.
--- There are two common patterns for sandbox-local custom tooling:
| MCP Server | Skill | |
|---|---|---|
| How it works | Agent connects to an MCP server (mcpServers) | Agent follows SKILL.md instructions and runs scripts |
| Best for | Typed tool calls and structured protocols | Lightweight task-specific guidance |
| Requires | MCP server process (stdio/http/sse) | Script + SKILL.md |
Option A: MCP server (stdio)
Write and bundle your MCP server
```ts src/mcp-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod";
const server = new McpServer({ name: "rand", version: "1.0.0" });
server.tool( "random_number", "Generate a random integer between min and max", { min: z.number(), max: z.number(), }, async ({ min, max }) => ({ content: [{ type: "text", text: String(Math.floor(Math.random() * (max - min + 1)) + min) }], }), );
await server.connect(new StdioServerTransport());
npx esbuild src/mcp-server.ts --bundle --format=cjs --platform=node --target=node18 --outfile=dist/mcp-server.cjs
### Upload it into the sandbox
import { SandboxAgent } from "sandbox-agent"; import fs from "node:fs";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468" }); const content = await fs.promises.readFile("./dist/mcp-server.cjs");
await sdk.writeFsFile({ path: "/opt/mcp/custom-tools/mcp-server.cjs" }, content);
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/mcp/custom-tools/mcp-server.cjs" \ --data-binary @./dist/mcp-server.cjs
### Register MCP config and create a session
await sdk.setMcpConfig( { directory: "/workspace", mcpName: "customTools", }, { type: "local", command: "node", args: ["/opt/mcp/custom-tools/mcp-server.cjs"], }, );
const session = await sdk.createSession({ agent: "claude", cwd: "/workspace", });
await session.prompt([ { type: "text", text: "Use the random_number tool with min=1 and max=10." }, ]);
## Option B: Skills
### Write script + skill file
const min = Number(process.argv[2]); const max = Number(process.argv[3]);
if (Number.isNaN(min) || Number.isNaN(max)) { console.error("Usage: random-number <min> <max>"); process.exit(1); }
console.log(Math.floor(Math.random() * (max - min + 1)) + min);
--- name: random-number description: Generate a random integer between min and max. ---
Run:
node /opt/skills/random-number/random-number.cjs <min> <max>````
npx esbuild src/random-number.ts --bundle --format=cjs --platform=node --target=node18 --outfile=dist/random-number.cjsUpload files
import fs from "node:fs";
const script = await fs.promises.readFile("./dist/random-number.cjs");
await sdk.writeFsFile({ path: "/opt/skills/random-number/random-number.cjs" }, script);
const skill = await fs.promises.readFile("./SKILL.md");
await sdk.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skill);Use in a session
const session = await sdk.createSession({
agent: "claude",
cwd: "/workspace",
});
await session.prompt([
{ type: "text", text: "Use the random-number skill to pick a number from 1 to 100." },
]);Notes
- The sandbox runtime must include Node.js (or your chosen runtime).
- For persistent skill-source wiring by directory, see Skills.
Daemon
Source: docs/daemon.mdxCanonical URL: https://sandboxagent.dev/docs/daemon
Description: Background daemon lifecycle and management.
--- The sandbox-agent daemon is a background server process. Commands like sandbox-agent opencode and gigacode can ensure it is running.
How it works
1. A daemon-aware command checks for a healthy daemon at host/port. 2. If missing, it starts one in the background and records PID/version files. 3. Subsequent checks can compare build/version and restart when required.
Auto-upgrade behavior
sandbox-agent opencodeandgigacodeuse ensure-running behavior with upgrade checks.sandbox-agent daemon startuses direct start by default.sandbox-agent daemon start --upgradeuses ensure-running behavior (including version check/restart).
Managing the daemon
Start
sandbox-agent daemon start [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host |
-p, --port | 2468 | Port |
--upgrade | false | Use ensure-running + upgrade behavior |
sandbox-agent daemon start
sandbox-agent daemon start --upgradeStop
sandbox-agent daemon stop [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host |
-p, --port | 2468 | Port |
Status
sandbox-agent daemon status [OPTIONS]| Option | Default | Description |
|---|---|---|
-H, --host | 127.0.0.1 | Host |
-p, --port | 2468 | Port |
Files
Daemon state is stored under the sandbox-agent data directory (for example ~/.local/share/sandbox-agent/daemon/):
| File | Purpose |
|---|---|
daemon-{host}-{port}.pid | PID of running daemon |
daemon-{host}-{port}.version | Build/version marker |
daemon-{host}-{port}.log | Daemon stdout/stderr log |
Agent Computer
Source: docs/deploy/agentcomputer.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/agentcomputer
Description: Run Sandbox Agent on Agent Computer managed workers.
---
Prerequisites
COMPUTER_API_KEYorAGENTCOMPUTER_API_KEY- An Agent Computer managed-worker image, or default machine source, that already has your coding agent authenticated
The platform image already includes Sandbox Agent, Claude Code, and Codex. Agent credentials still live inside the machine, so make sure your image or workspace bootstrap has already handled agent login.
TypeScript example
npm install sandbox-agent@0.4.ximport { SandboxAgent } from "sandbox-agent";
import { agentcomputer } from "sandbox-agent/agentcomputer";
const sdk = await SandboxAgent.start({
sandbox: agentcomputer(),
});
try {
const health = await sdk.getHealth();
console.log(health.status);
console.log(sdk.inspectorUrl);
const agents = await sdk.listAgents();
console.log(agents.agents.map((agent) => agent.id));
} finally {
await sdk.destroySandbox();
}The agentcomputer provider creates a managed-worker computer, waits until browser access is ready, and routes SDK requests through the machine's authenticated web host. sdk.inspectorUrl is already a browser-ready URL, so it opens the built-in Inspector directly.
By default, the provider creates:
runtimeFamily: "managed-worker"usePlatformDefault: true
Pass create when you want to override the handle, workspace name, or other machine-create settings:
const sdk = await SandboxAgent.start({
sandbox: agentcomputer({
create: {
handle: "sandbox-agent-demo",
workspaceName: "sandbox-agent-demo",
usePlatformDefault: false,
},
}),
});Set apiUrl when you are targeting a self-hosted or local Agent Computer API instead of https://api.computer.agentcomputer.ai.
BoxLite
Source: docs/deploy/boxlite.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/boxlite
Description: Run Sandbox Agent inside a BoxLite micro-VM.
--- BoxLite is a local-first micro-VM sandbox — no cloud account needed. See BoxLite docs for platform requirements (KVM on Linux, Apple Silicon on macOS).
Prerequisites
@boxlite-ai/boxliteinstalled (requires KVM or Apple Hypervisor)- Docker (to build the base image)
ANTHROPIC_API_KEYorOPENAI_API_KEY
Base image
Build a Docker image with Sandbox Agent pre-installed, then export it as an OCI layout that BoxLite can load directly (BoxLite has its own image store separate from Docker):
FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
RUN sandbox-agent install-agent claude
RUN sandbox-agent install-agent codexdocker build -t sandbox-agent-boxlite .
mkdir -p oci-image
docker save sandbox-agent-boxlite | tar -xf - -C oci-imageTypeScript example
import { SimpleBox } from "@boxlite-ai/boxlite";
import { SandboxAgent } from "sandbox-agent";
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 box = new SimpleBox({
rootfsPath: "./oci-image",
env,
ports: [{ hostPort: 3000, guestPort: 3000 }],
diskSizeGb: 4,
});
await box.exec("sh", "-c",
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"
);
const baseUrl = "http://localhost: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 box.stop();Cloudflare
Source: docs/deploy/cloudflare.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/cloudflare
Description: Deploy Sandbox Agent inside a Cloudflare Sandbox.
---
Prerequisites
- Cloudflare account with Workers paid plan
- Docker for local
wrangler dev ANTHROPIC_API_KEYorOPENAI_API_KEY
Cloudflare Sandbox SDK is beta. See Sandbox SDK docs.
Quick start
npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
cd my-sandboxDockerfile
FROM cloudflare/sandbox:0.7.0
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex
EXPOSE 8000TypeScript example (with provider)
For standalone scripts, use the cloudflare provider:
npm install sandbox-agent@0.4.x @cloudflare/sandboximport { 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(...):
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
import { Hono } from "hono";
import { SandboxAgent } from "sandbox-agent";
export { Sandbox } from "@cloudflare/sandbox";
type Bindings = {
Sandbox: DurableObjectNamespace<Sandbox>;
ASSETS: Fetcher;
ANTHROPIC_API_KEY?: string;
OPENAI_API_KEY?: string;
};
const app = new Hono<{ Bindings: Bindings }>();
const PORT = 8000;
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
try {
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
return result.success;
} catch {
return false;
}
}
async function getReadySandbox(name: string, env: Bindings): Promise<Sandbox> {
const sandbox = getSandbox(env.Sandbox, name);
if (!(await isServerRunning(sandbox))) {
const envVars: Record<string, string> = {};
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
await sandbox.setEnvVars(envVars);
await sandbox.startProcess(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`);
}
return sandbox;
}
app.post("/sandbox/:name/prompt", async (c) => {
const sandbox = await getReadySandbox(c.req.param("name"), c.env);
const sdk = await SandboxAgent.connect({
fetch: (input, init) =>
sandbox.containerFetch(
input as Request | string | URL,
{
...(init ?? {}),
// Avoid passing AbortSignal through containerFetch; it can drop streamed session updates.
signal: undefined,
},
PORT,
),
});
const session = await sdk.createSession({ agent: "codex" });
const response = await session.prompt([{ type: "text", text: "Summarize this repository" }]);
await sdk.destroySession(session.id);
await sdk.dispose();
return c.json(response);
});
app.all("/sandbox/:name/proxy/*", async (c) => {
const sandbox = await getReadySandbox(c.req.param("name"), c.env);
const wildcard = c.req.param("*");
const path = wildcard ? `/${wildcard}` : "/";
const query = new URL(c.req.raw.url).search;
return sandbox.containerFetch(new Request(`http://localhost${path}${query}`, c.req.raw), PORT);
});
app.all("*", (c) => c.env.ASSETS.fetch(c.req.raw));
export default app;This keeps all Sandbox Agent calls inside the Cloudflare sandbox routing path and does not require a baseUrl.
Troubleshooting streaming updates
If you only receive:
- the outbound prompt request
- the final
{ stopReason: "end_turn" }response
then the streamed update channel dropped. In Cloudflare sandbox paths, this is typically caused by forwarding AbortSignal from SDK fetch init into containerFetch(...).
Fix:
const sdk = await SandboxAgent.connect({
fetch: (input, init) =>
sandbox.containerFetch(
input as Request | string | URL,
{
...(init ?? {}),
// Avoid passing AbortSignal through containerFetch; it can drop streamed session updates.
signal: undefined,
},
PORT,
),
});This keeps prompt completion behavior the same, but restores streamed text/tool updates.
Local development
npm run devTest health:
curl http://localhost:8787/sandbox/demo/proxy/v1/healthProduction deployment
wrangler deployComputeSDK
Source: docs/deploy/computesdk.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/computesdk
Description: Deploy Sandbox Agent using ComputeSDK's provider-agnostic sandbox API.
--- ComputeSDK provides a unified interface for managing sandboxes across multiple providers. Write once, deploy anywhere by changing environment variables.
Prerequisites
COMPUTESDK_API_KEYfrom console.computesdk.com- Provider API key (one of:
E2B_API_KEY,DAYTONA_API_KEY,VERCEL_TOKEN,MODAL_TOKEN_ID+MODAL_TOKEN_SECRET,BLAXEL_API_KEY,CSB_API_KEY) ANTHROPIC_API_KEYorOPENAI_API_KEY
TypeScript example
npm install sandbox-agent@0.4.x computesdkimport { SandboxAgent } from "sandbox-agent";
import { computesdk } from "sandbox-agent/computesdk";
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 sdk = await SandboxAgent.start({
sandbox: computesdk({
create: {
envs,
image: process.env.COMPUTESDK_IMAGE,
templateId: process.env.COMPUTESDK_TEMPLATE_ID,
},
}),
});
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 computesdk provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. ComputeSDK routes to your configured provider behind the scenes. The create option now forwards the full ComputeSDK sandbox-create payload, including provider-specific fields such as image and templateId when the selected provider supports them.
Before calling SandboxAgent.start(), configure ComputeSDK with your provider:
import { compute } from "computesdk";
compute.setConfig({
provider: "e2b", // or auto-detect via detectProvider()
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
});Supported providers
ComputeSDK auto-detects your provider from environment variables:
| Provider | Environment Variables |
|---|---|
| E2B | E2B_API_KEY |
| Daytona | DAYTONA_API_KEY |
| Vercel | VERCEL_TOKEN or VERCEL_OIDC_TOKEN |
| Modal | MODAL_TOKEN_ID + MODAL_TOKEN_SECRET |
| Blaxel | BLAXEL_API_KEY |
| CodeSandbox | CSB_API_KEY |
Notes
- Provider resolution: Set
COMPUTESDK_PROVIDERto force a specific provider, or let ComputeSDK auto-detect from API keys. sandbox.runCommand(..., { background: true })keeps the server running while your app continues.sandbox.getUrl({ port })returns a public URL for the sandbox port.- Always destroy the sandbox when done to avoid leaking resources.
Daytona
Source: docs/deploy/daytona.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/daytona
Description: Run Sandbox Agent in a Daytona workspace.
--- Daytona Tier 3+ is required for access to common model provider endpoints. See Daytona network limits.
Prerequisites
DAYTONA_API_KEYANTHROPIC_API_KEYorOPENAI_API_KEY
TypeScript example
npm install sandbox-agent@0.4.x @daytonaio/sdkimport { 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 sdk = await SandboxAgent.start({
sandbox: daytona({
create: { envVars },
}),
});
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.4.2-full image by default and starts the server automatically.
Using snapshots for faster startup
import { Daytona, Image } from "@daytonaio/sdk";
const daytona = new Daytona();
const SNAPSHOT = "sandbox-agent-ready";
const hasSnapshot = await daytona.snapshot.get(SNAPSHOT).then(() => true, () => false);
if (!hasSnapshot) {
await daytona.snapshot.create({
name: SNAPSHOT,
image: Image.base("ubuntu:22.04").runCommands(
"apt-get update && apt-get install -y curl ca-certificates",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh",
"sandbox-agent install-agent claude",
"sandbox-agent install-agent codex",
),
});
}Docker
Source: docs/deploy/docker.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/docker
Description: Build and run Sandbox Agent in a Docker container.
--- Docker is not recommended for production isolation of untrusted workloads. Use dedicated sandbox providers (E2B, Daytona, etc.) for stronger isolation.
Quick start
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.4.2-full \
server --no-token --host 0.0.0.0 --port 3000The 0.4.2-full tag pins the exact version. The moving full tag is also published for contributors who want the latest full image.
If you also want the desktop API inside the container, install desktop dependencies before starting the server:
docker run --rm -p 3000:3000 \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
node:22-bookworm-slim sh -c "\
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6 && \
rm -rf /var/lib/apt/lists/* && \
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && \
sandbox-agent install desktop --yes && \
sandbox-agent server --no-token --host 0.0.0.0 --port 3000"In a Dockerfile:
RUN sandbox-agent install desktop --yesTypeScript with dockerode
import Docker from "dockerode";
import { SandboxAgent } from "sandbox-agent";
const docker = new Docker();
const PORT = 3000;
const container = await docker.createContainer({
Image: "rivetdev/sandbox-agent:0.4.2-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}` }] },
},
});
await container.start();
const baseUrl = `http://127.0.0.1:${PORT}`;
const sdk = await SandboxAgent.connect({ baseUrl });
const session = await sdk.createSession({ agent: "codex" });
await session.prompt([{ type: "text", text: "Summarize this repository." }]);Building a custom image with everything preinstalled
If you need to extend your own base image, install Sandbox Agent and preinstall every supported agent in one step:
FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
bash ca-certificates curl git && \
rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && \
sandbox-agent install-agent --all
RUN useradd -m -s /bin/bash sandbox
USER sandbox
WORKDIR /home/sandbox
EXPOSE 2468
ENTRYPOINT ["sandbox-agent"]
CMD ["server", "--host", "0.0.0.0", "--port", "2468"]Building from source
docker build -f docker/release/linux-x86_64.Dockerfile -t sandbox-agent-build .
docker run --rm -v "$PWD/artifacts:/artifacts" sandbox-agent-buildBinary output: ./artifacts/sandbox-agent-x86_64-unknown-linux-musl.
E2B
Source: docs/deploy/e2b.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/e2b
Description: Deploy Sandbox Agent inside an E2B sandbox.
---
Prerequisites
E2B_API_KEYANTHROPIC_API_KEYorOPENAI_API_KEY
TypeScript example
npm install sandbox-agent@0.4.x @e2b/code-interpreterimport { 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 template = process.env.E2B_TEMPLATE;
const sdk = await SandboxAgent.start({
sandbox: e2b({
template,
create: { envs },
}),
});
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. Sandboxes pause by default instead of being deleted, and reconnecting with the same sandboxId resumes them automatically.
Pass template when you want to start from a custom E2B template alias or template ID. E2B base-image selection happens when you build the template, then sandbox-agent/e2b uses that template at sandbox creation time.
Faster cold starts
For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed. Build System 2.0 also lets you choose the template's base image in code. See E2B Custom Templates and E2B Base Images.
Local
Source: docs/deploy/local.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/local
Description: Run Sandbox Agent locally for development.
--- For local development, run Sandbox Agent directly on your machine.
With the CLI
# Install
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
# Run
sandbox-agent server --no-token --host 127.0.0.1 --port 2468Or with npm/Bun:
npx
npx @sandbox-agent/cli@0.4.x server --no-token --host 127.0.0.1 --port 2468bunx
bunx @sandbox-agent/cli@0.4.x server --no-token --host 127.0.0.1 --port 2468With the TypeScript SDK
The SDK can spawn and manage the server as a subprocess using the local provider:
import { SandboxAgent } from "sandbox-agent";
import { local } from "sandbox-agent/local";
const sdk = await SandboxAgent.start({
sandbox: local(),
});
const session = await sdk.createSession({
agent: "claude",
});
await session.prompt([
{ type: "text", text: "Summarize this repository." },
]);
await sdk.destroySandbox();This starts the server on an available local port and connects automatically.
Pass options to customize the local provider:
const sdk = await SandboxAgent.start({
sandbox: local({
port: 3000,
log: "inherit",
env: {
ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY,
},
}),
});Modal
Source: docs/deploy/modal.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/modal
Description: Deploy Sandbox Agent inside a Modal sandbox.
---
Prerequisites
MODAL_TOKEN_IDandMODAL_TOKEN_SECRETfrom modal.com/settingsANTHROPIC_API_KEYorOPENAI_API_KEY
TypeScript example
npm install sandbox-agent@0.4.x modalimport { SandboxAgent } from "sandbox-agent";
import { modal } from "sandbox-agent/modal";
const secrets: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const baseImage = process.env.MODAL_BASE_IMAGE ?? "node:22-slim";
const sdk = await SandboxAgent.start({
sandbox: modal({
image: baseImage,
create: { secrets },
}),
});
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 modal provider handles app creation, image building, sandbox provisioning, agent installation, server startup, and tunnel networking automatically. Set image to change the base Docker image before Sandbox Agent and its agent binaries are layered on top. You can also pass a prebuilt Modal Image object.
Faster cold starts
Modal caches image layers, so the Dockerfile commands that install curl and sandbox-agent only run on the first build. Subsequent sandbox creates reuse the cached image.
Notes
Vercel
Source: docs/deploy/vercel.mdxCanonical URL: https://sandboxagent.dev/docs/deploy/vercel
Description: Deploy Sandbox Agent inside a Vercel Sandbox.
---
Prerequisites
VERCEL_OIDC_TOKENorVERCEL_ACCESS_TOKENANTHROPIC_API_KEYorOPENAI_API_KEY
TypeScript example
npm install sandbox-agent@0.4.x @vercel/sandboximport { 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. See Vercel Sandbox docs.
File System
Source: docs/file-system.mdxCanonical URL: https://sandboxagent.dev/docs/file-system
Description: Read, write, and manage files inside the sandbox.
--- The filesystem API lets you list, read, write, move, and delete files inside the sandbox, plus upload tar archives in batch.
Path resolution
- Absolute paths are used as-is.
- Relative paths resolve from the server process working directory.
- Requests that attempt to escape allowed roots are rejected by the server.
List entries
```ts TypeScript import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
const entries = await sdk.listFsEntries({ path: "./workspace", });
console.log(entries);
curl -X GET "http://127.0.0.1:2468/v1/fs/entries?path=./workspace"
## Read and write files
`PUT /v1/fs/file` writes raw bytes. `GET /v1/fs/file` returns raw bytes.
import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
await sdk.writeFsFile({ path: "./notes.txt" }, "hello");
const bytes = await sdk.readFsFile({ path: "./notes.txt" }); const text = new TextDecoder().decode(bytes);
console.log(text);
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt" \ --data-binary "hello"
curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt" \ --output ./notes.txt
## Create directories
import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
await sdk.mkdirFs({ path: "./data" });
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=./data"
## Move, delete, and stat
import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
await sdk.moveFs({ from: "./notes.txt", to: "./notes-old.txt", overwrite: true, });
const stat = await sdk.statFs({ path: "./notes-old.txt" }); await sdk.deleteFsEntry({ path: "./notes-old.txt" });
console.log(stat);
curl -X POST "http://127.0.0.1:2468/v1/fs/move" \ -H "Content-Type: application/json" \ -d '{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true}'
curl -X GET "http://127.0.0.1:2468/v1/fs/stat?path=./notes-old.txt"
curl -X DELETE "http://127.0.0.1:2468/v1/fs/entry?path=./notes-old.txt"
## Batch upload (tar)
Batch upload accepts `application/x-tar` and extracts into the destination directory.
import { SandboxAgent } from "sandbox-agent"; import fs from "node:fs"; import path from "node:path"; import tar from "tar";
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", });
const archivePath = path.join(process.cwd(), "skills.tar"); await tar.c({ cwd: "./skills", file: archivePath, }, ["."]);
const tarBuffer = await fs.promises.readFile(archivePath); const result = await sdk.uploadFsBatch(tarBuffer, { path: "./skills", });
console.log(result);
tar -cf skills.tar -C ./skills .
curl -X POST "http://127.0.0.1:2468/v1/fs/upload-batch?path=./skills" \ -H "Content-Type: application/x-tar" \ --data-binary @skills.tar
Inspector
Source: docs/inspector.mdxCanonical URL: https://sandboxagent.dev/docs/inspector
Description: Debug and inspect agent sessions with the Inspector UI.
--- The Inspector is a web UI for inspecting Sandbox Agent sessions. Use it to view events, inspect payloads, and troubleshoot behavior.

Open the Inspector
The Inspector is served at /ui/ on your Sandbox Agent server. For example, if your server runs at http://localhost:2468, open http://localhost:2468/ui/.
You can also generate a pre-filled Inspector URL from the SDK:
import { buildInspectorUrl } from "sandbox-agent";
const url = buildInspectorUrl({
baseUrl: "http://127.0.0.1:2468",
});
console.log(url);
// http://127.0.0.1:2468/ui/Features
- Session list
- Event stream view
- Event JSON inspector
- Prompt testing
- Request/response debugging
- Interactive permission prompts (approve, always-allow, or reject tool-use requests)
- Desktop panel for status, remediation, start/stop, and screenshot refresh
- Process management (create, stop, kill, delete, view logs)
- Interactive PTY terminal for tty processes
- One-shot command execution
When to use
- Development: validate session behavior quickly
- Debugging: inspect raw event payloads
- Integration work: compare UI behavior with SDK/API calls
Process terminal
The Inspector includes an embedded Ghostty-based terminal for interactive tty processes. The UI uses the SDK's high-level connectProcessTerminal(...) wrapper via the shared @sandbox-agent/react ProcessTerminal component.
Desktop panel
The Desktop panel shows the current desktop runtime state, missing dependencies, the suggested install command, last error details, process/log paths, and the latest captured screenshot.
Use it to:
- Check whether desktop dependencies are installed
- Start or stop the managed desktop runtime
- Refresh desktop status
- Capture a fresh screenshot on demand
LLM Credentials
Source: docs/llm-credentials.mdxCanonical URL: https://sandboxagent.dev/docs/llm-credentials
Description: Strategies for providing LLM provider credentials to agents.
--- Sandbox Agent needs LLM provider credentials (Anthropic, OpenAI, etc.) to run agent sessions.
Configuration
Pass credentials via spawn.env when starting a sandbox. Each call to SandboxAgent.start() can use different credentials:
import { SandboxAgent } from "sandbox-agent";
const sdk = await SandboxAgent.start({
spawn: {
env: {
ANTHROPIC_API_KEY: "sk-ant-...",
OPENAI_API_KEY: "sk-...",
},
},
});Each agent requires credentials from a specific provider. Sandbox Agent checks environment variables (including those passed via spawn.env) and host config files:
| Agent | Provider | Environment variables | Config files |
|---|---|---|---|
| Claude Code | Anthropic | ANTHROPIC_API_KEY, CLAUDE_API_KEY | ~/.claude.json, ~/.claude/.credentials.json |
| Amp | Anthropic | ANTHROPIC_API_KEY, CLAUDE_API_KEY | ~/.amp/config.json |
| Codex | OpenAI | OPENAI_API_KEY, CODEX_API_KEY | ~/.codex/auth.json |
| OpenCode | Anthropic or OpenAI | ANTHROPIC_API_KEY, OPENAI_API_KEY | ~/.local/share/opencode/auth.json |
| Mock | None | - | - |
Credential strategies
LLM credentials are passed into the sandbox as environment variables. The agent and everything inside the sandbox has access to the token, so it's important to choose the right strategy for how you provision and scope these credentials.
| Strategy | Who pays | Cost attribution | Best for |
|---|---|---|---|
| Per-tenant gateway (recommended) | Your organization, billed back per tenant | Per-tenant keys with budgets | Multi-tenant SaaS, usage-based billing |
| Bring your own key | Each user (usage-based) | Per-user by default | Dev environments, internal tools |
| Shared API key | Your organization | None (single bill) | Single-tenant apps, internal platforms |
| Personal subscription | Each user (existing subscription) | Per-user by default | Local dev, internal tools where users have Claude or Codex subscriptions |
Per-tenant gateway (recommended)
Route LLM traffic through a gateway that mints per-tenant API keys, each with its own spend tracking and budget limits.
graph LR
B[Your Backend] -->|tenant key| S[Sandbox]
S -->|LLM requests| G[Gateway]
G -->|scoped key| P[LLM Provider]Your backend issues a scoped key per tenant, then passes it to the sandbox. This is the typical pattern when using sandbox providers (E2B, Daytona, Docker).
```typescript expandable import { SandboxAgent } from "sandbox-agent";
async function createTenantSandbox(tenantId: string) { // Issue a scoped key for this tenant via OpenRouter const res = await fetch("https://openrouter.ai/api/v1/keys", { method: "POST", headers: { Authorization: Bearer ${process.env.OPENROUTER_PROVISIONING_KEY}, "Content-Type": "application/json", }, body: JSON.stringify({ name: tenant-${tenantId}, limit: 50, limitResetType: "monthly", }), }); const { key } = await res.json();
// Start a sandbox with the tenant's scoped key const sdk = await SandboxAgent.start({ spawn: { env: { OPENAI_API_KEY: key, // OpenRouter uses OpenAI-compatible endpoints }, }, });
const session = await sdk.createSession({ agent: "claude", sessionInit: { cwd: "/workspace" }, });
return { sdk, session }; }
#### Security
Recommended for multi-tenant applications. Each tenant gets a scoped key with its own budget, so exfiltration only exposes that tenant's allowance.
#### Use cases
- **Multi-tenant SaaS**: per-tenant spend tracking and budget limits
- **Production apps**: exposed to end users who need isolated credentials
- **Usage-based billing**: each tenant pays for their own consumption
#### Choosing a gateway
#### OpenRouter provisioned keys
Managed service, zero infrastructure. [OpenRouter](https://openrouter.ai/docs/features/provisioning-api-keys) provides per-tenant API keys with spend tracking and budget limits via their Provisioning API. Pass the tenant key to Sandbox Agent as `OPENAI_API_KEY` (OpenRouter uses OpenAI-compatible endpoints).
Create a key for a tenant with a $50/month budget
curl https://openrouter.ai/api/v1/keys \ -H "Authorization: Bearer $PROVISIONING_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "tenant-acme", "limit": 50, "limitResetType": "monthly" }'
Easiest to set up but not open-source. See [OpenRouter pricing](https://openrouter.ai/docs/framework/pricing) for details.
#### LiteLLM proxy
Self-hosted, open-source (MIT). [LiteLLM](https://github.com/BerriAI/litellm) is an OpenAI-compatible proxy with hierarchical budgets (org, team, user, key), virtual keys, and spend tracking. Requires Python + PostgreSQL.
Create a team (tenant) with a $500 budget
curl http://litellm:4000/team/new \ -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "team_alias": "tenant-acme", "max_budget": 500 }'
Generate a key for that team
curl http://litellm:4000/key/generate \ -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "team_id": "team-abc123", "max_budget": 100 }'
Full control with no vendor lock-in. Organization-level features require an enterprise license.
#### Portkey gateway
Self-hosted, open-source (Apache 2.0). [Portkey](https://github.com/Portkey-AI/gateway) is a lightweight OpenAI-compatible gateway supporting 200+ providers. Single binary, no database required. Create virtual keys with per-tenant budget limits and pass them to Sandbox Agent.
Lightest operational footprint of the self-hosted options. Observability and analytics require the managed platform or your own tooling.
To bill tenants for LLM usage, use [Stripe token billing](https://docs.stripe.com/billing/token-billing) (integrates natively with OpenRouter) or query your gateway's spend API and feed usage into your billing system.
### Bring your own key
Each user provides their own API key. Users are billed directly by the LLM provider with no additional infrastructure needed.
Pass the user's key via `spawn.env`:
const sdk = await SandboxAgent.start({ spawn: { env: { ANTHROPIC_API_KEY: userProvidedKey, }, }, });
#### Security
API keys are typically long-lived. The key is visible to the agent and anything running inside the sandbox, so exfiltration is possible. This is usually acceptable for developer-facing tools where the user owns the key.
#### Use cases
- **Developer tools**: each user manages their own API key
- **Internal platforms**: users already have LLM provider accounts
- **Per-user billing**: no extra infrastructure needed
### Shared credentials
A single organization-wide API key is used for all sessions. All token usage appears on one bill with no per-user or per-tenant cost attribution.
const sdk = await SandboxAgent.start({ spawn: { env: { ANTHROPIC_API_KEY: process.env.ORG_ANTHROPIC_KEY!, OPENAI_API_KEY: process.env.ORG_OPENAI_KEY!, }, }, });
If you need to track or limit spend per tenant, use a per-tenant gateway instead.
#### Security
Not recommended for anything other than internal tooling. A single exfiltrated key exposes your organization's entire LLM budget. If you need org-paid credentials for external users, use a per-tenant gateway with scoped keys instead.
#### Use cases
- **Single-tenant apps**: small number of users, one bill
- **Prototyping**: cost attribution not needed yet
- **Simplicity over security**: acceptable when exfiltration risk is low
### Personal subscription
If the user is signed into Claude Code or Codex on the host machine, Sandbox Agent automatically picks up their OAuth tokens. No configuration is needed.
#### Remote sandboxes
Extract credentials locally and pass them to a remote sandbox via `spawn.env`:
$ sandbox-agent credentials extract-env ANTHROPIC_API_KEY=sk-ant-... CLAUDE_API_KEY=sk-ant-... OPENAI_API_KEY=sk-... CODEX_API_KEY=sk-...
Use `-e` to prefix with `export` for shell sourcing.
#### Security
Personal subscriptions use OAuth tokens with a limited lifespan. These are the same credentials used when running an agent normally on the host. If a token is exfiltrated from the sandbox, the exposure window is short.
#### Use cases
- **Local development**: users are already signed into Claude Code or Codex
- **Internal tools**: every user has their own subscription
- **Prototyping**: no key management needed
Manage Sessions
Source: docs/manage-sessions.mdxCanonical URL: https://sandboxagent.dev/docs/manage-sessions
Description: Persist and replay agent transcripts across connections.
--- Sandbox Agent stores sessions in memory only. When the server restarts or the sandbox is destroyed, all session data is lost. It's your responsibility to persist events to your own database.
Recommended approach
1. Store events to your database as they arrive 2. On reconnect, get the last event's sequence and pass it as offset 3. The API returns events where sequence > offset
This prevents duplicate writes and lets you recover from disconnects.
Receiving Events
Two ways to receive events: streaming (recommended) or polling.
Streaming
Use streaming for real-time events with automatic reconnection support.
import { SandboxAgentClient } from "sandbox-agent";
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
agent: "mock",
});
// Get offset from last stored event (0 returns all events)
const lastEvent = await db.getLastEvent("my-session");
const offset = lastEvent?.sequence ?? 0;
// Stream from where you left off
for await (const event of client.streamEvents("my-session", { offset })) {
await db.insertEvent("my-session", event);
}Polling
If you can't use streaming, poll the events endpoint:
const lastEvent = await db.getLastEvent("my-session");
let offset = lastEvent?.sequence ?? 0;
while (true) {
const { events } = await client.getEvents("my-session", {
offset,
limit: 100
});
for (const event of events) {
await db.insertEvent("my-session", event);
offset = event.sequence;
}
await sleep(1000);
}Database options
Choose where to persist events based on your requirements. For most use cases, we recommend Rivet Actors.
| Durable | Real-time | Multiplayer | Scaling | Throughput | Complexity | |
|---|---|---|---|---|---|---|
| Rivet Actors | ✓ | ✓ | ✓ | Auto-sharded, one actor per session | Millions of concurrent sessions | Zero infrastructure |
| PostgreSQL | ✓ | Manual sharding | Connection pool limited | Connection pools, migrations | ||
| Redis | ✓ | Redis Cluster | High, in-memory | Memory management, Sentinel for failover |
Rivet Actors
For production workloads, Rivet Actors provide a managed solution for:
- Persistent state: Events survive crashes and restarts
- Real-time streaming: Built-in WebSocket support for clients
- Horizontal scaling: Run thousands of concurrent sessions
- Observability: Built-in logging and metrics
Actor
import { actor } from "rivetkit";
import { Daytona } from "@daytonaio/sdk";
import { SandboxAgent, SandboxAgentClient, AgentEvent } from "sandbox-agent";
interface CodingSessionState {
sandboxId: string;
baseUrl: string;
sessionId: string;
events: AgentEvent[];
}
interface CodingSessionVars {
client: SandboxAgentClient;
}
const daytona = new Daytona();
const codingSession = actor({
createState: async (): Promise<CodingSessionState> => {
const sandbox = await daytona.create({
snapshot: "sandbox-agent-ready",
envVars: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
},
autoStopInterval: 0,
});
await sandbox.process.executeCommand(
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 &"
);
const baseUrl = (await sandbox.getSignedPreviewUrl(3000)).url;
const sessionId = crypto.randomUUID();
return {
sandboxId: sandbox.id,
baseUrl,
sessionId,
events: [],
};
},
createVars: async (c): Promise<CodingSessionVars> => {
const client = new SandboxAgentClient({
baseUrl: c.state.baseUrl,
agent: "mock",
});
await client.createSession(c.state.sessionId, { agent: "claude" });
return { client };
},
onDestroy: async (c) => {
const sandbox = await daytona.get(c.state.sandboxId);
await sandbox.delete();
},
run: async (c) => {
for await (const event of c.vars.client.streamEvents(c.state.sessionId)) {
c.state.events.push(event);
c.broadcast("agentEvent", event);
}
},
actions: {
postMessage: async (c, message: string) => {
await c.vars.client.postMessage(c.state.sessionId, message);
},
getTranscript: (c) => c.state.events,
},
});Client
```typescript TypeScript import { createClient } from "rivetkit/client";
const client = createClient(); const session = client.codingSession.getOrCreate(["my-session"]);
const conn = session.connect(); conn.on("agentEvent", (event) => { console.log(event.type, event.data); });
await conn.postMessage("Create a new React component for user profiles");
const transcript = await conn.getTranscript();
import { createRivetKit } from "@rivetkit/react";
const { useActor } = createRivetKit();
function CodingSession() { const [messages, setMessages] = useState<AgentEvent[]>([]); const session = useActor({ name: "codingSession", key: ["my-session"] });
session.useEvent("agentEvent", (event) => { setMessages((prev) => [...prev, event]); });
const sendPrompt = async (prompt: string) => { await session.connection?.postMessage(prompt); };
return ( <div> {messages.map((msg, i) => ( <div key={i}>{JSON.stringify(msg)}</div> ))} <button onClick={() => sendPrompt("Build a login page")}> Send Prompt </button> </div> ); }
### PostgreSQL
CREATE TABLE agent_events ( event_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, native_session_id TEXT, sequence INTEGER NOT NULL, time TIMESTAMPTZ NOT NULL, type TEXT NOT NULL, source TEXT NOT NULL, synthetic BOOLEAN NOT NULL DEFAULT FALSE, data JSONB NOT NULL, UNIQUE(session_id, sequence) );
CREATE INDEX idx_events_session ON agent_events(session_id, sequence);
### Redis
// Append event to list await redis.rpush(session:${sessionId}, JSON.stringify(event));
// Get events from offset const events = await redis.lrange(session:${sessionId}, offset, -1);
## Handling disconnects
The event stream may disconnect due to network issues. Handle reconnection gracefully:
async function streamWithRetry(sessionId: string) { while (true) { try { const lastEvent = await db.getLastEvent(sessionId); const offset = lastEvent?.sequence ?? 0;
for await (const event of client.streamEvents(sessionId, { offset })) { await db.insertEvent(sessionId, event); } } catch (error) { console.error("Stream disconnected, reconnecting...", error); await sleep(1000); } } }
MCP
Source: docs/mcp-config.mdxCanonical URL: https://sandboxagent.dev/docs/mcp-config
Description: Configure MCP servers for agent sessions.
--- MCP (Model Context Protocol) servers extend agents with tools and external context.
Configuring MCP servers
The HTTP config endpoints let you store/retrieve MCP server configs by directory + name.
// Create MCP config
await sdk.setMcpConfig(
{
directory: "/workspace",
mcpName: "github",
},
{
type: "remote",
url: "https://example.com/mcp",
},
);
// Create a session using the configured MCP servers
const session = await sdk.createSession({
agent: "claude",
cwd: "/workspace",
});
await session.prompt([
{ type: "text", text: "Use available MCP servers to help with this task." },
]);
// List MCP configs
const config = await sdk.getMcpConfig({
directory: "/workspace",
mcpName: "github",
});
console.log(config.type);
// Delete MCP config
await sdk.deleteMcpConfig({
directory: "/workspace",
mcpName: "github",
});Config fields
Local server
| Field | Description |
|---|---|
type | local |
command | executable path |
args | array of CLI args |
env | environment variable map |
cwd | working directory |
enabled | enable/disable server |
timeoutMs | timeout override |
Remote server
| Field | Description |
|---|---|
type | remote |
url | MCP server URL |
transport | http or sse |
headers | static headers map |
bearerTokenEnvVar | env var name to inject in auth header |
envHeaders | header name to env var map |
oauth | optional OAuth config object |
enabled | enable/disable server |
timeoutMs | timeout override |
Custom MCP servers
To bundle and upload your own MCP server into the sandbox, see Custom Tools.
Multiplayer
Source: docs/multiplayer.mdxCanonical URL: https://sandboxagent.dev/docs/multiplayer
Description: Use Rivet Actors to coordinate shared sessions.
--- For multiplayer orchestration, use Rivet Actors.
Recommended model:
- One actor per collaborative workspace/thread.
- The actor owns Sandbox Agent session lifecycle and persistence.
- Clients connect to the actor and receive realtime broadcasts.
Use actor keys to map each workspace to one actor, events for realtime updates, and lifecycle hooks for cleanup.
Example
```ts Actor (server) import { actor, setup } from "rivetkit"; import { SandboxAgent, type SessionPersistDriver, type SessionRecord, type SessionEvent, type ListPageRequest, type ListPage, type ListEventsRequest } from "sandbox-agent";
interface RivetPersistData { sessions: Record<string, SessionRecord>; events: Record<string, SessionEvent[]>; } type RivetPersistState = { _sandboxAgentPersist: RivetPersistData };
class RivetSessionPersistDriver implements SessionPersistDriver { private readonly stateKey: string; private readonly ctx: { state: Record<string, unknown> }; constructor(ctx: { state: Record<string, unknown> }, options: { stateKey?: string } = {}) { this.ctx = ctx; this.stateKey = options.stateKey ?? "_sandboxAgentPersist"; if (!this.ctx.state[this.stateKey]) { this.ctx.state[this.stateKey] = { sessions: {}, events: {} }; } } private get data(): RivetPersistData { return this.ctx.state[this.stateKey] as RivetPersistData; } async getSession(id: string) { const s = this.data.sessions[id]; return s ? { ...s } : undefined; } async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> { const sorted = Object.values(this.data.sessions).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)); const offset = Number(request.cursor ?? 0); const limit = request.limit ?? 100; const slice = sorted.slice(offset, offset + limit); return { items: slice, nextCursor: offset + slice.length < sorted.length ? String(offset + slice.length) : undefined }; } async updateSession(session: SessionRecord) { this.data.sessions[session.id] = { ...session }; if (!this.data.events[session.id]) this.data.events[session.id] = []; } async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> { const all = [...(this.data.events[request.sessionId] ?? [])].sort((a, b) => a.eventIndex - b.eventIndex || a.id.localeCompare(b.id)); const offset = Number(request.cursor ?? 0); const limit = request.limit ?? 100; const slice = all.slice(offset, offset + limit); return { items: slice, nextCursor: offset + slice.length < all.length ? String(offset + slice.length) : undefined }; } async insertEvent(sessionId: string, event: SessionEvent) { const events = this.data.events[sessionId] ?? []; events.push({ ...event, payload: JSON.parse(JSON.stringify(event.payload)) }); this.data.events[sessionId] = events; } }
type WorkspaceState = RivetPersistState & { sandboxId: string; baseUrl: string; };
export const workspace = actor({ createState: async () => { return { sandboxId: "sbx_123", baseUrl: "http://127.0.0.1:2468", } satisfies Partial<WorkspaceState>; },
createVars: async (c) => { const persist = new RivetSessionPersistDriver(c); const sdk = await SandboxAgent.connect({ baseUrl: c.state.baseUrl, persist, });
const session = await sdk.resumeOrCreateSession({ id: "default", agent: "codex" });
const unsubscribe = session.onEvent((event) => { c.broadcast("session.event", event); });
return { sdk, session, unsubscribe }; },
actions: { getSessionInfo: (c) => ({ workspaceId: c.key[0], sandboxId: c.state.sandboxId, }),
prompt: async (c, input: { userId: string; text: string }) => { c.broadcast("chat.user", { userId: input.userId, text: input.text, createdAt: Date.now(), });
await c.vars.session.prompt([{ type: "text", text: input.text }]); }, },
onSleep: async (c) => { c.vars.unsubscribe?.(); await c.vars.sdk.dispose(); }, });
export const registry = setup({ use: { workspace }, });
import { createClient } from "rivetkit/client"; import type { registry } from "./actors";
const client = createClient<typeof registry>({ endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!, });
const workspaceId = "workspace-42"; const room = client.workspace.getOrCreate([workspaceId]); const conn = room.connect();
conn.on("chat.user", (event) => { console.log("user message", event); });
conn.on("session.event", (event) => { console.log("sandbox event", event); });
await conn.prompt({ userId: "user-123", text: "Propose a refactor plan for auth middleware.", });
## Notes
- Keep sandbox calls actor-only. Browser clients should not call Sandbox Agent directly.
- Copy the Rivet persist driver from the example above into your project so session history persists in actor state.
- For client connection patterns, see [Rivet JavaScript client](https://rivet.dev/docs/clients/javascript).
Observability
Source: docs/observability.mdxCanonical URL: https://sandboxagent.dev/docs/observability
Description: Track session activity with OpenTelemetry.
--- Use OpenTelemetry to instrument session traffic, then ship telemetry to your collector/backend.
Common collectors and backends
Example: trace a prompt round-trip
Wrap session.prompt() in a span to measure the full round-trip, then log individual events as span events.
Assumes your OTEL provider/exporter is already configured.
import { trace } from "@opentelemetry/api";
import { SandboxAgent } from "sandbox-agent";
const tracer = trace.getTracer("my-app/sandbox-agent");
const sdk = await SandboxAgent.connect({
baseUrl: process.env.SANDBOX_URL!,
});
const session = await sdk.createSession({ agent: "mock" });
// Log each event as an OTEL span event on the active span
const unsubscribe = session.onEvent((event) => {
const activeSpan = trace.getActiveSpan();
if (!activeSpan) return;
activeSpan.addEvent("session.event", {
"sandbox.sender": event.sender,
"sandbox.event_index": event.eventIndex,
});
});
// The span covers the full prompt round-trip
await tracer.startActiveSpan("sandbox_agent.prompt", async (span) => {
span.setAttribute("sandbox.session_id", session.id);
try {
const result = await session.prompt([
{ type: "text", text: "Summarize this repository." },
]);
span.setAttribute("sandbox.stop_reason", result.stopReason);
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
});
unsubscribe();OpenCode Compatibility
Source: docs/opencode-compatibility.mdxCanonical URL: https://sandboxagent.dev/docs/opencode-compatibility
Description: Connect OpenCode clients, SDKs, and web UI to Sandbox Agent.
--- Experimental: OpenCode SDK/UI compatibility may change.
Sandbox Agent exposes an OpenCode-compatible API at /opencode.
Why use OpenCode clients with Sandbox Agent?
- OpenCode CLI (
opencode attach) - OpenCode web UI
- OpenCode TypeScript SDK (
@opencode-ai/sdk)
Quick start
OpenCode CLI / TUI
sandbox-agent opencode --port 2468 --no-tokenOr start server + attach manually:
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
opencode attach http://localhost:2468/opencodeWith authentication enabled:
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
opencode attach http://localhost:2468/opencode --password "$SANDBOX_TOKEN"OpenCode web UI
Start Sandbox Agent with CORS
sandbox-agent server --no-token --host 127.0.0.1 --port 2468 --cors-allow-origin http://127.0.0.1:5173Run OpenCode web app
git clone https://github.com/anomalyco/opencode
cd opencode/packages/app
export VITE_OPENCODE_SERVER_HOST=127.0.0.1
export VITE_OPENCODE_SERVER_PORT=2468
bun install
bun run dev -- --host 127.0.0.1 --port 5173Open UI
Visit http://127.0.0.1:5173/.
OpenCode SDK
import { createOpencodeClient } from "@opencode-ai/sdk";
const client = createOpencodeClient({
baseUrl: "http://localhost:2468/opencode",
});
const session = await client.session.create();
await client.session.promptAsync({
path: { id: session.data.id },
body: {
parts: [{ type: "text", text: "Hello, write a hello world script" }],
},
});
const events = await client.event.subscribe({});
for await (const event of events.stream) {
console.log(event);
}Notes
- API base path:
/opencode - If server auth is enabled, pass bearer auth (or
--passwordin OpenCode CLI) - For browser UIs, configure CORS with
--cors-allow-origin - Provider selector currently exposes compatible providers (
mock,amp,claude,codex) - Provider/model metadata for compatibility endpoints is normalized and may differ from native OpenCode grouping
- Optional proxy: set
OPENCODE_COMPAT_PROXY_URLto forward selected endpoints to native OpenCode
Endpoint coverage
Endpoint Status Table
| Endpoint | Status | Notes |
|---|---|---|
GET /event | ✓ | Session/message updates (SSE) |
GET /global/event | ✓ | GlobalEvent-wrapped stream |
GET /session | ✓ | Session list |
POST /session | ✓ | Create session |
GET /session/{id} | ✓ | Session details |
POST /session/{id}/message | ✓ | Send message |
GET /session/{id}/message | ✓ | Session messages |
GET /permission | ✓ | Pending permissions |
POST /permission/{id}/reply | ✓ | Permission reply |
GET /question | ✓ | Pending questions |
POST /question/{id}/reply | ✓ | Question reply |
GET /provider | ✓ | Provider metadata |
GET /command | ↔ | Proxied when OPENCODE_COMPAT_PROXY_URL is set; otherwise stub |
GET /config | ↔ | Proxied when set; otherwise stub |
PATCH /config | ↔ | Proxied when set; otherwise local compatibility behavior |
GET /global/config | ↔ | Proxied when set; otherwise stub |
PATCH /global/config | ↔ | Proxied when set; otherwise local compatibility behavior |
/tui/* | ↔ | Proxied when set; otherwise local compatibility behavior |
GET /agent | − | Agent list |
| other endpoints | − | Empty/stub responses |
✓ Functional ↔ Proxied optional − Stubbed
Orchestration Architecture
Source: docs/orchestration-architecture.mdxCanonical URL: https://sandboxagent.dev/docs/orchestration-architecture
Description: Production topology, backend requirements, and session persistence.
--- This page covers production topology and backend requirements. Read 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).
Related skills
How it compares
Pick sandbox-agent when you need scripted SDK control over agent sessions; use IDE chat skills when manual prompting inside an editor is enough.
FAQ
What does sandbox-agent do?
Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environments. Use when setting up sandbox-agent server locally or in cl
When should I use sandbox-agent?
Deploy, configure, and integrate Sandbox Agent - a universal API for orchestrating AI coding agents (Claude Code, Codex, OpenCode, Amp) in sandboxed environments. Use when setting up sandbox-agent server locally or in cl
Is sandbox-agent safe to install?
Review the Security Audits panel on this page before installing in production.