This commit is contained in:
NathanFlurry 2026-02-11 14:47:41 +00:00
parent 70287ec471
commit e72eb9f611
No known key found for this signature in database
GPG key ID: 6A5F43A4F3241BCA
264 changed files with 18559 additions and 51021 deletions

View file

@ -0,0 +1,67 @@
---
title: "ACP HTTP Client"
description: "Protocol-pure ACP JSON-RPC over streamable HTTP client."
---
`acp-http-client` is a standalone, low-level package for ACP over HTTP (`/v2/rpc`).
Use it when you want strict ACP protocol behavior with no Sandbox-specific metadata or extension adaptation.
## Install
```bash
npm install acp-http-client
```
## Usage
```ts
import { AcpHttpClient } from "acp-http-client";
const client = new AcpHttpClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
await client.initialize({
_meta: {
"sandboxagent.dev": {
agent: "mock",
},
},
});
const session = await client.newSession({
cwd: "/",
mcpServers: [],
_meta: {
"sandboxagent.dev": {
agent: "mock",
},
},
});
const result = await client.prompt({
sessionId: session.sessionId,
prompt: [{ type: "text", text: "hello" }],
});
console.log(result.stopReason);
await client.disconnect();
```
## Scope
- Implements ACP HTTP transport and connection lifecycle.
- Supports ACP requests/notifications and session streaming.
- Does not inject `_meta["sandboxagent.dev"]`.
- Does not wrap `_sandboxagent/*` extension methods/events.
## Transport Contract
- `POST /v2/rpc` is JSON-only. Send `Content-Type: application/json` and `Accept: application/json`.
- `GET /v2/rpc` is SSE-only. Send `Accept: text/event-stream`.
- Keep one active SSE stream per ACP connection id.
- `x-acp-agent` is removed. Provide agent via `_meta["sandboxagent.dev"].agent` on `initialize` and `session/new`.
If you want Sandbox Agent metadata/extensions and higher-level helpers, use `sandbox-agent` and `SandboxAgentClient` instead.

View file

@ -24,12 +24,13 @@ Sessions are the unit of interaction with an agent. You create one session per t
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.createSession("build-session", {
agent: "codex",
@ -60,12 +61,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session" \
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.postMessage("build-session", {
message: "Summarize the repository structure.",
@ -84,12 +86,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages" \
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
const response = await client.postMessageStream("build-session", {
message: "Explain the main entrypoints.",
@ -118,12 +121,13 @@ curl -N -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages/stream
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
const events = await client.getEvents("build-session", {
offset: 0,
@ -146,12 +150,13 @@ curl -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events?offset=0&lim
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
for await (const event of client.streamEvents("build-session", { offset: 0 })) {
console.log(event.type, event.data);
@ -168,12 +173,13 @@ curl -N -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events/sse?offse
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
const sessions = await client.listSessions();
console.log(sessions.sessions);
@ -191,12 +197,13 @@ When the agent asks a question, reply with an array of answers. Each inner array
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.replyQuestion("build-session", "question-1", {
answers: [["yes"]],
@ -215,12 +222,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/questions/question
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.rejectQuestion("build-session", "question-1");
```
@ -237,12 +245,13 @@ Use `once`, `always`, or `reject`.
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.replyPermission("build-session", "permission-1", {
reply: "once",
@ -261,12 +270,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/permissions/permis
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.terminateSession("build-session");
```

View file

@ -11,13 +11,14 @@ Use the filesystem API to upload files, then reference them as attachments when
<Step title="Upload a file">
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
import fs from "node:fs";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
const buffer = await fs.promises.readFile("./data.csv");
@ -42,12 +43,13 @@ Use the filesystem API to upload files, then reference them as attachments when
<Step title="Attach the file in a prompt">
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.postMessage("my-session", {
message: "Please analyze the attached CSV.",

View file

@ -1,113 +0,0 @@
# Universal ↔ Agent Term Mapping
Source of truth: generated agent schemas in `resources/agent-schemas/artifacts/json-schema/`.
Identifiers
+----------------------+------------------------+------------------------------------------+-----------------------------+------------------------+----------------------+
| Universal term | Claude | Codex (app-server) | OpenCode | Amp | Pi (RPC) |
+----------------------+------------------------+------------------------------------------+-----------------------------+------------------------+----------------------+
| session_id | n/a (daemon-only) | n/a (daemon-only) | n/a (daemon-only) | n/a (daemon-only) | n/a (daemon-only) |
| native_session_id | none | threadId | sessionID | none | sessionId |
| item_id | synthetic | ThreadItem.id | Message.id | StreamJSONMessage.id | messageId/toolCallId |
| native_item_id | none | ThreadItem.id | Message.id | StreamJSONMessage.id | messageId/toolCallId |
+----------------------+------------------------+------------------------------------------+-----------------------------+------------------------+----------------------+
Notes:
- When a provider does not supply IDs (Claude), we synthesize item_id values and keep native_item_id null.
- native_session_id is the only provider session identifier. It is intentionally used for thread/session/run ids.
- native_item_id preserves the agent-native item/message id when present.
- source indicates who emitted the event: agent (native) or daemon (synthetic).
- raw is always present on events. When clients do not opt-in to raw payloads, raw is null.
- opt-in via `include_raw=true` on events endpoints (HTTP + SSE).
- If parsing fails, emit agent.unparsed (source=daemon, synthetic=true). Tests must assert zero unparsed events.
Runtime model by agent
| Agent | Runtime model | Notes |
|---|---|---|
| Claude | Per-message subprocess streaming | Routed through `AgentManager::spawn_streaming` with Claude stream-json stdin. |
| Amp | Per-message subprocess streaming | Routed through `AgentManager::spawn_streaming` with parsed JSONL output. |
| Codex | Shared app-server (stdio JSON-RPC) | One shared server process, daemon sessions map to Codex thread IDs. |
| OpenCode | Shared HTTP server + SSE | One shared HTTP server, daemon sessions map to OpenCode session IDs. |
| Pi | Dedicated per-session RPC process | Canonical path is router-managed Pi runtime (`pi --mode rpc`), one process per daemon session. |
Pi runtime contract:
- Session/message lifecycle for Pi must stay on router-managed per-session RPC runtime.
- `AgentManager::spawn(Pi)` is kept for one-shot utility/testing flows.
- `AgentManager::spawn_streaming(Pi)` is intentionally unsupported.
Events / Message Flow
+------------------------+------------------------------+--------------------------------------------+-----------------------------------------+----------------------------------+----------------------------+
| Universal term | Claude | Codex (app-server) | OpenCode | Amp | Pi (RPC) |
+------------------------+------------------------------+--------------------------------------------+-----------------------------------------+----------------------------------+----------------------------+
| session.started | none | method=thread/started | type=session.created | none | none |
| session.ended | SDKMessage.type=result | no explicit session end (turn/completed) | no explicit session end (session.deleted)| type=done | none (daemon synthetic) |
| turn.started | synthetic on message send | method=turn/started | type=session.status (busy) | synthetic on message send | none (daemon synthetic) |
| turn.ended | synthetic after result | method=turn/completed | type=session.idle | synthetic on done | none (daemon synthetic) |
| message (user) | SDKMessage.type=user | item/completed (ThreadItem.type=userMessage)| message.updated (Message.role=user) | type=message | none (daemon synthetic) |
| message (assistant) | SDKMessage.type=assistant | item/completed (ThreadItem.type=agentMessage)| message.updated (Message.role=assistant)| type=message | message_start/message_end |
| message.delta | stream_event (partial) or synthetic | method=item/agentMessage/delta | type=message.part.updated (text-part delta) | synthetic | message_update (text_delta/thinking_delta) |
| tool call | type=tool_use | method=item/mcpToolCall/progress | message.part.updated (part.type=tool) | type=tool_call | tool_execution_start |
| tool result | user.message.content.tool_result | item/completed (tool result ThreadItem variants) | message.part.updated (part.type=tool, state=completed) | type=tool_result | tool_execution_end |
| permission.requested | control_request.can_use_tool | none | type=permission.asked | none | none |
| permission.resolved | daemon reply to can_use_tool | none | type=permission.replied | none | none |
| question.requested | tool_use (AskUserQuestion) | experimental request_user_input (payload) | type=question.asked | none | none |
| question.resolved | tool_result (AskUserQuestion) | experimental request_user_input (payload) | type=question.replied / question.rejected | none | none |
| error | SDKResultMessage.error | method=error | type=session.error (or message error) | type=error | hook_error (status item) |
+------------------------+------------------------------+--------------------------------------------+-----------------------------------------+----------------------------------+----------------------------+
Permission status normalization:
- `permission.requested` uses `status=requested`.
- `permission.resolved` uses `status=accept`, `accept_for_session`, or `reject`.
Synthetics
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
| Synthetic element | When it appears | Stored as | Notes |
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
| session.started | When agent emits no explicit start | session.started event | Mark source=daemon |
| session.ended | When agent emits no explicit end | session.ended event | Mark source=daemon; reason may be inferred |
| turn.started | When agent emits no explicit turn start | turn.started event | Mark source=daemon |
| turn.ended | When agent emits no explicit turn end | turn.ended event | Mark source=daemon |
| item_id (Claude) | Claude provides no item IDs | item_id | Maintain provider_item_id map when possible |
| user message (Claude) | Claude emits only assistant output | item.completed | Mark source=daemon; preserve raw input in event metadata |
| question events (Claude) | AskUserQuestion tool usage | question.requested/resolved | Derived from tool_use blocks (source=agent) |
| native_session_id (Codex) | Codex uses threadId | native_session_id | Intentionally merged threadId into native_session_id |
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
| message.delta (Claude) | No native deltas emitted | item.delta | Synthetic delta with full message content; source=daemon |
| message.delta (Amp) | No native deltas | item.delta | Synthetic delta with full message content; source=daemon |
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
| message.delta (OpenCode) | text part delta before message | item.delta | If part arrives first, create item.started stub then delta |
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
Delta handling
- Codex emits agent message and other deltas (e.g., item/agentMessage/delta).
- OpenCode emits part deltas via message.part.updated with a delta string.
- Claude can emit stream_event deltas when partial streaming is enabled; Amp does not emit deltas.
- Pi emits message_update deltas and cumulative tool_execution_update partialResult values (we diff to produce deltas).
Policy:
- Emit item.delta for streamable text content across providers.
- For providers without native deltas, emit a single synthetic delta containing the full content prior to item.completed.
- For Claude when partial streaming is enabled, forward native deltas and skip the synthetic full-content delta.
- For providers with native deltas, forward as-is; also emit item.completed when final content is known.
- For OpenCode reasoning part deltas, emit typed reasoning item updates (item.started/item.completed with content.type=reasoning) instead of item.delta.
Message normalization notes
- user vs assistant: normalized via role in the universal item; provider role fields or item types determine role.
- file artifacts: always represented as content parts (type=file_ref) inside message/tool_result items, not a separate item kind.
- reasoning: represented as content parts (type=reasoning) inside message items, with visibility when available.
- subagents: OpenCode subtask parts and Claude Task tool usage are currently normalized into standard message/tool flow (no dedicated subagent fields).
- OpenCode unrolling: message.updated creates/updates the parent message item; tool-related parts emit separate tool item events (item.started/ item.completed) with parent_id pointing to the message item.
- If a message.part.updated arrives before message.updated, we create a stub item.started (source=daemon) so deltas have a parent.
- Tool calls/results are always emitted as separate tool items to keep behavior consistent across agents.
- If Pi message_update events omit messageId, we synthesize a stable message id and emit a synthetic item.started before the first delta so streaming text stays grouped.
- Pi auto_compaction_start/auto_compaction_end and auto_retry_start/auto_retry_end events are mapped to status items (label `pi.*`).
- Pi extension_ui_request/extension_error events are mapped to status items.
- Pi RPC from pi-coding-agent does not include sessionId in events; each daemon session owns a dedicated Pi RPC process, so events are routed by runtime ownership (parallel sessions supported).
- PI `variant` maps directly to PI RPC `set_thinking_level.level` before prompts are sent.
- PI remains source of truth for thinking-level constraints: unsupported levels (including non-reasoning models and model-specific limits such as `xhigh`) are PI-native clamped or rejected.

View file

@ -1,144 +1,55 @@
---
title: "Credentials"
description: "How sandbox-agent discovers and uses provider credentials."
description: "How sandbox-agent discovers and exposes provider credentials."
icon: "key"
---
Sandbox-agent automatically discovers API credentials from environment variables and agent config files. Credentials are used to authenticate with AI providers (Anthropic, OpenAI) when spawning agents.
`sandbox-agent` can discover provider credentials from environment variables and local agent config files.
## Credential sources
## Supported providers
Credentials are extracted in priority order. The first valid credential found for each provider is used.
- Anthropic
- OpenAI
- Additional provider entries discovered via OpenCode config
### Environment variables (highest priority)
**API keys** (checked first):
## Common environment variables
| Variable | Provider |
|----------|----------|
| --- | --- |
| `ANTHROPIC_API_KEY` | Anthropic |
| `CLAUDE_API_KEY` | Anthropic (fallback) |
| `CLAUDE_API_KEY` | Anthropic fallback |
| `OPENAI_API_KEY` | OpenAI |
| `CODEX_API_KEY` | OpenAI (fallback) |
| `CODEX_API_KEY` | OpenAI fallback |
**OAuth tokens** (checked if no API key found):
## Extract credentials (CLI)
| Variable | Provider |
|----------|----------|
| `CLAUDE_CODE_OAUTH_TOKEN` | Anthropic (OAuth) |
| `ANTHROPIC_AUTH_TOKEN` | Anthropic (OAuth fallback) |
OAuth tokens from environment variables are only used when `include_oauth` is enabled (the default).
### Agent config files
If no environment variable is set, sandbox-agent checks agent-specific config files:
| Agent | Config path | Provider |
|-------|-------------|----------|
| Amp | `~/.amp/config.json` | Anthropic |
| Claude Code | `~/.claude.json`, `~/.claude/.credentials.json` | Anthropic |
| Codex | `~/.codex/auth.json` | OpenAI |
| OpenCode | `~/.local/share/opencode/auth.json` | Both |
OAuth tokens are supported for Claude Code, Codex, and OpenCode. Expired tokens are automatically skipped.
## Provider requirements by agent
| Agent | Required provider |
|-------|-------------------|
| Claude Code | Anthropic |
| Amp | Anthropic |
| Codex | OpenAI |
| OpenCode | Anthropic or OpenAI |
| Mock | None |
## Error handling behavior
Sandbox-agent uses a **best-effort, fail-forward** approach to credentials:
### Extraction failures are silent
If a config file is missing, unreadable, or malformed, extraction continues to the next source. No errors are thrown. Missing credentials simply mean the provider is marked as unavailable.
```
~/.claude.json missing → try ~/.claude/.credentials.json
~/.claude/.credentials.json missing → try OpenCode config
All sources exhausted → anthropic = None (not an error)
```
### Agents spawn without credential validation
When you send a message to a session, sandbox-agent does **not** pre-validate credentials. The agent process is spawned with whatever credentials were found (or none), and the agent's native error surfaces if authentication fails.
This design:
- Lets you test agent error handling behavior
- Avoids duplicating provider-specific auth validation
- Ensures sandbox-agent faithfully proxies agent behavior
For example, sending a message to Claude Code without Anthropic credentials will spawn the agent, which will then emit its own "ANTHROPIC_API_KEY not set" error through the event stream.
## Checking credential status
### API endpoint
The `GET /v1/agents` endpoint includes a `credentialsAvailable` field for each agent:
```json
{
"agents": [
{
"id": "claude",
"installed": true,
"credentialsAvailable": true,
...
},
{
"id": "codex",
"installed": true,
"credentialsAvailable": false,
...
}
]
}
```
### TypeScript SDK
```typescript
const { agents } = await client.listAgents();
for (const agent of agents) {
console.log(`${agent.id}: ${agent.credentialsAvailable ? 'authenticated' : 'no credentials'}`);
}
```
### OpenCode compatibility
The `/opencode/provider` endpoint returns a `connected` array listing providers with valid credentials:
```json
{
"all": [...],
"connected": ["claude", "mock"]
}
```
## Passing credentials explicitly
You can override auto-discovered credentials by setting environment variables before starting sandbox-agent:
Show discovered credentials (redacted by default):
```bash
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
sandbox-agent daemon start
sandbox-agent credentials extract
```
Or when using the SDK in embedded mode:
Reveal raw values:
```typescript
const client = await SandboxAgentClient.spawn({
env: {
ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY,
},
});
```bash
sandbox-agent credentials extract --reveal
```
Filter by agent/provider:
```bash
sandbox-agent credentials extract --agent codex
sandbox-agent credentials extract --provider openai
```
Emit shell exports:
```bash
sandbox-agent credentials extract-env --export
```
## Notes
- Discovery is best-effort: missing/invalid files do not crash extraction.
- v2 does not expose legacy v1 `credentialsAvailable` agent fields.
- Authentication failures are surfaced by the selected ACP agent process/agent during ACP requests.

View file

@ -66,13 +66,14 @@ Both approaches execute code inside the sandbox, so your tools have full access
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
import fs from "node:fs";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
const content = await fs.promises.readFile("./dist/mcp-server.cjs");
await client.writeFsFile(
@ -175,13 +176,14 @@ Skills are markdown files that instruct the agent how to use a script. Upload th
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
import fs from "node:fs";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
const script = await fs.promises.readFile("./dist/random-number.cjs");
await client.writeFsFile(

View file

@ -98,7 +98,7 @@ const PORT = 8000;
/** Check if sandbox-agent is already running */
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
try {
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v2/health`);
return result.success;
} catch {
return false;
@ -131,7 +131,7 @@ export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Proxy requests: /sandbox/:name/v1/...
// Proxy requests: /sandbox/:name/v2/...
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
if (match) {
const [, name, path = "/"] = match;
@ -154,11 +154,12 @@ export default {
## Connect from Client
```typescript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
// Connect via the proxy endpoint
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://localhost:8787/sandbox/my-sandbox",
agent: "mock",
});
// Wait for server to be ready
@ -230,7 +231,7 @@ First run builds the Docker container (2-3 minutes). Subsequent runs are much fa
Test with curl:
```bash
curl http://localhost:8787/sandbox/demo/v1/health
curl http://localhost:8787/sandbox/demo/v2/health
```
<Tip>

View file

@ -16,7 +16,7 @@ Daytona Tier 3+ is required to access api.anthropic.com and api.openai.com. Tier
```typescript
import { Daytona } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const daytona = new Daytona();
@ -44,7 +44,7 @@ await new Promise((r) => setTimeout(r, 2000));
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
// Connect and use the SDK
const client = await SandboxAgent.connect({ baseUrl });
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
await client.createSession("my-session", {
agent: "claude",

View file

@ -33,7 +33,7 @@ Access the API at `http://localhost:3000`.
```typescript
import Docker from "dockerode";
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const docker = new Docker();
const PORT = 3000;
@ -62,7 +62,7 @@ await container.start();
// Wait for server and connect
const baseUrl = `http://127.0.0.1:${PORT}`;
const client = await SandboxAgent.connect({ baseUrl });
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
// Use the client...
await client.createSession("my-session", {

View file

@ -12,7 +12,7 @@ description: "Deploy the daemon inside an E2B sandbox."
```typescript
import { Sandbox } from "@e2b/code-interpreter";
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
// Pass API keys to the sandbox
const envs: Record<string, string> = {};
@ -38,7 +38,7 @@ await sandbox.commands.run(
// Connect to the server
const baseUrl = `https://${sandbox.getHost(3000)}`;
const client = await SandboxAgent.connect({ baseUrl });
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
// Wait for server to be ready
for (let i = 0; i < 30; i++) {

View file

@ -12,7 +12,7 @@ description: "Deploy the daemon inside a Vercel Sandbox."
```typescript
import { Sandbox } from "@vercel/sandbox";
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
// Pass API keys to the sandbox
const envs: Record<string, string> = {};
@ -51,7 +51,7 @@ await sandbox.runCommand({
// Connect to the server
const baseUrl = sandbox.domain(3000);
const client = await SandboxAgent.connect({ baseUrl });
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
// Wait for server to be ready
for (let i = 0; i < 30; i++) {

View file

@ -85,6 +85,10 @@
"group": "Features",
"pages": ["file-system"]
},
{
"group": "Advanced",
"pages": ["advanced/acp-http-client"]
},
{
"group": "Reference",
"pages": [

View file

@ -6,6 +6,17 @@ icon: "folder"
---
The filesystem API lets you list, read, write, move, and delete files inside the sandbox, plus upload batches of files via tar archives.
Control operations (`list`, `mkdir`, `move`, `stat`, `delete`) are ACP extensions on `/v2/rpc` and require an active ACP connection in the SDK.
Binary transfer is intentionally a separate HTTP API (not ACP extension methods):
- `GET /v2/fs/file`
- `PUT /v2/fs/file`
- `POST /v2/fs/upload-batch`
Reason: these are host/runtime capabilities implemented by Sandbox Agent for cross-agent-consistent behavior, and they may require streaming very large binary payloads that ACP JSON-RPC is not suited to transport efficiently.
This is intentionally separate from ACP native `fs/read_text_file` and `fs/write_text_file`.
ACP extension variants may exist in parallel for compatibility, but SDK defaults should use the HTTP endpoints above for binary transfer.
## Path Resolution
@ -18,14 +29,15 @@ The session working directory is the server process current working directory at
## List Entries
`listFsEntries()` uses ACP extension method `_sandboxagent/fs/list_entries`.
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock" });
const entries = await client.listFsEntries({
path: "./workspace",
@ -36,23 +48,25 @@ console.log(entries);
```
```bash cURL
curl -X GET "http://127.0.0.1:2468/v1/fs/entries?path=./workspace&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN"
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "x-acp-connection-id: acp_conn_1" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"_sandboxagent/fs/list_entries","params":{"path":"./workspace","sessionId":"my-session"}}'
```
</CodeGroup>
## Read And Write Files
`PUT /v1/fs/file` writes raw bytes. `GET /v1/fs/file` returns raw bytes.
`PUT /v2/fs/file` writes raw bytes. `GET /v2/fs/file` returns raw bytes.
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock" });
await client.writeFsFile({ path: "./notes.txt", sessionId: "my-session" }, "hello");
@ -66,11 +80,11 @@ console.log(text);
```
```bash cURL
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-session" \
curl -X PUT "http://127.0.0.1:2468/v2/fs/file?path=./notes.txt&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
--data-binary "hello"
curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-session" \
curl -X GET "http://127.0.0.1:2468/v2/fs/file?path=./notes.txt&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
--output ./notes.txt
```
@ -78,14 +92,15 @@ curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-sess
## Create Directories
`mkdirFs()` uses ACP extension method `_sandboxagent/fs/mkdir`.
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock" });
await client.mkdirFs({
path: "./data",
@ -94,21 +109,25 @@ await client.mkdirFs({
```
```bash cURL
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=./data&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN"
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "x-acp-connection-id: acp_conn_1" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"_sandboxagent/fs/mkdir","params":{"path":"./data","sessionId":"my-session"}}'
```
</CodeGroup>
## Move, Delete, And Stat
`moveFs()`, `statFs()`, and `deleteFsEntry()` use ACP extension methods (`_sandboxagent/fs/move`, `_sandboxagent/fs/stat`, `_sandboxagent/fs/delete_entry`).
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock" });
await client.moveFs(
{ from: "./notes.txt", to: "./notes-old.txt", overwrite: true },
@ -129,16 +148,23 @@ console.log(stat);
```
```bash cURL
curl -X POST "http://127.0.0.1:2468/v1/fs/move?sessionId=my-session" \
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "x-acp-connection-id: acp_conn_1" \
-H "Content-Type: application/json" \
-d '{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true}'
-d '{"jsonrpc":"2.0","id":3,"method":"_sandboxagent/fs/move","params":{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true,"sessionId":"my-session"}}'
curl -X GET "http://127.0.0.1:2468/v1/fs/stat?path=./notes-old.txt&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN"
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "x-acp-connection-id: acp_conn_1" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"_sandboxagent/fs/stat","params":{"path":"./notes-old.txt","sessionId":"my-session"}}'
curl -X DELETE "http://127.0.0.1:2468/v1/fs/entry?path=./notes-old.txt&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN"
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "x-acp-connection-id: acp_conn_1" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":5,"method":"_sandboxagent/fs/delete_entry","params":{"path":"./notes-old.txt","sessionId":"my-session"}}'
```
</CodeGroup>
@ -148,15 +174,14 @@ Batch upload accepts `application/x-tar` only and extracts into the destination
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
import fs from "node:fs";
import path from "node:path";
import tar from "tar";
const client = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock" });
const archivePath = path.join(process.cwd(), "skills.tar");
await tar.c({
@ -176,7 +201,7 @@ console.log(result);
```bash cURL
tar -cf skills.tar -C ./skills .
curl -X POST "http://127.0.0.1:2468/v1/fs/upload-batch?path=./skills&sessionId=my-session" \
curl -X POST "http://127.0.0.1:2468/v2/fs/upload-batch?path=./skills&sessionId=my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "Content-Type: application/x-tar" \
--data-binary @skills.tar

View file

@ -25,10 +25,11 @@ Two ways to receive events: SSE streaming (recommended) or polling.
Use SSE for real-time events with automatic reconnection support.
```typescript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
agent: "mock",
});
// Get offset from last stored event (0 returns all events)
@ -130,7 +131,10 @@ const codingSession = actor({
},
createVars: async (c): Promise<CodingSessionVars> => {
const client = await SandboxAgent.connect({ baseUrl: c.state.baseUrl });
const client = new SandboxAgentClient({
baseUrl: c.state.baseUrl,
agent: "mock",
});
await client.createSession(c.state.sessionId, { agent: "claude" });
return { client };
},

View file

@ -14,12 +14,13 @@ The `mcp` field is a map of server name to config. Use `type: "local"` for stdio
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.createSession("claude-mcp", {
agent: "claude",

File diff suppressed because it is too large Load diff

View file

@ -1,149 +1,26 @@
---
title: "OpenCode Compatibility"
description: "Connect OpenCode clients, SDKs, and web UI to Sandbox Agent."
description: "Status of the OpenCode bridge during ACP v2 migration."
---
<Warning>
**Experimental**: OpenCode SDK & UI support is experimental and may change without notice.
</Warning>
OpenCode compatibility is intentionally deferred during ACP core migration.
Sandbox Agent exposes an OpenCode-compatible API, allowing you to connect any OpenCode client, SDK, or web UI to control coding agents running inside sandboxes.
## Current status (v2 core phases)
## Why Use OpenCode Clients with Sandbox Agent?
- `/opencode/*` routes are disabled.
- `sandbox-agent opencode` returns an explicit disabled error.
- This is expected while ACP runtime, SDK, and inspector migration is completed.
OpenCode provides a rich ecosystem of clients:
## Planned re-enable step
- **OpenCode CLI** (`opencode attach`): Terminal-based interface
- **OpenCode Web UI**: Browser-based chat interface
- **OpenCode SDK** (`@opencode-ai/sdk`): Rich TypeScript SDK
OpenCode support is restored in a dedicated phase after ACP core is stable:
## Quick Start
1. Reintroduce `/opencode/*` routing on top of ACP internals.
2. Add dedicated OpenCode ↔ ACP integration tests.
3. Re-enable OpenCode docs and operational guidance.
### Using OpenCode CLI & TUI
Track details in:
Sandbox Agent provides an all-in-one command to setup Sandbox Agent and connect an OpenCode session, great for local development:
```bash
sandbox-agent opencode --port 2468 --no-token
```
Or, start the server and attach separately:
```bash
# Start sandbox-agent
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
# Attach OpenCode CLI
opencode attach http://localhost:2468/opencode
```
With authentication enabled:
```bash
# Start with token
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
# Attach with password
opencode attach http://localhost:2468/opencode --password "$SANDBOX_TOKEN"
```
### Using the OpenCode Web UI
The OpenCode web UI can connect to Sandbox Agent for a full browser-based experience.
<Steps>
<Step title="Start Sandbox Agent with CORS">
```bash
sandbox-agent server --no-token --host 127.0.0.1 --port 2468 --cors-allow-origin http://127.0.0.1:5173
```
</Step>
<Step title="Clone and Start the OpenCode Web App">
```bash
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 5173
```
</Step>
<Step title="Open the UI">
Navigate to `http://127.0.0.1:5173/` in your browser.
</Step>
</Steps>
<Note>
If you see `Error: Could not connect to server`, check that:
- The sandbox-agent server is running
- `--cors-allow-origin` matches the **exact** browser origin (`localhost` and `127.0.0.1` are different origins)
</Note>
### Using OpenCode SDK
```typescript
import { createOpencodeClient } from "@opencode-ai/sdk";
const client = createOpencodeClient({
baseUrl: "http://localhost:2468/opencode",
headers: { Authorization: "Bearer YOUR_TOKEN" }, // if using auth
});
// Create a session
const session = await client.session.create();
// Send a prompt
await client.session.promptAsync({
path: { id: session.data.id },
body: {
parts: [{ type: "text", text: "Hello, write a hello world script" }],
},
});
// Subscribe to events
const events = await client.event.subscribe({});
for await (const event of events.stream) {
console.log(event);
}
```
## Notes
- **API Routing**: The OpenCode API is available at the `/opencode` base path
- **Authentication**: If sandbox-agent is started with `--token`, include `Authorization: Bearer <token>` header or use `--password` flag with CLI
- **CORS**: When using the web UI from a different origin, configure `--cors-allow-origin`
- **Provider Selection**: Use the provider/model selector in the UI to choose which backing agent to use (claude, codex, opencode, amp)
- **Models & Variants**: Providers are grouped by backing agent (e.g. Claude Code, Codex, Amp). OpenCode models are grouped by `OpenCode (<provider>)` to preserve their native provider grouping. Each model keeps its real model ID, and variants are exposed when available (Codex/OpenCode/Amp).
- **Optional Native Proxy for TUI/Config Endpoints**: Set `OPENCODE_COMPAT_PROXY_URL` (for example `http://127.0.0.1:4096`) to proxy select OpenCode-native endpoints to a real OpenCode server. This currently applies to `/command`, `/config`, `/global/config`, and `/tui/*`. If not set, sandbox-agent uses its built-in compatibility handlers.
## Endpoint Coverage
See the full endpoint compatibility table below. Most endpoints are functional for session management, messaging, and event streaming. Some endpoints return stub responses for features not yet implemented.
<Accordion title="Endpoint Status Table">
| Endpoint | Status | Notes |
|---|---|---|
| `GET /event` | ✓ | Emits events for session/message updates (SSE) |
| `GET /global/event` | ✓ | Wraps events in GlobalEvent format (SSE) |
| `GET /session` | ✓ | In-memory session store |
| `POST /session` | ✓ | Create new sessions |
| `GET /session/{id}` | ✓ | Get session details |
| `POST /session/{id}/message` | ✓ | Send messages to session |
| `GET /session/{id}/message` | ✓ | Get session messages |
| `GET /permission` | ✓ | List pending permissions |
| `POST /permission/{id}/reply` | ✓ | Respond to permission requests |
| `GET /question` | ✓ | List pending questions |
| `POST /question/{id}/reply` | ✓ | Answer agent questions |
| `GET /provider` | ✓ | Returns provider metadata |
| `GET /command` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub response |
| `GET /config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub response |
| `PATCH /config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise local compatibility behavior |
| `GET /global/config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub response |
| `PATCH /global/config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise local compatibility behavior |
| `/tui/*` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise local compatibility behavior |
| `GET /agent` | | Returns agent list |
| *other endpoints* | | Return empty/stub responses |
✓ Functional &nbsp;&nbsp; ↔ Proxied (optional) &nbsp;&nbsp; Stubbed
</Accordion>
- `research/acp/spec.md`
- `research/acp/migration-steps.md`
- `research/acp/todo.md`

View file

@ -184,9 +184,12 @@ icon: "rocket"
<Tabs>
<Tab title="TypeScript">
```typescript
const client = await SandboxAgent.connect({
import { SandboxAgentClient } from "sandbox-agent";
const client = new SandboxAgentClient({
baseUrl: "http://your-server:2468",
token: process.env.SANDBOX_TOKEN,
agent: "mock",
});
```
</Tab>
@ -230,10 +233,11 @@ icon: "rocket"
<Tabs>
<Tab title="TypeScript">
```typescript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
agent: "claude",
});
await client.createSession("my-session", {

View file

@ -1,11 +1,10 @@
---
title: "TypeScript"
description: "Use the generated client to manage sessions and stream events."
description: "Use the TypeScript SDK to manage ACP sessions and Sandbox Agent HTTP APIs."
icon: "js"
---
The TypeScript SDK is generated from the OpenAPI spec that ships with the server. It provides a typed
client for sessions, events, and agent operations.
The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgentClient`, which provides a Sandbox-facing API for session flows, ACP extensions, and binary HTTP filesystem helpers.
## Install
@ -27,14 +26,17 @@ client for sessions, events, and agent operations.
## Create a client
```ts
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
agent: "mock",
});
```
`SandboxAgentClient` is the canonical API. By default it auto-connects (`autoConnect: true`), so provide `agent` in the constructor. Use the instance method `client.connect()` only when you explicitly set `autoConnect: false`.
## Autospawn (Node only)
If you run locally, the SDK can launch the server for you.
@ -42,7 +44,9 @@ If you run locally, the SDK can launch the server for you.
```ts
import { SandboxAgent } from "sandbox-agent";
const client = await SandboxAgent.start();
const client = await SandboxAgent.start({
agent: "mock",
});
await client.dispose();
```
@ -50,69 +54,162 @@ await client.dispose();
Autospawn uses the local `sandbox-agent` binary. Install `@sandbox-agent/cli` (recommended) or set
`SANDBOX_AGENT_BIN` to a custom path.
## Sessions and messages
## Connect lifecycle
Use manual mode when you want explicit ACP session lifecycle control.
```ts
await client.createSession("demo-session", {
agent: "codex",
agentMode: "default",
permissionMode: "plan",
import {
AlreadyConnectedError,
NotConnectedError,
SandboxAgentClient,
} from "sandbox-agent";
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
agent: "mock",
autoConnect: false,
});
await client.postMessage("demo-session", { message: "Hello" });
await client.connect();
try {
await client.connect();
} catch (error) {
if (error instanceof AlreadyConnectedError) {
console.error("already connected");
}
}
await client.disconnect();
try {
await client.prompt({ sessionId: "s", prompt: [{ type: "text", text: "hi" }] });
} catch (error) {
if (error instanceof NotConnectedError) {
console.error("connect first");
}
}
```
List agents and inspect feature coverage (available on `capabilities`):
## Session flow
```ts
const session = await client.newSession({
cwd: "/",
mcpServers: [],
metadata: {
agent: "mock",
title: "Demo Session",
variant: "high",
permissionMode: "ask",
},
});
const result = await client.prompt({
sessionId: session.sessionId,
prompt: [{ type: "text", text: "Summarize this repository." }],
});
console.log(result.stopReason);
```
Load, cancel, and runtime settings use ACP-aligned method names:
```ts
await client.loadSession({ sessionId: session.sessionId, cwd: "/", mcpServers: [] });
await client.cancel({ sessionId: session.sessionId });
await client.setSessionMode({ sessionId: session.sessionId, modeId: "default" });
await client.setSessionConfigOption({
sessionId: session.sessionId,
configId: "config-id-from-session",
value: "config-value-id",
});
```
## Extension helpers
Sandbox extensions are exposed as first-class methods:
```ts
const models = await client.listModels({ sessionId: session.sessionId });
console.log(models.currentModelId, models.availableModels.length);
await client.setMetadata(session.sessionId, {
title: "Renamed Session",
model: "mock",
permissionMode: "ask",
});
await client.detachSession(session.sessionId);
await client.terminateSession(session.sessionId);
```
## Event handling
Use `onEvent` to consume converted SDK events.
```ts
import { SandboxAgentClient, type AgentEvent } from "sandbox-agent";
const events: AgentEvent[] = [];
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
agent: "mock",
onEvent: (event) => {
events.push(event);
if (event.type === "sessionEnded") {
console.log("ended", event.notification.params.sessionId ?? event.notification.params.session_id);
}
if (event.type === "agentUnparsed") {
console.warn("unparsed", event.notification.params);
}
},
});
```
You can also handle raw session update notifications directly:
```ts
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
agent: "mock",
onSessionUpdate: (notification) => {
console.log(notification.update.sessionUpdate);
},
});
```
## Control + HTTP helpers
Agent/session and non-binary filesystem control helpers use ACP extension methods over `/v2/rpc`:
```ts
const health = await client.getHealth();
const agents = await client.listAgents();
const codex = agents.agents.find((agent) => agent.id === "codex");
console.log(codex?.capabilities);
await client.installAgent("codex", { reinstall: true });
const sessions = await client.listSessions();
const sessionInfo = await client.getSession(sessions.sessions[0].session_id);
```
## Poll events
These methods require an active ACP connection and throw `NotConnectedError` when disconnected.
```ts
const events = await client.getEvents("demo-session", {
offset: 0,
limit: 200,
includeRaw: false,
});
Binary filesystem transfer intentionally remains HTTP:
for (const event of events.events) {
console.log(event.type, event.data);
}
```
- `readFsFile` -> `GET /v2/fs/file`
- `writeFsFile` -> `PUT /v2/fs/file`
- `uploadFsBatch` -> `POST /v2/fs/upload-batch`
## Stream events (SSE)
Reason: these are Sandbox Agent host/runtime filesystem operations (not agent-specific ACP behavior), intentionally separate from ACP native `fs/read_text_file` / `fs/write_text_file`, and they may require streaming very large binary payloads that ACP JSON-RPC is not suited to transport efficiently.
```ts
for await (const event of client.streamEvents("demo-session", {
offset: 0,
includeRaw: false,
})) {
console.log(event.type, event.data);
}
```
The SDK parses `text/event-stream` into `UniversalEvent` objects. If you want full control, use
`getEventsSse()` and parse the stream yourself.
## Stream a single turn
```ts
for await (const event of client.streamTurn("demo-session", { message: "Hello" })) {
console.log(event.type, event.data);
}
```
This method posts the message and streams only the next turn. For manual control, call
`postMessageStream()` and parse the SSE response yourself.
## Optional raw payloads
Set `includeRaw: true` on `getEvents`, `streamEvents`, or `streamTurn` to include the raw provider
payload in `event.raw`. This is useful for debugging and conversion analysis.
ACP extension variants can exist in parallel for compatibility, but `SandboxAgentClient` should prefer the HTTP endpoints above by default.
## Error handling
@ -122,7 +219,7 @@ All HTTP errors throw `SandboxAgentError`:
import { SandboxAgentError } from "sandbox-agent";
try {
await client.postMessage("missing-session", { message: "Hi" });
await client.listAgents();
} catch (error) {
if (error instanceof SandboxAgentError) {
console.error(error.status, error.problem);
@ -142,6 +239,7 @@ const url = buildInspectorUrl({
token: "optional-bearer-token",
headers: { "X-Custom-Header": "value" },
});
console.log(url);
// https://your-sandbox-agent.example.com/ui/?token=...&headers=...
```
@ -153,10 +251,17 @@ Parameters:
## Types
The SDK exports OpenAPI-derived types for events, items, and feature coverage:
The SDK exports typed events and responses for the Sandbox layer:
```ts
import type { UniversalEvent, UniversalItem, AgentCapabilities } from "sandbox-agent";
import type {
AgentEvent,
AgentInfo,
HealthResponse,
SessionInfo,
SessionListResponse,
SessionTerminateResponse,
} from "sandbox-agent";
```
See the [API Reference](/api) for schema details.
For low-level protocol transport details, see [ACP HTTP Client](/advanced/acp-http-client).

View file

@ -14,12 +14,13 @@ Pass `skills.sources` when creating a session to load skills from GitHub repos,
<CodeGroup>
```ts TypeScript
import { SandboxAgent } from "sandbox-agent";
import { SandboxAgentClient } from "sandbox-agent";
const client = await SandboxAgent.connect({
const client = new SandboxAgentClient({
baseUrl: "http://127.0.0.1:2468",
token: process.env.SANDBOX_TOKEN,
});
agent: "mock",
});
await client.createSession("claude-skills", {
agent: "claude",