mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 18:01:30 +00:00
acp spec (#155)
This commit is contained in:
parent
70287ec471
commit
e72eb9f611
264 changed files with 18559 additions and 51021 deletions
67
docs/advanced/acp-http-client.mdx
Normal file
67
docs/advanced/acp-http-client.mdx
Normal 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.
|
||||
|
|
@ -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");
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@
|
|||
"group": "Features",
|
||||
"pages": ["file-system"]
|
||||
},
|
||||
{
|
||||
"group": "Advanced",
|
||||
"pages": ["advanced/acp-http-client"]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
2189
docs/openapi.json
2189
docs/openapi.json
File diff suppressed because it is too large
Load diff
|
|
@ -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 ↔ Proxied (optional) − Stubbed
|
||||
|
||||
</Accordion>
|
||||
- `research/acp/spec.md`
|
||||
- `research/acp/migration-steps.md`
|
||||
- `research/acp/todo.md`
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue