mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 10:02:26 +00:00
chore: fix bad merge
This commit is contained in:
parent
1dd45908a3
commit
94353f7696
205 changed files with 19244 additions and 14866 deletions
|
|
@ -43,7 +43,7 @@ Manually verify the install script works in a fresh environment:
|
||||||
```bash
|
```bash
|
||||||
docker run --rm alpine:latest sh -c "
|
docker run --rm alpine:latest sh -c "
|
||||||
apk add --no-cache curl ca-certificates libstdc++ libgcc bash &&
|
apk add --no-cache curl ca-certificates libstdc++ libgcc bash &&
|
||||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh &&
|
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh &&
|
||||||
sandbox-agent --version
|
sandbox-agent --version
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@main
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -23,11 +23,11 @@ jobs:
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: ./scripts/release/main.ts --version 0.0.0 --check
|
run: ./scripts/release/main.ts --version 0.0.0 --check
|
||||||
- name: Run ACP v2 server tests
|
- name: Run ACP v1 server tests
|
||||||
run: |
|
run: |
|
||||||
cargo test -p sandbox-agent-agent-management
|
cargo test -p sandbox-agent-agent-management
|
||||||
cargo test -p sandbox-agent --test v2_api
|
cargo test -p sandbox-agent --test v1_api
|
||||||
cargo test -p sandbox-agent --test v2_agent_process_matrix
|
cargo test -p sandbox-agent --test v1_agent_process_matrix
|
||||||
cargo test -p sandbox-agent --lib
|
cargo test -p sandbox-agent --lib
|
||||||
- name: Run SDK tests
|
- name: Run SDK tests
|
||||||
run: pnpm --dir sdks/typescript test
|
run: pnpm --dir sdks/typescript test
|
||||||
|
|
|
||||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
|
|
@ -147,8 +147,8 @@ jobs:
|
||||||
sudo apt-get install -y unzip curl
|
sudo apt-get install -y unzip curl
|
||||||
|
|
||||||
# Install AWS CLI
|
# Install AWS CLI
|
||||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscli.zip"
|
||||||
unzip awscliv2.zip
|
unzip awscli.zip
|
||||||
sudo ./aws/install --update
|
sudo ./aws/install --update
|
||||||
|
|
||||||
COMMIT_SHA_SHORT="${GITHUB_SHA::7}"
|
COMMIT_SHA_SHORT="${GITHUB_SHA::7}"
|
||||||
|
|
|
||||||
94
CLAUDE.md
94
CLAUDE.md
|
|
@ -1,24 +1,24 @@
|
||||||
# Instructions
|
# Instructions
|
||||||
|
|
||||||
## ACP v2 Baseline
|
## ACP v1 Baseline
|
||||||
|
|
||||||
- v2 is ACP-native.
|
- v1 is ACP-native.
|
||||||
- `/v1/*` is removed and returns `410 Gone` (`application/problem+json`).
|
- `/v1/*` is removed and returns `410 Gone` (`application/problem+json`).
|
||||||
- `/opencode/*` is disabled during ACP core phases and returns `503`.
|
- `/opencode/*` is disabled during ACP core phases and returns `503`.
|
||||||
- Prompt/session traffic is ACP JSON-RPC over streamable HTTP on `/v2/rpc`:
|
- Prompt/session traffic is ACP JSON-RPC over streamable HTTP on `/v1/rpc`:
|
||||||
- `POST /v2/rpc`
|
- `POST /v1/rpc`
|
||||||
- `GET /v2/rpc` (SSE)
|
- `GET /v1/rpc` (SSE)
|
||||||
- `DELETE /v2/rpc`
|
- `DELETE /v1/rpc`
|
||||||
- Control-plane endpoints:
|
- Control-plane endpoints:
|
||||||
- `GET /v2/health`
|
- `GET /v1/health`
|
||||||
- `GET /v2/agents`
|
- `GET /v1/agents`
|
||||||
- `POST /v2/agents/{agent}/install`
|
- `POST /v1/agents/{agent}/install`
|
||||||
- Binary filesystem transfer endpoints (intentionally HTTP, not ACP extension methods):
|
- Binary filesystem transfer endpoints (intentionally HTTP, not ACP extension methods):
|
||||||
- `GET /v2/fs/file`
|
- `GET /v1/fs/file`
|
||||||
- `PUT /v2/fs/file`
|
- `PUT /v1/fs/file`
|
||||||
- `POST /v2/fs/upload-batch`
|
- `POST /v1/fs/upload-batch`
|
||||||
- Sandbox Agent ACP extension method naming:
|
- Sandbox Agent ACP extension method naming:
|
||||||
- Custom ACP methods use `_sandboxagent/...` (not `_sandboxagent/v2/...`).
|
- Custom ACP methods use `_sandboxagent/...` (not `_sandboxagent/v1/...`).
|
||||||
- Session detach method is `_sandboxagent/session/detach`.
|
- Session detach method is `_sandboxagent/session/detach`.
|
||||||
|
|
||||||
## API Scope
|
## API Scope
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
- ACP extensions may be used for gaps (for example `skills`, `models`, and related metadata), but the default is that agent-facing behavior is implemented by the agent through ACP.
|
- ACP extensions may be used for gaps (for example `skills`, `models`, and related metadata), but the default is that agent-facing behavior is implemented by the agent through ACP.
|
||||||
- Custom HTTP APIs are for non-agent/session platform services (for example filesystem, terminals, and other host/runtime capabilities).
|
- Custom HTTP APIs are for non-agent/session platform services (for example filesystem, terminals, and other host/runtime capabilities).
|
||||||
- Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP.
|
- Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP.
|
||||||
- Keep `GET /v2/fs/file`, `PUT /v2/fs/file`, and `POST /v2/fs/upload-batch` on HTTP:
|
- Keep `GET /v1/fs/file`, `PUT /v1/fs/file`, and `POST /v1/fs/upload-batch` on HTTP:
|
||||||
- These are Sandbox Agent host/runtime operations with cross-agent-consistent behavior.
|
- These are Sandbox Agent host/runtime operations with cross-agent-consistent behavior.
|
||||||
- They may involve very large binary transfers that ACP JSON-RPC envelopes are not suited to stream.
|
- They may involve very large binary transfers that ACP JSON-RPC envelopes are not suited to stream.
|
||||||
- This is intentionally separate from ACP native `fs/read_text_file` and `fs/write_text_file`.
|
- This is intentionally separate from ACP native `fs/read_text_file` and `fs/write_text_file`.
|
||||||
|
|
@ -51,14 +51,24 @@
|
||||||
## TypeScript SDK Architecture
|
## TypeScript SDK Architecture
|
||||||
|
|
||||||
- TypeScript clients are split into:
|
- TypeScript clients are split into:
|
||||||
- `acp-http-client`: protocol-pure ACP-over-HTTP (`/v2/rpc`) with no Sandbox-specific metadata/extensions.
|
- `acp-http-client`: protocol-pure ACP-over-HTTP (`/v1/acp`) with no Sandbox-specific HTTP helpers.
|
||||||
- `sandbox-agent`: `SandboxAgentClient` wrapper that adds Sandbox metadata/extension helpers and keeps non-ACP HTTP helpers.
|
- `sandbox-agent`: `SandboxAgent` SDK wrapper that combines ACP session operations with Sandbox control-plane and filesystem helpers.
|
||||||
- `SandboxAgentClient` constructor is `new SandboxAgentClient(...)`.
|
- `SandboxAgent` entry points are `SandboxAgent.connect(...)` and `SandboxAgent.start(...)`.
|
||||||
- `SandboxAgentClient` auto-connects by default; `autoConnect: false` requires explicit `.connect()`.
|
- Stable Sandbox session methods are `createSession`, `resumeSession`, `resumeOrCreateSession`, `destroySession`, `sendSessionMethod`, and `onSessionEvent`.
|
||||||
- ACP/session methods must throw when disconnected (`NotConnectedError`), and `.connect()` must throw when already connected (`AlreadyConnectedError`).
|
- `Session` helpers are `prompt(...)`, `send(...)`, and `onEvent(...)`.
|
||||||
- A `SandboxAgentClient` instance may have at most one active ACP connection at a time.
|
- Cleanup is `sdk.dispose()`.
|
||||||
- Stable ACP session method names should stay ACP-aligned in the Sandbox wrapper (`newSession`, `loadSession`, `prompt`, `cancel`, `setSessionMode`, `setSessionConfigOption`).
|
|
||||||
- Sandbox extension methods are first-class wrapper helpers (`listModels`, `setMetadata`, `detachSession`, `terminateSession`).
|
### Docs Source Of Truth
|
||||||
|
|
||||||
|
- For TypeScript docs/examples, source of truth is implementation in:
|
||||||
|
- `sdks/typescript/src/client.ts`
|
||||||
|
- `sdks/typescript/src/index.ts`
|
||||||
|
- `sdks/acp-http-client/src/index.ts`
|
||||||
|
- Do not document TypeScript APIs unless they are exported and implemented in those files.
|
||||||
|
- For HTTP/CLI docs/examples, source of truth is:
|
||||||
|
- `server/packages/sandbox-agent/src/router.rs`
|
||||||
|
- `server/packages/sandbox-agent/src/cli.rs`
|
||||||
|
- Keep docs aligned to implemented endpoints/commands only (for example ACP under `/v1/acp`, not legacy `/v1/sessions` APIs).
|
||||||
|
|
||||||
## Source Documents
|
## Source Documents
|
||||||
|
|
||||||
|
|
@ -76,5 +86,43 @@
|
||||||
- Regenerate `docs/openapi.json` when HTTP contracts change.
|
- Regenerate `docs/openapi.json` when HTTP contracts change.
|
||||||
- Keep `docs/inspector.mdx` and `docs/sdks/typescript.mdx` aligned with implementation.
|
- Keep `docs/inspector.mdx` and `docs/sdks/typescript.mdx` aligned with implementation.
|
||||||
- Append blockers/decisions to `research/acp/friction.md` during ACP work.
|
- Append blockers/decisions to `research/acp/friction.md` during ACP work.
|
||||||
- TypeScript SDK tests should run against a real running server/runtime over real `/v2` HTTP APIs, typically using the real `mock` agent for deterministic behavior.
|
- TypeScript SDK tests should run against a real running server/runtime over real `/v1` HTTP APIs, typically using the real `mock` agent for deterministic behavior.
|
||||||
- Do not use Vitest fetch/transport mocks to simulate server functionality in TypeScript SDK tests.
|
- Do not use Vitest fetch/transport mocks to simulate server functionality in TypeScript SDK tests.
|
||||||
|
|
||||||
|
## Docker Examples (Dev Testing)
|
||||||
|
|
||||||
|
- When manually testing bleeding-edge (unreleased) versions of sandbox-agent in `examples/`, use `SANDBOX_AGENT_DEV=1` with the Docker-based examples.
|
||||||
|
- This triggers `examples/shared/Dockerfile.dev` which builds the server binary from local source and packages it into the Docker image.
|
||||||
|
- Example: `SANDBOX_AGENT_DEV=1 pnpm --filter @sandbox-agent/example-mcp start`
|
||||||
|
|
||||||
|
## Install Version References
|
||||||
|
|
||||||
|
- Channel policy:
|
||||||
|
- Sandbox Agent install/version references use a pinned minor channel `0.N.x` (for curl URLs and `sandbox-agent` / `@sandbox-agent/cli` npm/bun installs).
|
||||||
|
- Gigacode install/version references use `latest` (for `@sandbox-agent/gigacode` install/run commands and `gigacode-install.*` release promotion).
|
||||||
|
- Release promotion policy: `latest` releases must still update `latest`; when a release is `latest`, Sandbox Agent must also be promoted to the matching minor channel `0.N.x`.
|
||||||
|
- Keep every install-version reference below in sync whenever versions/channels change:
|
||||||
|
- `README.md`
|
||||||
|
- `docs/acp-http-client.mdx`
|
||||||
|
- `docs/cli.mdx`
|
||||||
|
- `docs/quickstart.mdx`
|
||||||
|
- `docs/sdk-overview.mdx`
|
||||||
|
- `docs/session-persistence.mdx`
|
||||||
|
- `docs/deploy/local.mdx`
|
||||||
|
- `docs/deploy/cloudflare.mdx`
|
||||||
|
- `docs/deploy/vercel.mdx`
|
||||||
|
- `docs/deploy/daytona.mdx`
|
||||||
|
- `docs/deploy/e2b.mdx`
|
||||||
|
- `docs/deploy/docker.mdx`
|
||||||
|
- `frontend/packages/website/src/components/GetStarted.tsx`
|
||||||
|
- `.claude/commands/post-release-testing.md`
|
||||||
|
- `examples/cloudflare/Dockerfile`
|
||||||
|
- `examples/daytona/src/index.ts`
|
||||||
|
- `examples/daytona/src/daytona-with-snapshot.ts`
|
||||||
|
- `examples/docker/src/index.ts`
|
||||||
|
- `examples/e2b/src/index.ts`
|
||||||
|
- `examples/vercel/src/index.ts`
|
||||||
|
- `scripts/release/main.ts`
|
||||||
|
- `scripts/release/promote-artifacts.ts`
|
||||||
|
- `scripts/release/sdk.ts`
|
||||||
|
- `scripts/sandbox-testing/test-sandbox.ts`
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ sandbox-agent = { version = "0.2.0", path = "server/packages/sandbox-agent" }
|
||||||
sandbox-agent-error = { version = "0.2.0", path = "server/packages/error" }
|
sandbox-agent-error = { version = "0.2.0", path = "server/packages/error" }
|
||||||
sandbox-agent-agent-management = { version = "0.2.0", path = "server/packages/agent-management" }
|
sandbox-agent-agent-management = { version = "0.2.0", path = "server/packages/agent-management" }
|
||||||
sandbox-agent-agent-credentials = { version = "0.2.0", path = "server/packages/agent-credentials" }
|
sandbox-agent-agent-credentials = { version = "0.2.0", path = "server/packages/agent-credentials" }
|
||||||
|
sandbox-agent-opencode-adapter = { version = "0.2.0", path = "server/packages/opencode-adapter" }
|
||||||
|
sandbox-agent-opencode-server-manager = { version = "0.2.0", path = "server/packages/opencode-server-manager" }
|
||||||
|
acp-http-adapter = { version = "0.2.0", path = "server/packages/acp-http-adapter" }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -1,288 +1,90 @@
|
||||||
---
|
---
|
||||||
title: "Agent Sessions"
|
title: "Agent Sessions"
|
||||||
description: "Create sessions and send messages to agents."
|
description: "Create sessions, prompt agents, and inspect event history."
|
||||||
sidebarTitle: "Sessions"
|
sidebarTitle: "Sessions"
|
||||||
icon: "comments"
|
icon: "comments"
|
||||||
---
|
---
|
||||||
|
|
||||||
Sessions are the unit of interaction with an agent. You create one session per task, then send messages and stream events.
|
Sessions are the unit of interaction with an agent. Create one session per task, send prompts, and consume event history.
|
||||||
|
|
||||||
## Session Options
|
For SDK-based flows, sessions can be restored after runtime/session loss when persistence is enabled.
|
||||||
|
See [Session Restoration](/session-restoration).
|
||||||
|
|
||||||
`POST /v1/sessions/{sessionId}` accepts the following fields:
|
## Create a session
|
||||||
|
|
||||||
- `agent` (required): `claude`, `codex`, `opencode`, `amp`, or `mock`
|
```ts
|
||||||
- `agentMode`: agent mode string (for example, `build`, `plan`)
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
- `permissionMode`: permission mode string (`default`, `plan`, `bypass`, etc.)
|
|
||||||
- `model`: model override (agent-specific)
|
|
||||||
- `variant`: model variant (agent-specific)
|
|
||||||
- `agentVersion`: agent version override
|
|
||||||
- `mcp`: MCP server config map (see `MCP`)
|
|
||||||
- `skills`: skill path config (see `Skills`)
|
|
||||||
|
|
||||||
## Create A Session
|
const sdk = await SandboxAgent.connect({
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.createSession("build-session", {
|
const session = await sdk.createSession({
|
||||||
agent: "codex",
|
agent: "codex",
|
||||||
agentMode: "build",
|
sessionInit: {
|
||||||
permissionMode: "default",
|
cwd: "/",
|
||||||
model: "gpt-4.1",
|
mcpServers: [],
|
||||||
variant: "reasoning",
|
},
|
||||||
agentVersion: "latest",
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"agent": "codex",
|
|
||||||
"agentMode": "build",
|
|
||||||
"permissionMode": "default",
|
|
||||||
"model": "gpt-4.1",
|
|
||||||
"variant": "reasoning",
|
|
||||||
"agentVersion": "latest"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Send A Message
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.postMessage("build-session", {
|
console.log(session.id, session.agentSessionId);
|
||||||
message: "Summarize the repository structure.",
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
## Send a prompt
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Summarize the repository structure."}'
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Stream A Turn
|
```ts
|
||||||
|
const response = await session.prompt([
|
||||||
|
{ type: "text", text: "Summarize the repository structure." },
|
||||||
|
]);
|
||||||
|
|
||||||
<CodeGroup>
|
console.log(response.stopReason);
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
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.",
|
|
||||||
});
|
|
||||||
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (reader) {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
console.log(decoder.decode(value, { stream: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
## Subscribe to live events
|
||||||
curl -N -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages/stream" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Explain the main entrypoints."}'
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Fetch Events
|
```ts
|
||||||
|
const unsubscribe = session.onEvent((event) => {
|
||||||
<CodeGroup>
|
console.log(event.eventIndex, event.sender, event.payload);
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
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", {
|
await session.prompt([
|
||||||
offset: 0,
|
{ type: "text", text: "Explain the main entrypoints." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fetch persisted event history
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const page = await sdk.getEvents({
|
||||||
|
sessionId: session.id,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
includeRaw: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(events.events);
|
for (const event of page.items) {
|
||||||
```
|
console.log(event.id, event.createdAt, event.sender);
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events?offset=0&limit=50" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
`GET /v1/sessions/{sessionId}/get-messages` is an alias for `events`.
|
|
||||||
|
|
||||||
## Stream Events (SSE)
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
## List and load sessions
|
||||||
curl -N -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events/sse?offset=0" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## List Sessions
|
```ts
|
||||||
|
const sessions = await sdk.listSessions({ limit: 20 });
|
||||||
|
|
||||||
<CodeGroup>
|
for (const item of sessions.items) {
|
||||||
```ts TypeScript
|
console.log(item.id, item.agent, item.createdAt);
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
}
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
if (sessions.items.length > 0) {
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
const loaded = await sdk.resumeSession(sessions.items[0]!.id);
|
||||||
token: process.env.SANDBOX_TOKEN,
|
await loaded.prompt([{ type: "text", text: "Continue." }]);
|
||||||
agent: "mock",
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const sessions = await client.listSessions();
|
|
||||||
console.log(sessions.sessions);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
## Destroy a session
|
||||||
curl -X GET "http://127.0.0.1:2468/v1/sessions" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Reply To A Question
|
```ts
|
||||||
|
await sdk.destroySession(session.id);
|
||||||
When the agent asks a question, reply with an array of answers. Each inner array is one multi-select response.
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
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"]],
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/questions/question-1/reply" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"answers":[["yes"]]}'
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Reject A Question
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
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");
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/questions/question-1/reject" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Reply To A Permission Request
|
|
||||||
|
|
||||||
Use `once`, `always`, or `reject`.
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/permissions/permission-1/reply" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"reply":"once"}'
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Terminate A Session
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.terminateSession("build-session");
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/terminate" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
|
||||||
64
docs/architecture.mdx
Normal file
64
docs/architecture.mdx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
title: "Architecture"
|
||||||
|
description: "How the client, sandbox, server, and agent fit together."
|
||||||
|
icon: "microchip"
|
||||||
|
---
|
||||||
|
|
||||||
|
Sandbox Agent runs as an HTTP server inside your sandbox. Your app talks to it remotely.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- `Your client`: your app code using the `sandbox-agent` SDK.
|
||||||
|
- `Sandbox`: isolated runtime (E2B, Daytona, Docker, etc.).
|
||||||
|
- `Sandbox Agent server`: process inside the sandbox exposing HTTP transport.
|
||||||
|
- `Agent`: Claude/Codex/OpenCode/Amp process managed by Sandbox Agent.
|
||||||
|
|
||||||
|
```mermaid placement="top-right"
|
||||||
|
flowchart LR
|
||||||
|
CLIENT["Sandbox Agent SDK"]
|
||||||
|
SERVER["Sandbox Agent server"]
|
||||||
|
AGENT["Agent process"]
|
||||||
|
|
||||||
|
subgraph SANDBOX["Sandbox"]
|
||||||
|
direction TB
|
||||||
|
SERVER --> AGENT
|
||||||
|
end
|
||||||
|
|
||||||
|
CLIENT -->|HTTP| SERVER
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested Topology
|
||||||
|
|
||||||
|
Run the SDK on your backend, then call it from your frontend.
|
||||||
|
|
||||||
|
This extra hop is recommended because it keeps auth/token logic on the backend and makes persistence simpler.
|
||||||
|
|
||||||
|
```mermaid placement="top-right"
|
||||||
|
flowchart LR
|
||||||
|
BROWSER["Browser"]
|
||||||
|
subgraph BACKEND["Your backend"]
|
||||||
|
direction TB
|
||||||
|
SDK["Sandbox Agent SDK"]
|
||||||
|
end
|
||||||
|
subgraph SANDBOX_SIMPLE["Sandbox"]
|
||||||
|
SERVER_SIMPLE["Sandbox Agent server"]
|
||||||
|
end
|
||||||
|
|
||||||
|
BROWSER --> BACKEND
|
||||||
|
BACKEND --> SDK --> SERVER_SIMPLE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend requirements
|
||||||
|
|
||||||
|
Your backend layer needs to handle:
|
||||||
|
|
||||||
|
- **Long-running connections**: prompts can take minutes.
|
||||||
|
- **Session affinity**: follow-up messages must reach the same session.
|
||||||
|
- **State between requests**: session metadata and event history must persist across requests.
|
||||||
|
- **Graceful recovery**: sessions should resume after backend restarts.
|
||||||
|
|
||||||
|
We recommend [Rivet](https://rivet.dev) over serverless because actors natively support the long-lived connections, session routing, and state persistence that agent workloads require.
|
||||||
|
|
||||||
|
## Session persistence
|
||||||
|
|
||||||
|
For storage driver options and replay behavior, see [Persisting Sessions](/session-persistence).
|
||||||
|
|
@ -1,29 +1,27 @@
|
||||||
---
|
---
|
||||||
title: "Attachments"
|
title: "Attachments"
|
||||||
description: "Upload files into the sandbox and attach them to prompts."
|
description: "Upload files into the sandbox and reference them in prompts."
|
||||||
sidebarTitle: "Attachments"
|
sidebarTitle: "Attachments"
|
||||||
icon: "paperclip"
|
icon: "paperclip"
|
||||||
---
|
---
|
||||||
|
|
||||||
Use the filesystem API to upload files, then reference them as attachments when sending prompts.
|
Use the filesystem API to upload files, then include file references in prompt content.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Upload a file">
|
<Step title="Upload a file">
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
const sdk = await SandboxAgent.connect({
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const buffer = await fs.promises.readFile("./data.csv");
|
const buffer = await fs.promises.readFile("./data.csv");
|
||||||
|
|
||||||
const upload = await client.writeFsFile(
|
const upload = await sdk.writeFsFile(
|
||||||
{ path: "./uploads/data.csv", sessionId: "my-session" },
|
{ path: "./uploads/data.csv" },
|
||||||
buffer,
|
buffer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -31,59 +29,33 @@ Use the filesystem API to upload files, then reference them as attachments when
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
```bash cURL
|
||||||
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./uploads/data.csv&sessionId=my-session" \
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./uploads/data.csv" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
--data-binary @./data.csv
|
--data-binary @./data.csv
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
The response returns the absolute path that you should use for attachments.
|
The upload response returns the absolute path.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Attach the file in a prompt">
|
<Step title="Reference the file in a prompt">
|
||||||
<CodeGroup>
|
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
const session = await sdk.createSession({ agent: "mock" });
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
await session.prompt([
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
{ type: "text", text: "Please analyze the attached CSV." },
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.postMessage("my-session", {
|
|
||||||
message: "Please analyze the attached CSV.",
|
|
||||||
attachments: [
|
|
||||||
{
|
{
|
||||||
path: "/home/sandbox/uploads/data.csv",
|
type: "resource_link",
|
||||||
mime: "text/csv",
|
name: "data.csv",
|
||||||
filename: "data.csv",
|
uri: "file:///home/sandbox/uploads/data.csv",
|
||||||
|
mimeType: "text/csv",
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"message": "Please analyze the attached CSV.",
|
|
||||||
"attachments": [
|
|
||||||
{
|
|
||||||
"path": "/home/sandbox/uploads/data.csv",
|
|
||||||
"mime": "text/csv",
|
|
||||||
"filename": "data.csv"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Use absolute paths from the upload response to avoid ambiguity.
|
- Use absolute file URIs in `resource_link` blocks.
|
||||||
- If `mime` is omitted, the server defaults to `application/octet-stream`.
|
- If `mimeType` is omitted, the agent/runtime may infer a default.
|
||||||
- OpenCode receives file parts directly; other agents will see the attachment paths appended to the prompt.
|
- Support for non-text resources depends on each agent's ACP prompt capabilities.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
title: "CORS Configuration"
|
title: "CORS Configuration"
|
||||||
description: "Configure CORS for browser-based applications."
|
description: "Configure CORS for browser-based applications."
|
||||||
sidebarTitle: "CORS"
|
sidebarTitle: "CORS"
|
||||||
icon: "globe"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
When calling the Sandbox Agent server from a browser, CORS (Cross-Origin Resource Sharing) controls which origins can make requests.
|
When calling the Sandbox Agent server from a browser, CORS (Cross-Origin Resource Sharing) controls which origins can make requests.
|
||||||
|
|
@ -13,7 +12,6 @@ By default, no CORS origins are allowed. You must explicitly specify origins for
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent server \
|
sandbox-agent server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
|
||||||
--cors-allow-origin "http://localhost:5173"
|
--cors-allow-origin "http://localhost:5173"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -36,7 +34,6 @@ Specify the flag multiple times to allow multiple origins:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent server \
|
sandbox-agent server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
|
||||||
--cors-allow-origin "http://localhost:5173" \
|
--cors-allow-origin "http://localhost:5173" \
|
||||||
--cors-allow-origin "http://localhost:3000"
|
--cors-allow-origin "http://localhost:3000"
|
||||||
```
|
```
|
||||||
|
|
@ -47,7 +44,6 @@ By default, all methods and headers are allowed. To restrict them:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent server \
|
sandbox-agent server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
|
||||||
--cors-allow-origin "https://your-app.com" \
|
--cors-allow-origin "https://your-app.com" \
|
||||||
--cors-allow-method "GET" \
|
--cors-allow-method "GET" \
|
||||||
--cors-allow-method "POST" \
|
--cors-allow-method "POST" \
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,115 @@
|
||||||
---
|
---
|
||||||
title: "Credentials"
|
title: "Credentials"
|
||||||
description: "How sandbox-agent discovers and exposes provider credentials."
|
description: "How Sandbox Agent discovers and uses provider credentials."
|
||||||
icon: "key"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
`sandbox-agent` can discover provider credentials from environment variables and local agent config files.
|
Sandbox Agent discovers API credentials from environment variables and local agent config files.
|
||||||
|
These credentials are passed through to underlying agent runtimes.
|
||||||
|
|
||||||
## Supported providers
|
## Credential sources
|
||||||
|
|
||||||
- Anthropic
|
Credentials are discovered in priority order.
|
||||||
- OpenAI
|
|
||||||
- Additional provider entries discovered via OpenCode config
|
|
||||||
|
|
||||||
## Common environment variables
|
### Environment variables (highest priority)
|
||||||
|
|
||||||
|
API keys first:
|
||||||
|
|
||||||
| Variable | Provider |
|
| Variable | Provider |
|
||||||
| --- | --- |
|
|----------|----------|
|
||||||
| `ANTHROPIC_API_KEY` | Anthropic |
|
| `ANTHROPIC_API_KEY` | Anthropic |
|
||||||
| `CLAUDE_API_KEY` | Anthropic fallback |
|
| `CLAUDE_API_KEY` | Anthropic fallback |
|
||||||
| `OPENAI_API_KEY` | OpenAI |
|
| `OPENAI_API_KEY` | OpenAI |
|
||||||
| `CODEX_API_KEY` | OpenAI fallback |
|
| `CODEX_API_KEY` | OpenAI fallback |
|
||||||
|
|
||||||
## Extract credentials (CLI)
|
OAuth tokens (used when OAuth extraction is enabled):
|
||||||
|
|
||||||
Show discovered credentials (redacted by default):
|
| Variable | Provider |
|
||||||
|
|----------|----------|
|
||||||
|
| `CLAUDE_CODE_OAUTH_TOKEN` | Anthropic |
|
||||||
|
| `ANTHROPIC_AUTH_TOKEN` | Anthropic fallback |
|
||||||
|
|
||||||
```bash
|
### Agent config files
|
||||||
sandbox-agent credentials extract
|
|
||||||
|
| 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` | Anthropic/OpenAI |
|
||||||
|
|
||||||
|
## Provider requirements by agent
|
||||||
|
|
||||||
|
| Agent | Required provider |
|
||||||
|
|-------|-------------------|
|
||||||
|
| Claude Code | Anthropic |
|
||||||
|
| Amp | Anthropic |
|
||||||
|
| Codex | OpenAI |
|
||||||
|
| OpenCode | Anthropic or OpenAI |
|
||||||
|
| Mock | None |
|
||||||
|
|
||||||
|
## Error handling behavior
|
||||||
|
|
||||||
|
Credential extraction is best-effort:
|
||||||
|
|
||||||
|
- Missing or malformed files are skipped.
|
||||||
|
- Discovery continues to later sources.
|
||||||
|
- Missing credentials mark providers unavailable instead of failing server startup.
|
||||||
|
|
||||||
|
When prompting, Sandbox Agent does not pre-validate provider credentials. Agent-native authentication errors surface through session events/output.
|
||||||
|
|
||||||
|
## Checking credential status
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
`GET /v1/agents` includes `credentialsAvailable` per agent.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"id": "claude",
|
||||||
|
"installed": true,
|
||||||
|
"credentialsAvailable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "codex",
|
||||||
|
"installed": true,
|
||||||
|
"credentialsAvailable": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Reveal raw values:
|
### TypeScript SDK
|
||||||
|
|
||||||
```bash
|
```typescript
|
||||||
sandbox-agent credentials extract --reveal
|
const result = await sdk.listAgents();
|
||||||
|
|
||||||
|
for (const agent of result.agents) {
|
||||||
|
console.log(`${agent.id}: ${agent.credentialsAvailable ? "authenticated" : "no credentials"}`);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Filter by agent/provider:
|
## Passing credentials explicitly
|
||||||
|
|
||||||
|
Set environment variables before starting Sandbox Agent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent credentials extract --agent codex
|
export ANTHROPIC_API_KEY=sk-ant-...
|
||||||
sandbox-agent credentials extract --provider openai
|
export OPENAI_API_KEY=sk-...
|
||||||
|
sandbox-agent daemon start
|
||||||
```
|
```
|
||||||
|
|
||||||
Emit shell exports:
|
Or with SDK-managed local spawn:
|
||||||
|
|
||||||
```bash
|
```typescript
|
||||||
sandbox-agent credentials extract-env --export
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.start({
|
||||||
|
spawn: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
|
||||||
|
|
@ -5,132 +5,97 @@ sidebarTitle: "Custom Tools"
|
||||||
icon: "wrench"
|
icon: "wrench"
|
||||||
---
|
---
|
||||||
|
|
||||||
There are two ways to give agents custom tools that run inside the sandbox:
|
There are two common patterns for sandbox-local custom tooling:
|
||||||
|
|
||||||
| | MCP Server | Skill |
|
| | MCP Server | Skill |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **How it works** | Sandbox Agent spawns your MCP server process and routes tool calls to it via stdio | A markdown file that instructs the agent to run your script with `node` (or any command) |
|
| **How it works** | Agent connects to an MCP server (`mcpServers`) | Agent follows `SKILL.md` instructions and runs scripts |
|
||||||
| **Tool discovery** | Agent sees tools automatically via MCP protocol | Agent reads instructions from the skill file |
|
| **Best for** | Typed tool calls and structured protocols | Lightweight task-specific guidance |
|
||||||
| **Best for** | Structured tools with typed inputs/outputs | Lightweight scripts with natural-language instructions |
|
| **Requires** | MCP server process (stdio/http/sse) | Script + `SKILL.md` |
|
||||||
| **Requires** | `@modelcontextprotocol/sdk` dependency | Just a markdown file and a script |
|
|
||||||
|
|
||||||
Both approaches execute code inside the sandbox, so your tools have full access to the sandbox filesystem, network, and installed system tools.
|
## Option A: MCP server (stdio)
|
||||||
|
|
||||||
## Option A: Tools via MCP
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Write your MCP server">
|
<Step title="Write and bundle your MCP server">
|
||||||
Create an MCP server that exposes tools using `@modelcontextprotocol/sdk` with `StdioServerTransport`. This server will run inside the sandbox.
|
|
||||||
|
|
||||||
```ts src/mcp-server.ts
|
```ts src/mcp-server.ts
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({ name: "rand", version: "1.0.0" });
|
||||||
name: "rand",
|
|
||||||
version: "1.0.0",
|
|
||||||
});
|
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"random_number",
|
"random_number",
|
||||||
"Generate a random integer between min and max (inclusive)",
|
"Generate a random integer between min and max",
|
||||||
{
|
{
|
||||||
min: z.number().describe("Minimum value"),
|
min: z.number(),
|
||||||
max: z.number().describe("Maximum value"),
|
max: z.number(),
|
||||||
},
|
},
|
||||||
async ({ min, max }) => ({
|
async ({ min, max }) => ({
|
||||||
content: [{ type: "text", text: String(Math.floor(Math.random() * (max - min + 1)) + min) }],
|
content: [{ type: "text", text: String(Math.floor(Math.random() * (max - min + 1)) + min) }],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
await server.connect(new StdioServerTransport());
|
||||||
await server.connect(transport);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This is a simple example. Your MCP server runs inside the sandbox, so you can execute any code you'd like: query databases, call internal APIs, run shell commands, or interact with any service available in the container.
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Package the MCP server">
|
|
||||||
Bundle into a single JS file so it can be uploaded and executed without a `node_modules` folder.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx esbuild src/mcp-server.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/mcp-server.cjs
|
npx esbuild src/mcp-server.ts --bundle --format=cjs --platform=node --target=node18 --outfile=dist/mcp-server.cjs
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `dist/mcp-server.cjs` ready to upload.
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Create sandbox and upload MCP server">
|
<Step title="Upload it into the sandbox">
|
||||||
Start your sandbox, then write the bundled file into it.
|
|
||||||
|
|
||||||
<CodeGroup>
|
```ts
|
||||||
```ts TypeScript
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468" });
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
|
||||||
|
|
||||||
const content = await fs.promises.readFile("./dist/mcp-server.cjs");
|
const content = await fs.promises.readFile("./dist/mcp-server.cjs");
|
||||||
await client.writeFsFile(
|
|
||||||
{ path: "/opt/mcp/custom-tools/mcp-server.cjs" },
|
await sdk.writeFsFile({ path: "/opt/mcp/custom-tools/mcp-server.cjs" }, content);
|
||||||
content,
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
```bash
|
||||||
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/mcp/custom-tools/mcp-server.cjs" \
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/mcp/custom-tools/mcp-server.cjs" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
--data-binary @./dist/mcp-server.cjs
|
--data-binary @./dist/mcp-server.cjs
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Create a session">
|
<Step title="Register MCP config and create a session">
|
||||||
Point an MCP server config at the bundled JS file. When the session starts, Sandbox Agent spawns the MCP server process and routes tool calls to it.
|
|
||||||
|
|
||||||
<CodeGroup>
|
```ts
|
||||||
```ts TypeScript
|
await sdk.setMcpConfig(
|
||||||
await client.createSession("custom-tools", {
|
{
|
||||||
agent: "claude",
|
directory: "/workspace",
|
||||||
mcp: {
|
mcpName: "customTools",
|
||||||
customTools: {
|
|
||||||
type: "local",
|
|
||||||
command: ["node", "/opt/mcp/custom-tools/mcp-server.cjs"],
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "local",
|
||||||
|
command: "node",
|
||||||
|
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const session = await sdk.createSession({
|
||||||
|
agent: "claude",
|
||||||
|
sessionInit: {
|
||||||
|
cwd: "/workspace",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
```bash cURL
|
await session.prompt([
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/custom-tools" \
|
{ type: "text", text: "Use the random_number tool with min=1 and max=10." },
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
]);
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"agent": "claude",
|
|
||||||
"mcp": {
|
|
||||||
"customTools": {
|
|
||||||
"type": "local",
|
|
||||||
"command": ["node", "/opt/mcp/custom-tools/mcp-server.cjs"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Option B: Tools via Skills
|
## Option B: Skills
|
||||||
|
|
||||||
Skills are markdown files that instruct the agent how to use a script. Upload the script and a skill file, then point the session at the skill directory.
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Write your script">
|
<Step title="Write script + skill file">
|
||||||
Write a script that the agent will execute. This runs inside the sandbox just like an MCP server, but the agent invokes it directly via its shell tool.
|
|
||||||
|
|
||||||
```ts src/random-number.ts
|
```ts src/random-number.ts
|
||||||
const min = Number(process.argv[2]);
|
const min = Number(process.argv[2]);
|
||||||
|
|
@ -143,105 +108,56 @@ Skills are markdown files that instruct the agent how to use a script. Upload th
|
||||||
|
|
||||||
console.log(Math.floor(Math.random() * (max - min + 1)) + min);
|
console.log(Math.floor(Math.random() * (max - min + 1)) + min);
|
||||||
```
|
```
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Write a skill file">
|
````md SKILL.md
|
||||||
Create a `SKILL.md` that tells the agent what the script does and how to run it. The frontmatter `name` and `description` fields are required. See [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) for tips on writing effective skills.
|
|
||||||
|
|
||||||
```md SKILL.md
|
|
||||||
---
|
---
|
||||||
name: random-number
|
name: random-number
|
||||||
description: Generate a random integer between min and max (inclusive). Use when the user asks for a random number.
|
description: Generate a random integer between min and max.
|
||||||
---
|
---
|
||||||
|
|
||||||
To generate a random number, run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node /opt/skills/random-number/random-number.cjs <min> <max>
|
node /opt/skills/random-number/random-number.cjs <min> <max>
|
||||||
```
|
```
|
||||||
|
````
|
||||||
This prints a single random integer between min and max (inclusive).
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Package the script">
|
|
||||||
Bundle the script just like an MCP server so it has no dependencies at runtime.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx esbuild src/random-number.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/random-number.cjs
|
npx esbuild src/random-number.ts --bundle --format=cjs --platform=node --target=node18 --outfile=dist/random-number.cjs
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Create sandbox and upload files">
|
<Step title="Upload files">
|
||||||
Upload both the bundled script and the skill file.
|
|
||||||
|
|
||||||
<CodeGroup>
|
```ts
|
||||||
```ts TypeScript
|
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
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");
|
const script = await fs.promises.readFile("./dist/random-number.cjs");
|
||||||
await client.writeFsFile(
|
await sdk.writeFsFile({ path: "/opt/skills/random-number/random-number.cjs" }, script);
|
||||||
{ path: "/opt/skills/random-number/random-number.cjs" },
|
|
||||||
script,
|
|
||||||
);
|
|
||||||
|
|
||||||
const skill = await fs.promises.readFile("./SKILL.md");
|
const skill = await fs.promises.readFile("./SKILL.md");
|
||||||
await client.writeFsFile(
|
await sdk.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skill);
|
||||||
{ path: "/opt/skills/random-number/SKILL.md" },
|
|
||||||
skill,
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/skills/random-number/random-number.cjs" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
--data-binary @./dist/random-number.cjs
|
|
||||||
|
|
||||||
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/skills/random-number/SKILL.md" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
--data-binary @./SKILL.md
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Create a session">
|
<Step title="Use in a session">
|
||||||
Point the session at the skill directory. The agent reads `SKILL.md` and learns how to use your script.
|
|
||||||
|
|
||||||
<CodeGroup>
|
```ts
|
||||||
```ts TypeScript
|
const session = await sdk.createSession({
|
||||||
await client.createSession("custom-tools", {
|
|
||||||
agent: "claude",
|
agent: "claude",
|
||||||
skills: {
|
sessionInit: {
|
||||||
sources: [
|
cwd: "/workspace",
|
||||||
{ type: "local", source: "/opt/skills/random-number" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
```bash cURL
|
await session.prompt([
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/custom-tools" \
|
{ type: "text", text: "Use the random-number skill to pick a number from 1 to 100." },
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
]);
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"agent": "claude",
|
|
||||||
"skills": {
|
|
||||||
"sources": [
|
|
||||||
{ "type": "local", "source": "/opt/skills/random-number" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The sandbox image must include a Node.js runtime that can execute the bundled files.
|
- The sandbox runtime must include Node.js (or your chosen runtime).
|
||||||
|
- For persistent skill-source wiring by directory, see [Skills](/skills-config).
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,69 @@
|
||||||
---
|
---
|
||||||
title: "Daemon"
|
title: "Daemon"
|
||||||
description: "Background daemon lifecycle, auto-upgrade, and management."
|
description: "Background daemon lifecycle and management."
|
||||||
icon: "microchip"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
The sandbox-agent daemon is a background server process that stays running between sessions. Commands like `sandbox-agent opencode` and `gigacode` automatically start it when needed and restart it when the binary is updated.
|
The sandbox-agent daemon is a background server process. Commands like `sandbox-agent opencode` and `gigacode` can ensure it is running.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. When you run `sandbox-agent opencode`, `sandbox-agent daemon start`, or `gigacode`, the CLI checks if a daemon is already healthy at the configured host and port.
|
1. A daemon-aware command checks for a healthy daemon at host/port.
|
||||||
2. If no daemon is running, one is spawned in the background with stdout/stderr redirected to a log file.
|
2. If missing, it starts one in the background and records PID/version files.
|
||||||
3. The CLI writes a PID file and a build ID file to track the running process and its version.
|
3. Subsequent checks can compare build/version and restart when required.
|
||||||
4. On subsequent invocations, if the daemon is still running but was built from a different commit, the CLI automatically stops the old daemon and starts a new one.
|
|
||||||
|
|
||||||
## Auto-upgrade
|
## Auto-upgrade behavior
|
||||||
|
|
||||||
Each build of sandbox-agent embeds a unique **build ID** (the git short hash, or a version-timestamp fallback). When a daemon is started, this build ID is written to a version file alongside the PID file.
|
- `sandbox-agent opencode` and `gigacode` use ensure-running behavior with upgrade checks.
|
||||||
|
- `sandbox-agent daemon start` uses direct start by default.
|
||||||
On every invocation of `ensure_running` (called by `opencode`, `gigacode`, and `daemon start`), the CLI compares the stored build ID against the current binary's build ID. If they differ, the running daemon is stopped and replaced automatically:
|
- `sandbox-agent daemon start --upgrade` uses ensure-running behavior (including version check/restart).
|
||||||
|
|
||||||
```
|
|
||||||
daemon outdated (build a1b2c3d -> f4e5d6c), restarting...
|
|
||||||
```
|
|
||||||
|
|
||||||
This means installing a new version of sandbox-agent and running any daemon-aware command is enough to upgrade — no manual restart needed.
|
|
||||||
|
|
||||||
## Managing the daemon
|
## Managing the daemon
|
||||||
|
|
||||||
### Start
|
### Start
|
||||||
|
|
||||||
Start a daemon in the background. If one is already running and healthy, this is a no-op.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent daemon start [OPTIONS]
|
sandbox-agent daemon start [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `-H, --host <HOST>` | `127.0.0.1` | Host to bind to |
|
| `-H, --host <HOST>` | `127.0.0.1` | Host |
|
||||||
| `-p, --port <PORT>` | `2468` | Port to bind to |
|
| `-p, --port <PORT>` | `2468` | Port |
|
||||||
| `-t, --token <TOKEN>` | - | Authentication token |
|
| `--upgrade` | false | Use ensure-running + upgrade behavior |
|
||||||
| `-n, --no-token` | - | Disable authentication |
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent daemon start --no-token
|
sandbox-agent daemon start
|
||||||
|
sandbox-agent daemon start --upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stop
|
### Stop
|
||||||
|
|
||||||
Stop a running daemon. Sends SIGTERM and waits up to 5 seconds for a graceful shutdown before falling back to SIGKILL.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent daemon stop [OPTIONS]
|
sandbox-agent daemon stop [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `-H, --host <HOST>` | `127.0.0.1` | Host of the daemon |
|
| `-H, --host <HOST>` | `127.0.0.1` | Host |
|
||||||
| `-p, --port <PORT>` | `2468` | Port of the daemon |
|
| `-p, --port <PORT>` | `2468` | Port |
|
||||||
|
|
||||||
```bash
|
|
||||||
sandbox-agent daemon stop
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
Show whether the daemon is running, its PID, build ID, and log path.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent daemon status [OPTIONS]
|
sandbox-agent daemon status [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `-H, --host <HOST>` | `127.0.0.1` | Host of the daemon |
|
| `-H, --host <HOST>` | `127.0.0.1` | Host |
|
||||||
| `-p, --port <PORT>` | `2468` | Port of the daemon |
|
| `-p, --port <PORT>` | `2468` | Port |
|
||||||
|
|
||||||
```bash
|
|
||||||
sandbox-agent daemon status
|
|
||||||
# Daemon running (PID 12345, build a1b2c3d, logs: ~/.local/share/sandbox-agent/daemon/daemon-127-0-0-1-2468.log)
|
|
||||||
```
|
|
||||||
|
|
||||||
If the daemon was started with an older binary, the status includes an `[outdated, restart recommended]` notice.
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
All daemon state files live under the sandbox-agent data directory (typically `~/.local/share/sandbox-agent/daemon/`):
|
Daemon state is stored under the sandbox-agent data directory (for example `~/.local/share/sandbox-agent/daemon/`):
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `daemon-{host}-{port}.pid` | PID of the running daemon process |
|
| `daemon-{host}-{port}.pid` | PID of running daemon |
|
||||||
| `daemon-{host}-{port}.version` | Build ID of the running daemon |
|
| `daemon-{host}-{port}.version` | Build/version marker |
|
||||||
| `daemon-{host}-{port}.log` | Daemon stdout/stderr log output |
|
| `daemon-{host}-{port}.log` | Daemon stdout/stderr log |
|
||||||
|
|
||||||
Multiple daemons can run on different host/port combinations without conflicting.
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
---
|
---
|
||||||
title: "Cloudflare"
|
title: "Cloudflare"
|
||||||
description: "Deploy the daemon inside a Cloudflare Sandbox."
|
description: "Deploy Sandbox Agent inside a Cloudflare Sandbox."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Cloudflare account with Workers Paid plan
|
- Cloudflare account with Workers paid plan
|
||||||
- Docker running locally for `wrangler dev`
|
- Docker for local `wrangler dev`
|
||||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Cloudflare Sandbox SDK is in beta. See [Sandbox SDK docs](https://developers.cloudflare.com/sandbox/) for details.
|
Cloudflare Sandbox SDK is beta. See [Sandbox SDK docs](https://developers.cloudflare.com/sandbox/).
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Quick Start
|
## Quick start
|
||||||
|
|
||||||
Create a new Sandbox SDK project:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
|
npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
|
||||||
|
|
@ -24,64 +22,16 @@ cd my-sandbox
|
||||||
|
|
||||||
## Dockerfile
|
## Dockerfile
|
||||||
|
|
||||||
Create a `Dockerfile` with sandbox-agent and agents pre-installed:
|
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM cloudflare/sandbox:0.7.0
|
FROM cloudflare/sandbox:0.7.0
|
||||||
|
|
||||||
# Install sandbox-agent
|
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh
|
||||||
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex
|
||||||
|
|
||||||
# Pre-install agents
|
|
||||||
RUN sandbox-agent install-agent claude && \
|
|
||||||
sandbox-agent install-agent codex
|
|
||||||
|
|
||||||
# Required for local development with wrangler dev
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
## TypeScript proxy example
|
||||||
The `EXPOSE 8000` directive is required for `wrangler dev` to proxy requests to the container. Port 3000 is reserved for the Cloudflare control plane.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Wrangler Configuration
|
|
||||||
|
|
||||||
Update `wrangler.jsonc` to use your Dockerfile:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"name": "my-sandbox-agent",
|
|
||||||
"main": "src/index.ts",
|
|
||||||
"compatibility_date": "2025-01-01",
|
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
|
||||||
"containers": [
|
|
||||||
{
|
|
||||||
"class_name": "Sandbox",
|
|
||||||
"image": "./Dockerfile",
|
|
||||||
"instance_type": "lite",
|
|
||||||
"max_instances": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"durable_objects": {
|
|
||||||
"bindings": [
|
|
||||||
{
|
|
||||||
"class_name": "Sandbox",
|
|
||||||
"name": "Sandbox"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"migrations": [
|
|
||||||
{
|
|
||||||
"new_sqlite_classes": ["Sandbox"],
|
|
||||||
"tag": "v1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## TypeScript Example
|
|
||||||
|
|
||||||
This example proxies requests to sandbox-agent via `containerFetch`, which works reliably in both local development and production:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
|
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
|
||||||
|
|
@ -95,158 +45,87 @@ type Env = {
|
||||||
|
|
||||||
const PORT = 8000;
|
const PORT = 8000;
|
||||||
|
|
||||||
/** Check if sandbox-agent is already running */
|
|
||||||
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
|
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v2/health`);
|
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
|
||||||
return result.success;
|
return result.success;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure sandbox-agent is running in the container */
|
|
||||||
async function ensureRunning(sandbox: Sandbox, env: Env): Promise<void> {
|
async function ensureRunning(sandbox: Sandbox, env: Env): Promise<void> {
|
||||||
if (await isServerRunning(sandbox)) return;
|
if (await isServerRunning(sandbox)) return;
|
||||||
|
|
||||||
// Set environment variables for agents
|
|
||||||
const envVars: Record<string, string> = {};
|
const envVars: Record<string, string> = {};
|
||||||
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
|
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
|
||||||
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
|
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
|
||||||
await sandbox.setEnvVars(envVars);
|
await sandbox.setEnvVars(envVars);
|
||||||
|
|
||||||
// Start sandbox-agent server
|
await sandbox.startProcess(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`);
|
||||||
await sandbox.startProcess(
|
|
||||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Poll health endpoint until server is ready
|
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
if (await isServerRunning(sandbox)) return;
|
if (await isServerRunning(sandbox)) return;
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
await new Promise((r) => setTimeout(r, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("sandbox-agent failed to start");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request: Request, env: Env): Promise<Response> {
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
// Proxy requests: /sandbox/:name/v2/...
|
|
||||||
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
|
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
|
||||||
if (match) {
|
|
||||||
const [, name, path = "/"] = match;
|
|
||||||
const sandbox = getSandbox(env.Sandbox, name);
|
|
||||||
|
|
||||||
await ensureRunning(sandbox, env);
|
if (!match) {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
// Proxy request to container
|
|
||||||
return sandbox.containerFetch(
|
|
||||||
new Request(`http://localhost${path}${url.search}`, request),
|
|
||||||
PORT
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response("Not found", { status: 404 });
|
const [, name, path = "/"] = match;
|
||||||
|
const sandbox = getSandbox(env.Sandbox, name);
|
||||||
|
await ensureRunning(sandbox, env);
|
||||||
|
|
||||||
|
return sandbox.containerFetch(
|
||||||
|
new Request(`http://localhost${path}${url.search}`, request),
|
||||||
|
PORT,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connect from Client
|
## Connect from a client
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
// Connect via the proxy endpoint
|
const sdk = await SandboxAgent.connect({
|
||||||
const client = new SandboxAgentClient({
|
|
||||||
baseUrl: "http://localhost:8787/sandbox/my-sandbox",
|
baseUrl: "http://localhost:8787/sandbox/my-sandbox",
|
||||||
agent: "mock",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for server to be ready
|
const session = await sdk.createSession({ agent: "claude" });
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
try {
|
|
||||||
await client.getHealth();
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a session and start coding
|
const off = session.onEvent((event) => {
|
||||||
await client.createSession("my-session", { agent: "claude" });
|
console.log(event.sender, event.payload);
|
||||||
|
|
||||||
await client.postMessage("my-session", {
|
|
||||||
message: "Summarize this repository",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const event of client.streamEvents("my-session")) {
|
await session.prompt([{ type: "text", text: "Summarize this repository" }]);
|
||||||
// Auto-approve permissions
|
off();
|
||||||
if (event.type === "permission.requested") {
|
|
||||||
await client.replyPermission("my-session", event.data.permission_id, {
|
|
||||||
reply: "once",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle text output
|
|
||||||
if (event.type === "item.delta" && event.data?.delta) {
|
|
||||||
process.stdout.write(event.data.delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Local development
|
||||||
|
|
||||||
Use `.dev.vars` for local development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "ANTHROPIC_API_KEY=your-api-key" > .dev.vars
|
|
||||||
```
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
Use plain `KEY=value` format in `.dev.vars`. Do not use `export KEY=value` - wrangler won't parse the bash syntax.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
For production, set secrets via wrangler:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wrangler secret put ANTHROPIC_API_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
Start the development server:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
Test health:
|
||||||
First run builds the Docker container (2-3 minutes). Subsequent runs are much faster.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
Test with curl:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8787/sandbox/demo/v2/health
|
curl http://localhost:8787/sandbox/demo/v1/health
|
||||||
```
|
```
|
||||||
|
|
||||||
<Tip>
|
## Production deployment
|
||||||
Containers cache environment variables. If you change `.dev.vars`, either use a new sandbox name or clear existing containers:
|
|
||||||
```bash
|
|
||||||
docker ps -a | grep sandbox | awk '{print $1}' | xargs -r docker rm -f
|
|
||||||
```
|
|
||||||
</Tip>
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
Deploy to Cloudflare:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wrangler deploy
|
wrangler deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
For production with preview URLs (direct container access), you'll need a custom domain with wildcard DNS routing. See [Cloudflare Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) for setup instructions.
|
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,52 @@
|
||||||
---
|
---
|
||||||
title: "Daytona"
|
title: "Daytona"
|
||||||
description: "Run the daemon in a Daytona workspace."
|
description: "Run Sandbox Agent in a Daytona workspace."
|
||||||
---
|
---
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
Daytona Tier 3+ is required to access api.anthropic.com and api.openai.com. Tier 1/2 sandboxes have restricted network access that will cause agent failures. See [Daytona network limits](https://www.daytona.io/docs/en/network-limits/) for details.
|
Daytona Tier 3+ is required for access to common model provider endpoints.
|
||||||
|
See [Daytona network limits](https://www.daytona.io/docs/en/network-limits/).
|
||||||
</Warning>
|
</Warning>
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `DAYTONA_API_KEY` environment variable
|
- `DAYTONA_API_KEY`
|
||||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
|
||||||
|
|
||||||
## TypeScript Example
|
## TypeScript example
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Daytona } from "@daytonaio/sdk";
|
import { Daytona } from "@daytonaio/sdk";
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
|
|
||||||
// Pass API keys to the sandbox
|
|
||||||
const envVars: Record<string, string> = {};
|
const envVars: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
|
|
||||||
const sandbox = await daytona.create({ envVars });
|
const sandbox = await daytona.create({ envVars });
|
||||||
|
|
||||||
// Install sandbox-agent
|
|
||||||
await sandbox.process.executeCommand(
|
await sandbox.process.executeCommand(
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"
|
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start the server in the background
|
|
||||||
await sandbox.process.executeCommand(
|
await sandbox.process.executeCommand(
|
||||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"
|
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for server to be ready
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
await new Promise((r) => setTimeout(r, 2000));
|
||||||
|
|
||||||
// Get the public URL
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||||
|
const sdk = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
// Connect and use the SDK
|
const session = await sdk.createSession({ agent: "claude" });
|
||||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
await session.prompt([{ type: "text", text: "Summarize this repository" }]);
|
||||||
|
|
||||||
await client.createSession("my-session", {
|
|
||||||
agent: "claude",
|
|
||||||
permissionMode: "default",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup when done
|
|
||||||
await sandbox.delete();
|
await sandbox.delete();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Snapshots for Faster Startup
|
## Using snapshots for faster startup
|
||||||
|
|
||||||
For production, use snapshots with pre-installed binaries:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Daytona, Image } from "@daytonaio/sdk";
|
import { Daytona, Image } from "@daytonaio/sdk";
|
||||||
|
|
@ -65,7 +54,6 @@ import { Daytona, Image } from "@daytonaio/sdk";
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
const SNAPSHOT = "sandbox-agent-ready";
|
const SNAPSHOT = "sandbox-agent-ready";
|
||||||
|
|
||||||
// Create snapshot once (takes 2-3 minutes)
|
|
||||||
const hasSnapshot = await daytona.snapshot.get(SNAPSHOT).then(() => true, () => false);
|
const hasSnapshot = await daytona.snapshot.get(SNAPSHOT).then(() => true, () => false);
|
||||||
|
|
||||||
if (!hasSnapshot) {
|
if (!hasSnapshot) {
|
||||||
|
|
@ -73,18 +61,10 @@ if (!hasSnapshot) {
|
||||||
name: SNAPSHOT,
|
name: SNAPSHOT,
|
||||||
image: Image.base("ubuntu:22.04").runCommands(
|
image: Image.base("ubuntu:22.04").runCommands(
|
||||||
"apt-get update && apt-get install -y curl ca-certificates",
|
"apt-get update && apt-get install -y curl ca-certificates",
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh",
|
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh",
|
||||||
"sandbox-agent install-agent claude",
|
"sandbox-agent install-agent claude",
|
||||||
"sandbox-agent install-agent codex",
|
"sandbox-agent install-agent codex",
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now sandboxes start instantly
|
|
||||||
const sandbox = await daytona.create({
|
|
||||||
snapshot: SNAPSHOT,
|
|
||||||
envVars,
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Daytona Snapshots](https://daytona.io/docs/snapshots) for details.
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
---
|
---
|
||||||
title: "Docker"
|
title: "Docker"
|
||||||
description: "Build and run the daemon in a Docker container."
|
description: "Build and run Sandbox Agent in a Docker container."
|
||||||
---
|
---
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
Docker is not recommended for production. Standard Docker containers don't provide sufficient isolation for running untrusted code. Use a dedicated sandbox provider like E2B or Daytona for production workloads.
|
Docker is not recommended for production isolation of untrusted workloads. Use dedicated sandbox providers (E2B, Daytona, etc.) for stronger isolation.
|
||||||
</Warning>
|
</Warning>
|
||||||
|
|
||||||
## Quick Start
|
## Quick start
|
||||||
|
|
||||||
Run sandbox-agent in a container with agents pre-installed:
|
Run Sandbox Agent with agents pre-installed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -p 3000:3000 \
|
docker run --rm -p 3000:3000 \
|
||||||
|
|
@ -17,23 +17,21 @@ docker run --rm -p 3000:3000 \
|
||||||
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
|
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
|
||||||
alpine:latest sh -c "\
|
alpine:latest sh -c "\
|
||||||
apk add --no-cache curl ca-certificates libstdc++ libgcc bash && \
|
apk add --no-cache curl ca-certificates libstdc++ libgcc bash && \
|
||||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh && \
|
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh && \
|
||||||
sandbox-agent install-agent claude && \
|
sandbox-agent install-agent claude && \
|
||||||
sandbox-agent install-agent codex && \
|
sandbox-agent install-agent codex && \
|
||||||
sandbox-agent server --no-token --host 0.0.0.0 --port 3000"
|
sandbox-agent server --no-token --host 0.0.0.0 --port 3000"
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Alpine is required because Claude Code is built for musl libc. Debian/Ubuntu images use glibc and won't work.
|
Alpine is required for some agent binaries that target musl libc.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
Access the API at `http://localhost:3000`.
|
|
||||||
|
|
||||||
## TypeScript with dockerode
|
## TypeScript with dockerode
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const docker = new Docker();
|
const docker = new Docker();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
|
@ -42,7 +40,7 @@ const container = await docker.createContainer({
|
||||||
Image: "alpine:latest",
|
Image: "alpine:latest",
|
||||||
Cmd: ["sh", "-c", [
|
Cmd: ["sh", "-c", [
|
||||||
"apk add --no-cache curl ca-certificates libstdc++ libgcc bash",
|
"apk add --no-cache curl ca-certificates libstdc++ libgcc bash",
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh",
|
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh",
|
||||||
"sandbox-agent install-agent claude",
|
"sandbox-agent install-agent claude",
|
||||||
"sandbox-agent install-agent codex",
|
"sandbox-agent install-agent codex",
|
||||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
||||||
|
|
@ -60,24 +58,18 @@ const container = await docker.createContainer({
|
||||||
|
|
||||||
await container.start();
|
await container.start();
|
||||||
|
|
||||||
// Wait for server and connect
|
|
||||||
const baseUrl = `http://127.0.0.1:${PORT}`;
|
const baseUrl = `http://127.0.0.1:${PORT}`;
|
||||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
const sdk = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
// Use the client...
|
const session = await sdk.createSession({ agent: "claude" });
|
||||||
await client.createSession("my-session", {
|
await session.prompt([{ type: "text", text: "Summarize this repository." }]);
|
||||||
agent: "claude",
|
|
||||||
permissionMode: "default",
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building from Source
|
## Building from source
|
||||||
|
|
||||||
To build a static binary for use in minimal containers:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f docker/release/linux-x86_64.Dockerfile -t sandbox-agent-build .
|
docker build -f docker/release/linux-x86_64.Dockerfile -t sandbox-agent-build .
|
||||||
docker run --rm -v "$PWD/artifacts:/artifacts" sandbox-agent-build
|
docker run --rm -v "$PWD/artifacts:/artifacts" sandbox-agent-build
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary will be at `./artifacts/sandbox-agent-x86_64-unknown-linux-musl`.
|
Binary output: `./artifacts/sandbox-agent-x86_64-unknown-linux-musl`.
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,52 @@
|
||||||
---
|
---
|
||||||
title: "E2B"
|
title: "E2B"
|
||||||
description: "Deploy the daemon inside an E2B sandbox."
|
description: "Deploy Sandbox Agent inside an E2B sandbox."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `E2B_API_KEY` environment variable
|
- `E2B_API_KEY`
|
||||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
|
||||||
|
|
||||||
## TypeScript Example
|
## TypeScript example
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Sandbox } from "@e2b/code-interpreter";
|
import { Sandbox } from "@e2b/code-interpreter";
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
// Pass API keys to the sandbox
|
|
||||||
const envs: Record<string, string> = {};
|
const envs: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
|
|
||||||
const sandbox = await Sandbox.create({ allowInternetAccess: true, envs });
|
const sandbox = await Sandbox.create({ allowInternetAccess: true, envs });
|
||||||
|
|
||||||
// Install sandbox-agent
|
|
||||||
await sandbox.commands.run(
|
await sandbox.commands.run(
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"
|
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Install agents before starting the server
|
|
||||||
await sandbox.commands.run("sandbox-agent install-agent claude");
|
await sandbox.commands.run("sandbox-agent install-agent claude");
|
||||||
await sandbox.commands.run("sandbox-agent install-agent codex");
|
await sandbox.commands.run("sandbox-agent install-agent codex");
|
||||||
|
|
||||||
// Start the server in the background
|
|
||||||
await sandbox.commands.run(
|
await sandbox.commands.run(
|
||||||
"sandbox-agent server --no-token --host 0.0.0.0 --port 3000",
|
"sandbox-agent server --no-token --host 0.0.0.0 --port 3000",
|
||||||
{ background: true }
|
{ background: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect to the server
|
|
||||||
const baseUrl = `https://${sandbox.getHost(3000)}`;
|
const baseUrl = `https://${sandbox.getHost(3000)}`;
|
||||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
const sdk = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
// Wait for server to be ready
|
const session = await sdk.createSession({ agent: "claude" });
|
||||||
for (let i = 0; i < 30; i++) {
|
const off = session.onEvent((event) => {
|
||||||
try {
|
console.log(event.sender, event.payload);
|
||||||
await client.getHealth();
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a session and start coding
|
|
||||||
await client.createSession("my-session", {
|
|
||||||
agent: "claude",
|
|
||||||
permissionMode: "default",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.postMessage("my-session", {
|
await session.prompt([{ type: "text", text: "Summarize this repository" }]);
|
||||||
message: "Summarize this repository",
|
off();
|
||||||
});
|
|
||||||
|
|
||||||
for await (const event of client.streamEvents("my-session")) {
|
|
||||||
console.log(event.type, event.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await sandbox.kill();
|
await sandbox.kill();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Faster Cold Starts
|
## Faster cold starts
|
||||||
|
|
||||||
For faster startup, create a custom E2B template with sandbox-agent and agents pre-installed:
|
For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed.
|
||||||
|
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template).
|
||||||
1. Create a template with the install script baked in
|
|
||||||
2. Pre-install agents: `sandbox-agent install-agent claude codex`
|
|
||||||
3. Use the template ID when creating sandboxes
|
|
||||||
|
|
||||||
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template) for details.
|
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,53 @@
|
||||||
---
|
---
|
||||||
title: "Local"
|
title: "Local"
|
||||||
description: "Run the daemon locally for development."
|
description: "Run Sandbox Agent locally for development."
|
||||||
---
|
---
|
||||||
|
|
||||||
For local development, you can run the daemon directly on your machine.
|
For local development, run Sandbox Agent directly on your machine.
|
||||||
|
|
||||||
## With the CLI
|
## With the CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install
|
# Install
|
||||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with npm or Bun:
|
Or with npm/Bun:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="npx">
|
<Tab title="npx">
|
||||||
```bash
|
```bash
|
||||||
npx sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
npx @sandbox-agent/cli@0.2.x server --no-token --host 127.0.0.1 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="bunx">
|
<Tab title="bunx">
|
||||||
```bash
|
```bash
|
||||||
bunx sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
bunx @sandbox-agent/cli@0.2.x server --no-token --host 127.0.0.1 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## With the TypeScript SDK
|
## With the TypeScript SDK
|
||||||
|
|
||||||
The SDK can automatically spawn and manage the server as a subprocess:
|
The SDK can spawn and manage the server as a subprocess:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
// Spawns sandbox-agent server as a subprocess
|
const sdk = await SandboxAgent.start();
|
||||||
const client = await SandboxAgent.start();
|
|
||||||
|
|
||||||
await client.createSession("my-session", {
|
const session = await sdk.createSession({
|
||||||
agent: "claude",
|
agent: "claude",
|
||||||
permissionMode: "default",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// When done
|
await session.prompt([
|
||||||
await client.dispose();
|
{ type: "text", text: "Summarize this repository." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sdk.dispose();
|
||||||
```
|
```
|
||||||
|
|
||||||
This installs the binary (if needed) and starts the server on a random available port. No manual setup required.
|
This starts the server on an available local port and connects automatically.
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,39 @@
|
||||||
---
|
---
|
||||||
title: "Vercel"
|
title: "Vercel"
|
||||||
description: "Deploy the daemon inside a Vercel Sandbox."
|
description: "Deploy Sandbox Agent inside a Vercel Sandbox."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `VERCEL_OIDC_TOKEN` or `VERCEL_ACCESS_TOKEN` environment variable
|
- `VERCEL_OIDC_TOKEN` or `VERCEL_ACCESS_TOKEN`
|
||||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
|
||||||
|
|
||||||
## TypeScript Example
|
## TypeScript example
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Sandbox } from "@vercel/sandbox";
|
import { Sandbox } from "@vercel/sandbox";
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
// Pass API keys to the sandbox
|
|
||||||
const envs: Record<string, string> = {};
|
const envs: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
|
|
||||||
// Create sandbox with port 3000 exposed
|
|
||||||
const sandbox = await Sandbox.create({
|
const sandbox = await Sandbox.create({
|
||||||
runtime: "node24",
|
runtime: "node24",
|
||||||
ports: [3000],
|
ports: [3000],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to run commands
|
|
||||||
const run = async (cmd: string, args: string[] = []) => {
|
const run = async (cmd: string, args: string[] = []) => {
|
||||||
const result = await sandbox.runCommand({ cmd, args, env: envs });
|
const result = await sandbox.runCommand({ cmd, args, env: envs });
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
|
throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Install sandbox-agent
|
await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh"]);
|
||||||
await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"]);
|
|
||||||
|
|
||||||
// Install agents before starting the server
|
|
||||||
await run("sandbox-agent", ["install-agent", "claude"]);
|
await run("sandbox-agent", ["install-agent", "claude"]);
|
||||||
await run("sandbox-agent", ["install-agent", "codex"]);
|
await run("sandbox-agent", ["install-agent", "codex"]);
|
||||||
|
|
||||||
// Start the server in the background
|
|
||||||
await sandbox.runCommand({
|
await sandbox.runCommand({
|
||||||
cmd: "sandbox-agent",
|
cmd: "sandbox-agent",
|
||||||
args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"],
|
args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"],
|
||||||
|
|
@ -49,43 +41,22 @@ await sandbox.runCommand({
|
||||||
detached: true,
|
detached: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to the server
|
|
||||||
const baseUrl = sandbox.domain(3000);
|
const baseUrl = sandbox.domain(3000);
|
||||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
const sdk = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
// Wait for server to be ready
|
const session = await sdk.createSession({ agent: "claude" });
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
try {
|
|
||||||
await client.getHealth();
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a session and start coding
|
const off = session.onEvent((event) => {
|
||||||
await client.createSession("my-session", {
|
console.log(event.sender, event.payload);
|
||||||
agent: "claude",
|
|
||||||
permissionMode: "default",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.postMessage("my-session", {
|
await session.prompt([{ type: "text", text: "Summarize this repository" }]);
|
||||||
message: "Summarize this repository",
|
off();
|
||||||
});
|
|
||||||
|
|
||||||
for await (const event of client.streamEvents("my-session")) {
|
|
||||||
console.log(event.type, event.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await sandbox.stop();
|
await sandbox.stop();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
Vercel Sandboxes support two authentication methods:
|
Vercel Sandboxes support OIDC token auth (recommended) and access-token auth.
|
||||||
|
See [Vercel Sandbox docs](https://vercel.com/docs/functions/sandbox).
|
||||||
- **OIDC Token**: Set `VERCEL_OIDC_TOKEN` (recommended for CI/CD)
|
|
||||||
- **Access Token**: Set `VERCEL_ACCESS_TOKEN` (for local development, run `vercel env pull`)
|
|
||||||
|
|
||||||
See [Vercel Sandbox docs](https://vercel.com/docs/functions/sandbox) for details.
|
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@
|
||||||
"group": "Getting started",
|
"group": "Getting started",
|
||||||
"pages": [
|
"pages": [
|
||||||
"quickstart",
|
"quickstart",
|
||||||
"building-chat-ui",
|
"sdk-overview",
|
||||||
"manage-sessions",
|
|
||||||
{
|
{
|
||||||
"group": "Deploy",
|
"group": "Deploy",
|
||||||
"icon": "server",
|
"icon": "server",
|
||||||
|
|
@ -68,11 +67,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "SDKs",
|
"group": "Agent",
|
||||||
"pages": ["sdks/typescript", "sdks/python"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "Agent Features",
|
|
||||||
"pages": [
|
"pages": [
|
||||||
"agent-sessions",
|
"agent-sessions",
|
||||||
"attachments",
|
"attachments",
|
||||||
|
|
@ -82,19 +77,24 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Features",
|
"group": "System",
|
||||||
"pages": ["file-system"]
|
"pages": ["file-system"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Advanced",
|
"group": "Orchestration",
|
||||||
"pages": ["advanced/acp-http-client"]
|
"pages": [
|
||||||
|
"architecture",
|
||||||
|
"session-persistence",
|
||||||
|
"observability",
|
||||||
|
"multiplayer",
|
||||||
|
"security"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Reference",
|
"group": "Reference",
|
||||||
"pages": [
|
"pages": [
|
||||||
"cli",
|
"cli",
|
||||||
"inspector",
|
"inspector",
|
||||||
"session-transcript-schema",
|
|
||||||
"opencode-compatibility",
|
"opencode-compatibility",
|
||||||
{
|
{
|
||||||
"group": "More",
|
"group": "More",
|
||||||
|
|
@ -102,6 +102,7 @@
|
||||||
"credentials",
|
"credentials",
|
||||||
"daemon",
|
"daemon",
|
||||||
"cors",
|
"cors",
|
||||||
|
"session-restoration",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
{
|
{
|
||||||
"group": "AI",
|
"group": "AI",
|
||||||
|
|
|
||||||
|
|
@ -5,183 +5,130 @@ sidebarTitle: "File System"
|
||||||
icon: "folder"
|
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.
|
The filesystem API lets you list, read, write, move, and delete files inside the sandbox, plus upload tar archives in batch.
|
||||||
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):
|
## Path resolution
|
||||||
|
|
||||||
- `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
|
|
||||||
|
|
||||||
- Absolute paths are used as-is.
|
- Absolute paths are used as-is.
|
||||||
- Relative paths use the session working directory when `sessionId` is provided.
|
- Relative paths resolve from the server process working directory.
|
||||||
- Without `sessionId`, relative paths resolve against the server home directory.
|
- Requests that attempt to escape allowed roots are rejected by the server.
|
||||||
- Relative paths cannot contain `..` or absolute prefixes; requests that attempt to escape the root are rejected.
|
|
||||||
|
|
||||||
The session working directory is the server process current working directory at the moment the session is created.
|
## List entries
|
||||||
|
|
||||||
## List Entries
|
|
||||||
|
|
||||||
`listFsEntries()` uses ACP extension method `_sandboxagent/fs/list_entries`.
|
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
const sdk = await SandboxAgent.connect({
|
||||||
token: process.env.SANDBOX_TOKEN,
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
agent: "mock" });
|
});
|
||||||
|
|
||||||
const entries = await client.listFsEntries({
|
const entries = await sdk.listFsEntries({
|
||||||
path: "./workspace",
|
path: "./workspace",
|
||||||
sessionId: "my-session",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(entries);
|
console.log(entries);
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
```bash cURL
|
||||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
curl -X GET "http://127.0.0.1:2468/v1/fs/entries?path=./workspace"
|
||||||
-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>
|
</CodeGroup>
|
||||||
|
|
||||||
## Read And Write Files
|
## Read and write files
|
||||||
|
|
||||||
`PUT /v2/fs/file` writes raw bytes. `GET /v2/fs/file` returns raw bytes.
|
`PUT /v1/fs/file` writes raw bytes. `GET /v1/fs/file` returns raw bytes.
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
const sdk = await SandboxAgent.connect({
|
||||||
token: process.env.SANDBOX_TOKEN,
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
agent: "mock" });
|
|
||||||
|
|
||||||
await client.writeFsFile({ path: "./notes.txt", sessionId: "my-session" }, "hello");
|
|
||||||
|
|
||||||
const bytes = await client.readFsFile({
|
|
||||||
path: "./notes.txt",
|
|
||||||
sessionId: "my-session",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await sdk.writeFsFile({ path: "./notes.txt" }, "hello");
|
||||||
|
|
||||||
|
const bytes = await sdk.readFsFile({ path: "./notes.txt" });
|
||||||
const text = new TextDecoder().decode(bytes);
|
const text = new TextDecoder().decode(bytes);
|
||||||
|
|
||||||
console.log(text);
|
console.log(text);
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
```bash cURL
|
||||||
curl -X PUT "http://127.0.0.1:2468/v2/fs/file?path=./notes.txt&sessionId=my-session" \
|
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
--data-binary "hello"
|
--data-binary "hello"
|
||||||
|
|
||||||
curl -X GET "http://127.0.0.1:2468/v2/fs/file?path=./notes.txt&sessionId=my-session" \
|
curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
--output ./notes.txt
|
--output ./notes.txt
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
## Create Directories
|
## Create directories
|
||||||
|
|
||||||
`mkdirFs()` uses ACP extension method `_sandboxagent/fs/mkdir`.
|
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
const sdk = await SandboxAgent.connect({
|
||||||
token: process.env.SANDBOX_TOKEN,
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
agent: "mock" });
|
|
||||||
|
|
||||||
await client.mkdirFs({
|
|
||||||
path: "./data",
|
|
||||||
sessionId: "my-session",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await sdk.mkdirFs({ path: "./data" });
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
```bash cURL
|
||||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=./data"
|
||||||
-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>
|
</CodeGroup>
|
||||||
|
|
||||||
## Move, Delete, And Stat
|
## Move, delete, and stat
|
||||||
|
|
||||||
`moveFs()`, `statFs()`, and `deleteFsEntry()` use ACP extension methods (`_sandboxagent/fs/move`, `_sandboxagent/fs/stat`, `_sandboxagent/fs/delete_entry`).
|
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
const sdk = await SandboxAgent.connect({
|
||||||
token: process.env.SANDBOX_TOKEN,
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
agent: "mock" });
|
|
||||||
|
|
||||||
await client.moveFs(
|
|
||||||
{ from: "./notes.txt", to: "./notes-old.txt", overwrite: true },
|
|
||||||
{ sessionId: "my-session" },
|
|
||||||
);
|
|
||||||
|
|
||||||
const stat = await client.statFs({
|
|
||||||
path: "./notes-old.txt",
|
|
||||||
sessionId: "my-session",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.deleteFsEntry({
|
await sdk.moveFs({
|
||||||
path: "./notes-old.txt",
|
from: "./notes.txt",
|
||||||
sessionId: "my-session",
|
to: "./notes-old.txt",
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stat = await sdk.statFs({ path: "./notes-old.txt" });
|
||||||
|
await sdk.deleteFsEntry({ path: "./notes-old.txt" });
|
||||||
|
|
||||||
console.log(stat);
|
console.log(stat);
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
```bash cURL
|
||||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
curl -X POST "http://127.0.0.1:2468/v1/fs/move" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "x-acp-connection-id: acp_conn_1" \
|
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"jsonrpc":"2.0","id":3,"method":"_sandboxagent/fs/move","params":{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true,"sessionId":"my-session"}}'
|
-d '{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true}'
|
||||||
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
curl -X GET "http://127.0.0.1:2468/v1/fs/stat?path=./notes-old.txt"
|
||||||
-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 POST "http://127.0.0.1:2468/v2/rpc" \
|
curl -X DELETE "http://127.0.0.1:2468/v1/fs/entry?path=./notes-old.txt"
|
||||||
-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>
|
</CodeGroup>
|
||||||
|
|
||||||
## Batch Upload (Tar)
|
## Batch upload (tar)
|
||||||
|
|
||||||
Batch upload accepts `application/x-tar` only and extracts into the destination directory. The response returns absolute paths for extracted files, capped at 1024 entries.
|
Batch upload accepts `application/x-tar` and extracts into the destination directory.
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import tar from "tar";
|
import tar from "tar";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
const sdk = await SandboxAgent.connect({
|
||||||
token: process.env.SANDBOX_TOKEN,
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
agent: "mock" });
|
});
|
||||||
|
|
||||||
const archivePath = path.join(process.cwd(), "skills.tar");
|
const archivePath = path.join(process.cwd(), "skills.tar");
|
||||||
await tar.c({
|
await tar.c({
|
||||||
|
|
@ -190,9 +137,8 @@ await tar.c({
|
||||||
}, ["."]);
|
}, ["."]);
|
||||||
|
|
||||||
const tarBuffer = await fs.promises.readFile(archivePath);
|
const tarBuffer = await fs.promises.readFile(archivePath);
|
||||||
const result = await client.uploadFsBatch(tarBuffer, {
|
const result = await sdk.uploadFsBatch(tarBuffer, {
|
||||||
path: "./skills",
|
path: "./skills",
|
||||||
sessionId: "my-session",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|
@ -201,8 +147,7 @@ console.log(result);
|
||||||
```bash cURL
|
```bash cURL
|
||||||
tar -cf skills.tar -C ./skills .
|
tar -cf skills.tar -C ./skills .
|
||||||
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v2/fs/upload-batch?path=./skills&sessionId=my-session" \
|
curl -X POST "http://127.0.0.1:2468/v1/fs/upload-batch?path=./skills" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/x-tar" \
|
-H "Content-Type: application/x-tar" \
|
||||||
--data-binary @skills.tar
|
--data-binary @skills.tar
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,119 +5,80 @@ sidebarTitle: "MCP"
|
||||||
icon: "plug"
|
icon: "plug"
|
||||||
---
|
---
|
||||||
|
|
||||||
MCP (Model Context Protocol) servers extend agents with tools. Sandbox Agent can auto-load MCP servers when a session starts by passing an `mcp` map in the create-session request.
|
MCP (Model Context Protocol) servers extend agents with tools and external context.
|
||||||
|
|
||||||
## Session Config
|
## Configuring MCP servers
|
||||||
|
|
||||||
The `mcp` field is a map of server name to config. Use `type: "local"` for stdio servers and `type: "remote"` for HTTP/SSE servers:
|
The HTTP config endpoints let you store/retrieve MCP server configs by directory + name.
|
||||||
|
|
||||||
<CodeGroup>
|
```ts
|
||||||
|
// Create MCP config
|
||||||
```ts TypeScript
|
await sdk.setMcpConfig(
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
{
|
||||||
|
directory: "/workspace",
|
||||||
const client = new SandboxAgentClient({
|
mcpName: "github",
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.createSession("claude-mcp", {
|
|
||||||
agent: "claude",
|
|
||||||
mcp: {
|
|
||||||
filesystem: {
|
|
||||||
type: "local",
|
|
||||||
command: "my-mcp-server",
|
|
||||||
args: ["--root", "."],
|
|
||||||
},
|
},
|
||||||
github: {
|
{
|
||||||
type: "remote",
|
type: "remote",
|
||||||
url: "https://example.com/mcp",
|
url: "https://example.com/mcp",
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer ${GITHUB_TOKEN}",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a session using the configured MCP servers
|
||||||
|
const session = await sdk.createSession({
|
||||||
|
agent: "claude",
|
||||||
|
sessionInit: {
|
||||||
|
cwd: "/workspace",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await session.prompt([
|
||||||
|
{ type: "text", text: "Use available MCP servers to help with this task." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// List MCP configs
|
||||||
|
const config = await sdk.getMcpConfig({
|
||||||
|
directory: "/workspace",
|
||||||
|
mcpName: "github",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(config.type);
|
||||||
|
|
||||||
|
// Delete MCP config
|
||||||
|
await sdk.deleteMcpConfig({
|
||||||
|
directory: "/workspace",
|
||||||
|
mcpName: "github",
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
## Config fields
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/claude-mcp" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"agent": "claude",
|
|
||||||
"mcp": {
|
|
||||||
"filesystem": {
|
|
||||||
"type": "local",
|
|
||||||
"command": "my-mcp-server",
|
|
||||||
"args": ["--root", "."]
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "https://example.com/mcp",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer ${GITHUB_TOKEN}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
### Local server
|
||||||
|
|
||||||
## Config Fields
|
|
||||||
|
|
||||||
### Local Server
|
|
||||||
|
|
||||||
Stdio servers that run inside the sandbox.
|
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `type` | `local` |
|
| `type` | `local` |
|
||||||
| `command` | string or array (`["node", "server.js"]`) |
|
| `command` | executable path |
|
||||||
| `args` | array of string arguments |
|
| `args` | array of CLI args |
|
||||||
| `env` | environment variables map |
|
| `env` | environment variable map |
|
||||||
| `enabled` | enable or disable the server |
|
| `cwd` | working directory |
|
||||||
| `timeoutMs` | tool timeout override |
|
| `enabled` | enable/disable server |
|
||||||
| `cwd` | working directory for the MCP process |
|
| `timeoutMs` | timeout override |
|
||||||
|
|
||||||
```json
|
### Remote server
|
||||||
{
|
|
||||||
"type": "local",
|
|
||||||
"command": ["node", "./mcp/server.js"],
|
|
||||||
"args": ["--root", "."],
|
|
||||||
"env": { "LOG_LEVEL": "debug" },
|
|
||||||
"cwd": "/workspace"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remote Server
|
|
||||||
|
|
||||||
HTTP/SSE servers accessed over the network.
|
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `type` | `remote` |
|
| `type` | `remote` |
|
||||||
| `url` | MCP server URL |
|
| `url` | MCP server URL |
|
||||||
| `headers` | static headers map |
|
|
||||||
| `bearerTokenEnvVar` | env var name to inject into `Authorization: Bearer ...` |
|
|
||||||
| `envHeaders` | map of header name to env var name |
|
|
||||||
| `oauth` | object with `clientId`, `clientSecret`, `scope`, or `false` to disable |
|
|
||||||
| `enabled` | enable or disable the server |
|
|
||||||
| `timeoutMs` | tool timeout override |
|
|
||||||
| `transport` | `http` or `sse` |
|
| `transport` | `http` or `sse` |
|
||||||
|
| `headers` | static headers map |
|
||||||
|
| `bearerTokenEnvVar` | env var name to inject in auth header |
|
||||||
|
| `envHeaders` | header name to env var map |
|
||||||
|
| `oauth` | optional OAuth config object |
|
||||||
|
| `enabled` | enable/disable server |
|
||||||
|
| `timeoutMs` | timeout override |
|
||||||
|
|
||||||
```json
|
## Custom MCP servers
|
||||||
{
|
|
||||||
"type": "remote",
|
|
||||||
"url": "https://example.com/mcp",
|
|
||||||
"headers": { "x-client": "sandbox-agent" },
|
|
||||||
"bearerTokenEnvVar": "MCP_TOKEN",
|
|
||||||
"transport": "sse"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom MCP Servers
|
|
||||||
|
|
||||||
To bundle and upload your own MCP server into the sandbox, see [Custom Tools](/custom-tools).
|
To bundle and upload your own MCP server into the sandbox, see [Custom Tools](/custom-tools).
|
||||||
|
|
|
||||||
115
docs/multiplayer.mdx
Normal file
115
docs/multiplayer.mdx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
---
|
||||||
|
title: "Multiplayer"
|
||||||
|
description: "Use Rivet Actors to coordinate shared sessions."
|
||||||
|
icon: "users"
|
||||||
|
---
|
||||||
|
|
||||||
|
For multiplayer orchestration, use [Rivet Actors](https://rivet.dev/docs/actors).
|
||||||
|
|
||||||
|
Recommended model:
|
||||||
|
|
||||||
|
- One actor per collaborative workspace/thread.
|
||||||
|
- The actor owns Sandbox Agent session lifecycle and persistence.
|
||||||
|
- Clients connect to the actor and receive realtime broadcasts.
|
||||||
|
|
||||||
|
Use [actor keys](https://rivet.dev/docs/actors/keys) to map each workspace to one actor, [events](https://rivet.dev/docs/actors/events) for realtime updates, and [lifecycle hooks](https://rivet.dev/docs/actors/lifecycle) for cleanup.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
|
||||||
|
```ts Actor (server)
|
||||||
|
import { actor, setup } from "rivetkit";
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { RivetSessionPersistDriver, type RivetPersistState } from "@sandbox-agent/persist-rivet";
|
||||||
|
|
||||||
|
type WorkspaceState = RivetPersistState & {
|
||||||
|
sandboxId: string;
|
||||||
|
baseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const workspace = actor({
|
||||||
|
createState: async () => {
|
||||||
|
return {
|
||||||
|
sandboxId: "sbx_123",
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
} satisfies Partial<WorkspaceState>;
|
||||||
|
},
|
||||||
|
|
||||||
|
createVars: async (c) => {
|
||||||
|
const persist = new RivetSessionPersistDriver(c);
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: c.state.baseUrl,
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await sdk.resumeOrCreateSession({ id: "default", agent: "codex" });
|
||||||
|
|
||||||
|
const unsubscribe = session.onEvent((event) => {
|
||||||
|
c.broadcast("session.event", event);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sdk, session, unsubscribe };
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
getSessionInfo: (c) => ({
|
||||||
|
workspaceId: c.key[0],
|
||||||
|
sandboxId: c.state.sandboxId,
|
||||||
|
}),
|
||||||
|
|
||||||
|
prompt: async (c, input: { userId: string; text: string }) => {
|
||||||
|
c.broadcast("chat.user", {
|
||||||
|
userId: input.userId,
|
||||||
|
text: input.text,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await c.vars.session.prompt([{ type: "text", text: input.text }]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
onSleep: async (c) => {
|
||||||
|
c.vars.unsubscribe?.();
|
||||||
|
await c.vars.sdk.dispose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registry = setup({
|
||||||
|
use: { workspace },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts Client (browser)
|
||||||
|
import { createClient } from "rivetkit/client";
|
||||||
|
import type { registry } from "./actors";
|
||||||
|
|
||||||
|
const client = createClient<typeof registry>({
|
||||||
|
endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceId = "workspace-42";
|
||||||
|
const room = client.workspace.getOrCreate([workspaceId]);
|
||||||
|
const conn = room.connect();
|
||||||
|
|
||||||
|
conn.on("chat.user", (event) => {
|
||||||
|
console.log("user message", event);
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on("session.event", (event) => {
|
||||||
|
console.log("sandbox event", event);
|
||||||
|
});
|
||||||
|
|
||||||
|
await conn.prompt({
|
||||||
|
userId: "user-123",
|
||||||
|
text: "Propose a refactor plan for auth middleware.",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep sandbox calls actor-only. Browser clients should not call Sandbox Agent directly.
|
||||||
|
- Use `@sandbox-agent/persist-rivet` so session history persists in actor state.
|
||||||
|
- For client connection patterns, see [Rivet JavaScript client](https://rivet.dev/docs/clients/javascript).
|
||||||
64
docs/observability.mdx
Normal file
64
docs/observability.mdx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
title: "Observability"
|
||||||
|
description: "Track session activity with OpenTelemetry."
|
||||||
|
icon: "terminal"
|
||||||
|
---
|
||||||
|
|
||||||
|
Use OpenTelemetry to instrument session traffic, then ship telemetry to your collector/backend.
|
||||||
|
|
||||||
|
## Common collectors and backends
|
||||||
|
|
||||||
|
- [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/)
|
||||||
|
- [Jaeger](https://www.jaegertracing.io/)
|
||||||
|
- [Grafana Tempo](https://grafana.com/oss/tempo/)
|
||||||
|
- [Honeycomb](https://www.honeycomb.io/)
|
||||||
|
- [Datadog APM](https://docs.datadoghq.com/tracing/)
|
||||||
|
|
||||||
|
## Example: trace a prompt round-trip
|
||||||
|
|
||||||
|
Wrap `session.prompt()` in a span to measure the full round-trip, then log individual events as span events.
|
||||||
|
|
||||||
|
Assumes your OTEL provider/exporter is already configured.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { trace } from "@opentelemetry/api";
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const tracer = trace.getTracer("my-app/sandbox-agent");
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: process.env.SANDBOX_URL!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await sdk.createSession({ agent: "mock" });
|
||||||
|
|
||||||
|
// Log each event as an OTEL span event on the active span
|
||||||
|
const unsubscribe = session.onEvent((event) => {
|
||||||
|
const activeSpan = trace.getActiveSpan();
|
||||||
|
if (!activeSpan) return;
|
||||||
|
|
||||||
|
activeSpan.addEvent("session.event", {
|
||||||
|
"sandbox.sender": event.sender,
|
||||||
|
"sandbox.event_index": event.eventIndex,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// The span covers the full prompt round-trip
|
||||||
|
await tracer.startActiveSpan("sandbox_agent.prompt", async (span) => {
|
||||||
|
span.setAttribute("sandbox.session_id", session.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await session.prompt([
|
||||||
|
{ type: "text", text: "Summarize this repository." },
|
||||||
|
]);
|
||||||
|
span.setAttribute("sandbox.stop_reason", result.stopReason);
|
||||||
|
} catch (error) {
|
||||||
|
span.recordException(error as Error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
1413
docs/openapi.json
1413
docs/openapi.json
File diff suppressed because it is too large
Load diff
|
|
@ -1,26 +1,125 @@
|
||||||
---
|
---
|
||||||
title: "OpenCode Compatibility"
|
title: "OpenCode Compatibility"
|
||||||
description: "Status of the OpenCode bridge during ACP v2 migration."
|
description: "Connect OpenCode clients, SDKs, and web UI to Sandbox Agent."
|
||||||
---
|
---
|
||||||
|
|
||||||
OpenCode compatibility is intentionally deferred during ACP core migration.
|
<Warning>
|
||||||
|
**Experimental**: OpenCode SDK/UI compatibility may change.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
## Current status (v2 core phases)
|
Sandbox Agent exposes an OpenCode-compatible API at `/opencode`.
|
||||||
|
|
||||||
- `/opencode/*` routes are disabled.
|
## Why use OpenCode clients with Sandbox Agent?
|
||||||
- `sandbox-agent opencode` returns an explicit disabled error.
|
|
||||||
- This is expected while ACP runtime, SDK, and inspector migration is completed.
|
|
||||||
|
|
||||||
## Planned re-enable step
|
- OpenCode CLI (`opencode attach`)
|
||||||
|
- OpenCode web UI
|
||||||
|
- OpenCode TypeScript SDK (`@opencode-ai/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.
|
### OpenCode CLI / TUI
|
||||||
2. Add dedicated OpenCode ↔ ACP integration tests.
|
|
||||||
3. Re-enable OpenCode docs and operational guidance.
|
|
||||||
|
|
||||||
Track details in:
|
```bash
|
||||||
|
sandbox-agent opencode --port 2468 --no-token
|
||||||
|
```
|
||||||
|
|
||||||
- `research/acp/spec.md`
|
Or start server + attach manually:
|
||||||
- `research/acp/migration-steps.md`
|
|
||||||
- `research/acp/todo.md`
|
```bash
|
||||||
|
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
||||||
|
opencode attach http://localhost:2468/opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
With authentication enabled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||||
|
opencode attach http://localhost:2468/opencode --password "$SANDBOX_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenCode web UI
|
||||||
|
|
||||||
|
<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="Run 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 UI">
|
||||||
|
Visit `http://127.0.0.1:5173/`.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### OpenCode SDK
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
||||||
|
|
||||||
|
const client = createOpencodeClient({
|
||||||
|
baseUrl: "http://localhost:2468/opencode",
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await client.session.create();
|
||||||
|
|
||||||
|
await client.session.promptAsync({
|
||||||
|
path: { id: session.data.id },
|
||||||
|
body: {
|
||||||
|
parts: [{ type: "text", text: "Hello, write a hello world script" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = await client.event.subscribe({});
|
||||||
|
for await (const event of events.stream) {
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- API base path: `/opencode`
|
||||||
|
- If server auth is enabled, pass bearer auth (or `--password` in OpenCode CLI)
|
||||||
|
- For browser UIs, configure CORS with `--cors-allow-origin`
|
||||||
|
- Provider selector currently exposes compatible providers (`mock`, `amp`, `claude`, `codex`)
|
||||||
|
- Provider/model metadata for compatibility endpoints is normalized and may differ from native OpenCode grouping
|
||||||
|
- Optional proxy: set `OPENCODE_COMPAT_PROXY_URL` to forward selected endpoints to native OpenCode
|
||||||
|
|
||||||
|
## Endpoint coverage
|
||||||
|
|
||||||
|
<Accordion title="Endpoint Status Table">
|
||||||
|
|
||||||
|
| Endpoint | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /event` | ✓ | Session/message updates (SSE) |
|
||||||
|
| `GET /global/event` | ✓ | GlobalEvent-wrapped stream |
|
||||||
|
| `GET /session` | ✓ | Session list |
|
||||||
|
| `POST /session` | ✓ | Create session |
|
||||||
|
| `GET /session/{id}` | ✓ | Session details |
|
||||||
|
| `POST /session/{id}/message` | ✓ | Send message |
|
||||||
|
| `GET /session/{id}/message` | ✓ | Session messages |
|
||||||
|
| `GET /permission` | ✓ | Pending permissions |
|
||||||
|
| `POST /permission/{id}/reply` | ✓ | Permission reply |
|
||||||
|
| `GET /question` | ✓ | Pending questions |
|
||||||
|
| `POST /question/{id}/reply` | ✓ | Question reply |
|
||||||
|
| `GET /provider` | ✓ | Provider metadata |
|
||||||
|
| `GET /command` | ↔ | Proxied when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub |
|
||||||
|
| `GET /config` | ↔ | Proxied when set; otherwise stub |
|
||||||
|
| `PATCH /config` | ↔ | Proxied when set; otherwise local compatibility behavior |
|
||||||
|
| `GET /global/config` | ↔ | Proxied when set; otherwise stub |
|
||||||
|
| `PATCH /global/config` | ↔ | Proxied when set; otherwise local compatibility behavior |
|
||||||
|
| `/tui/*` | ↔ | Proxied when set; otherwise local compatibility behavior |
|
||||||
|
| `GET /agent` | − | Agent list |
|
||||||
|
| *other endpoints* | − | Empty/stub responses |
|
||||||
|
|
||||||
|
✓ Functional ↔ Proxied optional − Stubbed
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,10 @@ icon: "rocket"
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="Extracting API keys from current machine">
|
<Accordion title="Extracting API keys from current machine">
|
||||||
Use `sandbox-agent credentials extract-env --export` to extract your existing API keys (Anthropic, OpenAI, etc.) from your existing Claude Code or Codex config files on your machine.
|
Use `sandbox-agent credentials extract-env --export` to extract your existing API keys (Anthropic, OpenAI, etc.) from local Claude Code or Codex config files.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="Testing without API keys">
|
<Accordion title="Testing without API keys">
|
||||||
If you want to test Sandbox Agent without API keys, use the `mock` agent to test the SDK without any credentials. It simulates agent responses for development and testing.
|
Use the `mock` agent for SDK and integration testing without provider credentials.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
@ -84,7 +84,7 @@ icon: "rocket"
|
||||||
Install and run the binary directly.
|
Install and run the binary directly.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh
|
||||||
sandbox-agent server --no-token --host 0.0.0.0 --port 2468
|
sandbox-agent server --no-token --host 0.0.0.0 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
@ -93,7 +93,7 @@ icon: "rocket"
|
||||||
Run without installing globally.
|
Run without installing globally.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @sandbox-agent/cli server --no-token --host 0.0.0.0 --port 2468
|
npx @sandbox-agent/cli@0.2.x server --no-token --host 0.0.0.0 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ icon: "rocket"
|
||||||
Run without installing globally.
|
Run without installing globally.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bunx @sandbox-agent/cli server --no-token --host 0.0.0.0 --port 2468
|
bunx @sandbox-agent/cli@0.2.x server --no-token --host 0.0.0.0 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ icon: "rocket"
|
||||||
Install globally, then run.
|
Install globally, then run.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @sandbox-agent/cli
|
npm install -g @sandbox-agent/cli@0.2.x
|
||||||
sandbox-agent server --no-token --host 0.0.0.0 --port 2468
|
sandbox-agent server --no-token --host 0.0.0.0 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
@ -118,33 +118,32 @@ icon: "rocket"
|
||||||
Install globally, then run.
|
Install globally, then run.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun add -g @sandbox-agent/cli
|
bun add -g @sandbox-agent/cli@0.2.x
|
||||||
# Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
|
# Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
|
||||||
bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
|
bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
|
||||||
sandbox-agent server --no-token --host 0.0.0.0 --port 2468
|
sandbox-agent server --no-token --host 0.0.0.0 --port 2468
|
||||||
```
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Node.js (local)">
|
<Tab title="Node.js (local)">
|
||||||
For local development, use `SandboxAgent.start()` to automatically spawn and manage the server as a subprocess.
|
For local development, use `SandboxAgent.start()` to spawn and manage the server as a subprocess.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install sandbox-agent
|
npm install sandbox-agent@0.2.x
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = await SandboxAgent.start();
|
const sdk = await SandboxAgent.start();
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Bun (local)">
|
<Tab title="Bun (local)">
|
||||||
For local development, use `SandboxAgent.start()` to automatically spawn and manage the server as a subprocess.
|
For local development, use `SandboxAgent.start()` to spawn and manage the server as a subprocess.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun add sandbox-agent
|
bun add sandbox-agent@0.2.x
|
||||||
# Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
|
# Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
|
||||||
bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
|
bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
|
||||||
```
|
```
|
||||||
|
|
@ -152,10 +151,8 @@ icon: "rocket"
|
||||||
```typescript
|
```typescript
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = await SandboxAgent.start();
|
const sdk = await SandboxAgent.start();
|
||||||
```
|
```
|
||||||
|
|
||||||
This installs the binary and starts the server for you. No manual setup required.
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Build from source">
|
<Tab title="Build from source">
|
||||||
|
|
@ -171,9 +168,9 @@ icon: "rocket"
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="Configuring token">
|
<Accordion title="Configuring token">
|
||||||
Tokens are usually not required. Most sandbox providers (E2B, Daytona, etc.) already secure their networking at the infrastructure level, so the server endpoint is never publicly accessible. For local development, binding to `127.0.0.1` ensures only local connections are accepted.
|
Tokens are usually not required. Most sandbox providers (E2B, Daytona, etc.) already secure networking at the infrastructure layer.
|
||||||
|
|
||||||
If you need to expose the server on a public endpoint, use `--token "$SANDBOX_TOKEN"` to require authentication on all requests:
|
If you expose the server publicly, use `--token "$SANDBOX_TOKEN"` to require authentication:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468
|
sandbox-agent server --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468
|
||||||
|
|
@ -184,34 +181,32 @@ icon: "rocket"
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
```typescript
|
```typescript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
const sdk = await SandboxAgent.connect({
|
||||||
baseUrl: "http://your-server:2468",
|
baseUrl: "http://your-server:2468",
|
||||||
token: process.env.SANDBOX_TOKEN,
|
token: process.env.SANDBOX_TOKEN,
|
||||||
agent: "mock",
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="curl">
|
<Tab title="curl">
|
||||||
```bash
|
```bash
|
||||||
curl "http://your-server:2468/v1/sessions" \
|
curl "http://your-server:2468/v1/health" \
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="CLI">
|
<Tab title="CLI">
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent api sessions list \
|
sandbox-agent --token "$SANDBOX_TOKEN" api agents list \
|
||||||
--endpoint http://your-server:2468 \
|
--endpoint http://your-server:2468
|
||||||
--token "$SANDBOX_TOKEN"
|
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="CORS">
|
<Accordion title="CORS">
|
||||||
If you're calling the server from a browser, see the [CORS configuration guide](/docs/cors).
|
If you're calling the server from a browser, see the [CORS configuration guide](/cors).
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
@ -226,124 +221,57 @@ icon: "rocket"
|
||||||
sandbox-agent install-agent amp
|
sandbox-agent install-agent amp
|
||||||
```
|
```
|
||||||
|
|
||||||
If agents are not installed up front, they will be lazily installed when creating a session. It's recommended to pre-install agents then take a snapshot of the sandbox for faster coldstarts.
|
If agents are not installed up front, they are lazily installed when creating a session.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Create a session">
|
<Step title="Create a session">
|
||||||
<Tabs>
|
|
||||||
<Tab title="TypeScript">
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
const sdk = await SandboxAgent.connect({
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
agent: "claude",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.createSession("my-session", {
|
const session = await sdk.createSession({
|
||||||
agent: "claude",
|
agent: "claude",
|
||||||
agentMode: "build",
|
sessionInit: {
|
||||||
permissionMode: "default",
|
cwd: "/",
|
||||||
|
mcpServers: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="curl">
|
console.log(session.id);
|
||||||
```bash
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"agent":"claude","agentMode":"build","permissionMode":"default"}'
|
|
||||||
```
|
```
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="CLI">
|
|
||||||
```bash
|
|
||||||
sandbox-agent api sessions create my-session \
|
|
||||||
--agent claude \
|
|
||||||
--endpoint http://127.0.0.1:2468
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Send a message">
|
<Step title="Send a message">
|
||||||
<Tabs>
|
|
||||||
<Tab title="TypeScript">
|
|
||||||
```typescript
|
```typescript
|
||||||
await client.postMessage("my-session", {
|
const result = await session.prompt([
|
||||||
message: "Summarize the repository and suggest next steps.",
|
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||||
});
|
]);
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="curl">
|
console.log(result.stopReason);
|
||||||
```bash
|
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Summarize the repository and suggest next steps."}'
|
|
||||||
```
|
```
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="CLI">
|
|
||||||
```bash
|
|
||||||
sandbox-agent api sessions send-message my-session \
|
|
||||||
--message "Summarize the repository and suggest next steps." \
|
|
||||||
--endpoint http://127.0.0.1:2468
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Read events">
|
<Step title="Read events">
|
||||||
<Tabs>
|
|
||||||
<Tab title="TypeScript">
|
|
||||||
```typescript
|
```typescript
|
||||||
// Poll for events
|
const off = session.onEvent((event) => {
|
||||||
const events = await client.getEvents("my-session", { offset: 0, limit: 50 });
|
console.log(event.sender, event.payload);
|
||||||
|
});
|
||||||
|
|
||||||
// Or stream events
|
const page = await sdk.getEvents({
|
||||||
for await (const event of client.streamEvents("my-session", { offset: 0 })) {
|
sessionId: session.id,
|
||||||
console.log(event.type, event.data);
|
limit: 50,
|
||||||
}
|
});
|
||||||
|
|
||||||
|
console.log(page.items.length);
|
||||||
|
off();
|
||||||
```
|
```
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="curl">
|
|
||||||
```bash
|
|
||||||
# Poll for events
|
|
||||||
curl "http://127.0.0.1:2468/v1/sessions/my-session/events?offset=0&limit=50"
|
|
||||||
|
|
||||||
# Stream events via SSE
|
|
||||||
curl "http://127.0.0.1:2468/v1/sessions/my-session/events/sse?offset=0"
|
|
||||||
|
|
||||||
# Single-turn stream (post message and get streamed response)
|
|
||||||
curl -N -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages/stream" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Hello"}'
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="CLI">
|
|
||||||
```bash
|
|
||||||
# Poll for events
|
|
||||||
sandbox-agent api sessions events my-session \
|
|
||||||
--endpoint http://127.0.0.1:2468
|
|
||||||
|
|
||||||
# Stream events via SSE
|
|
||||||
sandbox-agent api sessions events-sse my-session \
|
|
||||||
--endpoint http://127.0.0.1:2468
|
|
||||||
|
|
||||||
# Single-turn stream
|
|
||||||
sandbox-agent api sessions send-message-stream my-session \
|
|
||||||
--message "Hello" \
|
|
||||||
--endpoint http://127.0.0.1:2468
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Test with Inspector">
|
<Step title="Test with Inspector">
|
||||||
Open the Inspector UI at `/ui/` on your server (e.g., `http://localhost:2468/ui/`) to inspect session state using a GUI.
|
Open the Inspector UI at `/ui/` on your server (for example, `http://localhost:2468/ui/`) to inspect sessions and events in a GUI.
|
||||||
|
|
||||||
<Frame>
|
<Frame>
|
||||||
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" />
|
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" />
|
||||||
|
|
@ -354,13 +282,13 @@ icon: "rocket"
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
<CardGroup cols={3}>
|
<CardGroup cols={3}>
|
||||||
<Card title="Build a Chat UI" icon="comments" href="/building-chat-ui">
|
<Card title="Session Persistence" icon="database" href="/session-persistence">
|
||||||
Learn how to build a chat interface for your agent.
|
Configure in-memory, Rivet Actor state, IndexedDB, SQLite, and Postgres persistence.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Manage Sessions" icon="database" href="/manage-sessions">
|
<Card title="Deploy to a Sandbox" icon="box" href="/deploy/local">
|
||||||
Persist and replay agent transcripts.
|
Deploy your agent to E2B, Daytona, Docker, Vercel, or Cloudflare.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Deploy to a Sandbox" icon="box" href="/deploy">
|
<Card title="SDK Overview" icon="compass" href="/sdk-overview">
|
||||||
Deploy your agent to E2B, Daytona, or Vercel Sandboxes.
|
Use the latest TypeScript SDK API.
|
||||||
</Card>
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|
|
||||||
174
docs/sdk-overview.mdx
Normal file
174
docs/sdk-overview.mdx
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
---
|
||||||
|
title: "SDK Overview"
|
||||||
|
description: "Use the TypeScript SDK to manage Sandbox Agent sessions and APIs."
|
||||||
|
icon: "compass"
|
||||||
|
---
|
||||||
|
|
||||||
|
The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgent` class.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="npm">
|
||||||
|
```bash
|
||||||
|
npm install sandbox-agent@0.2.x
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="bun">
|
||||||
|
```bash
|
||||||
|
bun add sandbox-agent@0.2.x
|
||||||
|
# Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
|
||||||
|
bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Optional persistence drivers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sandbox-agent/persist-indexeddb@0.2.x @sandbox-agent/persist-sqlite@0.2.x @sandbox-agent/persist-postgres@0.2.x
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create a client
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
With persistence:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { SQLiteSessionPersistDriver } from "@sandbox-agent/persist-sqlite";
|
||||||
|
|
||||||
|
const persist = new SQLiteSessionPersistDriver({
|
||||||
|
filename: "./sessions.db",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Local autospawn (Node.js only):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const localSdk = await SandboxAgent.start();
|
||||||
|
|
||||||
|
await localSdk.dispose();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session flow
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const session = await sdk.createSession({
|
||||||
|
agent: "mock",
|
||||||
|
sessionInit: {
|
||||||
|
cwd: "/",
|
||||||
|
mcpServers: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = await session.prompt([
|
||||||
|
{ type: "text", text: "Summarize this repository." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(prompt.stopReason);
|
||||||
|
```
|
||||||
|
|
||||||
|
Load and destroy:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const restored = await sdk.resumeSession(session.id);
|
||||||
|
await restored.prompt([{ type: "text", text: "Continue from previous context." }]);
|
||||||
|
|
||||||
|
await sdk.destroySession(restored.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
Subscribe to live events:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const unsubscribe = session.onEvent((event) => {
|
||||||
|
console.log(event.eventIndex, event.sender, event.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.prompt([{ type: "text", text: "Give me a short summary." }]);
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetch persisted events:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const page = await sdk.getEvents({
|
||||||
|
sessionId: session.id,
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(page.items.length);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Control-plane and HTTP helpers
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const health = await sdk.getHealth();
|
||||||
|
const agents = await sdk.listAgents();
|
||||||
|
await sdk.installAgent("codex", { reinstall: true });
|
||||||
|
|
||||||
|
const entries = await sdk.listFsEntries({ path: "." });
|
||||||
|
const writeResult = await sdk.writeFsFile({ path: "./hello.txt" }, "hello");
|
||||||
|
|
||||||
|
console.log(health.status, agents.agents.length, entries.length, writeResult.path);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgentError } from "sandbox-agent";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sdk.listAgents();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SandboxAgentError) {
|
||||||
|
console.error(error.status, error.problem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inspector URL
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { buildInspectorUrl } from "sandbox-agent";
|
||||||
|
|
||||||
|
const url = buildInspectorUrl({
|
||||||
|
baseUrl: "https://your-sandbox-agent.example.com",
|
||||||
|
headers: { "X-Custom-Header": "value" },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- `baseUrl` (required): Sandbox Agent server URL
|
||||||
|
- `token` (optional): Bearer token for authenticated servers
|
||||||
|
- `headers` (optional): Additional request headers
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type {
|
||||||
|
AgentInfo,
|
||||||
|
HealthResponse,
|
||||||
|
SessionEvent,
|
||||||
|
SessionRecord,
|
||||||
|
} from "sandbox-agent";
|
||||||
|
```
|
||||||
191
docs/security.mdx
Normal file
191
docs/security.mdx
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
---
|
||||||
|
title: "Security"
|
||||||
|
description: "Backend-first auth and access control patterns."
|
||||||
|
icon: "shield"
|
||||||
|
---
|
||||||
|
|
||||||
|
As covered in [Architecture](/architecture), run the Sandbox Agent client on your backend, not in the browser.
|
||||||
|
|
||||||
|
This keeps sandbox credentials private and gives you one place for authz, rate limiting, and audit logging.
|
||||||
|
|
||||||
|
## Auth model
|
||||||
|
|
||||||
|
Implement auth however it fits your stack (sessions, JWT, API keys, etc.), but enforce it before any sandbox-bound request.
|
||||||
|
|
||||||
|
Minimum checks:
|
||||||
|
|
||||||
|
- Authenticate the caller.
|
||||||
|
- Authorize access to the target workspace/sandbox/session.
|
||||||
|
- Apply request rate limits and request logging.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Rivet
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
|
||||||
|
```ts Actor (server)
|
||||||
|
import { UserError, actor } from "rivetkit";
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
type ConnParams = {
|
||||||
|
accessToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkspaceClaims = {
|
||||||
|
sub: string;
|
||||||
|
workspaceId: string;
|
||||||
|
role: "owner" | "member" | "viewer";
|
||||||
|
};
|
||||||
|
|
||||||
|
async function verifyWorkspaceToken(
|
||||||
|
token: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<WorkspaceClaims | null> {
|
||||||
|
// Validate JWT/session token here, then enforce workspace scope.
|
||||||
|
// Return null when invalid/expired/not a member.
|
||||||
|
if (!token) return null;
|
||||||
|
return { sub: "user_123", workspaceId, role: "member" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workspace = actor({
|
||||||
|
state: {
|
||||||
|
events: [] as Array<{ userId: string; prompt: string; createdAt: number }>,
|
||||||
|
},
|
||||||
|
|
||||||
|
onBeforeConnect: async (c, params: ConnParams) => {
|
||||||
|
const claims = await verifyWorkspaceToken(params.accessToken, c.key[0]);
|
||||||
|
if (!claims) {
|
||||||
|
throw new UserError("Forbidden", { code: "forbidden" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createConnState: async (c, params: ConnParams) => {
|
||||||
|
const claims = await verifyWorkspaceToken(params.accessToken, c.key[0]);
|
||||||
|
if (!claims) {
|
||||||
|
throw new UserError("Forbidden", { code: "forbidden" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: claims.sub,
|
||||||
|
role: claims.role,
|
||||||
|
workspaceId: claims.workspaceId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
submitPrompt: async (c, prompt: string) => {
|
||||||
|
if (!c.conn) {
|
||||||
|
throw new UserError("Connection required", { code: "connection_required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.conn.state.role === "viewer") {
|
||||||
|
throw new UserError("Insufficient permissions", { code: "forbidden" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to Sandbox Agent from the actor (server-side only).
|
||||||
|
// Sandbox credentials never reach the client.
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: process.env.SANDBOX_URL!,
|
||||||
|
token: process.env.SANDBOX_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await sdk.createSession({
|
||||||
|
agent: "claude",
|
||||||
|
sessionInit: { cwd: "/workspace" },
|
||||||
|
});
|
||||||
|
|
||||||
|
session.onEvent((event) => {
|
||||||
|
c.broadcast("session.event", {
|
||||||
|
userId: c.conn!.state.userId,
|
||||||
|
eventIndex: event.eventIndex,
|
||||||
|
sender: event.sender,
|
||||||
|
payload: event.payload,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await session.prompt([
|
||||||
|
{ type: "text", text: prompt },
|
||||||
|
]);
|
||||||
|
|
||||||
|
c.state.events.push({
|
||||||
|
userId: c.conn.state.userId,
|
||||||
|
prompt,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { stopReason: result.stopReason };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts Client (browser)
|
||||||
|
import { createClient } from "rivetkit/client";
|
||||||
|
import type { registry } from "./actors";
|
||||||
|
|
||||||
|
const client = createClient<typeof registry>({
|
||||||
|
endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handle = client.workspace.getOrCreate(["ws_123"], {
|
||||||
|
params: { accessToken: userJwt },
|
||||||
|
});
|
||||||
|
|
||||||
|
const conn = handle.connect();
|
||||||
|
|
||||||
|
conn.on("session.event", (event) => {
|
||||||
|
console.log(event.sender, event.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await conn.submitPrompt("Plan a refactor for auth middleware.");
|
||||||
|
console.log(result.stopReason);
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
Use [onBeforeConnect](https://rivet.dev/docs/actors/authentication), [connection params](https://rivet.dev/docs/actors/connections), and [actor keys](https://rivet.dev/docs/actors/keys) together so each actor enforces auth per workspace.
|
||||||
|
|
||||||
|
### Hono
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { bearerAuth } from "hono/bearer-auth";
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
app.use("/sandbox/*", bearerAuth({ token: process.env.APP_API_TOKEN! }));
|
||||||
|
|
||||||
|
app.all("/sandbox/*", async (c) => {
|
||||||
|
const incoming = new URL(c.req.url);
|
||||||
|
const upstreamUrl = new URL(process.env.SANDBOX_URL!);
|
||||||
|
upstreamUrl.pathname = incoming.pathname.replace(/^\/sandbox/, "/v1");
|
||||||
|
upstreamUrl.search = incoming.search;
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set("authorization", `Bearer ${process.env.SANDBOX_TOKEN ?? ""}`);
|
||||||
|
|
||||||
|
const accept = c.req.header("accept");
|
||||||
|
if (accept) headers.set("accept", accept);
|
||||||
|
|
||||||
|
const contentType = c.req.header("content-type");
|
||||||
|
if (contentType) headers.set("content-type", contentType);
|
||||||
|
|
||||||
|
const body =
|
||||||
|
c.req.method === "POST" || c.req.method === "PUT" || c.req.method === "PATCH"
|
||||||
|
? await c.req.text()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const upstream = await fetch(upstreamUrl, {
|
||||||
|
method: c.req.method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(upstream.body, {
|
||||||
|
status: upstream.status,
|
||||||
|
headers: upstream.headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
183
docs/session-persistence.mdx
Normal file
183
docs/session-persistence.mdx
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
---
|
||||||
|
title: "Persisting Sessions"
|
||||||
|
description: "Choose and configure session persistence for the TypeScript SDK."
|
||||||
|
icon: "database"
|
||||||
|
---
|
||||||
|
|
||||||
|
The TypeScript SDK uses a `SessionPersistDriver` to store session records and event history.
|
||||||
|
If you do not provide one, the SDK uses in-memory storage.
|
||||||
|
With persistence enabled, sessions can be restored after runtime/session loss. See [Session Restoration](/session-restoration).
|
||||||
|
|
||||||
|
Each driver stores:
|
||||||
|
|
||||||
|
- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sessionInit`)
|
||||||
|
- `SessionEvent` (`id`, `eventIndex`, `sessionId`, `connectionId`, `sender`, `payload`, `createdAt`)
|
||||||
|
|
||||||
|
## Persistence drivers
|
||||||
|
|
||||||
|
### In-memory
|
||||||
|
|
||||||
|
Best for local dev and ephemeral workloads.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { InMemorySessionPersistDriver, SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
|
const persist = new InMemorySessionPersistDriver({
|
||||||
|
maxSessions: 1024,
|
||||||
|
maxEventsPerSession: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rivet
|
||||||
|
|
||||||
|
Recommended for sandbox orchestration with actor state.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sandbox-agent/persist-rivet@0.1.x
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { actor } from "rivetkit";
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { RivetSessionPersistDriver, type RivetPersistState } from "@sandbox-agent/persist-rivet";
|
||||||
|
|
||||||
|
type PersistedState = RivetPersistState & {
|
||||||
|
sandboxId: string;
|
||||||
|
baseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default actor({
|
||||||
|
createState: async () => {
|
||||||
|
return {
|
||||||
|
sandboxId: "sbx_123",
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
} satisfies Partial<PersistedState>;
|
||||||
|
},
|
||||||
|
createVars: async (c) => {
|
||||||
|
const persist = new RivetSessionPersistDriver(c);
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: c.state.baseUrl,
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await sdk.resumeOrCreateSession({ id: "default", agent: "codex" });
|
||||||
|
|
||||||
|
const unsubscribe = session.onEvent((event) => {
|
||||||
|
c.broadcast("session.event", event);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sdk, session, unsubscribe };
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
sendMessage: async (c, message: string) => {
|
||||||
|
await c.vars.session.prompt([{ type: "text", text: message }]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSleep: async (c) => {
|
||||||
|
c.vars.unsubscribe?.();
|
||||||
|
await c.vars.sdk.dispose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### IndexedDB
|
||||||
|
|
||||||
|
Best for browser apps that should survive reloads.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sandbox-agent/persist-indexeddb@0.2.x
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { IndexedDbSessionPersistDriver } from "@sandbox-agent/persist-indexeddb";
|
||||||
|
|
||||||
|
const persist = new IndexedDbSessionPersistDriver({
|
||||||
|
databaseName: "sandbox-agent-session-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
|
||||||
|
Best for local/server Node apps that need durable storage without a DB server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sandbox-agent/persist-sqlite@0.2.x
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { SQLiteSessionPersistDriver } from "@sandbox-agent/persist-sqlite";
|
||||||
|
|
||||||
|
const persist = new SQLiteSessionPersistDriver({
|
||||||
|
filename: "./sandbox-agent.db",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Postgres
|
||||||
|
|
||||||
|
Use when you already run Postgres and want shared relational storage.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @sandbox-agent/persist-postgres@0.2.x
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { PostgresSessionPersistDriver } from "@sandbox-agent/persist-postgres";
|
||||||
|
|
||||||
|
const persist = new PostgresSessionPersistDriver({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
schema: "public",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sdk = await SandboxAgent.connect({
|
||||||
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
|
persist,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom driver
|
||||||
|
|
||||||
|
Implement `SessionPersistDriver` for custom backends.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { SessionPersistDriver } from "sandbox-agent";
|
||||||
|
|
||||||
|
class MyDriver implements SessionPersistDriver {
|
||||||
|
async getSession(id) { return null; }
|
||||||
|
async listSessions(request) { return { items: [] }; }
|
||||||
|
async updateSession(session) {}
|
||||||
|
async listEvents(request) { return { items: [] }; }
|
||||||
|
async insertEvent(event) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Replay controls
|
||||||
|
|
||||||
|
`SandboxAgent.connect(...)` supports:
|
||||||
|
|
||||||
|
- `replayMaxEvents` (default `50`)
|
||||||
|
- `replayMaxChars` (default `12000`)
|
||||||
|
|
||||||
|
These cap replay size when restoring sessions.
|
||||||
|
|
||||||
|
## Related docs
|
||||||
|
|
||||||
|
- [SDK Overview](/sdk-overview)
|
||||||
|
- [Session Restoration](/session-restoration)
|
||||||
33
docs/session-restoration.mdx
Normal file
33
docs/session-restoration.mdx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
title: "Session Restoration"
|
||||||
|
description: "How the TypeScript SDK restores sessions after connection/runtime loss."
|
||||||
|
---
|
||||||
|
|
||||||
|
Sandbox Agent automatically restores stale sessions when live session state is no longer available.
|
||||||
|
|
||||||
|
This is driven by the configured `SessionPersistDriver` (`inMemory`, IndexedDB, SQLite, Postgres, or custom).
|
||||||
|
|
||||||
|
## How Auto-Restore Works
|
||||||
|
|
||||||
|
When you call `session.prompt(...)` (or `resumeSession(...)`) and the saved session points to a stale connection, the SDK:
|
||||||
|
|
||||||
|
1. Recreates a fresh session for the same local session id.
|
||||||
|
2. Rebinds the local session to the new runtime session id.
|
||||||
|
3. Replays recent persisted events into the next prompt as context.
|
||||||
|
|
||||||
|
This happens automatically; you do not need to manually rebuild the session.
|
||||||
|
|
||||||
|
## Replay Limits
|
||||||
|
|
||||||
|
Replay payload size is capped by:
|
||||||
|
|
||||||
|
- `replayMaxEvents` (default `50`)
|
||||||
|
- `replayMaxChars` (default `12000`)
|
||||||
|
|
||||||
|
These controls limit prompt growth during restore while preserving recent context.
|
||||||
|
|
||||||
|
## Related Docs
|
||||||
|
|
||||||
|
- [SDK Overview](/sdk-overview)
|
||||||
|
- [Persisting Sessions](/session-persistence)
|
||||||
|
- [Agent Sessions](/agent-sessions)
|
||||||
|
|
@ -1,88 +1,81 @@
|
||||||
---
|
---
|
||||||
title: "Skills"
|
title: "Skills"
|
||||||
description: "Auto-load skills into agent sessions."
|
description: "Configure skill sources for agent sessions."
|
||||||
sidebarTitle: "Skills"
|
sidebarTitle: "Skills"
|
||||||
icon: "sparkles"
|
icon: "sparkles"
|
||||||
---
|
---
|
||||||
|
|
||||||
Skills are local instruction bundles stored in `SKILL.md` files. Sandbox Agent can fetch, discover, and link skill directories into agent-specific skill paths at session start using the `skills.sources` field. The format is fully compatible with [skills.sh](https://skills.sh).
|
Skills are local instruction bundles stored in `SKILL.md` files.
|
||||||
|
|
||||||
## Session Config
|
## Configuring skills
|
||||||
|
|
||||||
Pass `skills.sources` when creating a session to load skills from GitHub repos, local paths, or git URLs.
|
Use `setSkillsConfig` / `getSkillsConfig` / `deleteSkillsConfig` to manage skill source config by directory + skill name.
|
||||||
|
|
||||||
<CodeGroup>
|
```ts
|
||||||
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
```ts TypeScript
|
const sdk = await SandboxAgent.connect({
|
||||||
import { SandboxAgentClient } from "sandbox-agent";
|
|
||||||
|
|
||||||
const client = new SandboxAgentClient({
|
|
||||||
baseUrl: "http://127.0.0.1:2468",
|
baseUrl: "http://127.0.0.1:2468",
|
||||||
token: process.env.SANDBOX_TOKEN,
|
|
||||||
agent: "mock",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.createSession("claude-skills", {
|
// Add a skill
|
||||||
agent: "claude",
|
await sdk.setSkillsConfig(
|
||||||
skills: {
|
{
|
||||||
|
directory: "/workspace",
|
||||||
|
skillName: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
sources: [
|
sources: [
|
||||||
{ type: "github", source: "rivet-dev/skills", skills: ["sandbox-agent"] },
|
{ type: "github", source: "rivet-dev/skills", skills: ["sandbox-agent"] },
|
||||||
{ type: "local", source: "/workspace/my-custom-skill" },
|
{ type: "local", source: "/workspace/my-custom-skill" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a session using the configured skills
|
||||||
|
const session = await sdk.createSession({
|
||||||
|
agent: "claude",
|
||||||
|
sessionInit: {
|
||||||
|
cwd: "/workspace",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await session.prompt([
|
||||||
|
{ type: "text", text: "Use available skills to help with this task." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// List skills
|
||||||
|
const config = await sdk.getSkillsConfig({
|
||||||
|
directory: "/workspace",
|
||||||
|
skillName: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(config.sources.length);
|
||||||
|
|
||||||
|
// Delete skill
|
||||||
|
await sdk.deleteSkillsConfig({
|
||||||
|
directory: "/workspace",
|
||||||
|
skillName: "default",
|
||||||
|
});
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash cURL
|
## Skill sources
|
||||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/claude-skills" \
|
|
||||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"agent": "claude",
|
|
||||||
"skills": {
|
|
||||||
"sources": [
|
|
||||||
{ "type": "github", "source": "rivet-dev/skills", "skills": ["sandbox-agent"] },
|
|
||||||
{ "type": "local", "source": "/workspace/my-custom-skill" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
Each `skills.sources` entry describes where to find skills.
|
||||||
|
|
||||||
Each skill directory must contain `SKILL.md`. See [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) for tips on writing effective skills.
|
|
||||||
|
|
||||||
## Skill Sources
|
|
||||||
|
|
||||||
Each entry in `skills.sources` describes where to find skills. Three source types are supported:
|
|
||||||
|
|
||||||
| Type | `source` value | Example |
|
| Type | `source` value | Example |
|
||||||
|------|---------------|---------|
|
|------|---------------|---------|
|
||||||
| `github` | `owner/repo` | `"rivet-dev/skills"` |
|
| `github` | `owner/repo` | `"rivet-dev/skills"` |
|
||||||
| `local` | Filesystem path | `"/workspace/my-skill"` |
|
| `local` | filesystem path | `"/workspace/my-skill"` |
|
||||||
| `git` | Git clone URL | `"https://git.example.com/skills.git"` |
|
| `git` | git clone URL | `"https://git.example.com/skills.git"` |
|
||||||
|
|
||||||
### Optional fields
|
Optional fields:
|
||||||
|
|
||||||
- **`skills`** — Array of skill directory names to include. When omitted, all discovered skills are installed.
|
- `skills`: subset of skill directory names to include
|
||||||
- **`ref`** — Branch, tag, or commit to check out (default: HEAD). Applies to `github` and `git` types.
|
- `ref`: branch/tag/commit (for `github` and `git`)
|
||||||
- **`subpath`** — Subdirectory within the repo to search for skills.
|
- `subpath`: subdirectory within repo to scan
|
||||||
|
|
||||||
## Custom Skills
|
## Custom skills
|
||||||
|
|
||||||
To write, upload, and configure your own skills inside the sandbox, see [Custom Tools](/custom-tools).
|
To write, upload, and configure your own skills inside the sandbox, see [Custom Tools](/custom-tools).
|
||||||
|
|
||||||
## Advanced
|
|
||||||
|
|
||||||
### Discovery logic
|
|
||||||
|
|
||||||
After resolving a source to a local directory (cloning if needed), Sandbox Agent discovers skills by:
|
|
||||||
1. Checking if the directory itself contains `SKILL.md`.
|
|
||||||
2. Scanning `skills/` subdirectory for child directories containing `SKILL.md`.
|
|
||||||
3. Scanning immediate children of the directory for `SKILL.md`.
|
|
||||||
|
|
||||||
Discovered skills are symlinked into project-local skill roots (`.claude/skills/<name>`, `.agents/skills/<name>`, `.opencode/skill/<name>`).
|
|
||||||
|
|
||||||
### Caching
|
|
||||||
|
|
||||||
GitHub sources are downloaded as zip archives and git sources are cloned to `~/.sandbox-agent/skills-cache/` and updated on subsequent session creations. GitHub sources do not require `git` to be installed.
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
FROM cloudflare/sandbox:0.7.0
|
FROM cloudflare/sandbox:0.7.0
|
||||||
|
|
||||||
# Install sandbox-agent
|
# Install sandbox-agent
|
||||||
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh
|
||||||
|
|
||||||
# Pre-install agents
|
# Pre-install agents
|
||||||
RUN sandbox-agent install-agent claude && \
|
RUN sandbox-agent install-agent claude && \
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
||||||
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
@ -31,16 +31,19 @@ console.log(` Written: ${written.path} (${written.bytesWritten} bytes)`);
|
||||||
|
|
||||||
// Create a session with the uploaded MCP server as a local command.
|
// Create a session with the uploaded MCP server as a local command.
|
||||||
console.log("Creating session with custom MCP tool...");
|
console.log("Creating session with custom MCP tool...");
|
||||||
const sessionId = generateSessionId();
|
const session = await client.createSession({
|
||||||
await client.createSession(sessionId, {
|
|
||||||
agent: detectAgent(),
|
agent: detectAgent(),
|
||||||
mcp: {
|
sessionInit: {
|
||||||
customTools: {
|
cwd: "/root",
|
||||||
type: "local",
|
mcpServers: [{
|
||||||
command: ["node", "/opt/mcp/custom-tools/mcp-server.cjs"],
|
name: "customTools",
|
||||||
},
|
command: "node",
|
||||||
|
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||||
|
env: [],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const sessionId = session.id;
|
||||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
console.log(' Try: "generate a random number between 1 and 100"');
|
console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { detectAgent, buildInspectorUrl, generateSessionId } from "@sandbox-agent/example-shared";
|
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
||||||
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
|
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
|
|
@ -12,17 +12,19 @@ const { baseUrl, cleanup } = await startDockerSandbox({
|
||||||
|
|
||||||
console.log("Creating session with everything MCP server...");
|
console.log("Creating session with everything MCP server...");
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
const sessionId = generateSessionId();
|
const session = await client.createSession({
|
||||||
await client.createSession(sessionId, {
|
|
||||||
agent: detectAgent(),
|
agent: detectAgent(),
|
||||||
mcp: {
|
sessionInit: {
|
||||||
everything: {
|
cwd: "/root",
|
||||||
type: "local",
|
mcpServers: [{
|
||||||
command: ["mcp-server-everything"],
|
name: "everything",
|
||||||
timeoutMs: 10000,
|
command: "mcp-server-everything",
|
||||||
},
|
args: [],
|
||||||
|
env: [],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const sessionId = session.id;
|
||||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
console.log(' Try: "generate a random number between 1 and 100"');
|
console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
|
||||||
9
examples/mock-acp-agent/README.md
Normal file
9
examples/mock-acp-agent/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# @sandbox-agent/mock-acp-agent
|
||||||
|
|
||||||
|
Minimal newline-delimited ACP JSON-RPC mock agent.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Echoes every inbound message as `mock/echo` notification.
|
||||||
|
- For requests (`method` + `id`), returns `result.echoed` payload.
|
||||||
|
- For `mock/ask_client`, emits an agent-initiated `mock/request` before response.
|
||||||
|
- For responses from client (`id` without `method`), emits `mock/client_response` notification.
|
||||||
24
examples/mock-acp-agent/package.json
Normal file
24
examples/mock-acp-agent/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "@sandbox-agent/mock-acp-agent",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": false,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Mock ACP agent for adapter integration testing",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"start": "node ./dist/index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "latest",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
100
examples/mock-acp-agent/src/index.ts
Normal file
100
examples/mock-acp-agent/src/index.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { createInterface } from "node:readline";
|
||||||
|
|
||||||
|
interface JsonRpcRequest {
|
||||||
|
jsonrpc?: unknown;
|
||||||
|
id?: unknown;
|
||||||
|
method?: unknown;
|
||||||
|
params?: unknown;
|
||||||
|
result?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
let outboundRequestSeq = 0;
|
||||||
|
|
||||||
|
function writeMessage(payload: unknown): void {
|
||||||
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function echoNotification(message: unknown): void {
|
||||||
|
writeMessage({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "mock/echo",
|
||||||
|
params: {
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(raw: string): void {
|
||||||
|
if (!raw.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg: JsonRpcRequest;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(raw) as JsonRpcRequest;
|
||||||
|
} catch (error) {
|
||||||
|
writeMessage({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "mock/parse_error",
|
||||||
|
params: {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
raw,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echoNotification(msg);
|
||||||
|
|
||||||
|
const hasMethod = typeof msg.method === "string";
|
||||||
|
const hasId = msg.id !== undefined;
|
||||||
|
|
||||||
|
if (hasMethod && hasId) {
|
||||||
|
if (msg.method === "mock/ask_client") {
|
||||||
|
outboundRequestSeq += 1;
|
||||||
|
writeMessage({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: `agent-req-${outboundRequestSeq}`,
|
||||||
|
method: "mock/request",
|
||||||
|
params: {
|
||||||
|
prompt: "please respond",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
writeMessage({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: msg.id,
|
||||||
|
result: {
|
||||||
|
echoed: msg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMethod && hasId) {
|
||||||
|
writeMessage({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "mock/client_response",
|
||||||
|
params: {
|
||||||
|
id: msg.id,
|
||||||
|
result: msg.result ?? null,
|
||||||
|
error: msg.error ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rl = createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
handleMessage(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("close", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
11
examples/mock-acp-agent/tsconfig.build.json
Normal file
11
examples/mock-acp-agent/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
17
examples/mock-acp-agent/tsconfig.json
Normal file
17
examples/mock-acp-agent/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,11 @@ WORKDIR /build
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
# Copy packages needed for the inspector build chain:
|
# Copy packages needed for the inspector build chain:
|
||||||
# inspector -> sandbox-agent SDK -> cli-shared
|
# inspector -> sandbox-agent SDK -> acp-http-client, cli-shared, persist-indexeddb
|
||||||
COPY sdks/typescript/ sdks/typescript/
|
COPY sdks/typescript/ sdks/typescript/
|
||||||
|
COPY sdks/acp-http-client/ sdks/acp-http-client/
|
||||||
COPY sdks/cli-shared/ sdks/cli-shared/
|
COPY sdks/cli-shared/ sdks/cli-shared/
|
||||||
|
COPY sdks/persist-indexeddb/ sdks/persist-indexeddb/
|
||||||
COPY frontend/packages/inspector/ frontend/packages/inspector/
|
COPY frontend/packages/inspector/ frontend/packages/inspector/
|
||||||
COPY docs/openapi.json docs/
|
COPY docs/openapi.json docs/
|
||||||
|
|
||||||
|
|
@ -16,6 +18,7 @@ COPY docs/openapi.json docs/
|
||||||
# but not needed for the inspector build (avoids install errors).
|
# but not needed for the inspector build (avoids install errors).
|
||||||
RUN set -e; for dir in \
|
RUN set -e; for dir in \
|
||||||
sdks/cli sdks/gigacode \
|
sdks/cli sdks/gigacode \
|
||||||
|
sdks/persist-postgres sdks/persist-sqlite sdks/persist-rivet \
|
||||||
resources/agent-schemas resources/vercel-ai-sdk-schemas \
|
resources/agent-schemas resources/vercel-ai-sdk-schemas \
|
||||||
scripts/release scripts/sandbox-testing \
|
scripts/release scripts/sandbox-testing \
|
||||||
examples/shared examples/docker examples/e2b examples/vercel \
|
examples/shared examples/docker examples/e2b examples/vercel \
|
||||||
|
|
@ -44,6 +47,7 @@ COPY Cargo.toml Cargo.lock ./
|
||||||
COPY server/ ./server/
|
COPY server/ ./server/
|
||||||
COPY gigacode/ ./gigacode/
|
COPY gigacode/ ./gigacode/
|
||||||
COPY resources/agent-schemas/artifacts/ ./resources/agent-schemas/artifacts/
|
COPY resources/agent-schemas/artifacts/ ./resources/agent-schemas/artifacts/
|
||||||
|
COPY scripts/agent-configs/ ./scripts/agent-configs/
|
||||||
COPY --from=frontend /build/frontend/packages/inspector/dist/ ./frontend/packages/inspector/dist/
|
COPY --from=frontend /build/frontend/packages/inspector/dist/ ./frontend/packages/inspector/dist/
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=/usr/local/cargo/git \
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,25 @@
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid var(--border-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition), border-color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.status-indicator.disconnected {
|
.status-indicator.disconnected {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -404,7 +423,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
overflow: visible;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
|
|
@ -555,6 +574,20 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup-custom-back {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-custom-back:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.session-create-section {
|
.session-create-section {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -1055,6 +1088,23 @@
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-persistence-note {
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-persistence-note a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-persistence-note a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
/* Chat Panel */
|
/* Chat Panel */
|
||||||
.chat-panel {
|
.chat-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1322,6 +1372,64 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-stack {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: min(420px, calc(100vw - 24px));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid rgba(255, 59, 48, 0.35);
|
||||||
|
background: rgba(28, 8, 8, 0.95);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-title {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
color: var(--danger);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.cursor {
|
.cursor {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
|
@ -1932,84 +2040,82 @@
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-type.session,
|
/* ACP event categories: connection */
|
||||||
.event-type.session-started,
|
.event-type.connection,
|
||||||
.event-type.session-ended {
|
.event-type.session {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
.event-icon.connection,
|
||||||
.event-type.item,
|
.event-icon.session {
|
||||||
.event-type.item-started,
|
|
||||||
.event-type.item-completed {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-type.item-delta {
|
|
||||||
color: var(--cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-type.error,
|
|
||||||
.event-type.agent-unparsed {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-type.question,
|
|
||||||
.event-type.question-requested,
|
|
||||||
.event-type.question-resolved {
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-type.permission,
|
|
||||||
.event-type.permission-requested,
|
|
||||||
.event-type.permission-resolved {
|
|
||||||
color: var(--purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-icon.session,
|
|
||||||
.event-icon.session-started,
|
|
||||||
.event-icon.session-ended {
|
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
border-color: rgba(48, 209, 88, 0.35);
|
border-color: rgba(48, 209, 88, 0.35);
|
||||||
background: rgba(48, 209, 88, 0.12);
|
background: rgba(48, 209, 88, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-icon.item,
|
/* ACP event categories: prompt / tool */
|
||||||
.event-icon.item-started,
|
.event-type.prompt,
|
||||||
.event-icon.item-completed {
|
.event-type.tool {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.event-icon.prompt,
|
||||||
|
.event-icon.tool {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: rgba(255, 79, 0, 0.35);
|
border-color: rgba(255, 79, 0, 0.35);
|
||||||
background: rgba(255, 79, 0, 0.12);
|
background: rgba(255, 79, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-icon.item-delta {
|
/* ACP event categories: update / terminal (streaming, realtime) */
|
||||||
|
.event-type.update,
|
||||||
|
.event-type.terminal {
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
.event-icon.update,
|
||||||
|
.event-icon.terminal {
|
||||||
color: var(--cyan);
|
color: var(--cyan);
|
||||||
border-color: rgba(100, 210, 255, 0.35);
|
border-color: rgba(100, 210, 255, 0.35);
|
||||||
background: rgba(100, 210, 255, 0.12);
|
background: rgba(100, 210, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-icon.error,
|
/* ACP event categories: cancel */
|
||||||
.event-icon.agent-unparsed {
|
.event-type.cancel {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.event-icon.cancel {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
border-color: rgba(255, 59, 48, 0.35);
|
border-color: rgba(255, 59, 48, 0.35);
|
||||||
background: rgba(255, 59, 48, 0.12);
|
background: rgba(255, 59, 48, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-icon.question,
|
/* ACP event categories: filesystem */
|
||||||
.event-icon.question-requested,
|
.event-type.filesystem {
|
||||||
.event-icon.question-resolved {
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
.event-icon.filesystem {
|
||||||
color: var(--warning);
|
color: var(--warning);
|
||||||
border-color: rgba(255, 159, 10, 0.35);
|
border-color: rgba(255, 159, 10, 0.35);
|
||||||
background: rgba(255, 159, 10, 0.12);
|
background: rgba(255, 159, 10, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-icon.permission,
|
/* ACP event categories: config / permission */
|
||||||
.event-icon.permission-requested,
|
.event-type.config,
|
||||||
.event-icon.permission-resolved {
|
.event-type.permission {
|
||||||
|
color: var(--purple);
|
||||||
|
}
|
||||||
|
.event-icon.config,
|
||||||
|
.event-icon.permission {
|
||||||
color: var(--purple);
|
color: var(--purple);
|
||||||
border-color: rgba(191, 90, 242, 0.35);
|
border-color: rgba(191, 90, 242, 0.35);
|
||||||
background: rgba(191, 90, 242, 0.12);
|
background: rgba(191, 90, 242, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ACP event categories: response (fallback) */
|
||||||
|
.event-type.response {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.event-icon.response {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.event-time {
|
.event-time {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
@ -2247,6 +2353,13 @@
|
||||||
.header-title {
|
.header-title {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-stack {
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,23 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "pnpm --filter sandbox-agent build && vite build",
|
"build": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && tsc --noEmit",
|
||||||
|
"test": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && vitest run"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sandbox-agent": "workspace:*",
|
"sandbox-agent": "workspace:*",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"fake-indexeddb": "^6.2.4",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.7"
|
"vite": "^5.4.7",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sandbox-agent/persist-indexeddb": "workspace:*",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AlertTriangle, Zap } from "lucide-react";
|
import { AlertTriangle, BookOpen, Zap } from "lucide-react";
|
||||||
import { isHttpsToHttpConnection, isLocalNetworkTarget } from "../lib/permissions";
|
import { isHttpsToHttpConnection, isLocalNetworkTarget } from "../lib/permissions";
|
||||||
|
|
||||||
const logoUrl = `${import.meta.env.BASE_URL}logos/sandboxagent.svg`;
|
const logoUrl = `${import.meta.env.BASE_URL}logos/sandboxagent.svg`;
|
||||||
|
|
@ -11,7 +11,9 @@ const ConnectScreen = ({
|
||||||
onEndpointChange,
|
onEndpointChange,
|
||||||
onTokenChange,
|
onTokenChange,
|
||||||
onConnect,
|
onConnect,
|
||||||
reportUrl
|
reportUrl,
|
||||||
|
docsUrl,
|
||||||
|
discordUrl,
|
||||||
}: {
|
}: {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
token: string;
|
token: string;
|
||||||
|
|
@ -21,6 +23,8 @@ const ConnectScreen = ({
|
||||||
onTokenChange: (value: string) => void;
|
onTokenChange: (value: string) => void;
|
||||||
onConnect: () => void;
|
onConnect: () => void;
|
||||||
reportUrl?: string;
|
reportUrl?: string;
|
||||||
|
docsUrl?: string;
|
||||||
|
discordUrl?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
|
|
@ -28,11 +32,26 @@ const ConnectScreen = ({
|
||||||
<div className="header-left">
|
<div className="header-left">
|
||||||
<img src={logoUrl} alt="Sandbox Agent" className="logo-text" style={{ height: '20px', width: 'auto' }} />
|
<img src={logoUrl} alt="Sandbox Agent" className="logo-text" style={{ height: '20px', width: 'auto' }} />
|
||||||
</div>
|
</div>
|
||||||
{reportUrl && (
|
{(docsUrl || discordUrl || reportUrl) && (
|
||||||
<div className="header-right">
|
<div className="header-right">
|
||||||
<a className="button ghost small" href={reportUrl} target="_blank" rel="noreferrer">
|
{docsUrl && (
|
||||||
Report Bug
|
<a className="header-link" href={docsUrl} target="_blank" rel="noreferrer">
|
||||||
|
<BookOpen size={12} />
|
||||||
|
Docs
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
|
{discordUrl && (
|
||||||
|
<a className="header-link" href={discordUrl} target="_blank" rel="noreferrer">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{reportUrl && (
|
||||||
|
<a className="header-link" href={reportUrl} target="_blank" rel="noreferrer">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
Issues
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { ArrowLeft, ArrowRight, ChevronDown, ChevronRight, Pencil, Plus, X } from "lucide-react";
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { McpServerEntry } from "../App";
|
import type { AgentInfo } from "sandbox-agent";
|
||||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "../types/legacyApi";
|
|
||||||
|
type AgentModeInfo = { id: string; name: string; description: string };
|
||||||
|
type AgentModelInfo = { id: string; name?: string };
|
||||||
|
|
||||||
export type SessionConfig = {
|
export type SessionConfig = {
|
||||||
model: string;
|
|
||||||
agentMode: string;
|
agentMode: string;
|
||||||
permissionMode: string;
|
model: string;
|
||||||
variant: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CUSTOM_MODEL_VALUE = "__custom__";
|
||||||
|
|
||||||
const agentLabels: Record<string, string> = {
|
const agentLabels: Record<string, string> = {
|
||||||
claude: "Claude Code",
|
claude: "Claude Code",
|
||||||
codex: "Codex",
|
codex: "Codex",
|
||||||
|
|
@ -17,59 +19,6 @@ const agentLabels: Record<string, string> = {
|
||||||
amp: "Amp"
|
amp: "Amp"
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateServerJson = (json: string): string | null => {
|
|
||||||
const trimmed = json.trim();
|
|
||||||
if (!trimmed) return "Config is required";
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed);
|
|
||||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
||||||
return "Must be a JSON object";
|
|
||||||
}
|
|
||||||
if (!parsed.type) return 'Missing "type" field';
|
|
||||||
if (parsed.type !== "local" && parsed.type !== "remote") {
|
|
||||||
return 'Type must be "local" or "remote"';
|
|
||||||
}
|
|
||||||
if (parsed.type === "local" && !parsed.command) return 'Local server requires "command"';
|
|
||||||
if (parsed.type === "remote" && !parsed.url) return 'Remote server requires "url"';
|
|
||||||
return null;
|
|
||||||
} catch {
|
|
||||||
return "Invalid JSON";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerType = (configJson: string): string | null => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(configJson);
|
|
||||||
return parsed?.type ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerSummary = (configJson: string): string => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(configJson);
|
|
||||||
if (parsed?.type === "local") {
|
|
||||||
const cmd = Array.isArray(parsed.command) ? parsed.command.join(" ") : parsed.command;
|
|
||||||
return cmd ?? "local";
|
|
||||||
}
|
|
||||||
if (parsed?.type === "remote") {
|
|
||||||
return parsed.url ?? "remote";
|
|
||||||
}
|
|
||||||
return parsed?.type ?? "";
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const skillSourceSummary = (source: SkillSource): string => {
|
|
||||||
let summary = source.source;
|
|
||||||
if (source.skills && source.skills.length > 0) {
|
|
||||||
summary += ` [${source.skills.join(", ")}]`;
|
|
||||||
}
|
|
||||||
return summary;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SessionCreateMenu = ({
|
const SessionCreateMenu = ({
|
||||||
agents,
|
agents,
|
||||||
agentsLoading,
|
agentsLoading,
|
||||||
|
|
@ -77,17 +26,8 @@ const SessionCreateMenu = ({
|
||||||
modesByAgent,
|
modesByAgent,
|
||||||
modelsByAgent,
|
modelsByAgent,
|
||||||
defaultModelByAgent,
|
defaultModelByAgent,
|
||||||
modesLoadingByAgent,
|
|
||||||
modelsLoadingByAgent,
|
|
||||||
modesErrorByAgent,
|
|
||||||
modelsErrorByAgent,
|
|
||||||
mcpServers,
|
|
||||||
onMcpServersChange,
|
|
||||||
mcpConfigError,
|
|
||||||
skillSources,
|
|
||||||
onSkillSourcesChange,
|
|
||||||
onSelectAgent,
|
|
||||||
onCreateSession,
|
onCreateSession,
|
||||||
|
onSelectAgent,
|
||||||
open,
|
open,
|
||||||
onClose
|
onClose
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -97,60 +37,18 @@ const SessionCreateMenu = ({
|
||||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
modelsByAgent: Record<string, AgentModelInfo[]>;
|
modelsByAgent: Record<string, AgentModelInfo[]>;
|
||||||
defaultModelByAgent: Record<string, string>;
|
defaultModelByAgent: Record<string, string>;
|
||||||
modesLoadingByAgent: Record<string, boolean>;
|
|
||||||
modelsLoadingByAgent: Record<string, boolean>;
|
|
||||||
modesErrorByAgent: Record<string, string | null>;
|
|
||||||
modelsErrorByAgent: Record<string, string | null>;
|
|
||||||
mcpServers: McpServerEntry[];
|
|
||||||
onMcpServersChange: (servers: McpServerEntry[]) => void;
|
|
||||||
mcpConfigError: string | null;
|
|
||||||
skillSources: SkillSource[];
|
|
||||||
onSkillSourcesChange: (sources: SkillSource[]) => void;
|
|
||||||
onSelectAgent: (agentId: string) => void;
|
|
||||||
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||||
|
onSelectAgent: (agentId: string) => Promise<void>;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [phase, setPhase] = useState<"agent" | "config">("agent");
|
const [phase, setPhase] = useState<"agent" | "config" | "loading-config">("agent");
|
||||||
const [selectedAgent, setSelectedAgent] = useState("");
|
const [selectedAgent, setSelectedAgent] = useState("");
|
||||||
const [agentMode, setAgentMode] = useState("");
|
const [agentMode, setAgentMode] = useState("");
|
||||||
const [permissionMode, setPermissionMode] = useState("default");
|
const [selectedModel, setSelectedModel] = useState("");
|
||||||
const [model, setModel] = useState("");
|
const [customModel, setCustomModel] = useState("");
|
||||||
const [variant, setVariant] = useState("");
|
const [isCustomModel, setIsCustomModel] = useState(false);
|
||||||
|
const [configLoadDone, setConfigLoadDone] = useState(false);
|
||||||
const [mcpExpanded, setMcpExpanded] = useState(false);
|
|
||||||
const [skillsExpanded, setSkillsExpanded] = useState(false);
|
|
||||||
|
|
||||||
// Skill add/edit state
|
|
||||||
const [addingSkill, setAddingSkill] = useState(false);
|
|
||||||
const [editingSkillIndex, setEditingSkillIndex] = useState<number | null>(null);
|
|
||||||
const [skillType, setSkillType] = useState<"github" | "local" | "git">("github");
|
|
||||||
const [skillSource, setSkillSource] = useState("");
|
|
||||||
const [skillFilter, setSkillFilter] = useState("");
|
|
||||||
const [skillRef, setSkillRef] = useState("");
|
|
||||||
const [skillSubpath, setSkillSubpath] = useState("");
|
|
||||||
const [skillLocalError, setSkillLocalError] = useState<string | null>(null);
|
|
||||||
const skillSourceRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// MCP add/edit state
|
|
||||||
const [addingMcp, setAddingMcp] = useState(false);
|
|
||||||
const [editingMcpIndex, setEditingMcpIndex] = useState<number | null>(null);
|
|
||||||
const [mcpName, setMcpName] = useState("");
|
|
||||||
const [mcpJson, setMcpJson] = useState("");
|
|
||||||
const [mcpLocalError, setMcpLocalError] = useState<string | null>(null);
|
|
||||||
const mcpNameRef = useRef<HTMLInputElement>(null);
|
|
||||||
const mcpJsonRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const cancelSkillEdit = () => {
|
|
||||||
setAddingSkill(false);
|
|
||||||
setEditingSkillIndex(null);
|
|
||||||
setSkillType("github");
|
|
||||||
setSkillSource("");
|
|
||||||
setSkillFilter("");
|
|
||||||
setSkillRef("");
|
|
||||||
setSkillSubpath("");
|
|
||||||
setSkillLocalError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset state when menu closes
|
// Reset state when menu closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -158,20 +56,21 @@ const SessionCreateMenu = ({
|
||||||
setPhase("agent");
|
setPhase("agent");
|
||||||
setSelectedAgent("");
|
setSelectedAgent("");
|
||||||
setAgentMode("");
|
setAgentMode("");
|
||||||
setPermissionMode("default");
|
setSelectedModel("");
|
||||||
setModel("");
|
setCustomModel("");
|
||||||
setVariant("");
|
setIsCustomModel(false);
|
||||||
setMcpExpanded(false);
|
setConfigLoadDone(false);
|
||||||
setSkillsExpanded(false);
|
|
||||||
cancelSkillEdit();
|
|
||||||
setAddingMcp(false);
|
|
||||||
setEditingMcpIndex(null);
|
|
||||||
setMcpName("");
|
|
||||||
setMcpJson("");
|
|
||||||
setMcpLocalError(null);
|
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// Transition to config phase after load completes — deferred via useEffect
|
||||||
|
// so parent props (modelsByAgent) have settled before we render the config form
|
||||||
|
useEffect(() => {
|
||||||
|
if (phase === "loading-config" && configLoadDone) {
|
||||||
|
setPhase("config");
|
||||||
|
}
|
||||||
|
}, [phase, configLoadDone]);
|
||||||
|
|
||||||
// Auto-select first mode when modes load for selected agent
|
// Auto-select first mode when modes load for selected agent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedAgent) return;
|
if (!selectedAgent) return;
|
||||||
|
|
@ -181,174 +80,60 @@ const SessionCreateMenu = ({
|
||||||
}
|
}
|
||||||
}, [modesByAgent, selectedAgent, agentMode]);
|
}, [modesByAgent, selectedAgent, agentMode]);
|
||||||
|
|
||||||
// Focus skill source input when adding
|
// Auto-select default model when agent is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((addingSkill || editingSkillIndex !== null) && skillSourceRef.current) {
|
if (!selectedAgent) return;
|
||||||
skillSourceRef.current.focus();
|
if (selectedModel) return;
|
||||||
|
const defaultModel = defaultModelByAgent[selectedAgent];
|
||||||
|
if (defaultModel) {
|
||||||
|
setSelectedModel(defaultModel);
|
||||||
|
} else {
|
||||||
|
const models = modelsByAgent[selectedAgent];
|
||||||
|
if (models && models.length > 0) {
|
||||||
|
setSelectedModel(models[0].id);
|
||||||
}
|
}
|
||||||
}, [addingSkill, editingSkillIndex]);
|
|
||||||
|
|
||||||
// Focus MCP name input when adding
|
|
||||||
useEffect(() => {
|
|
||||||
if (addingMcp && mcpNameRef.current) {
|
|
||||||
mcpNameRef.current.focus();
|
|
||||||
}
|
}
|
||||||
}, [addingMcp]);
|
}, [modelsByAgent, defaultModelByAgent, selectedAgent, selectedModel]);
|
||||||
|
|
||||||
// Focus MCP json textarea when editing
|
|
||||||
useEffect(() => {
|
|
||||||
if (editingMcpIndex !== null && mcpJsonRef.current) {
|
|
||||||
mcpJsonRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [editingMcpIndex]);
|
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
const handleAgentClick = (agentId: string) => {
|
const handleAgentClick = (agentId: string) => {
|
||||||
setSelectedAgent(agentId);
|
setSelectedAgent(agentId);
|
||||||
setPhase("config");
|
setPhase("loading-config");
|
||||||
onSelectAgent(agentId);
|
setConfigLoadDone(false);
|
||||||
|
onSelectAgent(agentId).finally(() => {
|
||||||
|
setConfigLoadDone(true);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setPhase("agent");
|
setPhase("agent");
|
||||||
setSelectedAgent("");
|
setSelectedAgent("");
|
||||||
setAgentMode("");
|
setAgentMode("");
|
||||||
setPermissionMode("default");
|
setSelectedModel("");
|
||||||
setModel("");
|
setCustomModel("");
|
||||||
setVariant("");
|
setIsCustomModel(false);
|
||||||
|
setConfigLoadDone(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModelSelectChange = (value: string) => {
|
||||||
|
if (value === CUSTOM_MODEL_VALUE) {
|
||||||
|
setIsCustomModel(true);
|
||||||
|
setSelectedModel("");
|
||||||
|
} else {
|
||||||
|
setIsCustomModel(false);
|
||||||
|
setCustomModel("");
|
||||||
|
setSelectedModel(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvedModel = isCustomModel ? customModel : selectedModel;
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (mcpConfigError) return;
|
onCreateSession(selectedAgent, { agentMode, model: resolvedModel });
|
||||||
onCreateSession(selectedAgent, { model, agentMode, permissionMode, variant });
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skill source helpers
|
|
||||||
const startAddSkill = () => {
|
|
||||||
setAddingSkill(true);
|
|
||||||
setEditingSkillIndex(null);
|
|
||||||
setSkillType("github");
|
|
||||||
setSkillSource("rivet-dev/skills");
|
|
||||||
setSkillFilter("sandbox-agent");
|
|
||||||
setSkillRef("");
|
|
||||||
setSkillSubpath("");
|
|
||||||
setSkillLocalError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startEditSkill = (index: number) => {
|
|
||||||
const entry = skillSources[index];
|
|
||||||
setEditingSkillIndex(index);
|
|
||||||
setAddingSkill(false);
|
|
||||||
setSkillType(entry.type as "github" | "local" | "git");
|
|
||||||
setSkillSource(entry.source);
|
|
||||||
setSkillFilter(entry.skills?.join(", ") ?? "");
|
|
||||||
setSkillRef(entry.ref ?? "");
|
|
||||||
setSkillSubpath(entry.subpath ?? "");
|
|
||||||
setSkillLocalError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const commitSkill = () => {
|
|
||||||
const src = skillSource.trim();
|
|
||||||
if (!src) {
|
|
||||||
setSkillLocalError("Source is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const entry: SkillSource = {
|
|
||||||
type: skillType,
|
|
||||||
source: src,
|
|
||||||
};
|
|
||||||
const filterList = skillFilter.trim()
|
|
||||||
? skillFilter.split(",").map((s) => s.trim()).filter(Boolean)
|
|
||||||
: undefined;
|
|
||||||
if (filterList && filterList.length > 0) entry.skills = filterList;
|
|
||||||
if (skillRef.trim()) entry.ref = skillRef.trim();
|
|
||||||
if (skillSubpath.trim()) entry.subpath = skillSubpath.trim();
|
|
||||||
|
|
||||||
if (editingSkillIndex !== null) {
|
|
||||||
const updated = [...skillSources];
|
|
||||||
updated[editingSkillIndex] = entry;
|
|
||||||
onSkillSourcesChange(updated);
|
|
||||||
} else {
|
|
||||||
onSkillSourcesChange([...skillSources, entry]);
|
|
||||||
}
|
|
||||||
cancelSkillEdit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSkill = (index: number) => {
|
|
||||||
onSkillSourcesChange(skillSources.filter((_, i) => i !== index));
|
|
||||||
if (editingSkillIndex === index) {
|
|
||||||
cancelSkillEdit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEditingSkill = addingSkill || editingSkillIndex !== null;
|
|
||||||
|
|
||||||
const startAddMcp = () => {
|
|
||||||
setAddingMcp(true);
|
|
||||||
setEditingMcpIndex(null);
|
|
||||||
setMcpName("everything");
|
|
||||||
setMcpJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}');
|
|
||||||
setMcpLocalError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startEditMcp = (index: number) => {
|
|
||||||
const entry = mcpServers[index];
|
|
||||||
setEditingMcpIndex(index);
|
|
||||||
setAddingMcp(false);
|
|
||||||
setMcpName(entry.name);
|
|
||||||
setMcpJson(entry.configJson);
|
|
||||||
setMcpLocalError(entry.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelMcpEdit = () => {
|
|
||||||
setAddingMcp(false);
|
|
||||||
setEditingMcpIndex(null);
|
|
||||||
setMcpName("");
|
|
||||||
setMcpJson("");
|
|
||||||
setMcpLocalError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const commitMcp = () => {
|
|
||||||
const name = mcpName.trim();
|
|
||||||
if (!name) {
|
|
||||||
setMcpLocalError("Server name is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const error = validateServerJson(mcpJson);
|
|
||||||
if (error) {
|
|
||||||
setMcpLocalError(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for duplicate names (except when editing the same entry)
|
|
||||||
const duplicate = mcpServers.findIndex((e) => e.name === name);
|
|
||||||
if (duplicate !== -1 && duplicate !== editingMcpIndex) {
|
|
||||||
setMcpLocalError(`Server "${name}" already exists`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry: McpServerEntry = { name, configJson: mcpJson.trim(), error: null };
|
|
||||||
|
|
||||||
if (editingMcpIndex !== null) {
|
|
||||||
const updated = [...mcpServers];
|
|
||||||
updated[editingMcpIndex] = entry;
|
|
||||||
onMcpServersChange(updated);
|
|
||||||
} else {
|
|
||||||
onMcpServersChange([...mcpServers, entry]);
|
|
||||||
}
|
|
||||||
cancelMcpEdit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeMcp = (index: number) => {
|
|
||||||
onMcpServersChange(mcpServers.filter((_, i) => i !== index));
|
|
||||||
if (editingMcpIndex === index) {
|
|
||||||
cancelMcpEdit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEditingMcp = addingMcp || editingMcpIndex !== null;
|
|
||||||
|
|
||||||
if (phase === "agent") {
|
if (phase === "agent") {
|
||||||
return (
|
return (
|
||||||
<div className="session-create-menu">
|
<div className="session-create-menu">
|
||||||
|
|
@ -378,30 +163,25 @@ const SessionCreateMenu = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agentLabel = agentLabels[selectedAgent] ?? selectedAgent;
|
||||||
|
|
||||||
|
if (phase === "loading-config") {
|
||||||
|
return (
|
||||||
|
<div className="session-create-menu">
|
||||||
|
<div className="session-create-header">
|
||||||
|
<button className="session-create-back" onClick={handleBack} title="Back to agents">
|
||||||
|
<ArrowLeft size={14} />
|
||||||
|
</button>
|
||||||
|
<span className="session-create-agent-name">{agentLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div className="sidebar-add-status">Loading config...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 2: config form
|
// Phase 2: config form
|
||||||
const activeModes = modesByAgent[selectedAgent] ?? [];
|
const activeModes = modesByAgent[selectedAgent] ?? [];
|
||||||
const modesLoading = modesLoadingByAgent[selectedAgent] ?? false;
|
const activeModels = modelsByAgent[selectedAgent] ?? [];
|
||||||
const modesError = modesErrorByAgent[selectedAgent] ?? null;
|
|
||||||
const modelOptions = modelsByAgent[selectedAgent] ?? [];
|
|
||||||
const modelsLoading = modelsLoadingByAgent[selectedAgent] ?? false;
|
|
||||||
const modelsError = modelsErrorByAgent[selectedAgent] ?? null;
|
|
||||||
const defaultModel = defaultModelByAgent[selectedAgent] ?? "";
|
|
||||||
const selectedModelId = model || defaultModel;
|
|
||||||
const selectedModelObj = modelOptions.find((entry) => entry.id === selectedModelId);
|
|
||||||
const variantOptions = selectedModelObj?.variants ?? [];
|
|
||||||
const showModelSelect = modelsLoading || Boolean(modelsError) || modelOptions.length > 0;
|
|
||||||
const hasModelOptions = modelOptions.length > 0;
|
|
||||||
const modelCustom =
|
|
||||||
model && hasModelOptions && !modelOptions.some((entry) => entry.id === model);
|
|
||||||
const supportsVariants =
|
|
||||||
modelsLoading ||
|
|
||||||
Boolean(modelsError) ||
|
|
||||||
modelOptions.some((entry) => (entry.variants?.length ?? 0) > 0);
|
|
||||||
const showVariantSelect =
|
|
||||||
supportsVariants && (modelsLoading || Boolean(modelsError) || variantOptions.length > 0);
|
|
||||||
const hasVariantOptions = variantOptions.length > 0;
|
|
||||||
const variantCustom = variant && hasVariantOptions && !variantOptions.includes(variant);
|
|
||||||
const agentLabel = agentLabels[selectedAgent] ?? selectedAgent;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="session-create-menu">
|
<div className="session-create-menu">
|
||||||
|
|
@ -415,43 +195,49 @@ const SessionCreateMenu = ({
|
||||||
<div className="session-create-form">
|
<div className="session-create-form">
|
||||||
<div className="setup-field">
|
<div className="setup-field">
|
||||||
<span className="setup-label">Model</span>
|
<span className="setup-label">Model</span>
|
||||||
{showModelSelect ? (
|
{isCustomModel ? (
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={model}
|
|
||||||
onChange={(e) => { setModel(e.target.value); setVariant(""); }}
|
|
||||||
title="Model"
|
|
||||||
disabled={modelsLoading || Boolean(modelsError)}
|
|
||||||
>
|
|
||||||
{modelsLoading ? (
|
|
||||||
<option value="">Loading models...</option>
|
|
||||||
) : modelsError ? (
|
|
||||||
<option value="">{modelsError}</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="">
|
|
||||||
{defaultModel ? `Default (${defaultModel})` : "Default"}
|
|
||||||
</option>
|
|
||||||
{modelCustom && <option value={model}>{model} (custom)</option>}
|
|
||||||
{modelOptions.map((entry) => (
|
|
||||||
<option key={entry.id} value={entry.id}>
|
|
||||||
{entry.name ?? entry.id}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<input
|
<input
|
||||||
className="setup-input"
|
className="setup-input"
|
||||||
value={model}
|
type="text"
|
||||||
onChange={(e) => setModel(e.target.value)}
|
value={customModel}
|
||||||
placeholder="Model"
|
onChange={(e) => setCustomModel(e.target.value)}
|
||||||
title="Model"
|
placeholder="Enter model name..."
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
className="setup-select"
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={(e) => handleModelSelectChange(e.target.value)}
|
||||||
|
title="Model"
|
||||||
|
>
|
||||||
|
{activeModels.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.name || m.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value={CUSTOM_MODEL_VALUE}>Custom...</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{isCustomModel && (
|
||||||
|
<button
|
||||||
|
className="setup-custom-back"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCustomModel(false);
|
||||||
|
setCustomModel("");
|
||||||
|
const defaultModel = defaultModelByAgent[selectedAgent];
|
||||||
|
setSelectedModel(
|
||||||
|
defaultModel || (activeModels.length > 0 ? activeModels[0].id : "")
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
title="Back to model list"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
← List
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{activeModes.length > 0 && (
|
||||||
<div className="setup-field">
|
<div className="setup-field">
|
||||||
<span className="setup-label">Mode</span>
|
<span className="setup-label">Mode</span>
|
||||||
<select
|
<select
|
||||||
|
|
@ -459,286 +245,19 @@ const SessionCreateMenu = ({
|
||||||
value={agentMode}
|
value={agentMode}
|
||||||
onChange={(e) => setAgentMode(e.target.value)}
|
onChange={(e) => setAgentMode(e.target.value)}
|
||||||
title="Mode"
|
title="Mode"
|
||||||
disabled={modesLoading || Boolean(modesError)}
|
|
||||||
>
|
>
|
||||||
{modesLoading ? (
|
{activeModes.map((m) => (
|
||||||
<option value="">Loading modes...</option>
|
|
||||||
) : modesError ? (
|
|
||||||
<option value="">{modesError}</option>
|
|
||||||
) : activeModes.length > 0 ? (
|
|
||||||
activeModes.map((m) => (
|
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.name || m.id}
|
{m.name || m.id}
|
||||||
</option>
|
</option>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<option value="">Mode</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="setup-field">
|
|
||||||
<span className="setup-label">Permission</span>
|
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={permissionMode}
|
|
||||||
onChange={(e) => setPermissionMode(e.target.value)}
|
|
||||||
title="Permission Mode"
|
|
||||||
>
|
|
||||||
<option value="default">Default</option>
|
|
||||||
<option value="plan">Plan</option>
|
|
||||||
<option value="bypass">Bypass</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{supportsVariants && (
|
|
||||||
<div className="setup-field">
|
|
||||||
<span className="setup-label">Variant</span>
|
|
||||||
{showVariantSelect ? (
|
|
||||||
<select
|
|
||||||
className="setup-select"
|
|
||||||
value={variant}
|
|
||||||
onChange={(e) => setVariant(e.target.value)}
|
|
||||||
title="Variant"
|
|
||||||
disabled={modelsLoading || Boolean(modelsError)}
|
|
||||||
>
|
|
||||||
{modelsLoading ? (
|
|
||||||
<option value="">Loading variants...</option>
|
|
||||||
) : modelsError ? (
|
|
||||||
<option value="">{modelsError}</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="">Default</option>
|
|
||||||
{variantCustom && <option value={variant}>{variant} (custom)</option>}
|
|
||||||
{variantOptions.map((entry) => (
|
|
||||||
<option key={entry} value={entry}>
|
|
||||||
{entry}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className="setup-input"
|
|
||||||
value={variant}
|
|
||||||
onChange={(e) => setVariant(e.target.value)}
|
|
||||||
placeholder="Variant"
|
|
||||||
title="Variant"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* MCP Servers - collapsible */}
|
|
||||||
<div className="session-create-section">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-section-toggle"
|
|
||||||
onClick={() => setMcpExpanded(!mcpExpanded)}
|
|
||||||
>
|
|
||||||
<span className="setup-label">MCP</span>
|
|
||||||
<span className="session-create-section-count">{mcpServers.length} server{mcpServers.length !== 1 ? "s" : ""}</span>
|
|
||||||
{mcpExpanded ? <ChevronDown size={12} className="session-create-section-arrow" /> : <ChevronRight size={12} className="session-create-section-arrow" />}
|
|
||||||
</button>
|
|
||||||
{mcpExpanded && (
|
|
||||||
<div className="session-create-section-body">
|
|
||||||
{mcpServers.length > 0 && !isEditingMcp && (
|
|
||||||
<div className="session-create-mcp-list">
|
|
||||||
{mcpServers.map((entry, index) => (
|
|
||||||
<div key={entry.name} className="session-create-mcp-item">
|
|
||||||
<div className="session-create-mcp-info">
|
|
||||||
<span className="session-create-mcp-name">{entry.name}</span>
|
|
||||||
{getServerType(entry.configJson) && (
|
|
||||||
<span className="session-create-mcp-type">{getServerType(entry.configJson)}</span>
|
|
||||||
)}
|
|
||||||
<span className="session-create-mcp-summary mono">{getServerSummary(entry.configJson)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="session-create-mcp-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-skill-remove"
|
|
||||||
onClick={() => startEditMcp(index)}
|
|
||||||
title="Edit server"
|
|
||||||
>
|
|
||||||
<Pencil size={10} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-skill-remove"
|
|
||||||
onClick={() => removeMcp(index)}
|
|
||||||
title="Remove server"
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isEditingMcp ? (
|
|
||||||
<div className="session-create-mcp-edit">
|
|
||||||
<input
|
|
||||||
ref={mcpNameRef}
|
|
||||||
className="session-create-mcp-name-input"
|
|
||||||
value={mcpName}
|
|
||||||
onChange={(e) => { setMcpName(e.target.value); setMcpLocalError(null); }}
|
|
||||||
placeholder="server-name"
|
|
||||||
disabled={editingMcpIndex !== null}
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
ref={mcpJsonRef}
|
|
||||||
className="session-create-textarea mono"
|
|
||||||
value={mcpJson}
|
|
||||||
onChange={(e) => { setMcpJson(e.target.value); setMcpLocalError(null); }}
|
|
||||||
placeholder='{"type":"local","command":"node","args":["./server.js"]}'
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
{mcpLocalError && (
|
|
||||||
<div className="session-create-inline-error">{mcpLocalError}</div>
|
|
||||||
)}
|
|
||||||
<div className="session-create-mcp-edit-actions">
|
|
||||||
<button type="button" className="session-create-mcp-save" onClick={commitMcp}>
|
|
||||||
{editingMcpIndex !== null ? "Save" : "Add"}
|
|
||||||
</button>
|
|
||||||
<button type="button" className="session-create-mcp-cancel" onClick={cancelMcpEdit}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-add-btn"
|
|
||||||
onClick={startAddMcp}
|
|
||||||
>
|
|
||||||
<Plus size={12} />
|
|
||||||
Add server
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{mcpConfigError && !isEditingMcp && (
|
|
||||||
<div className="session-create-inline-error">{mcpConfigError}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Skills - collapsible with source-based list */}
|
|
||||||
<div className="session-create-section">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-section-toggle"
|
|
||||||
onClick={() => setSkillsExpanded(!skillsExpanded)}
|
|
||||||
>
|
|
||||||
<span className="setup-label">Skills</span>
|
|
||||||
<span className="session-create-section-count">{skillSources.length} source{skillSources.length !== 1 ? "s" : ""}</span>
|
|
||||||
{skillsExpanded ? <ChevronDown size={12} className="session-create-section-arrow" /> : <ChevronRight size={12} className="session-create-section-arrow" />}
|
|
||||||
</button>
|
|
||||||
{skillsExpanded && (
|
|
||||||
<div className="session-create-section-body">
|
|
||||||
{skillSources.length > 0 && !isEditingSkill && (
|
|
||||||
<div className="session-create-skill-list">
|
|
||||||
{skillSources.map((entry, index) => (
|
|
||||||
<div key={`${entry.type}-${entry.source}-${index}`} className="session-create-skill-item">
|
|
||||||
<span className="session-create-skill-type-badge">{entry.type}</span>
|
|
||||||
<span className="session-create-skill-path mono">{skillSourceSummary(entry)}</span>
|
|
||||||
<div className="session-create-mcp-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-skill-remove"
|
|
||||||
onClick={() => startEditSkill(index)}
|
|
||||||
title="Edit source"
|
|
||||||
>
|
|
||||||
<Pencil size={10} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-skill-remove"
|
|
||||||
onClick={() => removeSkill(index)}
|
|
||||||
title="Remove source"
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isEditingSkill ? (
|
|
||||||
<div className="session-create-mcp-edit">
|
|
||||||
<div className="session-create-skill-type-row">
|
|
||||||
<select
|
|
||||||
className="session-create-skill-type-select"
|
|
||||||
value={skillType}
|
|
||||||
onChange={(e) => { setSkillType(e.target.value as "github" | "local" | "git"); setSkillLocalError(null); }}
|
|
||||||
>
|
|
||||||
<option value="github">github</option>
|
|
||||||
<option value="local">local</option>
|
|
||||||
<option value="git">git</option>
|
|
||||||
</select>
|
|
||||||
<input
|
|
||||||
ref={skillSourceRef}
|
|
||||||
className="session-create-skill-input mono"
|
|
||||||
value={skillSource}
|
|
||||||
onChange={(e) => { setSkillSource(e.target.value); setSkillLocalError(null); }}
|
|
||||||
placeholder={skillType === "github" ? "owner/repo" : skillType === "local" ? "/path/to/skill" : "https://git.example.com/repo.git"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="session-create-skill-input mono"
|
|
||||||
value={skillFilter}
|
|
||||||
onChange={(e) => setSkillFilter(e.target.value)}
|
|
||||||
placeholder="Filter skills (comma-separated, optional)"
|
|
||||||
/>
|
|
||||||
{skillType !== "local" && (
|
|
||||||
<div className="session-create-skill-type-row">
|
|
||||||
<input
|
|
||||||
className="session-create-skill-input mono"
|
|
||||||
value={skillRef}
|
|
||||||
onChange={(e) => setSkillRef(e.target.value)}
|
|
||||||
placeholder="Branch/tag (optional)"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="session-create-skill-input mono"
|
|
||||||
value={skillSubpath}
|
|
||||||
onChange={(e) => setSkillSubpath(e.target.value)}
|
|
||||||
placeholder="Subpath (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{skillLocalError && (
|
|
||||||
<div className="session-create-inline-error">{skillLocalError}</div>
|
|
||||||
)}
|
|
||||||
<div className="session-create-mcp-edit-actions">
|
|
||||||
<button type="button" className="session-create-mcp-save" onClick={commitSkill}>
|
|
||||||
{editingSkillIndex !== null ? "Save" : "Add"}
|
|
||||||
</button>
|
|
||||||
<button type="button" className="session-create-mcp-cancel" onClick={cancelSkillEdit}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-create-add-btn"
|
|
||||||
onClick={startAddSkill}
|
|
||||||
>
|
|
||||||
<Plus size={12} />
|
|
||||||
Add source
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="session-create-actions">
|
<div className="session-create-actions">
|
||||||
<button
|
<button className="button primary" onClick={handleCreate}>
|
||||||
className="button primary"
|
|
||||||
onClick={handleCreate}
|
|
||||||
disabled={Boolean(mcpConfigError)}
|
|
||||||
>
|
|
||||||
Create Session
|
Create Session
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
import { getAvatarLabel, getMessageClass } from "./messageUtils";
|
import { getAvatarLabel, getMessageClass } from "./messageUtils";
|
||||||
import renderContentPart from "./renderContentPart";
|
|
||||||
import type { TimelineEntry } from "./types";
|
import type { TimelineEntry } from "./types";
|
||||||
|
import { formatJson } from "../../utils/format";
|
||||||
|
|
||||||
const ChatMessages = ({
|
const ChatMessages = ({
|
||||||
entries,
|
entries,
|
||||||
sessionError,
|
sessionError,
|
||||||
eventError,
|
|
||||||
messagesEndRef
|
messagesEndRef
|
||||||
}: {
|
}: {
|
||||||
entries: TimelineEntry[];
|
entries: TimelineEntry[];
|
||||||
sessionError: string | null;
|
sessionError: string | null;
|
||||||
eventError: string | null;
|
|
||||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="messages">
|
<div className="messages">
|
||||||
{entries.map((entry) => {
|
{entries.map((entry) => {
|
||||||
|
const messageClass = getMessageClass(entry);
|
||||||
|
|
||||||
if (entry.kind === "meta") {
|
if (entry.kind === "meta") {
|
||||||
const messageClass = entry.meta?.severity === "error" ? "error" : "system";
|
|
||||||
return (
|
return (
|
||||||
<div key={entry.id} className={`message ${messageClass}`}>
|
<div key={entry.id} className={`message ${messageClass}`}>
|
||||||
<div className="avatar">{getAvatarLabel(messageClass)}</div>
|
<div className="avatar">{getAvatarLabel(messageClass)}</div>
|
||||||
|
|
@ -31,53 +30,73 @@ const ChatMessages = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = entry.item;
|
if (entry.kind === "reasoning") {
|
||||||
if (!item) return null;
|
|
||||||
const hasParts = (item.content ?? []).length > 0;
|
|
||||||
const isInProgress = item.status === "in_progress";
|
|
||||||
const isFailed = item.status === "failed";
|
|
||||||
const messageClass = getMessageClass(item);
|
|
||||||
const statusValue = item.status ?? "";
|
|
||||||
const statusLabel =
|
|
||||||
statusValue && statusValue !== "completed" ? statusValue.replace("_", " ") : "";
|
|
||||||
const kindLabel = item.kind.replace("_", " ");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={entry.id} className={`message ${messageClass} ${isFailed ? "error" : ""}`}>
|
<div key={entry.id} className="message assistant">
|
||||||
<div className="avatar">{getAvatarLabel(isFailed ? "error" : messageClass)}</div>
|
<div className="avatar">AI</div>
|
||||||
<div className="message-content">
|
<div className="message-content">
|
||||||
{(item.kind !== "message" || item.status !== "completed") && (
|
|
||||||
<div className="message-meta">
|
<div className="message-meta">
|
||||||
<span>{kindLabel}</span>
|
<span>reasoning - {entry.reasoning?.visibility ?? "public"}</span>
|
||||||
{statusLabel && (
|
</div>
|
||||||
<span className={`pill ${item.status === "failed" ? "danger" : "accent"}`}>
|
<div className="part-body muted">{entry.reasoning?.text ?? ""}</div>
|
||||||
{statusLabel}
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.kind === "tool") {
|
||||||
|
const isComplete = entry.toolStatus === "completed" || entry.toolStatus === "failed";
|
||||||
|
const isFailed = entry.toolStatus === "failed";
|
||||||
|
return (
|
||||||
|
<div key={entry.id} className={`message tool ${isFailed ? "error" : ""}`}>
|
||||||
|
<div className="avatar">{getAvatarLabel(isFailed ? "error" : "tool")}</div>
|
||||||
|
<div className="message-content">
|
||||||
|
<div className="message-meta">
|
||||||
|
<span>tool call - {entry.toolName}</span>
|
||||||
|
{entry.toolStatus && entry.toolStatus !== "completed" && (
|
||||||
|
<span className={`pill ${isFailed ? "danger" : "accent"}`}>
|
||||||
|
{entry.toolStatus.replace("_", " ")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{entry.toolInput && <pre className="code-block">{entry.toolInput}</pre>}
|
||||||
|
{isComplete && entry.toolOutput && (
|
||||||
|
<div className="part">
|
||||||
|
<div className="part-title">result</div>
|
||||||
|
<pre className="code-block">{entry.toolOutput}</pre>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasParts ? (
|
{!isComplete && !entry.toolInput && (
|
||||||
(item.content ?? []).map(renderContentPart)
|
|
||||||
) : entry.deltaText ? (
|
|
||||||
<span>
|
|
||||||
{entry.deltaText}
|
|
||||||
{isInProgress && <span className="cursor" />}
|
|
||||||
</span>
|
|
||||||
) : isInProgress ? (
|
|
||||||
<span className="thinking-indicator">
|
<span className="thinking-indicator">
|
||||||
<span className="thinking-dot" />
|
<span className="thinking-dot" />
|
||||||
<span className="thinking-dot" />
|
<span className="thinking-dot" />
|
||||||
<span className="thinking-dot" />
|
<span className="thinking-dot" />
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message (user or assistant)
|
||||||
|
return (
|
||||||
|
<div key={entry.id} className={`message ${messageClass}`}>
|
||||||
|
<div className="avatar">{getAvatarLabel(messageClass)}</div>
|
||||||
|
<div className="message-content">
|
||||||
|
{entry.text ? (
|
||||||
|
<div className="part-body">{entry.text}</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="muted">No content yet.</span>
|
<span className="thinking-indicator">
|
||||||
|
<span className="thinking-dot" />
|
||||||
|
<span className="thinking-dot" />
|
||||||
|
<span className="thinking-dot" />
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{sessionError && <div className="message-error">{sessionError}</div>}
|
{sessionError && <div className="message-error">{sessionError}</div>}
|
||||||
{eventError && <div className="message-error">{eventError}</div>}
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
import { MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
import { CheckSquare, MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { McpServerEntry } from "../../App";
|
import type { AgentInfo } from "sandbox-agent";
|
||||||
import type {
|
|
||||||
AgentInfo,
|
type AgentModeInfo = { id: string; name: string; description: string };
|
||||||
AgentModelInfo,
|
type AgentModelInfo = { id: string; name?: string };
|
||||||
AgentModeInfo,
|
|
||||||
PermissionEventData,
|
|
||||||
QuestionEventData,
|
|
||||||
SkillSource
|
|
||||||
} from "../../types/legacyApi";
|
|
||||||
import ApprovalsTab from "../debug/ApprovalsTab";
|
|
||||||
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
||||||
import ChatInput from "./ChatInput";
|
import ChatInput from "./ChatInput";
|
||||||
import ChatMessages from "./ChatMessages";
|
import ChatMessages from "./ChatMessages";
|
||||||
|
|
@ -31,32 +25,11 @@ const ChatPanel = ({
|
||||||
messagesEndRef,
|
messagesEndRef,
|
||||||
agentLabel,
|
agentLabel,
|
||||||
currentAgentVersion,
|
currentAgentVersion,
|
||||||
sessionModel,
|
sessionEnded,
|
||||||
sessionVariant,
|
|
||||||
sessionPermissionMode,
|
|
||||||
sessionMcpServerCount,
|
|
||||||
sessionSkillSourceCount,
|
|
||||||
onEndSession,
|
onEndSession,
|
||||||
eventError,
|
|
||||||
questionRequests,
|
|
||||||
permissionRequests,
|
|
||||||
questionSelections,
|
|
||||||
onSelectQuestionOption,
|
|
||||||
onAnswerQuestion,
|
|
||||||
onRejectQuestion,
|
|
||||||
onReplyPermission,
|
|
||||||
modesByAgent,
|
modesByAgent,
|
||||||
modelsByAgent,
|
modelsByAgent,
|
||||||
defaultModelByAgent,
|
defaultModelByAgent,
|
||||||
modesLoadingByAgent,
|
|
||||||
modelsLoadingByAgent,
|
|
||||||
modesErrorByAgent,
|
|
||||||
modelsErrorByAgent,
|
|
||||||
mcpServers,
|
|
||||||
onMcpServersChange,
|
|
||||||
mcpConfigError,
|
|
||||||
skillSources,
|
|
||||||
onSkillSourcesChange
|
|
||||||
}: {
|
}: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
transcriptEntries: TimelineEntry[];
|
transcriptEntries: TimelineEntry[];
|
||||||
|
|
@ -66,39 +39,18 @@ const ChatPanel = ({
|
||||||
onSendMessage: () => void;
|
onSendMessage: () => void;
|
||||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||||
onSelectAgent: (agentId: string) => void;
|
onSelectAgent: (agentId: string) => Promise<void>;
|
||||||
agents: AgentInfo[];
|
agents: AgentInfo[];
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||||
agentLabel: string;
|
agentLabel: string;
|
||||||
currentAgentVersion?: string | null;
|
currentAgentVersion?: string | null;
|
||||||
sessionModel?: string | null;
|
sessionEnded: boolean;
|
||||||
sessionVariant?: string | null;
|
|
||||||
sessionPermissionMode?: string | null;
|
|
||||||
sessionMcpServerCount: number;
|
|
||||||
sessionSkillSourceCount: number;
|
|
||||||
onEndSession: () => void;
|
onEndSession: () => void;
|
||||||
eventError: string | null;
|
|
||||||
questionRequests: QuestionEventData[];
|
|
||||||
permissionRequests: PermissionEventData[];
|
|
||||||
questionSelections: Record<string, string[][]>;
|
|
||||||
onSelectQuestionOption: (requestId: string, optionLabel: string) => void;
|
|
||||||
onAnswerQuestion: (request: QuestionEventData) => void;
|
|
||||||
onRejectQuestion: (requestId: string) => void;
|
|
||||||
onReplyPermission: (requestId: string, reply: "once" | "always" | "reject") => void;
|
|
||||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||||
modelsByAgent: Record<string, AgentModelInfo[]>;
|
modelsByAgent: Record<string, AgentModelInfo[]>;
|
||||||
defaultModelByAgent: Record<string, string>;
|
defaultModelByAgent: Record<string, string>;
|
||||||
modesLoadingByAgent: Record<string, boolean>;
|
|
||||||
modelsLoadingByAgent: Record<string, boolean>;
|
|
||||||
modesErrorByAgent: Record<string, string | null>;
|
|
||||||
modelsErrorByAgent: Record<string, string | null>;
|
|
||||||
mcpServers: McpServerEntry[];
|
|
||||||
onMcpServersChange: (servers: McpServerEntry[]) => void;
|
|
||||||
mcpConfigError: string | null;
|
|
||||||
skillSources: SkillSource[];
|
|
||||||
onSkillSourcesChange: (sources: SkillSource[]) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [showAgentMenu, setShowAgentMenu] = useState(false);
|
const [showAgentMenu, setShowAgentMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
@ -115,8 +67,6 @@ const ChatPanel = ({
|
||||||
return () => document.removeEventListener("mousedown", handler);
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
}, [showAgentMenu]);
|
}, [showAgentMenu]);
|
||||||
|
|
||||||
const hasApprovals = questionRequests.length > 0 || permissionRequests.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-panel">
|
<div className="chat-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
|
|
@ -127,6 +77,12 @@ const ChatPanel = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-header-right">
|
<div className="panel-header-right">
|
||||||
{sessionId && (
|
{sessionId && (
|
||||||
|
sessionEnded ? (
|
||||||
|
<span className="button ghost small" style={{ opacity: 0.5, cursor: "default" }} title="Session ended">
|
||||||
|
<CheckSquare size={12} />
|
||||||
|
Ended
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button ghost small"
|
className="button ghost small"
|
||||||
|
|
@ -136,6 +92,7 @@ const ChatPanel = ({
|
||||||
<Square size={12} />
|
<Square size={12} />
|
||||||
End
|
End
|
||||||
</button>
|
</button>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -161,17 +118,8 @@ const ChatPanel = ({
|
||||||
modesByAgent={modesByAgent}
|
modesByAgent={modesByAgent}
|
||||||
modelsByAgent={modelsByAgent}
|
modelsByAgent={modelsByAgent}
|
||||||
defaultModelByAgent={defaultModelByAgent}
|
defaultModelByAgent={defaultModelByAgent}
|
||||||
modesLoadingByAgent={modesLoadingByAgent}
|
|
||||||
modelsLoadingByAgent={modelsLoadingByAgent}
|
|
||||||
modesErrorByAgent={modesErrorByAgent}
|
|
||||||
modelsErrorByAgent={modelsErrorByAgent}
|
|
||||||
mcpServers={mcpServers}
|
|
||||||
onMcpServersChange={onMcpServersChange}
|
|
||||||
mcpConfigError={mcpConfigError}
|
|
||||||
skillSources={skillSources}
|
|
||||||
onSkillSourcesChange={onSkillSourcesChange}
|
|
||||||
onSelectAgent={onSelectAgent}
|
|
||||||
onCreateSession={onCreateSession}
|
onCreateSession={onCreateSession}
|
||||||
|
onSelectAgent={onSelectAgent}
|
||||||
open={showAgentMenu}
|
open={showAgentMenu}
|
||||||
onClose={() => setShowAgentMenu(false)}
|
onClose={() => setShowAgentMenu(false)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -187,27 +135,11 @@ const ChatPanel = ({
|
||||||
<ChatMessages
|
<ChatMessages
|
||||||
entries={transcriptEntries}
|
entries={transcriptEntries}
|
||||||
sessionError={sessionError}
|
sessionError={sessionError}
|
||||||
eventError={eventError}
|
|
||||||
messagesEndRef={messagesEndRef}
|
messagesEndRef={messagesEndRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasApprovals && (
|
|
||||||
<div className="approvals-inline">
|
|
||||||
<div className="approvals-inline-header">Approvals</div>
|
|
||||||
<ApprovalsTab
|
|
||||||
questionRequests={questionRequests}
|
|
||||||
permissionRequests={permissionRequests}
|
|
||||||
questionSelections={questionSelections}
|
|
||||||
onSelectQuestionOption={onSelectQuestionOption}
|
|
||||||
onAnswerQuestion={onAnswerQuestion}
|
|
||||||
onRejectQuestion={onRejectQuestion}
|
|
||||||
onReplyPermission={onReplyPermission}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ChatInput
|
<ChatInput
|
||||||
message={message}
|
message={message}
|
||||||
onMessageChange={onMessageChange}
|
onMessageChange={onMessageChange}
|
||||||
|
|
@ -223,26 +155,12 @@ const ChatPanel = ({
|
||||||
<span className="session-config-label">Agent</span>
|
<span className="session-config-label">Agent</span>
|
||||||
<span className="session-config-value">{agentLabel}</span>
|
<span className="session-config-value">{agentLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{currentAgentVersion && (
|
||||||
<div className="session-config-field">
|
<div className="session-config-field">
|
||||||
<span className="session-config-label">Model</span>
|
<span className="session-config-label">Version</span>
|
||||||
<span className="session-config-value">{sessionModel || "-"}</span>
|
<span className="session-config-value">{currentAgentVersion}</span>
|
||||||
</div>
|
|
||||||
<div className="session-config-field">
|
|
||||||
<span className="session-config-label">Variant</span>
|
|
||||||
<span className="session-config-value">{sessionVariant || "-"}</span>
|
|
||||||
</div>
|
|
||||||
<div className="session-config-field">
|
|
||||||
<span className="session-config-label">Permission</span>
|
|
||||||
<span className="session-config-value">{sessionPermissionMode || "-"}</span>
|
|
||||||
</div>
|
|
||||||
<div className="session-config-field">
|
|
||||||
<span className="session-config-label">MCP Servers</span>
|
|
||||||
<span className="session-config-value">{sessionMcpServerCount}</span>
|
|
||||||
</div>
|
|
||||||
<div className="session-config-field">
|
|
||||||
<span className="session-config-label">Skills</span>
|
|
||||||
<span className="session-config-value">{sessionSkillSourceCount}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import type { UniversalItem } from "../../types/legacyApi";
|
import type { TimelineEntry } from "./types";
|
||||||
|
|
||||||
export const getMessageClass = (item: UniversalItem) => {
|
export const getMessageClass = (entry: TimelineEntry) => {
|
||||||
if (item.kind === "tool_call" || item.kind === "tool_result") return "tool";
|
if (entry.kind === "tool") return "tool";
|
||||||
if (item.kind === "system" || item.kind === "status") return "system";
|
if (entry.kind === "meta") return entry.meta?.severity === "error" ? "error" : "system";
|
||||||
if (item.role === "user") return "user";
|
if (entry.kind === "reasoning") return "assistant";
|
||||||
if (item.role === "tool") return "tool";
|
if (entry.role === "user") return "user";
|
||||||
if (item.role === "system") return "system";
|
|
||||||
return "assistant";
|
return "assistant";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import type { ContentPart } from "../../types/legacyApi";
|
|
||||||
import { formatJson } from "../../utils/format";
|
|
||||||
|
|
||||||
const renderContentPart = (part: ContentPart, index: number) => {
|
|
||||||
const partType = (part as { type?: string }).type ?? "unknown";
|
|
||||||
const key = `${partType}-${index}`;
|
|
||||||
switch (partType) {
|
|
||||||
case "text":
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-body">{(part as { text: string }).text}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "json":
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">json</div>
|
|
||||||
<pre className="code-block">{formatJson((part as { json: unknown }).json)}</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "tool_call": {
|
|
||||||
const { name, arguments: args, call_id } = part as {
|
|
||||||
name: string;
|
|
||||||
arguments: string;
|
|
||||||
call_id: string;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">
|
|
||||||
tool call - {name}
|
|
||||||
{call_id ? ` - ${call_id}` : ""}
|
|
||||||
</div>
|
|
||||||
{args ? <pre className="code-block">{args}</pre> : <div className="muted">No arguments</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "tool_result": {
|
|
||||||
const { call_id, output } = part as { call_id: string; output: string };
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">tool result - {call_id}</div>
|
|
||||||
{output ? <pre className="code-block">{output}</pre> : <div className="muted">No output</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "file_ref": {
|
|
||||||
const { path, action, diff } = part as { path: string; action: string; diff?: string | null };
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">file - {action}</div>
|
|
||||||
<div className="part-body mono">{path}</div>
|
|
||||||
{diff && <pre className="code-block">{diff}</pre>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "reasoning": {
|
|
||||||
const { text, visibility } = part as { text: string; visibility: string };
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">reasoning - {visibility}</div>
|
|
||||||
<div className="part-body muted">{text}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "image": {
|
|
||||||
const { path, mime } = part as { path: string; mime?: string | null };
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">image {mime ? `- ${mime}` : ""}</div>
|
|
||||||
<div className="part-body mono">{path}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "status": {
|
|
||||||
const { label, detail } = part as { label: string; detail?: string | null };
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">status - {label}</div>
|
|
||||||
{detail && <div className="part-body">{detail}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div key={key} className="part">
|
|
||||||
<div className="part-title">unknown</div>
|
|
||||||
<pre className="code-block">{formatJson(part)}</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default renderContentPart;
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import type { UniversalItem } from "../../types/legacyApi";
|
|
||||||
|
|
||||||
export type TimelineEntry = {
|
export type TimelineEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
kind: "item" | "meta";
|
kind: "message" | "tool" | "meta" | "reasoning";
|
||||||
time: string;
|
time: string;
|
||||||
item?: UniversalItem;
|
// For messages:
|
||||||
deltaText?: string;
|
role?: "user" | "assistant";
|
||||||
meta?: {
|
text?: string;
|
||||||
title: string;
|
// For tool calls:
|
||||||
detail?: string;
|
toolName?: string;
|
||||||
severity?: "info" | "error";
|
toolInput?: string;
|
||||||
};
|
toolOutput?: string;
|
||||||
|
toolStatus?: string;
|
||||||
|
// For reasoning:
|
||||||
|
reasoning?: { text: string; visibility?: string };
|
||||||
|
// For meta:
|
||||||
|
meta?: { title: string; detail?: string; severity?: "info" | "error" };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Download, Loader2, RefreshCw } from "lucide-react";
|
import { Download, Loader2, RefreshCw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { AgentInfo, AgentModeInfo } from "../../types/legacyApi";
|
import type { AgentInfo } from "sandbox-agent";
|
||||||
|
|
||||||
|
type AgentModeInfo = { id: string; name: string; description: string };
|
||||||
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
||||||
import { emptyFeatureCoverage } from "../../types/agents";
|
import { emptyFeatureCoverage } from "../../types/agents";
|
||||||
|
|
||||||
|
|
@ -52,9 +54,9 @@ const AgentsTab = ({
|
||||||
id,
|
id,
|
||||||
installed: false,
|
installed: false,
|
||||||
credentialsAvailable: false,
|
credentialsAvailable: false,
|
||||||
version: undefined,
|
version: undefined as string | undefined,
|
||||||
path: undefined,
|
path: undefined as string | undefined,
|
||||||
capabilities: emptyFeatureCoverage
|
capabilities: emptyFeatureCoverage as AgentInfo["capabilities"],
|
||||||
}))).map((agent) => {
|
}))).map((agent) => {
|
||||||
const isInstalling = installingAgent === agent.id;
|
const isInstalling = installingAgent === agent.id;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import { HelpCircle, Shield } from "lucide-react";
|
|
||||||
import type { PermissionEventData, QuestionEventData } from "../../types/legacyApi";
|
|
||||||
import { formatJson } from "../../utils/format";
|
|
||||||
|
|
||||||
const ApprovalsTab = ({
|
|
||||||
questionRequests,
|
|
||||||
permissionRequests,
|
|
||||||
questionSelections,
|
|
||||||
onSelectQuestionOption,
|
|
||||||
onAnswerQuestion,
|
|
||||||
onRejectQuestion,
|
|
||||||
onReplyPermission
|
|
||||||
}: {
|
|
||||||
questionRequests: QuestionEventData[];
|
|
||||||
permissionRequests: PermissionEventData[];
|
|
||||||
questionSelections: Record<string, string[][]>;
|
|
||||||
onSelectQuestionOption: (requestId: string, optionLabel: string) => void;
|
|
||||||
onAnswerQuestion: (request: QuestionEventData) => void;
|
|
||||||
onRejectQuestion: (requestId: string) => void;
|
|
||||||
onReplyPermission: (requestId: string, reply: "once" | "always" | "reject") => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{questionRequests.length === 0 && permissionRequests.length === 0 ? (
|
|
||||||
<div className="card-meta">No pending approvals.</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{questionRequests.map((request) => {
|
|
||||||
const selections = questionSelections[request.question_id] ?? [];
|
|
||||||
const selected = selections[0] ?? [];
|
|
||||||
const answered = selected.length > 0;
|
|
||||||
return (
|
|
||||||
<div key={request.question_id} className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
<span className="card-title">
|
|
||||||
<HelpCircle className="button-icon" style={{ marginRight: 6 }} />
|
|
||||||
Question
|
|
||||||
</span>
|
|
||||||
<span className="pill accent">Pending</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 12 }}>
|
|
||||||
<div style={{ fontSize: 12, marginBottom: 8 }}>{request.prompt}</div>
|
|
||||||
<div className="option-list">
|
|
||||||
{request.options.map((option) => {
|
|
||||||
const isSelected = selected.includes(option);
|
|
||||||
return (
|
|
||||||
<label key={option} className="option-item">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => onSelectQuestionOption(request.question_id, option)}
|
|
||||||
/>
|
|
||||||
<span>{option}</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-actions">
|
|
||||||
<button className="button success small" disabled={!answered} onClick={() => onAnswerQuestion(request)}>
|
|
||||||
Reply
|
|
||||||
</button>
|
|
||||||
<button className="button danger small" onClick={() => onRejectQuestion(request.question_id)}>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{permissionRequests.map((request) => (
|
|
||||||
<div key={request.permission_id} className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
<span className="card-title">
|
|
||||||
<Shield className="button-icon" style={{ marginRight: 6 }} />
|
|
||||||
Permission
|
|
||||||
</span>
|
|
||||||
<span className="pill accent">Pending</span>
|
|
||||||
</div>
|
|
||||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
|
||||||
{request.action}
|
|
||||||
</div>
|
|
||||||
{request.metadata !== null && request.metadata !== undefined && (
|
|
||||||
<pre className="code-block">{formatJson(request.metadata)}</pre>
|
|
||||||
)}
|
|
||||||
<div className="card-actions">
|
|
||||||
<button className="button success small" onClick={() => onReplyPermission(request.permission_id, "once")}>
|
|
||||||
Allow Once
|
|
||||||
</button>
|
|
||||||
<button className="button secondary small" onClick={() => onReplyPermission(request.permission_id, "always")}>
|
|
||||||
Always
|
|
||||||
</button>
|
|
||||||
<button className="button danger small" onClick={() => onReplyPermission(request.permission_id, "reject")}>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApprovalsTab;
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
import { Cloud, PlayCircle, Terminal } from "lucide-react";
|
import { Cloud, PlayCircle, Server, Terminal, Wrench } from "lucide-react";
|
||||||
import type { AgentInfo, AgentModeInfo, UniversalEvent } from "../../types/legacyApi";
|
import type { AgentInfo, SandboxAgent, SessionEvent } from "sandbox-agent";
|
||||||
|
|
||||||
|
type AgentModeInfo = { id: string; name: string; description: string };
|
||||||
import AgentsTab from "./AgentsTab";
|
import AgentsTab from "./AgentsTab";
|
||||||
import EventsTab from "./EventsTab";
|
import EventsTab from "./EventsTab";
|
||||||
|
import McpTab from "./McpTab";
|
||||||
|
import SkillsTab from "./SkillsTab";
|
||||||
import RequestLogTab from "./RequestLogTab";
|
import RequestLogTab from "./RequestLogTab";
|
||||||
import type { RequestLog } from "../../types/requestLog";
|
import type { RequestLog } from "../../types/requestLog";
|
||||||
|
|
||||||
export type DebugTab = "log" | "events" | "agents";
|
export type DebugTab = "log" | "events" | "agents" | "mcp" | "skills";
|
||||||
|
|
||||||
const DebugPanel = ({
|
const DebugPanel = ({
|
||||||
debugTab,
|
debugTab,
|
||||||
onDebugTabChange,
|
onDebugTabChange,
|
||||||
events,
|
events,
|
||||||
offset,
|
|
||||||
onResetEvents,
|
onResetEvents,
|
||||||
eventsError,
|
|
||||||
requestLog,
|
requestLog,
|
||||||
copiedLogId,
|
copiedLogId,
|
||||||
onClearRequestLog,
|
onClearRequestLog,
|
||||||
|
|
@ -24,14 +26,13 @@ const DebugPanel = ({
|
||||||
onRefreshAgents,
|
onRefreshAgents,
|
||||||
onInstallAgent,
|
onInstallAgent,
|
||||||
agentsLoading,
|
agentsLoading,
|
||||||
agentsError
|
agentsError,
|
||||||
|
getClient,
|
||||||
}: {
|
}: {
|
||||||
debugTab: DebugTab;
|
debugTab: DebugTab;
|
||||||
onDebugTabChange: (tab: DebugTab) => void;
|
onDebugTabChange: (tab: DebugTab) => void;
|
||||||
events: UniversalEvent[];
|
events: SessionEvent[];
|
||||||
offset: number;
|
|
||||||
onResetEvents: () => void;
|
onResetEvents: () => void;
|
||||||
eventsError: string | null;
|
|
||||||
requestLog: RequestLog[];
|
requestLog: RequestLog[];
|
||||||
copiedLogId: number | null;
|
copiedLogId: number | null;
|
||||||
onClearRequestLog: () => void;
|
onClearRequestLog: () => void;
|
||||||
|
|
@ -43,6 +44,7 @@ const DebugPanel = ({
|
||||||
onInstallAgent: (agentId: string, reinstall: boolean) => Promise<void>;
|
onInstallAgent: (agentId: string, reinstall: boolean) => Promise<void>;
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
|
getClient: () => SandboxAgent;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="debug-panel">
|
<div className="debug-panel">
|
||||||
|
|
@ -60,6 +62,14 @@ const DebugPanel = ({
|
||||||
<Cloud className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
|
<Cloud className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
|
||||||
Agents
|
Agents
|
||||||
</button>
|
</button>
|
||||||
|
<button className={`debug-tab ${debugTab === "mcp" ? "active" : ""}`} onClick={() => onDebugTabChange("mcp")}>
|
||||||
|
<Server className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
|
||||||
|
MCP
|
||||||
|
</button>
|
||||||
|
<button className={`debug-tab ${debugTab === "skills" ? "active" : ""}`} onClick={() => onDebugTabChange("skills")}>
|
||||||
|
<Wrench className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
|
||||||
|
Skills
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="debug-content">
|
<div className="debug-content">
|
||||||
|
|
@ -75,9 +85,7 @@ const DebugPanel = ({
|
||||||
{debugTab === "events" && (
|
{debugTab === "events" && (
|
||||||
<EventsTab
|
<EventsTab
|
||||||
events={events}
|
events={events}
|
||||||
offset={offset}
|
|
||||||
onClear={onResetEvents}
|
onClear={onResetEvents}
|
||||||
error={eventsError}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -92,6 +100,14 @@ const DebugPanel = ({
|
||||||
error={agentsError}
|
error={agentsError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{debugTab === "mcp" && (
|
||||||
|
<McpTab getClient={getClient} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{debugTab === "skills" && (
|
||||||
|
<SkillsTab getClient={getClient} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,119 @@
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
Bot,
|
||||||
|
Brain,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Circle,
|
||||||
|
CircleX,
|
||||||
|
Command,
|
||||||
|
CornerDownLeft,
|
||||||
|
FilePen,
|
||||||
|
FileText,
|
||||||
|
FolderOpen,
|
||||||
|
Hourglass,
|
||||||
|
KeyRound,
|
||||||
|
ListChecks,
|
||||||
|
MessageSquare,
|
||||||
|
Plug,
|
||||||
|
Radio,
|
||||||
|
ScrollText,
|
||||||
|
Settings,
|
||||||
|
ShieldCheck,
|
||||||
|
SquarePlus,
|
||||||
|
SquareTerminal,
|
||||||
|
ToggleLeft,
|
||||||
|
Trash2,
|
||||||
|
Unplug,
|
||||||
|
Wrench,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { UniversalEvent } from "../../types/legacyApi";
|
import type { SessionEvent } from "sandbox-agent";
|
||||||
import { formatJson, formatTime } from "../../utils/format";
|
import { formatJson, formatTime } from "../../utils/format";
|
||||||
import { getEventCategory, getEventClass, getEventIcon, getEventKey, getEventType } from "./eventUtils";
|
|
||||||
|
type EventIconInfo = { Icon: LucideIcon; category: string };
|
||||||
|
|
||||||
|
function getEventIcon(method: string, payload: Record<string, unknown>): EventIconInfo {
|
||||||
|
if (method === "session/update") {
|
||||||
|
const params = payload.params as Record<string, unknown> | undefined;
|
||||||
|
const update = params?.update as Record<string, unknown> | undefined;
|
||||||
|
const updateType = update?.sessionUpdate as string | undefined;
|
||||||
|
|
||||||
|
switch (updateType) {
|
||||||
|
case "user_message_chunk":
|
||||||
|
return { Icon: MessageSquare, category: "prompt" };
|
||||||
|
case "agent_message_chunk":
|
||||||
|
return { Icon: Bot, category: "update" };
|
||||||
|
case "agent_thought_chunk":
|
||||||
|
return { Icon: Brain, category: "update" };
|
||||||
|
case "tool_call":
|
||||||
|
case "tool_call_update":
|
||||||
|
return { Icon: Wrench, category: "tool" };
|
||||||
|
case "plan":
|
||||||
|
return { Icon: ListChecks, category: "config" };
|
||||||
|
case "available_commands_update":
|
||||||
|
return { Icon: Command, category: "config" };
|
||||||
|
case "current_mode_update":
|
||||||
|
return { Icon: ToggleLeft, category: "config" };
|
||||||
|
case "config_option_update":
|
||||||
|
return { Icon: Settings, category: "config" };
|
||||||
|
default:
|
||||||
|
return { Icon: Radio, category: "update" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case "initialize":
|
||||||
|
return { Icon: Plug, category: "connection" };
|
||||||
|
case "authenticate":
|
||||||
|
return { Icon: KeyRound, category: "connection" };
|
||||||
|
case "session/new":
|
||||||
|
return { Icon: SquarePlus, category: "session" };
|
||||||
|
case "session/load":
|
||||||
|
return { Icon: FolderOpen, category: "session" };
|
||||||
|
case "session/prompt":
|
||||||
|
return { Icon: MessageSquare, category: "prompt" };
|
||||||
|
case "session/cancel":
|
||||||
|
return { Icon: Ban, category: "cancel" };
|
||||||
|
case "session/set_mode":
|
||||||
|
return { Icon: ToggleLeft, category: "config" };
|
||||||
|
case "session/set_config_option":
|
||||||
|
return { Icon: Settings, category: "config" };
|
||||||
|
case "session/request_permission":
|
||||||
|
return { Icon: ShieldCheck, category: "permission" };
|
||||||
|
case "fs/read_text_file":
|
||||||
|
return { Icon: FileText, category: "filesystem" };
|
||||||
|
case "fs/write_text_file":
|
||||||
|
return { Icon: FilePen, category: "filesystem" };
|
||||||
|
case "terminal/create":
|
||||||
|
return { Icon: SquareTerminal, category: "terminal" };
|
||||||
|
case "terminal/kill":
|
||||||
|
return { Icon: CircleX, category: "terminal" };
|
||||||
|
case "terminal/output":
|
||||||
|
return { Icon: ScrollText, category: "terminal" };
|
||||||
|
case "terminal/release":
|
||||||
|
return { Icon: Trash2, category: "terminal" };
|
||||||
|
case "terminal/wait_for_exit":
|
||||||
|
return { Icon: Hourglass, category: "terminal" };
|
||||||
|
case "_sandboxagent/session/detach":
|
||||||
|
return { Icon: Unplug, category: "session" };
|
||||||
|
case "(response)":
|
||||||
|
return { Icon: CornerDownLeft, category: "response" };
|
||||||
|
default:
|
||||||
|
if (method.startsWith("_sandboxagent/")) {
|
||||||
|
return { Icon: Radio, category: "connection" };
|
||||||
|
}
|
||||||
|
return { Icon: Circle, category: "response" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const EventsTab = ({
|
const EventsTab = ({
|
||||||
events,
|
events,
|
||||||
offset,
|
|
||||||
onClear,
|
onClear,
|
||||||
error
|
|
||||||
}: {
|
}: {
|
||||||
events: UniversalEvent[];
|
events: SessionEvent[];
|
||||||
offset: number;
|
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
error: string | null;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [collapsedEvents, setCollapsedEvents] = useState<Record<string, boolean>>({});
|
const [collapsedEvents, setCollapsedEvents] = useState<Record<string, boolean>>({});
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
@ -55,10 +155,15 @@ const EventsTab = ({
|
||||||
}
|
}
|
||||||
}, [events.length]);
|
}, [events.length]);
|
||||||
|
|
||||||
|
const getMethod = (event: SessionEvent): string => {
|
||||||
|
const payload = event.payload as Record<string, unknown>;
|
||||||
|
return typeof payload.method === "string" ? payload.method : "(response)";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||||
<span className="card-meta">Offset: {offset}</span>
|
<span className="card-meta">{events.length} events</span>
|
||||||
<div className="inline-row">
|
<div className="inline-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -75,26 +180,26 @@ const EventsTab = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="banner error">{error}</div>}
|
|
||||||
|
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
<div className="card-meta">
|
<div className="card-meta">
|
||||||
No events yet. Start streaming to receive events.
|
No events yet. Create a session and send a message.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="event-list">
|
<div className="event-list">
|
||||||
{[...events].reverse().map((event) => {
|
{[...events].reverse().map((event) => {
|
||||||
const type = getEventType(event);
|
const eventKey = event.id;
|
||||||
const category = getEventCategory(type);
|
|
||||||
const eventClass = `${category} ${getEventClass(type)}`;
|
|
||||||
const eventKey = getEventKey(event);
|
|
||||||
const isCollapsed = collapsedEvents[eventKey] ?? true;
|
const isCollapsed = collapsedEvents[eventKey] ?? true;
|
||||||
const toggleCollapsed = () =>
|
const toggleCollapsed = () =>
|
||||||
setCollapsedEvents((prev) => ({
|
setCollapsedEvents((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[eventKey]: !(prev[eventKey] ?? true)
|
[eventKey]: !(prev[eventKey] ?? true)
|
||||||
}));
|
}));
|
||||||
const Icon = getEventIcon(type);
|
const method = getMethod(event);
|
||||||
|
const payload = event.payload as Record<string, unknown>;
|
||||||
|
const { Icon, category } = getEventIcon(method, payload);
|
||||||
|
const time = formatTime(new Date(event.createdAt).toISOString());
|
||||||
|
const senderClass = event.sender === "client" ? "client" : "agent";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={eventKey} className={`event-item ${isCollapsed ? "collapsed" : "expanded"}`}>
|
<div key={eventKey} className={`event-item ${isCollapsed ? "collapsed" : "expanded"}`}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -103,24 +208,26 @@ const EventsTab = ({
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
title={isCollapsed ? "Expand payload" : "Collapse payload"}
|
title={isCollapsed ? "Expand payload" : "Collapse payload"}
|
||||||
>
|
>
|
||||||
<span className={`event-icon ${eventClass}`}>
|
<span className={`event-icon ${category}`}>
|
||||||
<Icon size={14} />
|
<Icon size={14} />
|
||||||
</span>
|
</span>
|
||||||
<div className="event-summary-main">
|
<div className="event-summary-main">
|
||||||
<div className="event-title-row">
|
<div className="event-title-row">
|
||||||
<span className={`event-type ${eventClass}`}>{type}</span>
|
<span className={`event-type ${category}`}>{method}</span>
|
||||||
<span className="event-time">{formatTime(event.time)}</span>
|
<span className={`pill ${senderClass === "client" ? "accent" : "success"}`}>
|
||||||
|
{event.sender}
|
||||||
|
</span>
|
||||||
|
<span className="event-time">{time}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="event-id">
|
<div className="event-id">
|
||||||
Event #{event.event_id || event.sequence} - seq {event.sequence} - {event.source}
|
{event.id}
|
||||||
{event.synthetic ? " (synthetic)" : ""}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="event-chevron">
|
<span className="event-chevron">
|
||||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{!isCollapsed && <pre className="code-block event-payload">{formatJson(event.data)}</pre>}
|
{!isCollapsed && <pre className="code-block event-payload">{formatJson(event.payload)}</pre>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
210
frontend/packages/inspector/src/components/debug/McpTab.tsx
Normal file
210
frontend/packages/inspector/src/components/debug/McpTab.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import type { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { formatJson } from "../../utils/format";
|
||||||
|
|
||||||
|
type McpEntry = {
|
||||||
|
name: string;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const McpTab = ({
|
||||||
|
getClient,
|
||||||
|
}: {
|
||||||
|
getClient: () => SandboxAgent;
|
||||||
|
}) => {
|
||||||
|
const [directory, setDirectory] = useState("/");
|
||||||
|
const [entries, setEntries] = useState<McpEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Add/edit form state
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [editName, setEditName] = useState("");
|
||||||
|
const [editJson, setEditJson] = useState("");
|
||||||
|
const [editError, setEditError] = useState<string | null>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const loadAll = useCallback(async (dir: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/mcp.json`;
|
||||||
|
const bytes = await getClient().readFsFile({ path: configPath });
|
||||||
|
const text = new TextDecoder().decode(bytes);
|
||||||
|
if (!text.trim()) {
|
||||||
|
setEntries([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const map = JSON.parse(text) as Record<string, Record<string, unknown>>;
|
||||||
|
setEntries(
|
||||||
|
Object.entries(map).map(([name, config]) => ({ name, config })),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist yet or is empty — that's fine
|
||||||
|
setEntries([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [getClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAll(directory);
|
||||||
|
}, [directory, loadAll]);
|
||||||
|
|
||||||
|
const startAdd = () => {
|
||||||
|
setEditing(true);
|
||||||
|
setEditName("");
|
||||||
|
setEditJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}');
|
||||||
|
setEditError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setEditing(false);
|
||||||
|
setEditName("");
|
||||||
|
setEditJson("");
|
||||||
|
setEditError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const name = editName.trim();
|
||||||
|
if (!name) {
|
||||||
|
setEditError("Name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(editJson.trim());
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
setEditError("Must be a JSON object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setEditError("Invalid JSON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
setEditError(null);
|
||||||
|
try {
|
||||||
|
await getClient().setMcpConfig(
|
||||||
|
{ directory, mcpName: name },
|
||||||
|
parsed as Parameters<SandboxAgent["setMcpConfig"]>[1],
|
||||||
|
);
|
||||||
|
cancelEdit();
|
||||||
|
await loadAll(directory);
|
||||||
|
} catch (err) {
|
||||||
|
setEditError(err instanceof Error ? err.message : "Failed to save");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (name: string) => {
|
||||||
|
try {
|
||||||
|
await getClient().deleteMcpConfig({ directory, mcpName: name });
|
||||||
|
await loadAll(directory);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to delete");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||||
|
<span className="card-meta">MCP Server Configuration</span>
|
||||||
|
<div className="inline-row">
|
||||||
|
{!editing && (
|
||||||
|
<button className="button secondary small" onClick={startAdd}>
|
||||||
|
<Plus className="button-icon" style={{ width: 12, height: 12 }} />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="inline-row" style={{ marginBottom: 12, gap: 6 }}>
|
||||||
|
<FolderOpen size={14} className="muted" style={{ flexShrink: 0 }} />
|
||||||
|
<input
|
||||||
|
className="setup-input mono"
|
||||||
|
value={directory}
|
||||||
|
onChange={(e) => setDirectory(e.target.value)}
|
||||||
|
placeholder="/"
|
||||||
|
style={{ flex: 1, fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="banner error">{error}</div>}
|
||||||
|
{loading && <div className="card-meta">Loading...</div>}
|
||||||
|
|
||||||
|
{editing && (
|
||||||
|
<div className="card" style={{ marginBottom: 12 }}>
|
||||||
|
<div className="card-header">
|
||||||
|
<span className="card-title">
|
||||||
|
{editName ? `Edit: ${editName}` : "Add MCP Server"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => { setEditName(e.target.value); setEditError(null); }}
|
||||||
|
placeholder="server-name"
|
||||||
|
style={{ marginBottom: 8, width: "100%", boxSizing: "border-box" }}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="setup-input mono"
|
||||||
|
value={editJson}
|
||||||
|
onChange={(e) => { setEditJson(e.target.value); setEditError(null); }}
|
||||||
|
rows={6}
|
||||||
|
style={{ width: "100%", boxSizing: "border-box", fontFamily: "monospace", fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="card-actions">
|
||||||
|
<button className="button primary small" onClick={save} disabled={saving}>
|
||||||
|
{saving ? <Loader2 className="button-icon spinner-icon" /> : null}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button className="button ghost small" onClick={cancelEdit}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries.length === 0 && !editing && !loading && (
|
||||||
|
<div className="card-meta">
|
||||||
|
No MCP servers configured in this directory.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<div key={entry.name} className="card" style={{ marginBottom: 8 }}>
|
||||||
|
<div className="card-header">
|
||||||
|
<span className="card-title">{entry.name}</span>
|
||||||
|
<div className="card-header-pills">
|
||||||
|
<span className="pill accent">
|
||||||
|
{(entry.config as { type?: string }).type ?? "unknown"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="button ghost small"
|
||||||
|
onClick={() => remove(entry.name)}
|
||||||
|
title="Remove"
|
||||||
|
style={{ padding: "2px 4px" }}
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="code-block" style={{ marginTop: 4, fontSize: 10 }}>
|
||||||
|
{formatJson(entry.config)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default McpTab;
|
||||||
|
|
@ -44,21 +44,21 @@ const RequestLogTab = ({
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => hasDetails && toggleExpanded(entry.id)}
|
onClick={() => hasDetails && toggleExpanded(entry.id)}
|
||||||
title={hasDetails ? (isExpanded ? "Collapse" : "Expand") : undefined}
|
title={hasDetails ? (isExpanded ? "Collapse" : "Expand") : undefined}
|
||||||
style={{ cursor: hasDetails ? "pointer" : "default" }}
|
style={{ cursor: hasDetails ? "pointer" : "default", gridTemplateColumns: "1fr auto auto auto" }}
|
||||||
>
|
>
|
||||||
<div className="event-summary-main" style={{ flex: 1 }}>
|
<div className="event-summary-main">
|
||||||
<div className="event-title-row">
|
<div className="event-title-row">
|
||||||
<span className="log-method">{entry.method}</span>
|
<span className="log-method">{entry.method}</span>
|
||||||
<span className="log-url text-truncate" style={{ flex: 1 }}>{entry.url}</span>
|
<span className="log-url text-truncate" style={{ flex: 1 }}>{entry.url}</span>
|
||||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
|
||||||
{entry.status || "ERR"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="event-id">
|
<div className="event-id">
|
||||||
{entry.time}
|
{entry.time}
|
||||||
{entry.error && ` - ${entry.error}`}
|
{entry.error && ` - ${entry.error}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
||||||
|
{entry.status || "ERR"}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
className="copy-button"
|
className="copy-button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
|
||||||
263
frontend/packages/inspector/src/components/debug/SkillsTab.tsx
Normal file
263
frontend/packages/inspector/src/components/debug/SkillsTab.tsx
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import type { SandboxAgent } from "sandbox-agent";
|
||||||
|
import { formatJson } from "../../utils/format";
|
||||||
|
|
||||||
|
type SkillEntry = {
|
||||||
|
name: string;
|
||||||
|
config: { sources: Array<{ source: string; type: string; ref?: string | null; subpath?: string | null; skills?: string[] | null }> };
|
||||||
|
};
|
||||||
|
|
||||||
|
const SkillsTab = ({
|
||||||
|
getClient,
|
||||||
|
}: {
|
||||||
|
getClient: () => SandboxAgent;
|
||||||
|
}) => {
|
||||||
|
const [directory, setDirectory] = useState("/");
|
||||||
|
const [entries, setEntries] = useState<SkillEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Add form state
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [editName, setEditName] = useState("");
|
||||||
|
const [editSource, setEditSource] = useState("");
|
||||||
|
const [editType, setEditType] = useState("github");
|
||||||
|
const [editRef, setEditRef] = useState("");
|
||||||
|
const [editSubpath, setEditSubpath] = useState("");
|
||||||
|
const [editSkills, setEditSkills] = useState("");
|
||||||
|
const [editError, setEditError] = useState<string | null>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const loadAll = useCallback(async (dir: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/skills.json`;
|
||||||
|
const bytes = await getClient().readFsFile({ path: configPath });
|
||||||
|
const text = new TextDecoder().decode(bytes);
|
||||||
|
if (!text.trim()) {
|
||||||
|
setEntries([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const map = JSON.parse(text) as Record<string, SkillEntry["config"]>;
|
||||||
|
setEntries(
|
||||||
|
Object.entries(map).map(([name, config]) => ({ name, config })),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist yet or is empty — that's fine
|
||||||
|
setEntries([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [getClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAll(directory);
|
||||||
|
}, [directory, loadAll]);
|
||||||
|
|
||||||
|
const startAdd = () => {
|
||||||
|
setEditing(true);
|
||||||
|
setEditName("");
|
||||||
|
setEditSource("rivet-dev/skills");
|
||||||
|
setEditType("github");
|
||||||
|
setEditRef("");
|
||||||
|
setEditSubpath("");
|
||||||
|
setEditSkills("sandbox-agent");
|
||||||
|
setEditError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setEditing(false);
|
||||||
|
setEditName("");
|
||||||
|
setEditSource("");
|
||||||
|
setEditType("github");
|
||||||
|
setEditRef("");
|
||||||
|
setEditSubpath("");
|
||||||
|
setEditSkills("");
|
||||||
|
setEditError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const name = editName.trim();
|
||||||
|
if (!name) {
|
||||||
|
setEditError("Name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const source = editSource.trim();
|
||||||
|
if (!source) {
|
||||||
|
setEditError("Source is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillEntry: SkillEntry["config"]["sources"][0] = {
|
||||||
|
source,
|
||||||
|
type: editType,
|
||||||
|
};
|
||||||
|
if (editRef.trim()) skillEntry.ref = editRef.trim();
|
||||||
|
if (editSubpath.trim()) skillEntry.subpath = editSubpath.trim();
|
||||||
|
const skillsList = editSkills.trim()
|
||||||
|
? editSkills.split(",").map((s) => s.trim()).filter(Boolean)
|
||||||
|
: null;
|
||||||
|
if (skillsList && skillsList.length > 0) skillEntry.skills = skillsList;
|
||||||
|
|
||||||
|
const config = { sources: [skillEntry] };
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
setEditError(null);
|
||||||
|
try {
|
||||||
|
await getClient().setSkillsConfig(
|
||||||
|
{ directory, skillName: name },
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
cancelEdit();
|
||||||
|
await loadAll(directory);
|
||||||
|
} catch (err) {
|
||||||
|
setEditError(err instanceof Error ? err.message : "Failed to save");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (name: string) => {
|
||||||
|
try {
|
||||||
|
await getClient().deleteSkillsConfig({ directory, skillName: name });
|
||||||
|
await loadAll(directory);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to delete");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||||
|
<span className="card-meta">Skills Configuration</span>
|
||||||
|
<div className="inline-row">
|
||||||
|
{!editing && (
|
||||||
|
<button className="button secondary small" onClick={startAdd}>
|
||||||
|
<Plus className="button-icon" style={{ width: 12, height: 12 }} />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="inline-row" style={{ marginBottom: 12, gap: 6 }}>
|
||||||
|
<FolderOpen size={14} className="muted" style={{ flexShrink: 0 }} />
|
||||||
|
<input
|
||||||
|
className="setup-input mono"
|
||||||
|
value={directory}
|
||||||
|
onChange={(e) => setDirectory(e.target.value)}
|
||||||
|
placeholder="/"
|
||||||
|
style={{ flex: 1, fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="banner error">{error}</div>}
|
||||||
|
{loading && <div className="card-meta">Loading...</div>}
|
||||||
|
|
||||||
|
{editing && (
|
||||||
|
<div className="card" style={{ marginBottom: 12 }}>
|
||||||
|
<div className="card-header">
|
||||||
|
<span className="card-title">Add Skill Source</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => { setEditName(e.target.value); setEditError(null); }}
|
||||||
|
placeholder="skill-name"
|
||||||
|
style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }}
|
||||||
|
/>
|
||||||
|
<div className="inline-row" style={{ marginBottom: 6, gap: 4 }}>
|
||||||
|
<select
|
||||||
|
className="setup-select"
|
||||||
|
value={editType}
|
||||||
|
onChange={(e) => setEditType(e.target.value)}
|
||||||
|
style={{ width: 90 }}
|
||||||
|
>
|
||||||
|
<option value="github">github</option>
|
||||||
|
<option value="local">local</option>
|
||||||
|
<option value="git">git</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
className="setup-input mono"
|
||||||
|
value={editSource}
|
||||||
|
onChange={(e) => { setEditSource(e.target.value); setEditError(null); }}
|
||||||
|
placeholder={editType === "github" ? "owner/repo" : editType === "local" ? "/path/to/skill" : "https://..."}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
value={editSkills}
|
||||||
|
onChange={(e) => setEditSkills(e.target.value)}
|
||||||
|
placeholder="Skills filter (comma-separated, optional)"
|
||||||
|
style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }}
|
||||||
|
/>
|
||||||
|
{editType !== "local" && (
|
||||||
|
<div className="inline-row" style={{ gap: 4 }}>
|
||||||
|
<input
|
||||||
|
className="setup-input mono"
|
||||||
|
value={editRef}
|
||||||
|
onChange={(e) => setEditRef(e.target.value)}
|
||||||
|
placeholder="Branch/tag (optional)"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="setup-input mono"
|
||||||
|
value={editSubpath}
|
||||||
|
onChange={(e) => setEditSubpath(e.target.value)}
|
||||||
|
placeholder="Subpath (optional)"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="card-actions">
|
||||||
|
<button className="button primary small" onClick={save} disabled={saving}>
|
||||||
|
{saving ? <Loader2 className="button-icon spinner-icon" /> : null}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button className="button ghost small" onClick={cancelEdit}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries.length === 0 && !editing && !loading && (
|
||||||
|
<div className="card-meta">
|
||||||
|
No skills configured in this directory.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<div key={entry.name} className="card" style={{ marginBottom: 8 }}>
|
||||||
|
<div className="card-header">
|
||||||
|
<span className="card-title">{entry.name}</span>
|
||||||
|
<div className="card-header-pills">
|
||||||
|
<span className="pill accent">
|
||||||
|
{entry.config.sources.length} source{entry.config.sources.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="button ghost small"
|
||||||
|
onClick={() => remove(entry.name)}
|
||||||
|
title="Remove"
|
||||||
|
style={{ padding: "2px 4px" }}
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="code-block" style={{ marginTop: 4, fontSize: 10 }}>
|
||||||
|
{formatJson(entry.config)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkillsTab;
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
import {
|
|
||||||
Activity,
|
|
||||||
AlertTriangle,
|
|
||||||
Brain,
|
|
||||||
CheckCircle,
|
|
||||||
FileDiff,
|
|
||||||
HelpCircle,
|
|
||||||
Info,
|
|
||||||
MessageSquare,
|
|
||||||
PauseCircle,
|
|
||||||
PlayCircle,
|
|
||||||
Shield,
|
|
||||||
Terminal,
|
|
||||||
Wrench,
|
|
||||||
Zap
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { UniversalEvent } from "../../types/legacyApi";
|
|
||||||
|
|
||||||
export const getEventType = (event: UniversalEvent) => event.type;
|
|
||||||
|
|
||||||
export const getEventKey = (event: UniversalEvent) =>
|
|
||||||
event.event_id ? `id:${event.event_id}` : `seq:${event.sequence}`;
|
|
||||||
|
|
||||||
export const getEventCategory = (type: string) => type.split(".")[0] ?? type;
|
|
||||||
|
|
||||||
export const getEventClass = (type: string) => type.replace(/\./g, "-");
|
|
||||||
|
|
||||||
export const getEventIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
// ACP session update events
|
|
||||||
case "acp.agent_message_chunk":
|
|
||||||
return MessageSquare;
|
|
||||||
case "acp.user_message_chunk":
|
|
||||||
return MessageSquare;
|
|
||||||
case "acp.agent_thought_chunk":
|
|
||||||
return Brain;
|
|
||||||
case "acp.tool_call":
|
|
||||||
return Wrench;
|
|
||||||
case "acp.tool_call_update":
|
|
||||||
return Activity;
|
|
||||||
case "acp.plan":
|
|
||||||
return FileDiff;
|
|
||||||
case "acp.session_info_update":
|
|
||||||
return Info;
|
|
||||||
case "acp.usage_update":
|
|
||||||
return Info;
|
|
||||||
case "acp.current_mode_update":
|
|
||||||
return Info;
|
|
||||||
case "acp.config_option_update":
|
|
||||||
return Info;
|
|
||||||
case "acp.available_commands_update":
|
|
||||||
return Terminal;
|
|
||||||
|
|
||||||
// Inspector lifecycle events
|
|
||||||
case "inspector.turn_started":
|
|
||||||
return PlayCircle;
|
|
||||||
case "inspector.turn_ended":
|
|
||||||
return PauseCircle;
|
|
||||||
case "inspector.user_message":
|
|
||||||
return MessageSquare;
|
|
||||||
|
|
||||||
// Session lifecycle (inspector-emitted)
|
|
||||||
case "session.started":
|
|
||||||
return PlayCircle;
|
|
||||||
case "session.ended":
|
|
||||||
return PauseCircle;
|
|
||||||
|
|
||||||
// Legacy synthetic events
|
|
||||||
case "turn.started":
|
|
||||||
return PlayCircle;
|
|
||||||
case "turn.ended":
|
|
||||||
return PauseCircle;
|
|
||||||
case "item.started":
|
|
||||||
return MessageSquare;
|
|
||||||
case "item.delta":
|
|
||||||
return Activity;
|
|
||||||
case "item.completed":
|
|
||||||
return CheckCircle;
|
|
||||||
|
|
||||||
// Approval events
|
|
||||||
case "question.requested":
|
|
||||||
return HelpCircle;
|
|
||||||
case "question.resolved":
|
|
||||||
return CheckCircle;
|
|
||||||
case "permission.requested":
|
|
||||||
return Shield;
|
|
||||||
case "permission.resolved":
|
|
||||||
return CheckCircle;
|
|
||||||
|
|
||||||
// Error events
|
|
||||||
case "error":
|
|
||||||
return AlertTriangle;
|
|
||||||
case "agent.unparsed":
|
|
||||||
return Brain;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (type.startsWith("acp.")) return Zap;
|
|
||||||
if (type.startsWith("inspector.")) return Info;
|
|
||||||
if (type.startsWith("item.")) return MessageSquare;
|
|
||||||
if (type.startsWith("session.")) return PlayCircle;
|
|
||||||
if (type.startsWith("error")) return AlertTriangle;
|
|
||||||
if (type.startsWith("agent.")) return Brain;
|
|
||||||
if (type.startsWith("question.")) return HelpCircle;
|
|
||||||
if (type.startsWith("permission.")) return Shield;
|
|
||||||
if (type.startsWith("file.")) return FileDiff;
|
|
||||||
if (type.startsWith("command.")) return Terminal;
|
|
||||||
if (type.startsWith("tool.")) return Wrench;
|
|
||||||
return Zap;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,790 +0,0 @@
|
||||||
import {
|
|
||||||
SandboxAgent,
|
|
||||||
type PermissionOption,
|
|
||||||
type RequestPermissionRequest,
|
|
||||||
type RequestPermissionResponse,
|
|
||||||
type SandboxAgentAcpClient,
|
|
||||||
type SandboxAgentConnectOptions,
|
|
||||||
type SessionNotification,
|
|
||||||
} from "sandbox-agent";
|
|
||||||
import type {
|
|
||||||
AgentInfo,
|
|
||||||
AgentModelInfo,
|
|
||||||
AgentModeInfo,
|
|
||||||
AgentModelsResponse,
|
|
||||||
AgentModesResponse,
|
|
||||||
CreateSessionRequest,
|
|
||||||
EventsQuery,
|
|
||||||
EventsResponse,
|
|
||||||
MessageRequest,
|
|
||||||
PermissionEventData,
|
|
||||||
PermissionReplyRequest,
|
|
||||||
QuestionEventData,
|
|
||||||
QuestionReplyRequest,
|
|
||||||
SessionInfo,
|
|
||||||
SessionListResponse,
|
|
||||||
TurnStreamQuery,
|
|
||||||
UniversalEvent,
|
|
||||||
} from "../types/legacyApi";
|
|
||||||
|
|
||||||
type PendingPermission = {
|
|
||||||
request: RequestPermissionRequest;
|
|
||||||
resolve: (response: RequestPermissionResponse) => void;
|
|
||||||
autoEndTurnOnResolve?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PendingQuestion = {
|
|
||||||
prompt: string;
|
|
||||||
options: string[];
|
|
||||||
autoEndTurnOnResolve?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RuntimeSession = {
|
|
||||||
aliasSessionId: string;
|
|
||||||
realSessionId: string;
|
|
||||||
agent: string;
|
|
||||||
connection: SandboxAgentAcpClient;
|
|
||||||
events: UniversalEvent[];
|
|
||||||
nextSequence: number;
|
|
||||||
listeners: Set<(event: UniversalEvent) => void>;
|
|
||||||
info: SessionInfo;
|
|
||||||
pendingPermissions: Map<string, PendingPermission>;
|
|
||||||
pendingQuestions: Map<string, PendingQuestion>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TDOO_PERMISSION_MODE =
|
|
||||||
"TDOO: ACP permission mode preconfiguration is not implemented in inspector compatibility.";
|
|
||||||
const TDOO_VARIANT =
|
|
||||||
"TDOO: ACP session variants are not implemented in inspector compatibility.";
|
|
||||||
const TDOO_SKILLS =
|
|
||||||
"TDOO: ACP skills source configuration is not implemented in inspector compatibility.";
|
|
||||||
const TDOO_MODE_DISCOVERY =
|
|
||||||
"TDOO: ACP mode discovery before session creation is not implemented; returning cached/empty modes.";
|
|
||||||
const TDOO_MODEL_DISCOVERY =
|
|
||||||
"TDOO: ACP model discovery before session creation is not implemented; returning cached/empty models.";
|
|
||||||
|
|
||||||
export class InspectorLegacyClient {
|
|
||||||
private readonly base: SandboxAgent;
|
|
||||||
private readonly sessions = new Map<string, RuntimeSession>();
|
|
||||||
private readonly aliasByRealSessionId = new Map<string, string>();
|
|
||||||
private readonly modeCache = new Map<string, AgentModeInfo[]>();
|
|
||||||
private readonly modelCache = new Map<string, AgentModelsResponse>();
|
|
||||||
private permissionCounter = 0;
|
|
||||||
|
|
||||||
private constructor(base: SandboxAgent) {
|
|
||||||
this.base = base;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async connect(options: SandboxAgentConnectOptions): Promise<InspectorLegacyClient> {
|
|
||||||
const base = await SandboxAgent.connect(options);
|
|
||||||
return new InspectorLegacyClient(base);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHealth() {
|
|
||||||
return this.base.getHealth();
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAgents(): Promise<{ agents: AgentInfo[] }> {
|
|
||||||
const response = await this.base.listAgents();
|
|
||||||
|
|
||||||
return {
|
|
||||||
agents: response.agents.map((agent) => {
|
|
||||||
const installed =
|
|
||||||
agent.agent_process_installed &&
|
|
||||||
(!agent.native_required || agent.native_installed);
|
|
||||||
return {
|
|
||||||
id: agent.id,
|
|
||||||
installed,
|
|
||||||
credentialsAvailable: true,
|
|
||||||
version: agent.agent_process_version ?? agent.native_version ?? null,
|
|
||||||
path: null,
|
|
||||||
capabilities: {
|
|
||||||
unstable_methods: agent.capabilities.unstable_methods,
|
|
||||||
},
|
|
||||||
native_required: agent.native_required,
|
|
||||||
native_installed: agent.native_installed,
|
|
||||||
native_version: agent.native_version,
|
|
||||||
agent_process_installed: agent.agent_process_installed,
|
|
||||||
agent_process_source: agent.agent_process_source,
|
|
||||||
agent_process_version: agent.agent_process_version,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async installAgent(agent: string, request: { reinstall?: boolean } = {}) {
|
|
||||||
return this.base.installAgent(agent, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAgentModes(agentId: string): Promise<AgentModesResponse> {
|
|
||||||
const modes = this.modeCache.get(agentId);
|
|
||||||
if (modes) {
|
|
||||||
return { modes };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(TDOO_MODE_DISCOVERY);
|
|
||||||
return { modes: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAgentModels(agentId: string): Promise<AgentModelsResponse> {
|
|
||||||
const models = this.modelCache.get(agentId);
|
|
||||||
if (models) {
|
|
||||||
return models;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(TDOO_MODEL_DISCOVERY);
|
|
||||||
return { models: [], defaultModel: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(aliasSessionId: string, request: CreateSessionRequest): Promise<void> {
|
|
||||||
await this.terminateSession(aliasSessionId).catch(() => {
|
|
||||||
// Ignore if it doesn't exist yet.
|
|
||||||
});
|
|
||||||
|
|
||||||
const acp = await this.base.createAcpClient({
|
|
||||||
agent: request.agent,
|
|
||||||
client: {
|
|
||||||
sessionUpdate: async (notification) => {
|
|
||||||
this.handleSessionUpdate(notification);
|
|
||||||
},
|
|
||||||
requestPermission: async (permissionRequest) => {
|
|
||||||
return this.handlePermissionRequest(permissionRequest);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await acp.initialize();
|
|
||||||
|
|
||||||
const created = await acp.newSession({
|
|
||||||
cwd: "/",
|
|
||||||
mcpServers: convertMcpConfig(request.mcp ?? {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (created.modes?.availableModes) {
|
|
||||||
this.modeCache.set(
|
|
||||||
request.agent,
|
|
||||||
created.modes.availableModes.map((mode) => ({
|
|
||||||
id: mode.id,
|
|
||||||
name: mode.name,
|
|
||||||
description: mode.description ?? undefined,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (created.models?.availableModels) {
|
|
||||||
this.modelCache.set(request.agent, {
|
|
||||||
models: created.models.availableModels.map((model) => ({
|
|
||||||
id: model.modelId,
|
|
||||||
name: model.name,
|
|
||||||
description: model.description ?? undefined,
|
|
||||||
})),
|
|
||||||
defaultModel: created.models.currentModelId ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtime: RuntimeSession = {
|
|
||||||
aliasSessionId,
|
|
||||||
realSessionId: created.sessionId,
|
|
||||||
agent: request.agent,
|
|
||||||
connection: acp,
|
|
||||||
events: [],
|
|
||||||
nextSequence: 1,
|
|
||||||
listeners: new Set(),
|
|
||||||
info: {
|
|
||||||
sessionId: aliasSessionId,
|
|
||||||
agent: request.agent,
|
|
||||||
eventCount: 0,
|
|
||||||
ended: false,
|
|
||||||
model: request.model ?? null,
|
|
||||||
variant: request.variant ?? null,
|
|
||||||
permissionMode: request.permissionMode ?? null,
|
|
||||||
mcp: request.mcp,
|
|
||||||
skills: request.skills,
|
|
||||||
},
|
|
||||||
pendingPermissions: new Map(),
|
|
||||||
pendingQuestions: new Map(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.sessions.set(aliasSessionId, runtime);
|
|
||||||
this.aliasByRealSessionId.set(created.sessionId, aliasSessionId);
|
|
||||||
|
|
||||||
if (request.agentMode) {
|
|
||||||
try {
|
|
||||||
await acp.setSessionMode({ sessionId: created.sessionId, modeId: request.agentMode });
|
|
||||||
} catch {
|
|
||||||
this.emitError(aliasSessionId, `TDOO: Unable to apply mode \"${request.agentMode}\" via ACP.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.model) {
|
|
||||||
try {
|
|
||||||
await acp.unstableSetSessionModel({
|
|
||||||
sessionId: created.sessionId,
|
|
||||||
modelId: request.model,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
this.emitError(aliasSessionId, `TDOO: Unable to apply model \"${request.model}\" via ACP.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.permissionMode) {
|
|
||||||
this.emitError(aliasSessionId, TDOO_PERMISSION_MODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.variant) {
|
|
||||||
this.emitError(aliasSessionId, TDOO_VARIANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.skills?.sources && request.skills.sources.length > 0) {
|
|
||||||
this.emitError(aliasSessionId, TDOO_SKILLS);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(aliasSessionId, "session.started", {
|
|
||||||
session_id: aliasSessionId,
|
|
||||||
agent: request.agent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async listSessions(): Promise<SessionListResponse> {
|
|
||||||
const sessions = Array.from(this.sessions.values()).map((session) => {
|
|
||||||
return {
|
|
||||||
...session.info,
|
|
||||||
eventCount: session.events.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { sessions };
|
|
||||||
}
|
|
||||||
|
|
||||||
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
|
|
||||||
const runtime = this.requireActiveSession(sessionId);
|
|
||||||
const message = request.message.trim();
|
|
||||||
if (!message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(sessionId, "inspector.turn_started", {
|
|
||||||
session_id: sessionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emitEvent(sessionId, "inspector.user_message", {
|
|
||||||
session_id: sessionId,
|
|
||||||
text: message,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await runtime.connection.prompt({
|
|
||||||
sessionId: runtime.realSessionId,
|
|
||||||
prompt: [{ type: "text", text: message }],
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const detail = error instanceof Error ? error.message : "prompt failed";
|
|
||||||
this.emitError(sessionId, detail);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.emitEvent(sessionId, "inspector.turn_ended", {
|
|
||||||
session_id: sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEvents(sessionId: string, query: EventsQuery = {}): Promise<EventsResponse> {
|
|
||||||
const runtime = this.requireSession(sessionId);
|
|
||||||
const offset = query.offset ?? 0;
|
|
||||||
const limit = query.limit ?? 200;
|
|
||||||
|
|
||||||
const events = runtime.events.filter((event) => event.sequence > offset).slice(0, limit);
|
|
||||||
return { events };
|
|
||||||
}
|
|
||||||
|
|
||||||
async *streamEvents(
|
|
||||||
sessionId: string,
|
|
||||||
query: EventsQuery = {},
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): AsyncIterable<UniversalEvent> {
|
|
||||||
const runtime = this.requireSession(sessionId);
|
|
||||||
let cursor = query.offset ?? 0;
|
|
||||||
|
|
||||||
for (const event of runtime.events) {
|
|
||||||
if (event.sequence <= cursor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cursor = event.sequence;
|
|
||||||
yield event;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue: UniversalEvent[] = [];
|
|
||||||
let wake: (() => void) | null = null;
|
|
||||||
|
|
||||||
const listener = (event: UniversalEvent) => {
|
|
||||||
if (event.sequence <= cursor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
queue.push(event);
|
|
||||||
if (wake) {
|
|
||||||
wake();
|
|
||||||
wake = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
runtime.listeners.add(listener);
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (!signal?.aborted) {
|
|
||||||
if (queue.length === 0) {
|
|
||||||
await waitForSignalOrEvent(signal, () => {
|
|
||||||
wake = () => {};
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
wake = resolve;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = queue.shift();
|
|
||||||
if (!next) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = next.sequence;
|
|
||||||
yield next;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
runtime.listeners.delete(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async *streamTurn(
|
|
||||||
sessionId: string,
|
|
||||||
request: MessageRequest,
|
|
||||||
_query?: TurnStreamQuery,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): AsyncIterable<UniversalEvent> {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtime = this.requireActiveSession(sessionId);
|
|
||||||
let cursor = runtime.nextSequence - 1;
|
|
||||||
const queue: UniversalEvent[] = [];
|
|
||||||
let wake: (() => void) | null = null;
|
|
||||||
let promptDone = false;
|
|
||||||
let promptError: unknown = null;
|
|
||||||
|
|
||||||
const notify = () => {
|
|
||||||
if (wake) {
|
|
||||||
wake();
|
|
||||||
wake = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const listener = (event: UniversalEvent) => {
|
|
||||||
if (event.sequence <= cursor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
queue.push(event);
|
|
||||||
notify();
|
|
||||||
};
|
|
||||||
|
|
||||||
runtime.listeners.add(listener);
|
|
||||||
|
|
||||||
const promptPromise = this.postMessage(sessionId, request)
|
|
||||||
.catch((error) => {
|
|
||||||
promptError = error;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
promptDone = true;
|
|
||||||
notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (!signal?.aborted) {
|
|
||||||
if (queue.length === 0) {
|
|
||||||
if (promptDone) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitForSignalOrEvent(signal, () => {
|
|
||||||
wake = () => {};
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
wake = resolve;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = queue.shift();
|
|
||||||
if (!next) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = next.sequence;
|
|
||||||
yield next;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
runtime.listeners.delete(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
await promptPromise;
|
|
||||||
if (promptError) {
|
|
||||||
throw promptError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async replyQuestion(
|
|
||||||
sessionId: string,
|
|
||||||
questionId: string,
|
|
||||||
request: QuestionReplyRequest,
|
|
||||||
): Promise<void> {
|
|
||||||
const runtime = this.requireSession(sessionId);
|
|
||||||
const pending = runtime.pendingQuestions.get(questionId);
|
|
||||||
if (!pending) {
|
|
||||||
throw new Error("TDOO: Question request no longer pending.");
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.pendingQuestions.delete(questionId);
|
|
||||||
const response = request.answers?.[0]?.[0] ?? null;
|
|
||||||
const resolved: QuestionEventData & { response?: string | null } = {
|
|
||||||
question_id: questionId,
|
|
||||||
status: "resolved",
|
|
||||||
prompt: pending.prompt,
|
|
||||||
options: pending.options,
|
|
||||||
response,
|
|
||||||
};
|
|
||||||
this.emitEvent(sessionId, "question.resolved", resolved);
|
|
||||||
if (pending.autoEndTurnOnResolve) {
|
|
||||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectQuestion(sessionId: string, questionId: string): Promise<void> {
|
|
||||||
const runtime = this.requireSession(sessionId);
|
|
||||||
const pending = runtime.pendingQuestions.get(questionId);
|
|
||||||
if (!pending) {
|
|
||||||
throw new Error("TDOO: Question request no longer pending.");
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.pendingQuestions.delete(questionId);
|
|
||||||
const resolved: QuestionEventData & { response?: string | null } = {
|
|
||||||
question_id: questionId,
|
|
||||||
status: "resolved",
|
|
||||||
prompt: pending.prompt,
|
|
||||||
options: pending.options,
|
|
||||||
response: null,
|
|
||||||
};
|
|
||||||
this.emitEvent(sessionId, "question.resolved", resolved);
|
|
||||||
if (pending.autoEndTurnOnResolve) {
|
|
||||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async replyPermission(
|
|
||||||
sessionId: string,
|
|
||||||
permissionId: string,
|
|
||||||
request: PermissionReplyRequest,
|
|
||||||
): Promise<void> {
|
|
||||||
const runtime = this.requireSession(sessionId);
|
|
||||||
const pending = runtime.pendingPermissions.get(permissionId);
|
|
||||||
if (!pending) {
|
|
||||||
throw new Error("TDOO: Permission request no longer pending.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionId = selectPermissionOption(pending.request.options, request.reply);
|
|
||||||
const response: RequestPermissionResponse = optionId
|
|
||||||
? {
|
|
||||||
outcome: {
|
|
||||||
outcome: "selected",
|
|
||||||
optionId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
outcome: {
|
|
||||||
outcome: "cancelled",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pending.resolve(response);
|
|
||||||
runtime.pendingPermissions.delete(permissionId);
|
|
||||||
|
|
||||||
const action = pending.request.toolCall.title ?? pending.request.toolCall.kind ?? "permission";
|
|
||||||
const resolved: PermissionEventData = {
|
|
||||||
permission_id: permissionId,
|
|
||||||
status: "resolved",
|
|
||||||
action,
|
|
||||||
metadata: {
|
|
||||||
reply: request.reply,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.emitEvent(sessionId, "permission.resolved", resolved);
|
|
||||||
if (pending.autoEndTurnOnResolve) {
|
|
||||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async terminateSession(sessionId: string): Promise<void> {
|
|
||||||
const runtime = this.sessions.get(sessionId);
|
|
||||||
if (!runtime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitEvent(sessionId, "session.ended", {
|
|
||||||
reason: "terminated_by_user",
|
|
||||||
terminated_by: "inspector",
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.info.ended = true;
|
|
||||||
|
|
||||||
for (const pending of runtime.pendingPermissions.values()) {
|
|
||||||
pending.resolve({
|
|
||||||
outcome: {
|
|
||||||
outcome: "cancelled",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
runtime.pendingPermissions.clear();
|
|
||||||
runtime.pendingQuestions.clear();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await runtime.connection.close();
|
|
||||||
} catch {
|
|
||||||
// Best-effort close.
|
|
||||||
}
|
|
||||||
|
|
||||||
this.aliasByRealSessionId.delete(runtime.realSessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
|
||||||
for (const sessionId of Array.from(this.sessions.keys())) {
|
|
||||||
await this.terminateSession(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.base.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSessionUpdate(notification: SessionNotification): void {
|
|
||||||
const aliasSessionId = this.aliasByRealSessionId.get(notification.sessionId);
|
|
||||||
if (!aliasSessionId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtime = this.sessions.get(aliasSessionId);
|
|
||||||
if (!runtime || runtime.info.ended) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = notification.update;
|
|
||||||
|
|
||||||
// Still handle session_info_update for sidebar metadata
|
|
||||||
if (update.sessionUpdate === "session_info_update") {
|
|
||||||
runtime.info.title = update.title ?? runtime.info.title;
|
|
||||||
runtime.info.updatedAt = update.updatedAt ?? runtime.info.updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit the raw notification as the event data, using the ACP discriminator as the type
|
|
||||||
this.emitEvent(aliasSessionId, `acp.${update.sessionUpdate}`, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handlePermissionRequest(
|
|
||||||
request: RequestPermissionRequest,
|
|
||||||
): Promise<RequestPermissionResponse> {
|
|
||||||
const aliasSessionId = this.aliasByRealSessionId.get(request.sessionId);
|
|
||||||
if (!aliasSessionId) {
|
|
||||||
return {
|
|
||||||
outcome: {
|
|
||||||
outcome: "cancelled",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtime = this.sessions.get(aliasSessionId);
|
|
||||||
if (!runtime || runtime.info.ended) {
|
|
||||||
return {
|
|
||||||
outcome: {
|
|
||||||
outcome: "cancelled",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.permissionCounter += 1;
|
|
||||||
const permissionId = `permission-${this.permissionCounter}`;
|
|
||||||
|
|
||||||
const action = request.toolCall.title ?? request.toolCall.kind ?? "permission";
|
|
||||||
const pendingEvent: PermissionEventData = {
|
|
||||||
permission_id: permissionId,
|
|
||||||
status: "requested",
|
|
||||||
action,
|
|
||||||
metadata: request,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.emitEvent(aliasSessionId, "permission.requested", pendingEvent);
|
|
||||||
|
|
||||||
return await new Promise<RequestPermissionResponse>((resolve) => {
|
|
||||||
runtime.pendingPermissions.set(permissionId, { request, resolve });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitError(sessionId: string, message: string): void {
|
|
||||||
this.emitEvent(sessionId, "error", {
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitEvent(sessionId: string, type: string, data: unknown): void {
|
|
||||||
const runtime = this.sessions.get(sessionId);
|
|
||||||
if (!runtime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const event: UniversalEvent = {
|
|
||||||
event_id: `${sessionId}-${runtime.nextSequence}`,
|
|
||||||
sequence: runtime.nextSequence,
|
|
||||||
type,
|
|
||||||
source: "inspector.acp",
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
synthetic: true,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
|
|
||||||
runtime.nextSequence += 1;
|
|
||||||
runtime.events.push(event);
|
|
||||||
runtime.info.eventCount = runtime.events.length;
|
|
||||||
|
|
||||||
for (const listener of runtime.listeners) {
|
|
||||||
listener(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private requireSession(sessionId: string): RuntimeSession {
|
|
||||||
const runtime = this.sessions.get(sessionId);
|
|
||||||
if (!runtime) {
|
|
||||||
throw new Error(`Session not found: ${sessionId}`);
|
|
||||||
}
|
|
||||||
return runtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
private requireActiveSession(sessionId: string): RuntimeSession {
|
|
||||||
const runtime = this.requireSession(sessionId);
|
|
||||||
if (runtime.info.ended) {
|
|
||||||
throw new Error(`Session ended: ${sessionId}`);
|
|
||||||
}
|
|
||||||
return runtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertMcpConfig = (mcp: Record<string, unknown>) => {
|
|
||||||
return Object.entries(mcp)
|
|
||||||
.map(([name, config]) => {
|
|
||||||
if (!config || typeof config !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = config as Record<string, unknown>;
|
|
||||||
const type = value.type;
|
|
||||||
|
|
||||||
if (type === "local") {
|
|
||||||
const commandValue = value.command;
|
|
||||||
const argsValue = value.args;
|
|
||||||
|
|
||||||
let command = "";
|
|
||||||
let args: string[] = [];
|
|
||||||
|
|
||||||
if (Array.isArray(commandValue) && commandValue.length > 0) {
|
|
||||||
command = String(commandValue[0] ?? "");
|
|
||||||
args = commandValue.slice(1).map((part) => String(part));
|
|
||||||
} else if (typeof commandValue === "string") {
|
|
||||||
command = commandValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(argsValue)) {
|
|
||||||
args = argsValue.map((part) => String(part));
|
|
||||||
}
|
|
||||||
|
|
||||||
const envObject =
|
|
||||||
value.env && typeof value.env === "object" ? (value.env as Record<string, unknown>) : {};
|
|
||||||
const env = Object.entries(envObject).map(([envName, envValue]) => ({
|
|
||||||
name: envName,
|
|
||||||
value: String(envValue),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
command,
|
|
||||||
args,
|
|
||||||
env,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "remote") {
|
|
||||||
const headersObject =
|
|
||||||
value.headers && typeof value.headers === "object"
|
|
||||||
? (value.headers as Record<string, unknown>)
|
|
||||||
: {};
|
|
||||||
const headers = Object.entries(headersObject).map(([headerName, headerValue]) => ({
|
|
||||||
name: headerName,
|
|
||||||
value: String(headerValue),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "http" as const,
|
|
||||||
name,
|
|
||||||
url: String(value.url ?? ""),
|
|
||||||
headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectPermissionOption = (
|
|
||||||
options: PermissionOption[],
|
|
||||||
reply: PermissionReplyRequest["reply"],
|
|
||||||
): string | null => {
|
|
||||||
const pick = (...kinds: PermissionOption["kind"][]) => {
|
|
||||||
return options.find((option) => kinds.includes(option.kind))?.optionId ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (reply === "always") {
|
|
||||||
return pick("allow_always", "allow_once");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reply === "once") {
|
|
||||||
return pick("allow_once", "allow_always");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pick("reject_once", "reject_always");
|
|
||||||
};
|
|
||||||
|
|
||||||
const waitForSignalOrEvent = async (
|
|
||||||
signal: AbortSignal | undefined,
|
|
||||||
createWaitPromise: () => Promise<void>,
|
|
||||||
) => {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
let done = false;
|
|
||||||
const finish = () => {
|
|
||||||
if (done) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
if (signal) {
|
|
||||||
signal.removeEventListener("abort", onAbort);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAbort = () => finish();
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
createWaitPromise().then(finish).catch(finish);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
export type SkillSourceType = "github" | "local" | "git";
|
|
||||||
|
|
||||||
export type SkillSource = {
|
|
||||||
type: SkillSourceType;
|
|
||||||
source: string;
|
|
||||||
skills?: string[];
|
|
||||||
ref?: string;
|
|
||||||
subpath?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateSessionRequest = {
|
|
||||||
agent: string;
|
|
||||||
agentMode?: string;
|
|
||||||
permissionMode?: string;
|
|
||||||
model?: string;
|
|
||||||
variant?: string;
|
|
||||||
mcp?: Record<string, unknown>;
|
|
||||||
skills?: {
|
|
||||||
sources: SkillSource[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentModeInfo = {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentModelInfo = {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
variants?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentInfo = {
|
|
||||||
id: string;
|
|
||||||
installed: boolean;
|
|
||||||
credentialsAvailable: boolean;
|
|
||||||
version?: string | null;
|
|
||||||
path?: string | null;
|
|
||||||
capabilities: Record<string, boolean | undefined>;
|
|
||||||
native_required?: boolean;
|
|
||||||
native_installed?: boolean;
|
|
||||||
native_version?: string | null;
|
|
||||||
agent_process_installed?: boolean;
|
|
||||||
agent_process_source?: string | null;
|
|
||||||
agent_process_version?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ContentPart = {
|
|
||||||
type?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UniversalItem = {
|
|
||||||
item_id: string;
|
|
||||||
native_item_id?: string | null;
|
|
||||||
parent_id?: string | null;
|
|
||||||
kind: string;
|
|
||||||
role?: string | null;
|
|
||||||
content?: ContentPart[];
|
|
||||||
status?: string | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UniversalEvent = {
|
|
||||||
event_id: string;
|
|
||||||
sequence: number;
|
|
||||||
type: string;
|
|
||||||
source: string;
|
|
||||||
time: string;
|
|
||||||
synthetic?: boolean;
|
|
||||||
data: unknown;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PermissionEventData = {
|
|
||||||
permission_id: string;
|
|
||||||
status: "requested" | "resolved";
|
|
||||||
action: string;
|
|
||||||
metadata?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuestionEventData = {
|
|
||||||
question_id: string;
|
|
||||||
status: "requested" | "resolved";
|
|
||||||
prompt: string;
|
|
||||||
options: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SessionInfo = {
|
|
||||||
sessionId: string;
|
|
||||||
agent: string;
|
|
||||||
eventCount: number;
|
|
||||||
ended?: boolean;
|
|
||||||
model?: string | null;
|
|
||||||
variant?: string | null;
|
|
||||||
permissionMode?: string | null;
|
|
||||||
mcp?: Record<string, unknown>;
|
|
||||||
skills?: {
|
|
||||||
sources?: SkillSource[];
|
|
||||||
};
|
|
||||||
title?: string | null;
|
|
||||||
updatedAt?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EventsQuery = {
|
|
||||||
offset?: number;
|
|
||||||
limit?: number;
|
|
||||||
includeRaw?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EventsResponse = {
|
|
||||||
events: UniversalEvent[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SessionListResponse = {
|
|
||||||
sessions: SessionInfo[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentModesResponse = {
|
|
||||||
modes: AgentModeInfo[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentModelsResponse = {
|
|
||||||
models: AgentModelInfo[];
|
|
||||||
defaultModel?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MessageRequest = {
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TurnStreamQuery = {
|
|
||||||
includeRaw?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PermissionReplyRequest = {
|
|
||||||
reply: "once" | "always" | "reject";
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuestionReplyRequest = {
|
|
||||||
answers: string[][];
|
|
||||||
};
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default defineConfig(({ command }) => ({
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/v2": {
|
"/v1": {
|
||||||
target: "http://localhost:2468",
|
target: "http://localhost:2468",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from '@astrojs/tailwind';
|
||||||
import sitemap from '@astrojs/sitemap';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://sandbox-agent.dev',
|
|
||||||
output: 'static',
|
output: 'static',
|
||||||
integrations: [
|
integrations: [
|
||||||
react(),
|
react(),
|
||||||
tailwind(),
|
tailwind()
|
||||||
sitemap()
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^4.2.0",
|
"@astrojs/react": "^4.2.0",
|
||||||
"@astrojs/sitemap": "^3.2.0",
|
|
||||||
"@astrojs/tailwind": "^6.0.0",
|
"@astrojs/tailwind": "^6.0.0",
|
||||||
"astro": "^5.1.0",
|
"astro": "^5.1.0",
|
||||||
"framer-motion": "^12.0.0",
|
"framer-motion": "^12.0.0",
|
||||||
|
|
|
||||||
112
frontend/packages/website/src/components/CTASection.tsx
Normal file
112
frontend/packages/website/src/components/CTASection.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { ArrowRight, Terminal, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
const CTA_TITLES = [
|
||||||
|
'Run coding agents in sandboxes. Control them over HTTP.',
|
||||||
|
'A server inside your sandbox. An API for your app.',
|
||||||
|
'Claude Code, Codex, OpenCode, Amp, Pi — one HTTP API.',
|
||||||
|
'Your app connects remotely. The coding agent runs isolated.',
|
||||||
|
'Streaming events. Handling permissions. Managing sessions.',
|
||||||
|
'Install with curl. Connect over HTTP. Control any coding agent.',
|
||||||
|
'The bridge between your app and sandboxed coding agents.',
|
||||||
|
];
|
||||||
|
|
||||||
|
function AnimatedCTATitle() {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentIndex(prev => (prev + 1) % CTA_TITLES.length);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2 className='min-h-[1.2em] text-4xl font-medium tracking-tight text-white md:text-5xl'>
|
||||||
|
<AnimatePresence mode='wait'>
|
||||||
|
<motion.span
|
||||||
|
key={currentIndex}
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -5 }}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
>
|
||||||
|
{CTA_TITLES[currentIndex]}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyInstallButton = () => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const installCommand = 'curl -sSL https://sandboxagent.dev/install | sh';
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(installCommand);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className='inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-white/10 bg-white/5 px-4 py-2 text-sm text-white subpixel-antialiased shadow-sm transition-colors hover:border-white/20'
|
||||||
|
>
|
||||||
|
{copied ? <Check className='h-4 w-4' /> : <Terminal className='h-4 w-4' />}
|
||||||
|
{installCommand}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CTASection() {
|
||||||
|
return (
|
||||||
|
<section className='relative overflow-hidden border-t border-white/10 px-6 py-32 text-center'>
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: [0.3, 0.5, 0.3] }}
|
||||||
|
transition={{ duration: 4, repeat: Infinity }}
|
||||||
|
className='pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-zinc-500/10 via-transparent to-transparent opacity-50'
|
||||||
|
/>
|
||||||
|
<div className='relative z-10 mx-auto max-w-3xl'>
|
||||||
|
<div className='mb-8'>
|
||||||
|
<AnimatedCTATitle />
|
||||||
|
</div>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
className='mb-10 text-lg leading-relaxed text-zinc-400'
|
||||||
|
>
|
||||||
|
A server that runs inside isolated environments. <br className='hidden md:block' />
|
||||||
|
Your app connects remotely to control any coding agent.
|
||||||
|
</motion.p>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
className='flex flex-col items-center justify-center gap-4 sm:flex-row'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href='/docs'
|
||||||
|
className='inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-white/10 bg-white px-4 py-2 text-sm text-black subpixel-antialiased shadow-sm transition-colors hover:bg-zinc-200'
|
||||||
|
>
|
||||||
|
Read the Docs
|
||||||
|
<ArrowRight className='h-4 w-4' />
|
||||||
|
</a>
|
||||||
|
<CopyInstallButton />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ const faqs = [
|
||||||
{
|
{
|
||||||
question: 'How is session data persisted?',
|
question: 'How is session data persisted?',
|
||||||
answer:
|
answer:
|
||||||
"This SDK does not handle persisting session data. In v2, traffic is ACP JSON-RPC over <code>/v2/rpc</code>; persist envelopes in your own storage if you need replay or auditing.",
|
"This SDK does not handle persisting session data. Events stream in a universal JSON schema that you can persist anywhere. Consider using Postgres or <a href='https://rivet.gg' target='_blank' rel='noopener noreferrer' class='text-orange-400 hover:underline'>Rivet Actors</a> for data persistence.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Can I run this locally or does it require a sandbox provider?',
|
question: 'Can I run this locally or does it require a sandbox provider?',
|
||||||
|
|
@ -61,14 +61,14 @@ function FAQItem({ question, answer }: { question: string; answer: string }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-white/10 first:border-t-0">
|
<div className="border-b border-white/5">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="group flex w-full items-center justify-between py-5 text-left"
|
className="flex w-full items-center justify-between py-5 text-left"
|
||||||
>
|
>
|
||||||
<span className="text-base font-normal text-white pr-4 group-hover:text-zinc-300 transition-colors">{question}</span>
|
<span className="text-base font-medium text-white pr-4">{question}</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`h-4 w-4 shrink-0 text-zinc-500 transition-transform duration-200 ${
|
className={`h-5 w-5 shrink-0 text-zinc-500 transition-transform duration-200 ${
|
||||||
isOpen ? 'rotate-180' : ''
|
isOpen ? 'rotate-180' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -82,7 +82,7 @@ function FAQItem({ question, answer }: { question: string; answer: string }) {
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<p className="pb-5 text-sm leading-relaxed text-zinc-500" dangerouslySetInnerHTML={{ __html: answer }} />
|
<p className="pb-5 text-sm leading-relaxed text-zinc-400" dangerouslySetInnerHTML={{ __html: answer }} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
@ -92,40 +92,22 @@ function FAQItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
|
||||||
export function FAQ() {
|
export function FAQ() {
|
||||||
return (
|
return (
|
||||||
<section className="border-t border-white/10 py-48">
|
<section className="relative overflow-hidden border-t border-white/5 py-24">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-3xl px-6">
|
||||||
<div className="mb-12 text-center">
|
<div className="mb-12 text-center">
|
||||||
<motion.h2
|
<h2 className="mb-4 text-3xl font-medium tracking-tight text-white">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
|
|
||||||
>
|
|
||||||
Frequently Asked Questions
|
Frequently Asked Questions
|
||||||
</motion.h2>
|
</h2>
|
||||||
<motion.p
|
<p className="text-zinc-400">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="mx-auto max-w-xl text-base leading-relaxed text-zinc-500"
|
|
||||||
>
|
|
||||||
Common questions about running agents in sandboxes.
|
Common questions about running agents in sandboxes.
|
||||||
</motion.p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<div className="divide-y divide-white/5 rounded-2xl border border-white/5 bg-zinc-900/30 px-6">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
className="mx-auto max-w-3xl"
|
|
||||||
>
|
|
||||||
{faqs.map((faq, index) => (
|
{faqs.map((faq, index) => (
|
||||||
<FAQItem key={index} question={faq.question} answer={faq.answer} />
|
<FAQItem key={index} question={faq.question} answer={faq.answer} />
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,120 +1,169 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { Workflow, Server, Database, Download, Globe, Plug } from 'lucide-react';
|
import { Workflow, Server, Database, Download, Globe, Plug } from 'lucide-react';
|
||||||
|
import { FeatureIcon } from './ui/FeatureIcon';
|
||||||
|
|
||||||
export function FeatureGrid() {
|
export function FeatureGrid() {
|
||||||
return (
|
return (
|
||||||
<section id="features" className="border-t border-white/10 py-48">
|
<section id="features" className="relative overflow-hidden border-t border-white/5 py-32">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="relative z-10 mx-auto max-w-7xl px-6">
|
||||||
<div className="mb-12">
|
<div className="mb-16">
|
||||||
<motion.h2
|
<h2 className="mb-4 text-3xl font-medium tracking-tight text-white md:text-5xl">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
|
|
||||||
>
|
|
||||||
How it works.
|
How it works.
|
||||||
</motion.h2>
|
</h2>
|
||||||
<motion.p
|
<p className="text-lg leading-relaxed text-zinc-400">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="max-w-xl text-base leading-relaxed text-zinc-500"
|
|
||||||
>
|
|
||||||
A server runs inside your sandbox. Your app connects over HTTP to control any coding agent.
|
A server runs inside your sandbox. Your app connects over HTTP to control any coding agent.
|
||||||
</motion.p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
|
||||||
>
|
|
||||||
{/* Universal Agent API - Span full width */}
|
{/* Universal Agent API - Span full width */}
|
||||||
<div className="group col-span-full flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="col-span-full group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="flex items-center gap-3">
|
{/* Top Shine Highlight */}
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-orange-400">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<Workflow className="h-4 w-4" />
|
{/* Top Left Reflection/Glow */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,79,0,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
|
{/* Sharp Edge Highlight */}
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-orange-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-col gap-4">
|
||||||
|
<div className="relative z-10 mb-2 flex items-center gap-3">
|
||||||
|
<FeatureIcon
|
||||||
|
icon={Workflow}
|
||||||
|
color="text-orange-400"
|
||||||
|
bgColor="bg-orange-500/10"
|
||||||
|
hoverBgColor="group-hover:bg-orange-500/20"
|
||||||
|
glowShadow="group-hover:shadow-[0_0_15px_rgba(255,79,0,0.5)]"
|
||||||
|
/>
|
||||||
|
<h4 className="text-sm font-medium uppercase tracking-wider text-white">Universal Agent API</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-base font-normal text-white">Universal Agent API</h4>
|
<p className="text-zinc-400 leading-relaxed text-lg max-w-2xl">
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 leading-relaxed text-base max-w-2xl">
|
|
||||||
Claude Code, Codex, OpenCode, Amp, and Pi each have different APIs. We provide a single,
|
Claude Code, Codex, OpenCode, Amp, and Pi each have different APIs. We provide a single,
|
||||||
unified interface to control them all.
|
unified interface to control them all.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Streaming Events */}
|
{/* Streaming Events */}
|
||||||
<div className="group flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="flex items-center gap-3">
|
{/* Top Shine Highlight */}
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-green-400">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<Server className="h-4 w-4" />
|
{/* Top Left Reflection/Glow */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(34,197,94,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
|
{/* Sharp Edge Highlight */}
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-green-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-2 flex items-center gap-3">
|
||||||
|
<FeatureIcon
|
||||||
|
icon={Server}
|
||||||
|
color="text-green-400"
|
||||||
|
bgColor="bg-green-500/10"
|
||||||
|
hoverBgColor="group-hover:bg-green-500/20"
|
||||||
|
glowShadow="group-hover:shadow-[0_0_15px_rgba(34,197,94,0.5)]"
|
||||||
|
/>
|
||||||
|
<h4 className="text-sm font-medium uppercase tracking-wider text-white">Streaming Events</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-base font-normal text-white">Streaming Events</h4>
|
<p className="text-zinc-400 text-sm leading-relaxed">
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 text-sm leading-relaxed">
|
|
||||||
Real-time SSE stream of everything the agent does. Persist to your storage, replay sessions, audit everything.
|
Real-time SSE stream of everything the agent does. Persist to your storage, replay sessions, audit everything.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Universal Schema */}
|
{/* Handling Permissions */}
|
||||||
<div className="group flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="flex items-center gap-3">
|
{/* Top Shine Highlight */}
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-purple-400">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<Database className="h-4 w-4" />
|
{/* Top Left Reflection/Glow */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(168,85,247,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
|
{/* Sharp Edge Highlight */}
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-purple-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-2 flex items-center gap-3">
|
||||||
|
<FeatureIcon
|
||||||
|
icon={Database}
|
||||||
|
color="text-purple-400"
|
||||||
|
bgColor="bg-purple-500/10"
|
||||||
|
hoverBgColor="group-hover:bg-purple-500/20"
|
||||||
|
glowShadow="group-hover:shadow-[0_0_15px_rgba(168,85,247,0.5)]"
|
||||||
|
/>
|
||||||
|
<h4 className="text-sm font-medium uppercase tracking-wider text-white">Universal Schema</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-base font-normal text-white">Universal Schema</h4>
|
<p className="text-zinc-400 text-sm leading-relaxed">
|
||||||
</div>
|
Standardized session schema that covers all features of all agents. Includes tool calls, permission requests, file edits, etc. Approve or deny tool executions remotely over HTTP.
|
||||||
<p className="text-zinc-500 text-sm leading-relaxed">
|
|
||||||
Standardized session schema that covers all features of all agents. Includes tool calls, permission requests, file edits, etc.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Runs Inside Any Sandbox */}
|
{/* Runs Inside Any Sandbox */}
|
||||||
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="lg:col-span-2 group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="flex items-center gap-3">
|
{/* Top Shine Highlight */}
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-blue-400">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<Globe className="h-4 w-4" />
|
{/* Top Left Reflection/Glow */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
|
{/* Sharp Edge Highlight */}
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-blue-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-2 flex items-center gap-3">
|
||||||
|
<FeatureIcon
|
||||||
|
icon={Globe}
|
||||||
|
color="text-blue-400"
|
||||||
|
bgColor="bg-blue-500/10"
|
||||||
|
hoverBgColor="group-hover:bg-blue-500/20"
|
||||||
|
glowShadow="group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]"
|
||||||
|
/>
|
||||||
|
<h4 className="text-sm font-medium uppercase tracking-wider text-white">Runs Inside Any Sandbox</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-base font-normal text-white">Runs Inside Any Sandbox</h4>
|
<p className="text-zinc-400 text-sm leading-relaxed">
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 text-sm leading-relaxed">
|
|
||||||
Lightweight static binary. One curl command to install inside E2B, Daytona, Vercel Sandboxes, or Docker.
|
Lightweight static binary. One curl command to install inside E2B, Daytona, Vercel Sandboxes, or Docker.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Session Management */}
|
{/* Automatic Agent Installation */}
|
||||||
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="lg:col-span-2 group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="flex items-center gap-3">
|
{/* Top Shine Highlight */}
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-amber-400">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<Download className="h-4 w-4" />
|
{/* Top Left Reflection/Glow */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(245,158,11,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
|
{/* Sharp Edge Highlight */}
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-amber-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-2 flex items-center gap-3">
|
||||||
|
<FeatureIcon
|
||||||
|
icon={Download}
|
||||||
|
color="text-amber-400"
|
||||||
|
bgColor="bg-amber-500/10"
|
||||||
|
hoverBgColor="group-hover:bg-amber-500/20"
|
||||||
|
glowShadow="group-hover:shadow-[0_0_15px_rgba(245,158,11,0.5)]"
|
||||||
|
/>
|
||||||
|
<h4 className="text-sm font-medium uppercase tracking-wider text-white">Automatic Agent Installation</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-base font-normal text-white">Session Management</h4>
|
<p className="text-zinc-400 text-sm leading-relaxed">
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 text-sm leading-relaxed">
|
|
||||||
Create sessions, send messages, persist transcripts. Full session lifecycle management over HTTP.
|
Create sessions, send messages, persist transcripts. Full session lifecycle management over HTTP.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OpenCode SDK & UI Support */}
|
{/* OpenCode SDK & UI Support */}
|
||||||
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="lg:col-span-2 group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="flex items-center gap-3">
|
{/* Top Shine Highlight */}
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-pink-400">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<Plug className="h-4 w-4" />
|
{/* Top Left Reflection/Glow */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(236,72,153,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
|
{/* Sharp Edge Highlight */}
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-pink-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-2 flex items-center gap-3">
|
||||||
|
<FeatureIcon
|
||||||
|
icon={Plug}
|
||||||
|
color="text-pink-400"
|
||||||
|
bgColor="bg-pink-500/10"
|
||||||
|
hoverBgColor="group-hover:bg-pink-500/20"
|
||||||
|
glowShadow="group-hover:shadow-[0_0_15px_rgba(236,72,153,0.5)]"
|
||||||
|
/>
|
||||||
|
<h4 className="text-sm font-medium uppercase tracking-wider text-white">OpenCode SDK & UI Support</h4>
|
||||||
|
<span className="rounded-full bg-pink-500/20 px-2 py-0.5 text-xs font-medium text-pink-300">Experimental</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-base font-normal text-white">OpenCode Support</h4>
|
<p className="text-zinc-400 text-sm leading-relaxed">
|
||||||
<span className="rounded-full border border-white/10 px-2 py-0.5 text-[10px] font-medium text-zinc-500 transition-colors group-hover:text-pink-400 group-hover:border-pink-400/30">Experimental</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 text-sm leading-relaxed">
|
|
||||||
Connect OpenCode CLI, SDK, or web UI to control agents through familiar OpenCode tooling.
|
Connect OpenCode CLI, SDK, or web UI to control agents through familiar OpenCode tooling.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
const footer = {
|
const footer = {
|
||||||
products: [
|
products: [
|
||||||
{ name: 'Actors', href: 'https://rivet.dev/docs/actors' },
|
{ name: 'Actors', href: 'https://rivet.dev/docs/actors' },
|
||||||
|
|
@ -50,22 +48,16 @@ const footer = {
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-white/10 bg-black">
|
<footer className="border-t border-white/10 bg-zinc-950">
|
||||||
<div className="mx-auto max-w-6xl px-6 py-16 lg:py-20">
|
<div className="mx-auto max-w-7xl px-6 py-12 lg:py-16">
|
||||||
<div className="xl:grid xl:grid-cols-12 xl:gap-16">
|
<div className="xl:grid xl:grid-cols-12 xl:gap-16">
|
||||||
{/* Logo & Social */}
|
{/* Logo & Social */}
|
||||||
<motion.div
|
<div className="space-y-6 xl:col-span-4">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<a href="https://rivet.dev">
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<img src="/rivet-logo-text-white.svg" alt="Rivet" className="h-6 w-auto" />
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="space-y-6 xl:col-span-4"
|
|
||||||
>
|
|
||||||
<a href="https://rivet.dev" className="inline-block">
|
|
||||||
<img src="/rivet-logo-text-white.svg" alt="Rivet" className="h-6 w-auto opacity-90 hover:opacity-100 transition-opacity" />
|
|
||||||
</a>
|
</a>
|
||||||
<p className="text-sm leading-6 text-zinc-500">
|
<p className="text-sm leading-6 text-zinc-400">
|
||||||
Infrastructure for software that thinks
|
Build and scale stateful workloads
|
||||||
</p>
|
</p>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
{footer.social.map((item) => (
|
{footer.social.map((item) => (
|
||||||
|
|
@ -81,87 +73,64 @@ export function Footer() {
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Links */}
|
{/* Links */}
|
||||||
<div className="mt-12 grid grid-cols-2 gap-8 md:grid-cols-3 xl:col-span-8 xl:mt-0">
|
<div className="mt-12 grid grid-cols-2 gap-8 md:grid-cols-3 xl:col-span-8 xl:mt-0">
|
||||||
<motion.div
|
<div>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
>
|
|
||||||
<h3 className="text-sm font-semibold leading-6 text-white">Products</h3>
|
<h3 className="text-sm font-semibold leading-6 text-white">Products</h3>
|
||||||
<ul role="list" className="mt-4 space-y-3">
|
<ul role="list" className="mt-4 space-y-3">
|
||||||
{footer.products.map((item) => (
|
{footer.products.map((item) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-sm leading-6 text-zinc-500 hover:text-white transition-colors"
|
className="text-sm leading-6 text-zinc-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</div>
|
||||||
|
<div>
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.15 }}
|
|
||||||
>
|
|
||||||
<h3 className="text-sm font-semibold leading-6 text-white">Developers</h3>
|
<h3 className="text-sm font-semibold leading-6 text-white">Developers</h3>
|
||||||
<ul role="list" className="mt-4 space-y-3">
|
<ul role="list" className="mt-4 space-y-3">
|
||||||
{footer.developers.map((item) => (
|
{footer.developers.map((item) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-sm leading-6 text-zinc-500 hover:text-white transition-colors"
|
className="text-sm leading-6 text-zinc-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</div>
|
||||||
|
<div>
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
>
|
|
||||||
<h3 className="text-sm font-semibold leading-6 text-white">Legal</h3>
|
<h3 className="text-sm font-semibold leading-6 text-white">Legal</h3>
|
||||||
<ul role="list" className="mt-4 space-y-3">
|
<ul role="list" className="mt-4 space-y-3">
|
||||||
{footer.legal.map((item) => (
|
{footer.legal.map((item) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-sm leading-6 text-zinc-500 hover:text-white transition-colors"
|
className="text-sm leading-6 text-zinc-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom */}
|
{/* Bottom */}
|
||||||
<motion.div
|
<div className="mt-12 border-t border-white/10 pt-8">
|
||||||
initial={{ opacity: 0 }}
|
<p className="text-xs text-zinc-500 text-center">
|
||||||
whileInView={{ opacity: 1 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
|
||||||
className="mt-12 border-t border-white/10 pt-8"
|
|
||||||
>
|
|
||||||
<p className="text-xs text-zinc-600 text-center">
|
|
||||||
© {new Date().getFullYear()} Rivet Gaming, Inc. All rights reserved.
|
© {new Date().getFullYear()} Rivet Gaming, Inc. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { Code, Server, GitBranch } from 'lucide-react';
|
import { Code, Server, GitBranch } from 'lucide-react';
|
||||||
import { CopyButton } from './ui/CopyButton';
|
import { CopyButton } from './ui/CopyButton';
|
||||||
|
|
||||||
|
|
@ -87,7 +86,7 @@ function SdkCodeHighlighted() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh`;
|
const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh`;
|
||||||
|
|
||||||
const sourceCommands = `git clone https://github.com/rivet-dev/sandbox-agent
|
const sourceCommands = `git clone https://github.com/rivet-dev/sandbox-agent
|
||||||
cd sandbox-agent
|
cd sandbox-agent
|
||||||
|
|
@ -95,55 +94,44 @@ cargo run -p sandbox-agent --release`;
|
||||||
|
|
||||||
export function GetStarted() {
|
export function GetStarted() {
|
||||||
return (
|
return (
|
||||||
<section id="get-started" className="border-t border-white/10 py-48">
|
<section id="get-started" className="relative overflow-hidden border-t border-white/5 py-32">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="relative z-10 mx-auto max-w-7xl px-6">
|
||||||
<div className="mb-12">
|
<div className="mb-16 text-center">
|
||||||
<motion.h2
|
<h2 className="mb-4 text-3xl font-medium tracking-tight text-white md:text-5xl">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
|
|
||||||
>
|
|
||||||
Get Started
|
Get Started
|
||||||
</motion.h2>
|
</h2>
|
||||||
<motion.p
|
<p className="text-lg text-zinc-400">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="max-w-xl text-base leading-relaxed text-zinc-500"
|
|
||||||
>
|
|
||||||
Choose the installation method that works best for your use case.
|
Choose the installation method that works best for your use case.
|
||||||
</motion.p>
|
</p>
|
||||||
|
<p className="mt-4 text-sm text-zinc-500">
|
||||||
|
Quick OpenCode attach: <span className="font-mono text-white">npx @sandbox-agent/gigacode</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="grid grid-cols-1 gap-4 md:grid-cols-3"
|
|
||||||
>
|
|
||||||
{/* Option 1: SDK */}
|
{/* Option 1: SDK */}
|
||||||
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group relative flex flex-col overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<div className="text-zinc-500">
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
<Code className="h-4 w-4" />
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-blue-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-4 flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10 text-blue-400 transition-all duration-300 group-hover:bg-blue-500/20 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]">
|
||||||
|
<Code className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-normal text-white">TypeScript SDK</h3>
|
<h3 className="text-lg font-semibold text-white">TypeScript SDK</h3>
|
||||||
<p className="text-xs text-zinc-500">Embed in your application</p>
|
<p className="text-xs text-zinc-500">Embed in your application</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-4 text-sm leading-relaxed text-zinc-500">
|
<p className="relative z-10 mb-4 text-sm leading-relaxed text-zinc-400 min-h-[4.5rem]">
|
||||||
Import the TypeScript SDK directly into your Node or browser application. Full type safety and streaming support.
|
Import the TypeScript SDK directly into your Node or browser application. Full type safety and streaming support.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="relative z-10 flex-1 flex flex-col">
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
|
<div className="overflow-hidden rounded-lg border border-white/5 bg-black/50 flex-1 flex flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-white/5 bg-white/5 px-3 py-2">
|
||||||
<span className="text-[10px] font-medium text-zinc-500">example.ts</span>
|
<span className="text-[10px] font-medium text-zinc-500">example.ts</span>
|
||||||
<CopyButton text={sdkCodeRaw} />
|
<CopyButton text={sdkCodeRaw} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -152,25 +140,29 @@ export function GetStarted() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Option 2: HTTP API */}
|
{/* Option 2: Sandbox */}
|
||||||
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group relative flex flex-col overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<div className="text-zinc-500">
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(34,197,94,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
<Server className="h-4 w-4" />
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-green-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-4 flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-500/10 text-green-400 transition-all duration-300 group-hover:bg-green-500/20 group-hover:shadow-[0_0_15px_rgba(34,197,94,0.5)]">
|
||||||
|
<Server className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-normal text-white">HTTP API</h3>
|
<h3 className="text-lg font-semibold text-white">HTTP API</h3>
|
||||||
<p className="text-xs text-zinc-500">Run as a server</p>
|
<p className="text-xs text-zinc-500">Run as a server</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-4 text-sm leading-relaxed text-zinc-500">
|
<p className="relative z-10 mb-4 text-sm leading-relaxed text-zinc-400 min-h-[4.5rem]">
|
||||||
Run as an HTTP server and connect from any language. Deploy to E2B, Daytona, Vercel, or your own infrastructure.
|
Run as an HTTP server and connect from any language. Deploy to E2B, Daytona, Vercel, or your own infrastructure.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="relative z-10 flex-1 flex flex-col">
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
|
<div className="overflow-hidden rounded-lg border border-white/5 bg-black/50 flex-1 flex flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-white/5 bg-white/5 px-3 py-2">
|
||||||
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
||||||
<CopyButton text={sandboxCommand} />
|
<CopyButton text={sandboxCommand} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -180,7 +172,7 @@ export function GetStarted() {
|
||||||
<span className="text-zinc-300">curl -fsSL \</span>
|
<span className="text-zinc-300">curl -fsSL \</span>
|
||||||
{"\n"}
|
{"\n"}
|
||||||
<span className="text-zinc-300">{" "}</span>
|
<span className="text-zinc-300">{" "}</span>
|
||||||
<span className="text-green-400">https://releases.rivet.dev/sandbox-agent/latest/install.sh</span>
|
<span className="text-green-400">https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh</span>
|
||||||
<span className="text-zinc-300"> | </span>
|
<span className="text-zinc-300"> | </span>
|
||||||
<span className="text-blue-400">sh</span>
|
<span className="text-blue-400">sh</span>
|
||||||
</code>
|
</code>
|
||||||
|
|
@ -189,25 +181,29 @@ export function GetStarted() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Option 3: Open Source */}
|
{/* Option 3: Build from Source */}
|
||||||
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group relative flex flex-col overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
<div className="text-zinc-500">
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(245,158,11,0.15)_0%,transparent_50%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||||
<GitBranch className="h-4 w-4" />
|
<div className="pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t border-amber-500 opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100" />
|
||||||
|
|
||||||
|
<div className="relative z-10 mb-4 flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/10 text-amber-400 transition-all duration-300 group-hover:bg-amber-500/20 group-hover:shadow-[0_0_15px_rgba(245,158,11,0.5)]">
|
||||||
|
<GitBranch className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-normal text-white">Open Source</h3>
|
<h3 className="text-lg font-semibold text-white">Open Source</h3>
|
||||||
<p className="text-xs text-zinc-500">Full control</p>
|
<p className="text-xs text-zinc-500">Full control</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-4 text-sm leading-relaxed text-zinc-500">
|
<p className="relative z-10 mb-4 text-sm leading-relaxed text-zinc-400 min-h-[4.5rem]">
|
||||||
Clone the repo and build with Cargo. Customize, contribute, or embed directly in your Rust project.
|
Clone the repo and build with Cargo. Customize, contribute, or embed directly in your Rust project.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="relative z-10 flex-1 flex flex-col">
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
|
<div className="overflow-hidden rounded-lg border border-white/5 bg-black/50 flex-1 flex flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-white/5 bg-white/5 px-3 py-2">
|
||||||
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
||||||
<CopyButton text={sourceCommands} />
|
<CopyButton text={sourceCommands} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -230,7 +226,7 @@ export function GetStarted() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,23 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export function Inspector() {
|
export function Inspector() {
|
||||||
return (
|
return (
|
||||||
<section className="border-t border-white/10 py-48">
|
<section className="relative overflow-hidden border-t border-white/5 py-24">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-4xl px-6 text-center">
|
||||||
<div className="mb-12 text-center">
|
<h2 className="mb-4 text-3xl font-medium tracking-tight text-white md:text-5xl">
|
||||||
<motion.h2
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
|
|
||||||
>
|
|
||||||
Built-in Debugger
|
Built-in Debugger
|
||||||
</motion.h2>
|
</h2>
|
||||||
<motion.p
|
<p className="mb-12 text-lg text-zinc-400">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
Inspect sessions, view event payloads, and troubleshoot without writing code.
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
</p>
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="mx-auto max-w-xl text-base leading-relaxed text-zinc-500"
|
|
||||||
>
|
|
||||||
Inspect sessions, view event payloads, and troubleshoot without writing code.
|
|
||||||
</motion.p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
<div className="overflow-hidden rounded-2xl border border-white/10 shadow-2xl">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
className="overflow-hidden rounded-2xl border border-white/10"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src="/images/inspector.png"
|
src="/images/inspector.png"
|
||||||
alt="Sandbox Agent Inspector"
|
alt="Sandbox Agent Inspector"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ import { GitHubStars } from './GitHubStars';
|
||||||
|
|
||||||
function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
|
function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<a
|
<div className="px-2.5 py-2 opacity-60 hover:opacity-100 transition-all duration-200">
|
||||||
href={href}
|
<a href={href} className="text-white text-sm">
|
||||||
className="px-3 py-2 text-sm font-medium text-zinc-400 transition-colors duration-200 hover:text-white"
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,17 +34,15 @@ export function Navigation() {
|
||||||
isScrolled ? "before:border-white/10" : "before:border-transparent"
|
isScrolled ? "before:border-white/10" : "before:border-transparent"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Background with blur */}
|
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 -z-[1] hidden overflow-hidden rounded-2xl transition-all duration-300 ease-in-out md:block ${
|
className={`absolute inset-0 -z-[1] hidden overflow-hidden rounded-2xl transition-all duration-300 ease-in-out md:block ${
|
||||||
isScrolled
|
isScrolled
|
||||||
? "bg-black/80 backdrop-blur-lg"
|
? "bg-black/80 backdrop-blur-lg"
|
||||||
: "bg-transparent backdrop-blur-none"
|
: "bg-black backdrop-blur-none"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
className={`bg-black/60 border-b-transparent sticky top-0 z-10 flex flex-col items-center border-b backdrop-blur-md pt-2 pb-2 md:static md:bg-transparent md:rounded-2xl md:max-w-[1200px] md:border-transparent md:backdrop-blur-none transition-all hover:opacity-100 ${
|
className={`bg-black/60 border-b-transparent sticky top-0 z-10 flex flex-col items-center border-b backdrop-blur pt-2 pb-2 md:static md:bg-transparent md:rounded-2xl md:max-w-[1200px] md:border-transparent md:backdrop-none md:backdrop-blur-none transition-all hover:opacity-100 ${
|
||||||
isScrolled ? "opacity-100" : "opacity-80"
|
isScrolled ? "opacity-100" : "opacity-80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -56,24 +53,24 @@ export function Navigation() {
|
||||||
<a href="https://rivet.dev" className="flex items-center">
|
<a href="https://rivet.dev" className="flex items-center">
|
||||||
<img src="/rivet-icon.svg" alt="Rivet" className="size-8" />
|
<img src="/rivet-icon.svg" alt="Rivet" className="size-8" />
|
||||||
</a>
|
</a>
|
||||||
<span className="text-white/20">|</span>
|
<span className="text-white/30">|</span>
|
||||||
<a href="/" className="flex items-center">
|
<a href="/" className="flex items-center">
|
||||||
<img src="/logos/sandboxagent.svg" alt="Sandbox Agent SDK" className="h-6 w-auto" />
|
<img src="/logos/sandboxagent.svg" alt="Sandbox Agent SDK" className="h-6 w-auto" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Nav */}
|
{/* Desktop Nav */}
|
||||||
<div className="hidden md:flex items-center ml-2">
|
<div className="hidden md:flex items-center">
|
||||||
<NavItem href="/docs">Docs</NavItem>
|
<NavItem href="/docs">Docs</NavItem>
|
||||||
<NavItem href="https://github.com/rivet-dev/sandbox-agent/releases">Changelog</NavItem>
|
<NavItem href="https://github.com/rivet-dev/sandbox-agent/releases">Changelog</NavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side */}
|
{/* Right side */}
|
||||||
<div className="hidden md:flex flex-row items-center gap-2">
|
<div className="hidden md:flex flex-row items-center">
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/auCecybynK"
|
href="https://discord.gg/auCecybynK"
|
||||||
className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-white/10 px-4 py-2 h-10 text-sm hover:border-white/20 text-white/90 hover:text-white transition-colors"
|
className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-white/10 px-4 py-2 h-10 text-sm mr-2 hover:border-white/20 text-white/90 hover:text-white transition-colors"
|
||||||
aria-label="Discord"
|
aria-label="Discord"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -93,7 +90,7 @@ export function Navigation() {
|
||||||
|
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
<button
|
<button
|
||||||
className="md:hidden text-zinc-400 hover:text-white p-2 transition-colors"
|
className="md:hidden text-zinc-400 hover:text-white p-2"
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
>
|
>
|
||||||
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
|
|
@ -104,26 +101,26 @@ export function Navigation() {
|
||||||
|
|
||||||
{/* Mobile menu */}
|
{/* Mobile menu */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="md:hidden border border-white/10 bg-black/95 backdrop-blur-lg rounded-2xl mt-2 mx-2 shadow-xl">
|
<div className="md:hidden border border-white/10 bg-black/95 backdrop-blur-lg rounded-2xl mt-2 mx-2">
|
||||||
<div className="px-4 py-4 space-y-1">
|
<div className="px-4 py-4 space-y-2">
|
||||||
<a
|
<a
|
||||||
href="/docs"
|
href="/docs"
|
||||||
className="block py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors font-medium"
|
className="block py-2 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
Docs
|
Docs
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/rivet-dev/sandbox-agent/releases"
|
href="https://github.com/rivet-dev/sandbox-agent/releases"
|
||||||
className="block py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors font-medium"
|
className="block py-2 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
Changelog
|
Changelog
|
||||||
</a>
|
</a>
|
||||||
<div className="border-t border-white/10 pt-3 mt-3 space-y-1">
|
<div className="border-t border-white/10 pt-2 mt-2 space-y-2">
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/auCecybynK"
|
href="https://discord.gg/auCecybynK"
|
||||||
className="flex items-center gap-3 py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
|
className="flex items-center gap-2 py-2 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
aria-label="Discord"
|
aria-label="Discord"
|
||||||
>
|
>
|
||||||
|
|
@ -135,11 +132,11 @@ export function Navigation() {
|
||||||
>
|
>
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="font-medium">Discord</span>
|
<span>Discord</span>
|
||||||
</a>
|
</a>
|
||||||
<GitHubStars
|
<GitHubStars
|
||||||
repo="rivet-dev/sandbox-agent"
|
repo="rivet-dev/sandbox-agent"
|
||||||
className="flex items-center gap-3 py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors w-full"
|
className="flex items-center gap-2 py-2 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,133 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Shield, Layers, Database, X, Check } from 'lucide-react';
|
import { X, Check } from 'lucide-react';
|
||||||
|
|
||||||
const frictions = [
|
const frictions = [
|
||||||
{
|
{
|
||||||
icon: Shield,
|
number: '01',
|
||||||
title: 'Coding Agents Need Sandboxes',
|
title: 'Coding Agents Need Sandboxes',
|
||||||
problem:
|
problem:
|
||||||
"You can't let AI execute arbitrary code on your production servers. Coding agents need isolated environments, but existing SDKs assume local execution.",
|
"You can't let AI execute arbitrary code on your production servers. Coding agents need isolated environments, but existing SDKs assume local execution.",
|
||||||
solution: 'A server that runs inside the sandbox and exposes HTTP/SSE.',
|
solution: 'A server that runs inside the sandbox and exposes HTTP/SSE.',
|
||||||
|
accentColor: 'orange',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Layers,
|
number: '02',
|
||||||
title: 'Every Coding Agent is Different',
|
title: 'Every Coding Agent is Different',
|
||||||
problem:
|
problem:
|
||||||
'Claude Code, Codex, OpenCode, Amp, and Pi each have proprietary APIs, event formats, and behaviors. Swapping coding agents means rewriting your entire integration.',
|
'Claude Code, Codex, OpenCode, Amp, and Pi each have proprietary APIs, event formats, and behaviors. Swapping coding agents means rewriting your entire integration.',
|
||||||
solution: 'One HTTP API. Write your code once, swap coding agents with a config change.',
|
solution: 'One HTTP API. Write your code once, swap coding agents with a config change.',
|
||||||
|
accentColor: 'purple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Database,
|
number: '03',
|
||||||
title: 'Sessions Are Ephemeral',
|
title: 'Sessions Are Ephemeral',
|
||||||
problem:
|
problem:
|
||||||
'Coding agent transcripts live in the sandbox. When the process ends, you lose everything. Debugging and replay become impossible.',
|
'Coding agent transcripts live in the sandbox. When the process ends, you lose everything. Debugging and replay become impossible.',
|
||||||
solution: 'Universal event schema streams to your storage. Persist to Postgres or Rivet, replay later, audit everything.',
|
solution: 'Universal event schema streams to your storage. Persist to Postgres or Rivet, replay later, audit everything.',
|
||||||
|
accentColor: 'blue',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const accentStyles = {
|
||||||
|
orange: {
|
||||||
|
gradient: 'from-orange-500/20',
|
||||||
|
border: 'border-orange-500/30',
|
||||||
|
glow: 'rgba(255,79,0,0.15)',
|
||||||
|
number: 'text-orange-500',
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
gradient: 'from-purple-500/20',
|
||||||
|
border: 'border-purple-500/30',
|
||||||
|
glow: 'rgba(168,85,247,0.15)',
|
||||||
|
number: 'text-purple-500',
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
gradient: 'from-blue-500/20',
|
||||||
|
border: 'border-blue-500/30',
|
||||||
|
glow: 'rgba(59,130,246,0.15)',
|
||||||
|
number: 'text-blue-500',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function PainPoints() {
|
export function PainPoints() {
|
||||||
return (
|
return (
|
||||||
<section className="border-t border-white/10 py-48">
|
<section className="relative overflow-hidden border-t border-white/5 py-32">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
<div className="mb-12">
|
|
||||||
<motion.h2
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
|
|
||||||
>
|
|
||||||
Running coding agents remotely is hard.
|
|
||||||
</motion.h2>
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="max-w-2xl text-base leading-relaxed text-zinc-500"
|
|
||||||
>
|
|
||||||
The Sandbox Agent SDK is a server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, Amp, or Pi — streaming events, handling permissions, managing sessions.
|
|
||||||
</motion.p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="grid grid-cols-1 gap-8 md:grid-cols-3"
|
className="mb-16"
|
||||||
>
|
>
|
||||||
{frictions.map((friction) => (
|
<h2 className="mb-6 text-3xl font-medium tracking-tight text-white md:text-5xl">
|
||||||
<div key={friction.title} className="flex flex-col border-t border-white/10 pt-6">
|
Running coding agents remotely is hard.
|
||||||
<div className="mb-3 text-zinc-500">
|
</h2>
|
||||||
<friction.icon className="h-4 w-4" />
|
<p className="max-w-2xl text-lg leading-relaxed text-zinc-400">
|
||||||
</div>
|
Coding agents need sandboxes, but existing SDKs assume local execution. SSH breaks, CLI wrappers are fragile, and building from scratch means reimplementing everything for each coding agent.
|
||||||
<h3 className="mb-4 text-base font-normal text-white">{friction.title}</h3>
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
{frictions.map((friction, index) => {
|
||||||
|
const styles = accentStyles[friction.accentColor as keyof typeof accentStyles];
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={friction.number}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="group relative flex flex-col overflow-hidden rounded-2xl border border-white/5 bg-zinc-900/30 p-6 backdrop-blur-sm transition-colors duration-500 hover:bg-zinc-900/50"
|
||||||
|
>
|
||||||
|
{/* Top shine */}
|
||||||
|
<div className="absolute left-0 right-0 top-0 z-10 h-[1px] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
|
|
||||||
|
{/* Hover glow */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100"
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(circle at top left, ${styles.glow} 0%, transparent 50%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Corner highlight */}
|
||||||
|
<div
|
||||||
|
className={`pointer-events-none absolute left-0 top-0 z-20 h-24 w-24 rounded-tl-2xl border-l border-t ${styles.border} opacity-0 transition-opacity duration-500 [mask-image:linear-gradient(135deg,black_0%,transparent_50%)] group-hover:opacity-100`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-col h-full">
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="mb-4 text-xl font-medium text-white">{friction.title}</h3>
|
||||||
|
|
||||||
|
{/* Problem */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<X className="h-3 w-3 text-zinc-600" />
|
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-red-500/20">
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-600">Problem</span>
|
<X className="w-3 h-3 text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-relaxed text-zinc-500">
|
<span className="text-xs font-semibold uppercase tracking-wider text-red-400">Problem</span>
|
||||||
{friction.problem}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto border-t border-white/5 pt-4">
|
<p className="text-sm leading-relaxed text-zinc-500">{friction.problem}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Solution */}
|
||||||
|
<div className="mt-auto pt-4 border-t border-white/5">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Check className="h-3 w-3 text-green-400" />
|
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-green-500/20">
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-400">Solution</span>
|
<Check className="w-3 h-3 text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-relaxed text-zinc-300">
|
<span className="text-xs font-semibold uppercase tracking-wider text-green-400">Solution</span>
|
||||||
{friction.solution}
|
</div>
|
||||||
</p>
|
<p className="text-sm font-medium leading-relaxed text-zinc-300">{friction.solution}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,81 +4,32 @@ interface Props {
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description = "Universal SDK for coding agents. Control Claude Code, Codex, OpenCode, Amp, and Pi with unified events and sessions." } = Astro.props;
|
const { title, description = "Universal SDK for coding agents. Control Claude Code, Codex, OpenCode, and Amp with unified events and sessions." } = Astro.props;
|
||||||
const canonicalURL = new URL(Astro.url.pathname, 'https://sandbox-agent.dev');
|
|
||||||
const ogImageURL = new URL('/og.png', 'https://sandbox-agent.dev');
|
|
||||||
|
|
||||||
const structuredData = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "SoftwareApplication",
|
|
||||||
"name": "Sandbox Agent SDK",
|
|
||||||
"applicationCategory": "DeveloperApplication",
|
|
||||||
"operatingSystem": "Linux, macOS, Windows",
|
|
||||||
"description": description,
|
|
||||||
"url": "https://sandbox-agent.dev",
|
|
||||||
"author": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Rivet",
|
|
||||||
"url": "https://rivet.dev"
|
|
||||||
},
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"keywords": "coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, sandbox, remote code execution, developer tools"
|
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" class="dark">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta name="keywords" content="coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, Pi, sandbox, remote code execution, developer tools, AI coding assistant, code automation" />
|
|
||||||
<meta name="author" content="Rivet" />
|
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<link rel="canonical" href={canonicalURL} />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|
||||||
<!-- Preconnect to font providers -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
||||||
|
|
||||||
<!-- Satoshi for headings (from Fontshare) -->
|
|
||||||
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@700,900&display=swap" rel="stylesheet" />
|
|
||||||
|
|
||||||
<!-- Open Sans + JetBrains Mono (from Google Fonts) -->
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|
||||||
<!-- Open Graph -->
|
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content={canonicalURL} />
|
<meta property="og:image" content="/og.png" />
|
||||||
<meta property="og:site_name" content="Sandbox Agent SDK" />
|
|
||||||
<meta property="og:image" content={ogImageURL} />
|
|
||||||
<meta property="og:image:width" content="2400" />
|
<meta property="og:image:width" content="2400" />
|
||||||
<meta property="og:image:height" content="1260" />
|
<meta property="og:image:height" content="1260" />
|
||||||
<meta property="og:image:alt" content="Sandbox Agent SDK - Run Coding Agents in Sandboxes. Control Them Over HTTP." />
|
<meta property="og:image:alt" content="Sandbox Agent SDK - Run Coding Agents in Sandboxes. Control Them Over HTTP." />
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@rivet_dev" />
|
<meta name="twitter:image" content="/og.png" />
|
||||||
<meta name="twitter:creator" content="@rivet_dev" />
|
|
||||||
<meta name="twitter:title" content={title} />
|
|
||||||
<meta name="twitter:description" content={description} />
|
|
||||||
<meta name="twitter:image" content={ogImageURL} />
|
|
||||||
|
|
||||||
<!-- Structured Data -->
|
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-background text-foreground antialiased">
|
<body class="min-h-screen">
|
||||||
<slot />
|
<slot />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Footer } from '../components/Footer';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Sandbox Agent SDK - Run Coding Agents in Sandboxes. Control Them Over HTTP.">
|
<Layout title="Sandbox Agent SDK - Run Coding Agents in Sandboxes. Control Them Over HTTP.">
|
||||||
|
<div class="min-h-screen bg-black text-white selection:bg-accent/30">
|
||||||
<Navigation client:load />
|
<Navigation client:load />
|
||||||
<main>
|
<main>
|
||||||
<Hero client:load />
|
<Hero client:load />
|
||||||
|
|
@ -21,4 +22,5 @@ import { Footer } from '../components/Footer';
|
||||||
<FAQ client:visible />
|
<FAQ client:visible />
|
||||||
</main>
|
</main>
|
||||||
<Footer client:visible />
|
<Footer client:visible />
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -3,61 +3,13 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
|
||||||
--header-height: 3.5rem;
|
|
||||||
|
|
||||||
/* Theme colors (HSL for flexibility) */
|
|
||||||
--background: 20 14.3% 4.1%;
|
|
||||||
--foreground: 60 9.1% 97.8%;
|
|
||||||
--primary: 18.5 100% 50%;
|
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
|
||||||
--muted: 34 10% 10%;
|
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
|
||||||
--border: 12 6.5% 15.1%;
|
|
||||||
--card: 0 9.09% 6.47%;
|
|
||||||
|
|
||||||
/* Shiki syntax highlighting */
|
|
||||||
--shiki-color-text: theme('colors.white');
|
|
||||||
--shiki-foreground: hsl(var(--foreground));
|
|
||||||
--shiki-token-constant: theme('colors.violet.300');
|
|
||||||
--shiki-token-string: theme('colors.violet.300');
|
|
||||||
--shiki-token-comment: theme('colors.zinc.500');
|
|
||||||
--shiki-token-keyword: theme('colors.sky.300');
|
|
||||||
--shiki-token-parameter: theme('colors.pink.300');
|
|
||||||
--shiki-token-function: theme('colors.violet.300');
|
|
||||||
--shiki-token-string-expression: theme('colors.violet.300');
|
|
||||||
--shiki-token-punctuation: theme('colors.zinc.200');
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
@apply border-white/10;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-black text-white antialiased;
|
@apply bg-black text-white antialiased;
|
||||||
font-family: 'Open Sans', system-ui, sans-serif;
|
font-family: 'Open Sans', system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text selection - matches rivet.dev */
|
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgba(255, 79, 0, 0.3);
|
@apply bg-accent/30 text-white;
|
||||||
color: #fed7aa;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
|
||||||
background-color: rgba(255, 79, 0, 0.3);
|
|
||||||
color: #fed7aa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selection style for white/light backgrounds */
|
|
||||||
.selection-dark::selection {
|
|
||||||
background-color: #18181b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-dark::-moz-selection {
|
|
||||||
background-color: #18181b;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox scrollbar */
|
/* Firefox scrollbar */
|
||||||
|
|
@ -113,7 +65,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
/* Glass morphism effects */
|
|
||||||
.glass {
|
.glass {
|
||||||
@apply bg-white/[0.02] backdrop-blur-md border border-white/10;
|
@apply bg-white/[0.02] backdrop-blur-md border border-white/10;
|
||||||
}
|
}
|
||||||
|
|
@ -121,123 +72,4 @@
|
||||||
.glass-hover {
|
.glass-hover {
|
||||||
@apply hover:bg-white/[0.04] hover:border-white/20 transition-all;
|
@apply hover:bg-white/[0.04] hover:border-white/20 transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-strong {
|
|
||||||
@apply bg-black/95 backdrop-blur-lg border border-white/10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bento box card effects */
|
|
||||||
.bento-box {
|
|
||||||
transition: border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bento-box:hover {
|
|
||||||
border-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scroll-triggered animations */
|
|
||||||
.animate-on-scroll {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.8s ease-out, transform 0.8s cubic-bezier(0.19, 1, 0.22, 1);
|
|
||||||
transition-delay: var(--scroll-delay, 0s);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-up {
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-on-scroll.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Delay utilities for staggered animations */
|
|
||||||
.delay-100 { --scroll-delay: 100ms; }
|
|
||||||
.delay-200 { --scroll-delay: 200ms; }
|
|
||||||
.delay-300 { --scroll-delay: 300ms; }
|
|
||||||
.delay-400 { --scroll-delay: 400ms; }
|
|
||||||
.delay-500 { --scroll-delay: 500ms; }
|
|
||||||
.delay-600 { --scroll-delay: 600ms; }
|
|
||||||
|
|
||||||
/* Top shine highlight for cards */
|
|
||||||
.shine-top {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shine-top::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glow effect for buttons and interactive elements */
|
|
||||||
.glow-accent {
|
|
||||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-accent-hover:hover {
|
|
||||||
box-shadow: 0 0 30px rgba(255, 69, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code highlight styling */
|
|
||||||
.code-highlight-ref {
|
|
||||||
position: relative;
|
|
||||||
transition: background-color 0.3s ease-out;
|
|
||||||
display: block;
|
|
||||||
margin: 0 -1.5rem;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-highlight-ref.is-active {
|
|
||||||
background-color: rgba(255, 69, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-highlight-ref.is-active::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 2px;
|
|
||||||
background-color: #ff4500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar */
|
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-hide {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
/* Gradient text */
|
|
||||||
.text-gradient-accent {
|
|
||||||
@apply bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Backdrop with blur */
|
|
||||||
.backdrop-glow {
|
|
||||||
@apply backdrop-blur-lg bg-black/80;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Better focus ring */
|
|
||||||
.focus-ring {
|
|
||||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View transition disable (for smooth prefetching) */
|
|
||||||
::view-transition-old(root),
|
|
||||||
::view-transition-new(root) {
|
|
||||||
animation: none;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,59 +4,21 @@ export default {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// Primary accent (OrangeRed)
|
|
||||||
accent: '#FF4500',
|
accent: '#FF4500',
|
||||||
// Extended color palette
|
|
||||||
background: '#000000',
|
|
||||||
'text-primary': '#FAFAFA',
|
|
||||||
'text-secondary': '#A0A0A0',
|
|
||||||
border: '#252525',
|
|
||||||
// Code syntax highlighting
|
|
||||||
'code-keyword': '#c084fc',
|
|
||||||
'code-function': '#60a5fa',
|
|
||||||
'code-string': '#4ade80',
|
|
||||||
'code-comment': '#737373',
|
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Open Sans', 'system-ui', 'sans-serif'],
|
sans: ['Open Sans', 'system-ui', 'sans-serif'],
|
||||||
heading: ['Satoshi', 'Open Sans', 'system-ui', 'sans-serif'],
|
|
||||||
mono: ['JetBrains Mono', 'monospace'],
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'fade-in-up': 'fade-in-up 0.8s ease-out forwards',
|
'fade-in-up': 'fade-in-up 0.6s ease-out forwards',
|
||||||
'hero-line': 'hero-line 1s cubic-bezier(0.19, 1, 0.22, 1) forwards',
|
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
||||||
'hero-p': 'hero-p 0.8s ease-out 0.6s forwards',
|
|
||||||
'hero-cta': 'hero-p 0.8s ease-out 0.8s forwards',
|
|
||||||
'hero-visual': 'hero-p 0.8s ease-out 1s forwards',
|
|
||||||
'infinite-scroll': 'infinite-scroll 25s linear infinite',
|
|
||||||
'pulse-slow': 'pulse-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'fade-in-up': {
|
'fade-in-up': {
|
||||||
from: { opacity: '0', transform: 'translateY(24px)' },
|
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
||||||
to: { opacity: '1', transform: 'translateY(0)' },
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
},
|
},
|
||||||
'hero-line': {
|
|
||||||
'0%': { opacity: '0', transform: 'translateY(100%) skewY(6deg)' },
|
|
||||||
'100%': { opacity: '1', transform: 'translateY(0) skewY(0deg)' },
|
|
||||||
},
|
|
||||||
'hero-p': {
|
|
||||||
from: { opacity: '0', transform: 'translateY(20px)' },
|
|
||||||
to: { opacity: '1', transform: 'translateY(0)' },
|
|
||||||
},
|
|
||||||
'infinite-scroll': {
|
|
||||||
from: { transform: 'translateX(0)' },
|
|
||||||
to: { transform: 'translateX(-50%)' },
|
|
||||||
},
|
|
||||||
'pulse-slow': {
|
|
||||||
'50%': { opacity: '.5' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
spacing: {
|
|
||||||
header: 'var(--header-height, 3.5rem)',
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
'4xl': '2rem',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
64
pnpm-lock.yaml
generated
64
pnpm-lock.yaml
generated
|
|
@ -402,6 +402,34 @@ importers:
|
||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
resources/vercel-ai-sdk-schemas:
|
||||||
|
dependencies:
|
||||||
|
semver:
|
||||||
|
specifier: ^7.6.3
|
||||||
|
version: 7.7.3
|
||||||
|
tar:
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.5.6
|
||||||
|
ts-json-schema-generator:
|
||||||
|
specifier: ^2.4.0
|
||||||
|
version: 2.5.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.0
|
||||||
|
version: 5.9.3
|
||||||
|
devDependencies:
|
||||||
|
'@types/json-schema':
|
||||||
|
specifier: ^7.0.15
|
||||||
|
version: 7.0.15
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.0.0
|
||||||
|
version: 22.19.7
|
||||||
|
'@types/semver':
|
||||||
|
specifier: ^7.5.0
|
||||||
|
version: 7.7.1
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.19.0
|
||||||
|
version: 4.21.0
|
||||||
|
|
||||||
scripts/release:
|
scripts/release:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander:
|
commander:
|
||||||
|
|
@ -2608,6 +2636,9 @@ packages:
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15':
|
||||||
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||||
|
|
||||||
|
|
@ -3008,6 +3039,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
commander@14.0.3:
|
||||||
|
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
commander@4.1.1:
|
commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
@ -4780,6 +4815,11 @@ packages:
|
||||||
ts-interface-checker@0.1.13:
|
ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
|
ts-json-schema-generator@2.5.0:
|
||||||
|
resolution: {integrity: sha512-sYY7AInozRbtj9OD3ynJJuMDWZ5lGxzxTevtmH3W9Hnd2J2szBC0HdPqSyuIirXnQ6g8KDJxS/HENoypUwBrlg==}
|
||||||
|
engines: {node: '>=22.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tsconfck@3.1.6:
|
tsconfck@3.1.6:
|
||||||
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
|
|
@ -7346,6 +7386,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
@ -7466,14 +7508,6 @@ snapshots:
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.2.2))':
|
|
||||||
dependencies:
|
|
||||||
'@vitest/spy': 3.2.4
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
magic-string: 0.30.21
|
|
||||||
optionalDependencies:
|
|
||||||
vite: 5.4.21(@types/node@25.2.2)
|
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.2.3))':
|
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.2.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.4
|
||||||
|
|
@ -7895,6 +7929,8 @@ snapshots:
|
||||||
|
|
||||||
commander@12.1.0: {}
|
commander@12.1.0: {}
|
||||||
|
|
||||||
|
commander@14.0.3: {}
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
common-ancestor-path@1.0.1: {}
|
common-ancestor-path@1.0.1: {}
|
||||||
|
|
@ -10108,6 +10144,16 @@ snapshots:
|
||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
|
ts-json-schema-generator@2.5.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/json-schema': 7.0.15
|
||||||
|
commander: 14.0.3
|
||||||
|
json5: 2.2.3
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
tsconfck@3.1.6(typescript@5.9.3):
|
tsconfck@3.1.6(typescript@5.9.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
@ -10499,7 +10545,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.4
|
||||||
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.2.2))
|
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.2.3))
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.4
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.4
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.4
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Delete Or Comment Out First
|
# Delete Or Comment Out First
|
||||||
|
|
||||||
This is the initial, deliberate teardown list before building ACP-native v2.
|
This is the initial, deliberate teardown list before building ACP-native v1.
|
||||||
|
|
||||||
## Hard delete first (in-house protocol types and converters)
|
## Hard delete first (in-house protocol types and converters)
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ This is the initial, deliberate teardown list before building ACP-native v2.
|
||||||
|
|
||||||
Rationale: this layer is based on current v1 session/event model. Comment it out/disable it during ACP core implementation to avoid coupling and drift.
|
Rationale: this layer is based on current v1 session/event model. Comment it out/disable it during ACP core implementation to avoid coupling and drift.
|
||||||
|
|
||||||
Important: OpenCode <-> ACP support is still required, but it is explicitly reintroduced in Phase 7 after ACP v2 core transport/runtime are stable.
|
Important: OpenCode <-> ACP support is still required, but it is explicitly reintroduced in Phase 7 after ACP v1 core transport/runtime are stable.
|
||||||
|
|
||||||
## Tests to remove or disable with v1
|
## Tests to remove or disable with v1
|
||||||
|
|
||||||
|
|
@ -50,4 +50,4 @@ Important: OpenCode <-> ACP support is still required, but it is explicitly rein
|
||||||
- `server/packages/sandbox-agent/tests/sessions.rs`
|
- `server/packages/sandbox-agent/tests/sessions.rs`
|
||||||
- `server/packages/sandbox-agent/tests/agent_flows.rs`
|
- `server/packages/sandbox-agent/tests/agent_flows.rs`
|
||||||
|
|
||||||
Replace with ACP-native contract tests in v2.
|
Replace with ACP-native contract tests in v1.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# ACP Migration Research
|
# ACP Migration Research
|
||||||
|
|
||||||
This folder captures the v2 migration plan from the current in-house protocol to ACP-first architecture.
|
This folder captures the v1 migration plan from the current in-house protocol to ACP-first architecture.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `research/acp/00-delete-first.md`: delete/comment-out-first inventory for the rewrite kickoff.
|
- `research/acp/00-delete-first.md`: delete/comment-out-first inventory for the rewrite kickoff.
|
||||||
- `research/acp/acp-notes.md`: ACP protocol notes extracted from `~/misc/acp-docs`.
|
- `research/acp/acp-notes.md`: ACP protocol notes extracted from `~/misc/acp-docs`.
|
||||||
- `research/acp/acp-over-http-findings.md`: field research from ACP Zulip thread on real ACP-over-HTTP transport patterns and recommendations.
|
- `research/acp/acp-over-http-findings.md`: field research from ACP Zulip thread on real ACP-over-HTTP transport patterns and recommendations.
|
||||||
- `research/acp/spec.md`: proposed v2 protocol/transport spec (ACP over HTTP).
|
- `research/acp/spec.md`: proposed v1 protocol/transport spec (ACP over HTTP).
|
||||||
- `research/acp/v1-schema-to-acp-mapping.md`: exhaustive 1:1 mapping of all current v1 endpoints/events into ACP methods, notifications, responses, and `_meta` extensions.
|
- `research/acp/v1-schema-to-acp-mapping.md`: exhaustive 1:1 mapping of all current v1 endpoints/events into ACP methods, notifications, responses, and `_meta` extensions.
|
||||||
- `research/acp/rfds-vs-extensions.md`: simple list of which gaps should be raised as ACP RFDs vs remain product-specific extensions.
|
- `research/acp/rfds-vs-extensions.md`: simple list of which gaps should be raised as ACP RFDs vs remain product-specific extensions.
|
||||||
- `research/acp/migration-steps.md`: concrete implementation phases and execution checklist.
|
- `research/acp/migration-steps.md`: concrete implementation phases and execution checklist.
|
||||||
|
|
@ -35,7 +35,7 @@ This folder captures the v2 migration plan from the current in-house protocol to
|
||||||
## Important context
|
## Important context
|
||||||
|
|
||||||
- ACP stable transport is stdio; streamable HTTP is still draft in ACP docs.
|
- ACP stable transport is stdio; streamable HTTP is still draft in ACP docs.
|
||||||
- v2 in this repo is intentionally breaking and ACP-native.
|
- v1 in this repo is intentionally breaking and ACP-native.
|
||||||
- v1 is removed in v2 and returns HTTP 410 on `/v1/*`.
|
- v1 is removed in v1 and returns HTTP 410 on `/v1/*`.
|
||||||
- `/opencode/*` is disabled during ACP core phases and re-enabled in the dedicated bridge phase.
|
- `/opencode/*` is disabled during ACP core phases and re-enabled in the dedicated bridge phase.
|
||||||
- Keep `research/acp/friction.md` current as issues/ambiguities are discovered.
|
- Keep `research/acp/friction.md` current as issues/ambiguities are discovered.
|
||||||
|
|
|
||||||
|
|
@ -63,4 +63,4 @@ From ACP docs agent list:
|
||||||
Gap to confirm for launch scope:
|
Gap to confirm for launch scope:
|
||||||
|
|
||||||
- Amp is not currently listed in ACP docs as a native ACP agent or published agent process.
|
- Amp is not currently listed in ACP docs as a native ACP agent or published agent process.
|
||||||
- We need an explicit product decision: block Amp in v2 launch or provide/build an ACP agent process.
|
- We need an explicit product decision: block Amp in v1 launch or provide/build an ACP agent process.
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,14 @@ Why it matters:
|
||||||
- Lower complexity for bidirectional ACP semantics.
|
- Lower complexity for bidirectional ACP semantics.
|
||||||
- But less aligned with strict HTTP-only environments without additional gatewaying.
|
- But less aligned with strict HTTP-only environments without additional gatewaying.
|
||||||
|
|
||||||
## Recommended options for our v2
|
## Recommended options for our v1
|
||||||
|
|
||||||
## Option A (recommended): Streamable HTTP as canonical v2 transport
|
## Option A (recommended): Streamable HTTP as canonical v1 transport
|
||||||
|
|
||||||
Implement ACP over:
|
Implement ACP over:
|
||||||
- `POST /v2/rpc`
|
- `POST /v1/rpc`
|
||||||
- `GET /v2/rpc` (SSE, optional but recommended)
|
- `GET /v1/rpc` (SSE, optional but recommended)
|
||||||
- `DELETE /v2/rpc`
|
- `DELETE /v1/rpc`
|
||||||
|
|
||||||
Profile:
|
Profile:
|
||||||
- Keep JSON-RPC payloads pure ACP.
|
- Keep JSON-RPC payloads pure ACP.
|
||||||
|
|
@ -86,14 +86,14 @@ Pros:
|
||||||
- Potentially simpler core runtime behavior.
|
- Potentially simpler core runtime behavior.
|
||||||
|
|
||||||
Cons:
|
Cons:
|
||||||
- Less direct fit to your immediate "ACP over HTTP v2 API" objective.
|
- Less direct fit to your immediate "ACP over HTTP v1 API" objective.
|
||||||
- Requires and maintains a translation layer from day one.
|
- Requires and maintains a translation layer from day one.
|
||||||
|
|
||||||
## Recommendation
|
## Recommendation
|
||||||
|
|
||||||
Choose Option A for v2 launch and keep Option B as a later optimization path if operational pain appears.
|
Choose Option A for v1 launch and keep Option B as a later optimization path if operational pain appears.
|
||||||
|
|
||||||
Rationale:
|
Rationale:
|
||||||
- It matches current product direction.
|
- It matches current product direction.
|
||||||
- It aligns with concrete ecosystem work already visible (Goose Streamable HTTP).
|
- It aligns with concrete ecosystem work already visible (Goose Streamable HTTP).
|
||||||
- It can still preserve a future WebSocket backend if needed later, without changing v2 public semantics.
|
- It can still preserve a future WebSocket backend if needed later, without changing v1 public semantics.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ Primary references:
|
||||||
|
|
||||||
## 1) Status Matrix (Session-Centric)
|
## 1) Status Matrix (Session-Centric)
|
||||||
|
|
||||||
| v1 capability (session-related) | ACP stable | ACP unstable | Status in v2 | Recommendation |
|
| v1 capability (session-related) | ACP stable | ACP unstable | Status in v1 | Recommendation |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Create session | `session/new` | N/A | Covered | Use ACP standard only. |
|
| Create session | `session/new` | N/A | Covered | Use ACP standard only. |
|
||||||
| Load/replay prior session | `session/load` (capability-gated) | N/A | Covered when agent process supports `loadSession` | Keep standard behavior. |
|
| Load/replay prior session | `session/load` (capability-gated) | N/A | Covered when agent process supports `loadSession` | Keep standard behavior. |
|
||||||
|
|
@ -111,7 +111,7 @@ Advertise extension support in `initialize.result.agentCapabilities._meta["sandb
|
||||||
|
|
||||||
Clients must feature-detect and degrade gracefully.
|
Clients must feature-detect and degrade gracefully.
|
||||||
|
|
||||||
## 4) Recommendation for Current v2
|
## 4) Recommendation for Current v1
|
||||||
|
|
||||||
Recommended implementation order:
|
Recommended implementation order:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ Update this file continuously during the migration.
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: Agent process availability
|
- Area: Agent process availability
|
||||||
- Issue: Amp does not have a confirmed official ACP agent process in current ACP docs/research.
|
- Issue: Amp does not have a confirmed official ACP agent process in current ACP docs/research.
|
||||||
- Impact: Blocks full parity if Amp is required in v2 launch scope.
|
- Impact: Blocks full parity if Amp is required in v1 launch scope.
|
||||||
- Proposed direction: Treat Amp as conditional for v2.0 and support via pinned fallback only if agent process source is validated.
|
- Proposed direction: Treat Amp as conditional for v1.0 and support via pinned fallback only if agent process source is validated.
|
||||||
- Decision: Open.
|
- Decision: Open.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
- Status: open
|
- Status: open
|
||||||
|
|
@ -30,7 +30,7 @@ Update this file continuously during the migration.
|
||||||
|
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: Transport
|
- Area: Transport
|
||||||
- Issue: ACP streamable HTTP is still draft upstream; v2 requires ACP over HTTP now.
|
- Issue: ACP streamable HTTP is still draft upstream; v1 requires ACP over HTTP now.
|
||||||
- Impact: Potential divergence from upstream HTTP semantics.
|
- Impact: Potential divergence from upstream HTTP semantics.
|
||||||
- Proposed direction: Use strict JSON-RPC mapping and keep transport shim minimal/documented for later alignment.
|
- Proposed direction: Use strict JSON-RPC mapping and keep transport shim minimal/documented for later alignment.
|
||||||
- Decision: Open.
|
- Decision: Open.
|
||||||
|
|
@ -72,7 +72,7 @@ Update this file continuously during the migration.
|
||||||
- Area: ACP over HTTP standardization
|
- Area: ACP over HTTP standardization
|
||||||
- Issue: Community is actively piloting both Streamable HTTP and WebSocket; no final single transport profile has emerged yet.
|
- Issue: Community is actively piloting both Streamable HTTP and WebSocket; no final single transport profile has emerged yet.
|
||||||
- Impact: Risk of rework if we overfit to one draft behavior that later shifts.
|
- Impact: Risk of rework if we overfit to one draft behavior that later shifts.
|
||||||
- Proposed direction: Lock v2 public contract to Streamable HTTP with ACP JSON-RPC payloads, keep implementation modular so WebSocket can be added later without breaking v2 API.
|
- Proposed direction: Lock v1 public contract to Streamable HTTP with ACP JSON-RPC payloads, keep implementation modular so WebSocket can be added later without breaking v1 API.
|
||||||
- Decision: Accepted.
|
- Decision: Accepted.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
- Status: in_progress
|
- Status: in_progress
|
||||||
|
|
@ -121,7 +121,7 @@ Update this file continuously during the migration.
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: TypeScript ACP-over-HTTP client support
|
- Area: TypeScript ACP-over-HTTP client support
|
||||||
- Issue: Official ACP client SDK does not currently provide the exact Streamable HTTP transport behavior required by this project.
|
- Issue: Official ACP client SDK does not currently provide the exact Streamable HTTP transport behavior required by this project.
|
||||||
- Impact: SDK cannot target `/v2/rpc` without additional transport implementation.
|
- Impact: SDK cannot target `/v1/rpc` without additional transport implementation.
|
||||||
- Proposed direction: Embed upstream ACP SDK types/lifecycle and implement a project transport agent process for ACP-over-HTTP.
|
- Proposed direction: Embed upstream ACP SDK types/lifecycle and implement a project transport agent process for ACP-over-HTTP.
|
||||||
- Decision: Accepted.
|
- Decision: Accepted.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
|
|
@ -156,7 +156,7 @@ Update this file continuously during the migration.
|
||||||
- Decision: Accepted and implemented.
|
- Decision: Accepted and implemented.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
- Status: resolved
|
- Status: resolved
|
||||||
- Links: `server/packages/agent-management/src/agents.rs`, `server/packages/sandbox-agent/tests/v2_api.rs`
|
- Links: `server/packages/agent-management/src/agents.rs`, `server/packages/sandbox-agent/tests/v1_api.rs`
|
||||||
|
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: Inspector E2E tooling
|
- Area: Inspector E2E tooling
|
||||||
|
|
@ -179,9 +179,9 @@ Update this file continuously during the migration.
|
||||||
- Links: `research/acp/todo.md`
|
- Links: `research/acp/todo.md`
|
||||||
|
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: Inspector v1-to-v2 compatibility
|
- Area: Inspector v1-to-v1 compatibility
|
||||||
- Issue: Restored inspector UI expects legacy `/v1` session/event contracts that no longer exist in ACP-native v2.
|
- Issue: Restored inspector UI expects legacy `/v1` session/event contracts that no longer exist in ACP-native v1.
|
||||||
- Impact: Full parity would block migration; inspector would otherwise fail to run against v2.
|
- Impact: Full parity would block migration; inspector would otherwise fail to run against v1.
|
||||||
- Proposed direction: Keep the restored UI and bridge to ACP with a thin compatibility client (`src/lib/legacyClient.ts`), stubbing non-parity features with explicit `TDOO` markers.
|
- Proposed direction: Keep the restored UI and bridge to ACP with a thin compatibility client (`src/lib/legacyClient.ts`), stubbing non-parity features with explicit `TDOO` markers.
|
||||||
- Decision: Accepted.
|
- Decision: Accepted.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
|
|
@ -196,7 +196,7 @@ Update this file continuously during the migration.
|
||||||
- Decision: Accepted and implemented.
|
- Decision: Accepted and implemented.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
- Status: resolved
|
- Status: resolved
|
||||||
- Links: `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/src/acp_runtime/mock.rs`, `server/packages/sandbox-agent/tests/v2_api.rs`, `server/packages/sandbox-agent/tests/v2_agent_process_matrix.rs`
|
- Links: `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/src/acp_runtime/mock.rs`, `server/packages/sandbox-agent/tests/v1_api.rs`, `server/packages/sandbox-agent/tests/v1_agent_process_matrix.rs`
|
||||||
|
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: TypeScript SDK package split and ACP lifecycle
|
- Area: TypeScript SDK package split and ACP lifecycle
|
||||||
|
|
@ -210,13 +210,13 @@ Update this file continuously during the migration.
|
||||||
|
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: Streamable HTTP transport contract
|
- Area: Streamable HTTP transport contract
|
||||||
- Issue: Ambiguity over whether `/v2/rpc` should track MCP transport negotiation (`POST` accepting SSE responses, multi-stream fanout) versus Sandbox Agent's simpler JSON-only POST contract.
|
- Issue: Ambiguity over whether `/v1/rpc` should track MCP transport negotiation (`POST` accepting SSE responses, multi-stream fanout) versus Sandbox Agent's simpler JSON-only POST contract.
|
||||||
- Impact: Without an explicit contract, clients can assume incompatible Accept/media semantics and open duplicate GET streams that receive duplicate events.
|
- Impact: Without an explicit contract, clients can assume incompatible Accept/media semantics and open duplicate GET streams that receive duplicate events.
|
||||||
- Proposed direction: Define Sandbox Agent transport profile explicitly: `POST /v2/rpc` is JSON-only (`Content-Type` and `Accept` for `application/json`), `GET /v2/rpc` is SSE-only (`Accept: text/event-stream`), and allow only one active SSE stream per ACP connection id.
|
- Proposed direction: Define Sandbox Agent transport profile explicitly: `POST /v1/rpc` is JSON-only (`Content-Type` and `Accept` for `application/json`), `GET /v1/rpc` is SSE-only (`Accept: text/event-stream`), and allow only one active SSE stream per ACP connection id.
|
||||||
- Decision: Accepted and implemented.
|
- Decision: Accepted and implemented.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
- Status: resolved
|
- Status: resolved
|
||||||
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/tests/v2_api/acp_transport.rs`, `docs/advanced/acp-http-client.mdx`
|
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/tests/v1_api/acp_transport.rs`, `docs/advanced/acp-http-client.mdx`
|
||||||
|
|
||||||
- Date: 2026-02-10
|
- Date: 2026-02-10
|
||||||
- Area: Agent selection contract for ACP bootstrap/session creation
|
- Area: Agent selection contract for ACP bootstrap/session creation
|
||||||
|
|
@ -226,4 +226,24 @@ Update this file continuously during the migration.
|
||||||
- Decision: Accepted and implemented.
|
- Decision: Accepted and implemented.
|
||||||
- Owner: Unassigned.
|
- Owner: Unassigned.
|
||||||
- Status: resolved
|
- Status: resolved
|
||||||
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/acp_runtime/helpers.rs`, `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs`, `server/packages/sandbox-agent/tests/v2_api/acp_transport.rs`
|
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/acp_runtime/helpers.rs`, `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs`, `server/packages/sandbox-agent/tests/v1_api/acp_transport.rs`
|
||||||
|
|
||||||
|
- Date: 2026-02-11
|
||||||
|
- Area: ACP server simplification
|
||||||
|
- Issue: Current `/v1/rpc` runtime includes server-managed metadata/session registry and `_sandboxagent/*` ACP extensions, while the new direction is a dumb stdio proxy keyed by client-provided ACP server id.
|
||||||
|
- Impact: Requires removing extension/metadata semantics and reshaping transport to `/v1/acp/{server_id}` with per-id subprocess lifecycle.
|
||||||
|
- Proposed direction: Replace `/v1/rpc` with `/v1/acp/{server_id}` (`POST`/`GET` SSE/`DELETE`), drop connection-id headers, keep replay by `server_id`, move non-ACP concerns to HTTP endpoints, and disable OpenCode routes.
|
||||||
|
- Decision: Accepted (spec drafted).
|
||||||
|
- Owner: Unassigned.
|
||||||
|
- Status: in_progress
|
||||||
|
- Links: `research/acp/simplify-server.md`
|
||||||
|
|
||||||
|
- Date: 2026-02-11
|
||||||
|
- Area: Directory-scoped config ownership
|
||||||
|
- Issue: MCP/skills config previously traveled with session initialization payloads; simplified server needs standalone HTTP config scoped by directory.
|
||||||
|
- Impact: Requires new HTTP APIs and clear naming for per-directory/per-entry operations without ACP extension transport.
|
||||||
|
- Proposed direction: Add directory-scoped query APIs: `/v1/config/mcp?directory=...&mcpName=...` and `/v1/config/skills?directory=...&skillName=...` (name required), using v1 payload shapes for MCP/skills config values.
|
||||||
|
- Decision: Accepted (spec updated).
|
||||||
|
- Owner: Unassigned.
|
||||||
|
- Status: in_progress
|
||||||
|
- Links: `research/acp/simplify-server.md`, `docs/mcp-config.mdx`, `docs/skills-config.mdx`
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Updated: 2026-02-10
|
Updated: 2026-02-10
|
||||||
|
|
||||||
This tracks legacy inspector behaviors that do not yet have full parity on ACP v2.
|
This tracks legacy inspector behaviors that do not yet have full parity on ACP v1.
|
||||||
|
|
||||||
1. TDOO: Session `permissionMode` preconfiguration on create is not wired in ACP inspector compatibility.
|
1. TDOO: Session `permissionMode` preconfiguration on create is not wired in ACP inspector compatibility.
|
||||||
2. TDOO: Session `variant` preconfiguration on create is not wired in ACP inspector compatibility.
|
2. TDOO: Session `variant` preconfiguration on create is not wired in ACP inspector compatibility.
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,57 @@
|
||||||
# Proposal: Move Static v2 HTTP Endpoints into ACP Extensions
|
# Proposal: Move Static v1 HTTP Endpoints into ACP Extensions
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Keep `GET /v2/health` as the only static control endpoint, except for dedicated binary filesystem transfer endpoints.
|
Keep `GET /v1/health` as the only static control endpoint, except for dedicated binary filesystem transfer endpoints.
|
||||||
|
|
||||||
Move all other current static v2 HTTP routes to ACP JSON-RPC methods (Sandbox Agent extensions under `_sandboxagent/...`) on `/v2/rpc`.
|
Move all other current static v1 HTTP routes to ACP JSON-RPC methods (Sandbox Agent extensions under `_sandboxagent/...`) on `/v1/rpc`.
|
||||||
|
|
||||||
Retain these HTTP endpoints intentionally:
|
Retain these HTTP endpoints intentionally:
|
||||||
|
|
||||||
- `GET /v2/fs/file`
|
- `GET /v1/fs/file`
|
||||||
- `PUT /v2/fs/file`
|
- `PUT /v1/fs/file`
|
||||||
- `POST /v2/fs/upload-batch`
|
- `POST /v1/fs/upload-batch`
|
||||||
|
|
||||||
No implementation in this proposal. This is a migration plan.
|
No implementation in this proposal. This is a migration plan.
|
||||||
|
|
||||||
## Current State (from `server/packages/sandbox-agent/src/router.rs`)
|
## Current State (from `server/packages/sandbox-agent/src/router.rs`)
|
||||||
|
|
||||||
Static v2 endpoints today:
|
Static v1 endpoints today:
|
||||||
|
|
||||||
- `GET /v2/agents`
|
- `GET /v1/agents`
|
||||||
- `POST /v2/agents/:agent/install`
|
- `POST /v1/agents/:agent/install`
|
||||||
- `GET /v2/sessions`
|
- `GET /v1/sessions`
|
||||||
- `GET /v2/sessions/:session_id`
|
- `GET /v1/sessions/:session_id`
|
||||||
- `GET /v2/fs/entries`
|
- `GET /v1/fs/entries`
|
||||||
- `GET /v2/fs/file`
|
- `GET /v1/fs/file`
|
||||||
- `PUT /v2/fs/file`
|
- `PUT /v1/fs/file`
|
||||||
- `DELETE /v2/fs/entry`
|
- `DELETE /v1/fs/entry`
|
||||||
- `POST /v2/fs/mkdir`
|
- `POST /v1/fs/mkdir`
|
||||||
- `POST /v2/fs/move`
|
- `POST /v1/fs/move`
|
||||||
- `GET /v2/fs/stat`
|
- `GET /v1/fs/stat`
|
||||||
- `POST /v2/fs/upload-batch`
|
- `POST /v1/fs/upload-batch`
|
||||||
|
|
||||||
Non-static ACP transport endpoints (remain):
|
Non-static ACP transport endpoints (remain):
|
||||||
|
|
||||||
- `POST /v2/rpc`
|
- `POST /v1/rpc`
|
||||||
- `GET /v2/rpc` (SSE)
|
- `GET /v1/rpc` (SSE)
|
||||||
- `DELETE /v2/rpc`
|
- `DELETE /v1/rpc`
|
||||||
|
|
||||||
Health endpoint (remain):
|
Health endpoint (remain):
|
||||||
|
|
||||||
- `GET /v2/health`
|
- `GET /v1/health`
|
||||||
|
|
||||||
## Proposed Target Surface
|
## Proposed Target Surface
|
||||||
|
|
||||||
Keep:
|
Keep:
|
||||||
|
|
||||||
- `GET /v2/health`
|
- `GET /v1/health`
|
||||||
- `POST/GET/DELETE /v2/rpc`
|
- `POST/GET/DELETE /v1/rpc`
|
||||||
- `GET /v2/fs/file`
|
- `GET /v1/fs/file`
|
||||||
- `PUT /v2/fs/file`
|
- `PUT /v1/fs/file`
|
||||||
- `POST /v2/fs/upload-batch`
|
- `POST /v1/fs/upload-batch`
|
||||||
|
|
||||||
Remove all other static v2 control/file routes after migration.
|
Remove all other static v1 control/file routes after migration.
|
||||||
|
|
||||||
Add ACP extension methods:
|
Add ACP extension methods:
|
||||||
|
|
||||||
|
|
@ -68,24 +68,24 @@ Add ACP extension methods:
|
||||||
- `_sandboxagent/fs/stat`
|
- `_sandboxagent/fs/stat`
|
||||||
- `_sandboxagent/fs/upload_batch` (parallel with HTTP)
|
- `_sandboxagent/fs/upload_batch` (parallel with HTTP)
|
||||||
|
|
||||||
Interpretation for clients: all agent/session operations and non-binary filesystem operations move to ACP extension calls over `/v2/rpc`. Binary file transfer has a dual surface: ACP equivalents exist in parallel, but HTTP remains the primary transport for large/streaming payloads.
|
Interpretation for clients: all agent/session operations and non-binary filesystem operations move to ACP extension calls over `/v1/rpc`. Binary file transfer has a dual surface: ACP equivalents exist in parallel, but HTTP remains the primary transport for large/streaming payloads.
|
||||||
|
|
||||||
## Endpoint-to-Method Mapping
|
## Endpoint-to-Method Mapping
|
||||||
|
|
||||||
| Existing HTTP | New ACP method | Notes |
|
| Existing HTTP | New ACP method | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET /v2/agents` | `_sandboxagent/agent/list` | Response keeps current `AgentListResponse` shape for low migration risk. |
|
| `GET /v1/agents` | `_sandboxagent/agent/list` | Response keeps current `AgentListResponse` shape for low migration risk. |
|
||||||
| `POST /v2/agents/:agent/install` | `_sandboxagent/agent/install` | Params include `agent`, `reinstall`, `agentVersion`, `agentProcessVersion`. |
|
| `POST /v1/agents/:agent/install` | `_sandboxagent/agent/install` | Params include `agent`, `reinstall`, `agentVersion`, `agentProcessVersion`. |
|
||||||
| `GET /v2/sessions` | `_sandboxagent/session/list` | Return current `SessionListResponse` shape (not ACP unstable list shape). |
|
| `GET /v1/sessions` | `_sandboxagent/session/list` | Return current `SessionListResponse` shape (not ACP unstable list shape). |
|
||||||
| `GET /v2/sessions/:session_id` | `_sandboxagent/session/get` | Return current `SessionInfo` shape; error on missing session. |
|
| `GET /v1/sessions/:session_id` | `_sandboxagent/session/get` | Return current `SessionInfo` shape; error on missing session. |
|
||||||
| `GET /v2/fs/entries` | `_sandboxagent/fs/list_entries` | Preserve path + optional `sessionId` resolution semantics. |
|
| `GET /v1/fs/entries` | `_sandboxagent/fs/list_entries` | Preserve path + optional `sessionId` resolution semantics. |
|
||||||
| `GET /v2/fs/file` | keep HTTP + `_sandboxagent/fs/read_file` | HTTP is primary because responses may require large streaming reads; ACP variant exists for compatibility/smaller payloads. |
|
| `GET /v1/fs/file` | keep HTTP + `_sandboxagent/fs/read_file` | HTTP is primary because responses may require large streaming reads; ACP variant exists for compatibility/smaller payloads. |
|
||||||
| `PUT /v2/fs/file` | keep HTTP + `_sandboxagent/fs/write_file` | HTTP is primary for large binary writes; ACP variant exists for compatibility/smaller payloads. |
|
| `PUT /v1/fs/file` | keep HTTP + `_sandboxagent/fs/write_file` | HTTP is primary for large binary writes; ACP variant exists for compatibility/smaller payloads. |
|
||||||
| `DELETE /v2/fs/entry` | `_sandboxagent/fs/delete_entry` | Preserve recursive directory delete behavior. |
|
| `DELETE /v1/fs/entry` | `_sandboxagent/fs/delete_entry` | Preserve recursive directory delete behavior. |
|
||||||
| `POST /v2/fs/mkdir` | `_sandboxagent/fs/mkdir` | Preserve create-dir behavior. |
|
| `POST /v1/fs/mkdir` | `_sandboxagent/fs/mkdir` | Preserve create-dir behavior. |
|
||||||
| `POST /v2/fs/move` | `_sandboxagent/fs/move` | Preserve `overwrite` behavior. |
|
| `POST /v1/fs/move` | `_sandboxagent/fs/move` | Preserve `overwrite` behavior. |
|
||||||
| `GET /v2/fs/stat` | `_sandboxagent/fs/stat` | Preserve `FsStat` shape. |
|
| `GET /v1/fs/stat` | `_sandboxagent/fs/stat` | Preserve `FsStat` shape. |
|
||||||
| `POST /v2/fs/upload-batch` | keep HTTP + `_sandboxagent/fs/upload_batch` | HTTP is primary for large tar uploads; ACP variant exists for compatibility/smaller payloads. |
|
| `POST /v1/fs/upload-batch` | keep HTTP + `_sandboxagent/fs/upload_batch` | HTTP is primary for large tar uploads; ACP variant exists for compatibility/smaller payloads. |
|
||||||
|
|
||||||
## ACP Contract Details
|
## ACP Contract Details
|
||||||
|
|
||||||
|
|
@ -99,14 +99,14 @@ Add keys for new extensions (`agentList`, `agentInstall`, `fsListEntries`, `fsSt
|
||||||
|
|
||||||
### Filesystem Exception (Intentional)
|
### Filesystem Exception (Intentional)
|
||||||
|
|
||||||
`GET/PUT /v2/fs/file` and `POST /v2/fs/upload-batch` stay as first-class Sandbox Agent HTTP APIs.
|
`GET/PUT /v1/fs/file` and `POST /v1/fs/upload-batch` stay as first-class Sandbox Agent HTTP APIs.
|
||||||
|
|
||||||
Reason:
|
Reason:
|
||||||
|
|
||||||
- These operations are host/runtime capabilities implemented by Sandbox Agent, not agent-process behavior.
|
- These operations are host/runtime capabilities implemented by Sandbox Agent, not agent-process behavior.
|
||||||
- Keeping them server-owned gives consistent behavior across agents.
|
- Keeping them server-owned gives consistent behavior across agents.
|
||||||
- ACP envelopes are JSON-RPC payloads and are not suitable for streaming very large binary files efficiently.
|
- ACP envelopes are JSON-RPC payloads and are not suitable for streaming very large binary files efficiently.
|
||||||
- `GET /v2/fs/file` specifically needs efficient streamed responses for large reads.
|
- `GET /v1/fs/file` specifically needs efficient streamed responses for large reads.
|
||||||
|
|
||||||
ACP parity note:
|
ACP parity note:
|
||||||
|
|
||||||
|
|
@ -134,16 +134,16 @@ Required change for ACP-only behavior:
|
||||||
- Make ACP-backed helpers connection-scoped (same as ACP methods): they must throw `NotConnectedError` when disconnected.
|
- Make ACP-backed helpers connection-scoped (same as ACP methods): they must throw `NotConnectedError` when disconnected.
|
||||||
- Keep direct HTTP helper calls only for:
|
- Keep direct HTTP helper calls only for:
|
||||||
- `getHealth()`
|
- `getHealth()`
|
||||||
- `readFsFile()` (`GET /v2/fs/file`)
|
- `readFsFile()` (`GET /v1/fs/file`)
|
||||||
- `writeFsFile()` (`PUT /v2/fs/file`)
|
- `writeFsFile()` (`PUT /v1/fs/file`)
|
||||||
- `uploadFsBatch()` (`POST /v2/fs/upload-batch`)
|
- `uploadFsBatch()` (`POST /v1/fs/upload-batch`)
|
||||||
- Keep ACP variants available through low-level `extMethod(...)` for advanced/smaller-payload use cases, but do not make them the SDK default path.
|
- Keep ACP variants available through low-level `extMethod(...)` for advanced/smaller-payload use cases, but do not make them the SDK default path.
|
||||||
|
|
||||||
Package boundary after migration:
|
Package boundary after migration:
|
||||||
|
|
||||||
- `acp-http-client` remains protocol-pure ACP transport and generic `extMethod`/`extNotification`.
|
- `acp-http-client` remains protocol-pure ACP transport and generic `extMethod`/`extNotification`.
|
||||||
- `sandbox-agent` remains the typed wrapper that maps convenience methods to `_sandboxagent/...` extension methods.
|
- `sandbox-agent` remains the typed wrapper that maps convenience methods to `_sandboxagent/...` extension methods.
|
||||||
- No direct `/v2/agents*`, `/v2/sessions*`, or non-binary `/v2/fs/*` fetches in SDK runtime code.
|
- No direct `/v1/agents*`, `/v1/sessions*`, or non-binary `/v1/fs/*` fetches in SDK runtime code.
|
||||||
- Binary file transfer keeps direct HTTP fetches on the three endpoints listed above.
|
- Binary file transfer keeps direct HTTP fetches on the three endpoints listed above.
|
||||||
- SDK policy: prefer HTTP for `readFsFile`/`writeFsFile`/`uploadFsBatch` even if ACP extension variants exist.
|
- SDK policy: prefer HTTP for `readFsFile`/`writeFsFile`/`uploadFsBatch` even if ACP extension variants exist.
|
||||||
|
|
||||||
|
|
@ -160,7 +160,7 @@ Integration test impact (`sdks/typescript/tests/integration.test.ts`):
|
||||||
|
|
||||||
## Bootstrap Model (Important)
|
## Bootstrap Model (Important)
|
||||||
|
|
||||||
Today, first call without `x-acp-connection-id` must be `initialize`, and requires `params._meta["sandboxagent.dev"].agent`.
|
Today, first call to a new ACP server id should be `initialize`, and requires `params._meta["sandboxagent.dev"].agent`.
|
||||||
|
|
||||||
Implication after migration:
|
Implication after migration:
|
||||||
|
|
||||||
|
|
@ -177,30 +177,30 @@ Alternative (optional): introduce a runtime-only control connection mode that do
|
||||||
- Reuse existing router/support mapping logic where possible to keep response parity.
|
- Reuse existing router/support mapping logic where possible to keep response parity.
|
||||||
- Keep binary file-transfer ACP methods in parallel with HTTP (`_sandboxagent/fs/read_file`, `_sandboxagent/fs/write_file`, `_sandboxagent/fs/upload_batch`) and route both surfaces through shared implementation code.
|
- Keep binary file-transfer ACP methods in parallel with HTTP (`_sandboxagent/fs/read_file`, `_sandboxagent/fs/write_file`, `_sandboxagent/fs/upload_batch`) and route both surfaces through shared implementation code.
|
||||||
- Advertise new capabilities in `acp_runtime/ext_meta.rs`.
|
- Advertise new capabilities in `acp_runtime/ext_meta.rs`.
|
||||||
- Add ACP extension tests for each new method in `server/packages/sandbox-agent/tests/v2_api/acp_extensions.rs`.
|
- Add ACP extension tests for each new method in `server/packages/sandbox-agent/tests/v1_api/acp_extensions.rs`.
|
||||||
|
|
||||||
### Phase 2: Migrate Clients (No HTTP Route Removal Yet)
|
### Phase 2: Migrate Clients (No HTTP Route Removal Yet)
|
||||||
|
|
||||||
- TypeScript SDK (`sdks/typescript/src/client.ts`):
|
- TypeScript SDK (`sdks/typescript/src/client.ts`):
|
||||||
- Repoint `listAgents`, `installAgent`, `listSessions`, `getSession`, `listFsEntries`, `deleteFsEntry`, `mkdirFs`, `moveFs`, and `statFs` to ACP extension calls.
|
- Repoint `listAgents`, `installAgent`, `listSessions`, `getSession`, `listFsEntries`, `deleteFsEntry`, `mkdirFs`, `moveFs`, and `statFs` to ACP extension calls.
|
||||||
- Keep `readFsFile`, `writeFsFile`, and `uploadFsBatch` on HTTP endpoints.
|
- Keep `readFsFile`, `writeFsFile`, and `uploadFsBatch` on HTTP endpoints.
|
||||||
- Remove direct runtime fetch usage for `/v2/agents*`, `/v2/sessions*`, and non-binary `/v2/fs/*`.
|
- Remove direct runtime fetch usage for `/v1/agents*`, `/v1/sessions*`, and non-binary `/v1/fs/*`.
|
||||||
- Keep method names stable for callers.
|
- Keep method names stable for callers.
|
||||||
- Move these methods to connected-only semantics (`NotConnectedError` when disconnected).
|
- Move these methods to connected-only semantics (`NotConnectedError` when disconnected).
|
||||||
- CLI (`server/packages/sandbox-agent/src/cli.rs`):
|
- CLI (`server/packages/sandbox-agent/src/cli.rs`):
|
||||||
- Make `api agents list/install` call ACP extension methods (via ACP post flow), not direct `/v2/agents*` HTTP calls.
|
- Make `api agents list/install` call ACP extension methods (via ACP post flow), not direct `/v1/agents*` HTTP calls.
|
||||||
- Inspector flow/docs:
|
- Inspector flow/docs:
|
||||||
- Stop depending on `GET /v2/agents` in startup path; use ACP extension instead.
|
- Stop depending on `GET /v1/agents` in startup path; use ACP extension instead.
|
||||||
|
|
||||||
### Phase 3: Remove Static Endpoints (Except Health + Binary FS Transfer)
|
### Phase 3: Remove Static Endpoints (Except Health + Binary FS Transfer)
|
||||||
|
|
||||||
- Remove route registrations for `/v2/agents*`, `/v2/sessions*`, `/v2/fs/entries`, `/v2/fs/entry`, `/v2/fs/mkdir`, `/v2/fs/move`, `/v2/fs/stat` from `router.rs`.
|
- Remove route registrations for `/v1/agents*`, `/v1/sessions*`, `/v1/fs/entries`, `/v1/fs/entry`, `/v1/fs/mkdir`, `/v1/fs/move`, `/v1/fs/stat` from `router.rs`.
|
||||||
- Keep `/v2/health`, `/v2/rpc`, `GET /v2/fs/file`, `PUT /v2/fs/file`, and `POST /v2/fs/upload-batch`.
|
- Keep `/v1/health`, `/v1/rpc`, `GET /v1/fs/file`, `PUT /v1/fs/file`, and `POST /v1/fs/upload-batch`.
|
||||||
- Optional short deprecation period: convert removed routes to `410 Gone` with explicit extension method in `detail`.
|
- Optional short deprecation period: convert removed routes to `410 Gone` with explicit extension method in `detail`.
|
||||||
|
|
||||||
### Phase 4: Docs/OpenAPI/Test Cleanup
|
### Phase 4: Docs/OpenAPI/Test Cleanup
|
||||||
|
|
||||||
- Regenerate `docs/openapi.json` (should now primarily describe `/v2/health`, `/v2/rpc`, and retained binary fs transfer endpoints).
|
- Regenerate `docs/openapi.json` (should now primarily describe `/v1/health`, `/v1/rpc`, and retained binary fs transfer endpoints).
|
||||||
- Update:
|
- Update:
|
||||||
- `docs/cli.mdx`
|
- `docs/cli.mdx`
|
||||||
- `docs/inspector.mdx`
|
- `docs/inspector.mdx`
|
||||||
|
|
@ -237,6 +237,6 @@ Inspector:
|
||||||
|
|
||||||
## Open Decisions
|
## Open Decisions
|
||||||
|
|
||||||
1. Should removed `/v2/agents*`, `/v2/sessions*`, and non-binary `/v2/fs/*` return `410` for one release or be dropped immediately?
|
1. Should removed `/v1/agents*`, `/v1/sessions*`, and non-binary `/v1/fs/*` return `410` for one release or be dropped immediately?
|
||||||
2. Do we keep a strict response-shape parity layer for session/file methods, or normalize to ACP-native shapes?
|
2. Do we keep a strict response-shape parity layer for session/file methods, or normalize to ACP-native shapes?
|
||||||
3. Should `/` service-root remain as informational HTTP, or be treated as out-of-scope for this “only health static + binary fs transfer” policy?
|
3. Should `/` service-root remain as informational HTTP, or be treated as out-of-scope for this “only health static + binary fs transfer” policy?
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
The migration test plan is intentionally collapsed to avoid duplicate coverage.
|
The migration test plan is intentionally collapsed to avoid duplicate coverage.
|
||||||
|
|
||||||
1. ACP protocol conformance
|
1. ACP protocol conformance
|
||||||
2. Transport contract (`/v2/rpc`)
|
2. Transport contract (`/v1/rpc`)
|
||||||
3. End-to-end agent process matrix (core flow + cancel + HITL + streaming)
|
3. End-to-end agent process matrix (core flow + cancel + HITL + streaming)
|
||||||
4. Installer suite (explicit + lazy + registry/fallback provenance)
|
4. Installer suite (explicit + lazy + registry/fallback provenance)
|
||||||
5. Security/auth isolation
|
5. Security/auth isolation
|
||||||
|
|
@ -56,7 +56,7 @@ Validation gate:
|
||||||
2. Implement agent process process manager (spawn, supervise, reconnect policy).
|
2. Implement agent process process manager (spawn, supervise, reconnect policy).
|
||||||
3. Implement JSON-RPC bridge: HTTP POST/SSE <-> agent process stdio.
|
3. Implement JSON-RPC bridge: HTTP POST/SSE <-> agent process stdio.
|
||||||
4. Add connection registry keyed by `X-ACP-Connection-Id`.
|
4. Add connection registry keyed by `X-ACP-Connection-Id`.
|
||||||
5. Include unstable ACP methods in the v2 profile (`session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`).
|
5. Include unstable ACP methods in the v1 profile (`session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`).
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
||||||
|
|
@ -64,11 +64,11 @@ Exit criteria:
|
||||||
|
|
||||||
Validation gate:
|
Validation gate:
|
||||||
|
|
||||||
- End-to-end ACP flow test over `/v2/rpc` (request/response + streamed notifications).
|
- End-to-end ACP flow test over `/v1/rpc` (request/response + streamed notifications).
|
||||||
- Cancellation test (`session/cancel`) with proper terminal response behavior.
|
- Cancellation test (`session/cancel`) with proper terminal response behavior.
|
||||||
- HITL request/response round-trip test (`session/request_permission` path).
|
- HITL request/response round-trip test (`session/request_permission` path).
|
||||||
- SSE ordering and reconnection behavior test (`Last-Event-ID` replay path).
|
- SSE ordering and reconnection behavior test (`Last-Event-ID` replay path).
|
||||||
- Explicit close test (`DELETE /v2/rpc`) including idempotent double-close behavior.
|
- Explicit close test (`DELETE /v1/rpc`) including idempotent double-close behavior.
|
||||||
- Unstable ACP methods validation (`session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`) for agent processes that advertise support.
|
- Unstable ACP methods validation (`session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`) for agent processes that advertise support.
|
||||||
|
|
||||||
## Phase 3: Installer Refactor
|
## Phase 3: Installer Refactor
|
||||||
|
|
@ -79,7 +79,7 @@ Validation gate:
|
||||||
4. Add install verification command per agent process.
|
4. Add install verification command per agent process.
|
||||||
5. Add ACP registry integration for install metadata + fallback sources.
|
5. Add ACP registry integration for install metadata + fallback sources.
|
||||||
6. Generate install instructions from manifest and expose provenance (`registry` or `fallback`) in API/CLI.
|
6. Generate install instructions from manifest and expose provenance (`registry` or `fallback`) in API/CLI.
|
||||||
7. Implement lazy install path on first `/v2/rpc` initialize (with per-agent install lock and idempotent results).
|
7. Implement lazy install path on first `/v1/rpc` initialize (with per-agent install lock and idempotent results).
|
||||||
8. Add config to disable lazy install for preprovisioned environments.
|
8. Add config to disable lazy install for preprovisioned environments.
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
@ -92,39 +92,39 @@ Validation gate:
|
||||||
- Lazy install on first ACP `initialize` test.
|
- Lazy install on first ACP `initialize` test.
|
||||||
- Reinstall/version/provenance assertions.
|
- Reinstall/version/provenance assertions.
|
||||||
|
|
||||||
## Phase 4: v2 HTTP API
|
## Phase 4: v1 HTTP API
|
||||||
|
|
||||||
1. Mount `/v2/rpc` POST and SSE endpoints.
|
1. Mount `/v1/rpc` POST and SSE endpoints.
|
||||||
2. Add `/v2/health`, `/v2/agents`, `/v2/agents/{agent}/install`.
|
2. Add `/v1/health`, `/v1/agents`, `/v1/agents/{agent}/install`.
|
||||||
3. Add auth integration on connection lifecycle.
|
3. Add auth integration on connection lifecycle.
|
||||||
4. Keep `/ui/` inspector route and migrate inspector backend calls to ACP v2 transport.
|
4. Keep `/ui/` inspector route and migrate inspector backend calls to ACP v1 transport.
|
||||||
5. Remove v1 OpenAPI generation from default docs build.
|
5. Remove v1 OpenAPI generation from default docs build.
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
||||||
- v2 endpoints documented and passing integration tests.
|
- v1 endpoints documented and passing integration tests.
|
||||||
|
|
||||||
Validation gate:
|
Validation gate:
|
||||||
|
|
||||||
- Contract tests for all `/v2` endpoints (`/v2/rpc`, `/v2/health`, `/v2/agents`, install).
|
- Contract tests for all `/v1` endpoints (`/v1/rpc`, `/v1/health`, `/v1/agents`, install).
|
||||||
- Auth tests (valid, missing, invalid token).
|
- Auth tests (valid, missing, invalid token).
|
||||||
- Error mapping tests (bad envelope, unknown connection, timeout paths).
|
- Error mapping tests (bad envelope, unknown connection, timeout paths).
|
||||||
- `/v1/*` removal contract test (HTTP 410 + stable payload).
|
- `/v1/*` removal contract test (HTTP 410 + stable payload).
|
||||||
- Inspector ACP `agent-browser` flow tests pass.
|
- Inspector ACP `agent-browser` flow tests pass.
|
||||||
- `DELETE /v2/rpc` close contract tests pass.
|
- `DELETE /v1/rpc` close contract tests pass.
|
||||||
|
|
||||||
## Phase 5: SDK and CLI v2
|
## Phase 5: SDK and CLI v1
|
||||||
|
|
||||||
1. Add ACP transport client in `sdks/typescript` by embedding `@agentclientprotocol/sdk` (no in-house ACP reimplementation).
|
1. Add ACP transport client in `sdks/typescript` by embedding `@agentclientprotocol/sdk` (no in-house ACP reimplementation).
|
||||||
2. Implement custom ACP-over-HTTP transport agent process in our SDK (official ACP client SDK does not provide required Streamable HTTP behavior out of the box).
|
2. Implement custom ACP-over-HTTP transport agent process in our SDK (official ACP client SDK does not provide required Streamable HTTP behavior out of the box).
|
||||||
3. Add inspector frontend client wiring to use ACP-over-HTTP transport primitives.
|
3. Add inspector frontend client wiring to use ACP-over-HTTP transport primitives.
|
||||||
4. Add CLI commands for sending raw ACP envelopes and streaming ACP messages.
|
4. Add CLI commands for sending raw ACP envelopes and streaming ACP messages.
|
||||||
5. Remove v1-only SDK/CLI methods (or hard-fail with "v1 removed").
|
5. Remove v1-only SDK/CLI methods (or hard-fail with "v1 removed").
|
||||||
6. Regenerate docs to v2 ACP contract.
|
6. Regenerate docs to v1 ACP contract.
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
||||||
- SDK can complete a full ACP prompt turn over `/v2/rpc`.
|
- SDK can complete a full ACP prompt turn over `/v1/rpc`.
|
||||||
|
|
||||||
Validation gate:
|
Validation gate:
|
||||||
|
|
||||||
|
|
@ -136,9 +136,9 @@ Validation gate:
|
||||||
|
|
||||||
1. Replace v1 HTTP/session tests with ACP transport contract tests.
|
1. Replace v1 HTTP/session tests with ACP transport contract tests.
|
||||||
2. Add smoke tests per supported agent process.
|
2. Add smoke tests per supported agent process.
|
||||||
Current deterministic matrix: `server/packages/sandbox-agent/tests/v2_agent_process_matrix.rs`.
|
Current deterministic matrix: `server/packages/sandbox-agent/tests/v1_agent_process_matrix.rs`.
|
||||||
3. Add canary rollout notes directly in `docs/quickstart.mdx`, `docs/cli.mdx`, and `docs/sdks/typescript.mdx`.
|
3. Add canary rollout notes directly in `docs/quickstart.mdx`, `docs/cli.mdx`, and `docs/sdks/typescript.mdx`.
|
||||||
4. Update docs for v2 ACP, `/v1/*` removal, inspector ACP behavior, and SDK usage.
|
4. Update docs for v1 ACP, `/v1/*` removal, inspector ACP behavior, and SDK usage.
|
||||||
5. Keep v1 endpoints hard-removed (`410`) until/unless a separate compatibility project is approved.
|
5. Keep v1 endpoints hard-removed (`410`) until/unless a separate compatibility project is approved.
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
@ -155,9 +155,9 @@ Validation gate:
|
||||||
## Phase 7: OpenCode <-> ACP Bridge (Dedicated Step)
|
## Phase 7: OpenCode <-> ACP Bridge (Dedicated Step)
|
||||||
|
|
||||||
1. Keep `/opencode/*` commented out/disabled through Phases 1-6.
|
1. Keep `/opencode/*` commented out/disabled through Phases 1-6.
|
||||||
2. Implement OpenCode <-> ACP bridge on top of v2 ACP runtime.
|
2. Implement OpenCode <-> ACP bridge on top of v1 ACP runtime.
|
||||||
3. Re-enable `server/packages/sandbox-agent/src/opencode_compat.rs` routes/tests at full capability.
|
3. Re-enable `server/packages/sandbox-agent/src/opencode_compat.rs` routes/tests at full capability.
|
||||||
4. Add dedicated integration tests that validate OpenCode SDK/TUI flows through ACP v2 internals.
|
4. Add dedicated integration tests that validate OpenCode SDK/TUI flows through ACP v1 internals.
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
||||||
|
|
@ -170,5 +170,5 @@ Validation gate:
|
||||||
|
|
||||||
## Compatibility Layer (optional future project)
|
## Compatibility Layer (optional future project)
|
||||||
|
|
||||||
1. No compatibility layer is in the current v2 scope.
|
1. No compatibility layer is in the current v1 scope.
|
||||||
2. If later approved, it should be a separate project with a dedicated spec and test matrix.
|
2. If later approved, it should be a separate project with a dedicated spec and test matrix.
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had a full question subsystem: agent requests a question from the user, client replies with an answer or rejection, and the system tracks question status. v2 has partial stub implementation in mock only.
|
v1 had a full question subsystem: agent requests a question from the user, client replies with an answer or rejection, and the system tracks question status. v1 has partial stub implementation in mock only.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- `_sandboxagent/session/request_question` is declared as a constant in `acp_runtime/mod.rs:33`
|
- `_sandboxagent/session/request_question` is declared as a constant in `acp_runtime/mod.rs:33`
|
||||||
- Advertised in capability injection (`extensions.sessionRequestQuestion: true`)
|
- Advertised in capability injection (`extensions.sessionRequestQuestion: true`)
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had 8 filesystem endpoints. v2 has only ACP `fs/read_text_file` + `fs/write_text_file` (text-only, agent->client direction). The full filesystem API should be re-implemented as Sandbox Agent-specific HTTP contracts at `/v2/fs/*`.
|
v1 had 8 filesystem endpoints. v1 has only ACP `fs/read_text_file` + `fs/write_text_file` (text-only, agent->client direction). The full filesystem API should be re-implemented as Sandbox Agent-specific HTTP contracts at `/v1/fs/*`.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- ACP stable: `fs/read_text_file`, `fs/write_text_file` (client methods invoked by agents, text-only)
|
- ACP stable: `fs/read_text_file`, `fs/write_text_file` (client methods invoked by agents, text-only)
|
||||||
- No HTTP filesystem endpoints exist in current `router.rs`
|
- No HTTP filesystem endpoints exist in current `router.rs`
|
||||||
- `rfds-vs-extensions.md` confirms: "Already extension (`/v2/fs/*` custom HTTP surface)"
|
- `rfds-vs-extensions.md` confirms: "Already extension (`/v1/fs/*` custom HTTP surface)"
|
||||||
- CLAUDE.md: "Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP"
|
- CLAUDE.md: "Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP"
|
||||||
|
|
||||||
## v1 Reference (source commit)
|
## v1 Reference (source commit)
|
||||||
|
|
@ -356,32 +356,32 @@ async fn fs_upload_batch(
|
||||||
|
|
||||||
## Implementation Plan
|
## Implementation Plan
|
||||||
|
|
||||||
### New v2 Endpoints
|
### New v1 Endpoints
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/v2/fs/entries` | List directory entries |
|
| GET | `/v1/fs/entries` | List directory entries |
|
||||||
| GET | `/v2/fs/file` | Read file raw bytes |
|
| GET | `/v1/fs/file` | Read file raw bytes |
|
||||||
| PUT | `/v2/fs/file` | Write file raw bytes |
|
| PUT | `/v1/fs/file` | Write file raw bytes |
|
||||||
| DELETE | `/v2/fs/entry` | Delete file or directory |
|
| DELETE | `/v1/fs/entry` | Delete file or directory |
|
||||||
| POST | `/v2/fs/mkdir` | Create directory |
|
| POST | `/v1/fs/mkdir` | Create directory |
|
||||||
| POST | `/v2/fs/move` | Move/rename |
|
| POST | `/v1/fs/move` | Move/rename |
|
||||||
| GET | `/v2/fs/stat` | File metadata |
|
| GET | `/v1/fs/stat` | File metadata |
|
||||||
| POST | `/v2/fs/upload-batch` | Upload tar archive |
|
| POST | `/v1/fs/upload-batch` | Upload tar archive |
|
||||||
|
|
||||||
### Files to Modify
|
### Files to Modify
|
||||||
|
|
||||||
| File | Change |
|
| File | Change |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `server/packages/sandbox-agent/src/router.rs` | Add all 8 `/v2/fs/*` endpoints with handlers (port from v1 with v2 path prefix) |
|
| `server/packages/sandbox-agent/src/router.rs` | Add all 8 `/v1/fs/*` endpoints with handlers (port from v1 with v1 path prefix) |
|
||||||
| `server/packages/sandbox-agent/src/cli.rs` | Add CLI `fs` subcommands (list, read, write, delete, mkdir, move, stat) |
|
| `server/packages/sandbox-agent/src/cli.rs` | Add CLI `fs` subcommands (list, read, write, delete, mkdir, move, stat) |
|
||||||
| `sdks/typescript/src/client.ts` | Add filesystem methods to SDK |
|
| `sdks/typescript/src/client.ts` | Add filesystem methods to SDK |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add filesystem endpoint tests |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add filesystem endpoint tests |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
| Doc | Change |
|
| Doc | Change |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `docs/openapi.json` | Add `/v2/fs/*` endpoint specs |
|
| `docs/openapi.json` | Add `/v1/fs/*` endpoint specs |
|
||||||
| `docs/cli.mdx` | Add `fs` subcommand documentation |
|
| `docs/cli.mdx` | Add `fs` subcommand documentation |
|
||||||
| `docs/sdks/typescript.mdx` | Document filesystem SDK methods |
|
| `docs/sdks/typescript.mdx` | Document filesystem SDK methods |
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# Feature 5: Health Endpoint
|
# Feature 5: Health Endpoint
|
||||||
|
|
||||||
**Implementation approach:** Enhance existing `GET /v2/health`
|
**Implementation approach:** Enhance existing `GET /v1/health`
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had a typed `HealthResponse` with detailed status. v2 `GET /v2/health` exists but returns only `{ status: "ok", api_version: "v2" }`. Needs enrichment.
|
v1 had a typed `HealthResponse` with detailed status. v1 `GET /v1/health` exists but returns only `{ status: "ok", api_version: "v1" }`. Needs enrichment.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
From `router.rs:332-346`:
|
From `router.rs:332-346`:
|
||||||
|
|
||||||
|
|
@ -17,10 +17,10 @@ pub struct HealthResponse {
|
||||||
pub api_version: String,
|
pub api_version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_v2_health() -> Json<HealthResponse> {
|
async fn get_v1_health() -> Json<HealthResponse> {
|
||||||
Json(HealthResponse {
|
Json(HealthResponse {
|
||||||
status: "ok".to_string(),
|
status: "ok".to_string(),
|
||||||
api_version: "v2".to_string(),
|
api_version: "v1".to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -72,19 +72,19 @@ pub struct HealthResponse {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`GET /v2/health` should mirror v1 semantics and response shape (ported from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`), while keeping the v2 route path.
|
`GET /v1/health` should mirror v1 semantics and response shape (ported from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`), while keeping the v1 route path.
|
||||||
|
|
||||||
### Files to Modify
|
### Files to Modify
|
||||||
|
|
||||||
| File | Change |
|
| File | Change |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `server/packages/sandbox-agent/src/router.rs` | Port v1 health response types/logic onto `GET /v2/health` |
|
| `server/packages/sandbox-agent/src/router.rs` | Port v1 health response types/logic onto `GET /v1/health` |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Update health endpoint test for full v1-parity payload |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Update health endpoint test for full v1-parity payload |
|
||||||
| `sdks/typescript/src/client.ts` | Update `HealthResponse` type |
|
| `sdks/typescript/src/client.ts` | Update `HealthResponse` type |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
| Doc | Change |
|
| Doc | Change |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `docs/openapi.json` | Update `/v2/health` response schema |
|
| `docs/openapi.json` | Update `/v1/health` response schema |
|
||||||
| `docs/sdks/typescript.mdx` | Document enriched health response |
|
| `docs/sdks/typescript.mdx` | Document enriched health response |
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
# Feature 6: Server Status
|
# Feature 6: Server Status
|
||||||
|
|
||||||
**Implementation approach:** Extension fields on `GET /v2/agents` and `GET /v2/health`
|
**Implementation approach:** Extension fields on `GET /v1/agents` and `GET /v1/health`
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had `ServerStatus` (Running/Stopped/Error) and `ServerStatusInfo` (baseUrl, lastError, restartCount, uptimeMs) per agent. v2 has none of this. Add server/agent process status tracking.
|
v1 had `ServerStatus` (Running/Stopped/Error) and `ServerStatusInfo` (baseUrl, lastError, restartCount, uptimeMs) per agent. v1 has none of this. Add server/agent process status tracking.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
`GET /v2/agents` returns `AgentInfo` with install state only:
|
`GET /v1/agents` returns `AgentInfo` with install state only:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct AgentInfo {
|
pub struct AgentInfo {
|
||||||
|
|
@ -132,13 +132,13 @@ Only include `server_status` for agents that use shared processes (Codex, OpenCo
|
||||||
| File | Change |
|
| File | Change |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Track agent process lifecycle (start/stop/error/restart count) per `AgentId`; expose `status_snapshot()` method |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Track agent process lifecycle (start/stop/error/restart count) per `AgentId`; expose `status_snapshot()` method |
|
||||||
| `server/packages/sandbox-agent/src/router.rs` | Add `ServerStatus`, `ServerStatusInfo` types; add `server_status` to `AgentInfo`; query runtime for status in `get_v2_agents` |
|
| `server/packages/sandbox-agent/src/router.rs` | Add `ServerStatus`, `ServerStatusInfo` types; add `server_status` to `AgentInfo`; query runtime for status in `get_v1_agents` |
|
||||||
| `sdks/typescript/src/client.ts` | Update `AgentInfo` type with `serverStatus` |
|
| `sdks/typescript/src/client.ts` | Update `AgentInfo` type with `serverStatus` |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Test server status in agent listing |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Test server status in agent listing |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
| Doc | Change |
|
| Doc | Change |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `docs/openapi.json` | Update `/v2/agents` response with `server_status` |
|
| `docs/openapi.json` | Update `/v1/agents` response with `server_status` |
|
||||||
| `docs/sdks/typescript.mdx` | Document `serverStatus` field |
|
| `docs/sdks/typescript.mdx` | Document `serverStatus` field |
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had explicit session termination (`POST /v1/sessions/{id}/terminate`). v2 only has `session/cancel` (turn cancellation, not session kill) and `DELETE /v2/rpc` (connection close, not session termination). Need explicit session destroy/terminate semantics.
|
v1 had explicit session termination (`POST /v1/sessions/{id}/terminate`). v1 only has `session/cancel` (turn cancellation, not session kill) and `DELETE /v1/rpc` (connection close, not session termination). Need explicit session destroy/terminate semantics.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- `session/cancel` — cancels an in-flight prompt turn only
|
- `session/cancel` — cancels an in-flight prompt turn only
|
||||||
- `DELETE /v2/rpc` — closes the HTTP connection, does **not** terminate the session
|
- `DELETE /v1/rpc` — closes the HTTP connection, does **not** terminate the session
|
||||||
- `_sandboxagent/session/detach` — detaches a session from a connection (multi-client visibility)
|
- `_sandboxagent/session/detach` — detaches a session from a connection (multi-client visibility)
|
||||||
- No session termination/deletion exists
|
- No session termination/deletion exists
|
||||||
- `rfds-vs-extensions.md`: "Session Termination: Not covered by ACP. Only implement if product explicitly requires termination semantics beyond session/cancel"
|
- `rfds-vs-extensions.md`: "Session Termination: Not covered by ACP. Only implement if product explicitly requires termination semantics beyond session/cancel"
|
||||||
|
|
@ -112,7 +112,7 @@ Response:
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add `_sandboxagent/session/terminate` handler; add session removal from registry; add process kill logic |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add `_sandboxagent/session/terminate` handler; add session removal from registry; add process kill logic |
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock terminate support |
|
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock terminate support |
|
||||||
| `sdks/typescript/src/client.ts` | Add `terminateSession(sessionId)` method |
|
| `sdks/typescript/src/client.ts` | Add `terminateSession(sessionId)` method |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add session termination test |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add session termination test |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had `AgentModelInfo.variants`, `AgentModelInfo.defaultVariant`, and `CreateSessionRequest.variant`. v2 already has `_sandboxagent/session/list_models` but the variant fields need to be verified and the session-creation variant selection needs to work end-to-end.
|
v1 had `AgentModelInfo.variants`, `AgentModelInfo.defaultVariant`, and `CreateSessionRequest.variant`. v1 already has `_sandboxagent/session/list_models` but the variant fields need to be verified and the session-creation variant selection needs to work end-to-end.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
From `acp_runtime/mod.rs`, `_sandboxagent/session/list_models` is implemented and returns:
|
From `acp_runtime/mod.rs`, `_sandboxagent/session/list_models` is implemented and returns:
|
||||||
- `availableModels[]` with `modelId`, `name`, `description`
|
- `availableModels[]` with `modelId`, `name`, `description`
|
||||||
|
|
@ -120,7 +120,7 @@ The runtime should forward this variant to the agent process (e.g., as a model p
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Verify `list_models` response includes `variants`/`defaultVariant`; extract and forward `variant` from `session/new` `_meta` |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Verify `list_models` response includes `variants`/`defaultVariant`; extract and forward `variant` from `session/new` `_meta` |
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add variant support to mock model listing |
|
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add variant support to mock model listing |
|
||||||
| `sdks/typescript/src/client.ts` | Update `listModels` return type to include variants |
|
| `sdks/typescript/src/client.ts` | Update `listModels` return type to include variants |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add model variants test |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add model variants test |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 had an `include_raw` option that preserved the original agent JSON alongside normalized events. The `UniversalEvent.raw` field held the verbatim agent output. v2 has `_sandboxagent/agent/unparsed` for parse errors but no mechanism for clients to request raw agent payloads alongside normalized ACP events.
|
v1 had an `include_raw` option that preserved the original agent JSON alongside normalized events. The `UniversalEvent.raw` field held the verbatim agent output. v1 has `_sandboxagent/agent/unparsed` for parse errors but no mechanism for clients to request raw agent payloads alongside normalized ACP events.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- `_sandboxagent/agent/unparsed` — sends notifications when the runtime fails to parse agent output (error recovery only)
|
- `_sandboxagent/agent/unparsed` — sends notifications when the runtime fails to parse agent output (error recovery only)
|
||||||
- No option for clients to request raw agent JSON alongside normal ACP events
|
- No option for clients to request raw agent JSON alongside normal ACP events
|
||||||
|
|
@ -43,7 +43,7 @@ When `include_raw=true`, each `UniversalEvent` included the verbatim JSON the ag
|
||||||
|
|
||||||
### Extension Design
|
### Extension Design
|
||||||
|
|
||||||
Since v2 agents speak ACP natively (JSON-RPC), the "raw" concept changes:
|
Since v1 agents speak ACP natively (JSON-RPC), the "raw" concept changes:
|
||||||
- For ACP-native agents: raw = the ACP JSON-RPC envelope itself (which clients already see)
|
- For ACP-native agents: raw = the ACP JSON-RPC envelope itself (which clients already see)
|
||||||
- For non-native agents or runtime-synthesized events: raw = the original agent output before transformation
|
- For non-native agents or runtime-synthesized events: raw = the original agent output before transformation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
# Feature 12: Agent Listing (Typed Response)
|
# Feature 12: Agent Listing (Typed Response)
|
||||||
|
|
||||||
**Implementation approach:** Enhance existing `GET /v2/agents`
|
**Implementation approach:** Enhance existing `GET /v1/agents`
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 `GET /v1/agents` returned a typed `AgentListResponse` with `installed`, `credentialsAvailable`, `path`, `capabilities`, `serverStatus`. v2 `GET /v2/agents` returns a basic `AgentInfo` with only install state. Needs enrichment.
|
v1 `GET /v1/agents` returned a typed `AgentListResponse` with `installed`, `credentialsAvailable`, `path`, `capabilities`, `serverStatus`. v1 `GET /v1/agents` returns a basic `AgentInfo` with only install state. Needs enrichment.
|
||||||
|
|
||||||
This feature also carries pre-session models/modes as optional fields when the agent is installed (Feature #13), rather than using separate model/mode endpoints.
|
This feature also carries pre-session models/modes as optional fields when the agent is installed (Feature #13), rather than using separate model/mode endpoints.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
From `router.rs:265-275`:
|
From `router.rs:265-275`:
|
||||||
|
|
||||||
|
|
@ -183,19 +183,19 @@ fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
|
||||||
|
|
||||||
### Enriched AgentInfo
|
### Enriched AgentInfo
|
||||||
|
|
||||||
Merge v2 install fields with v1 richness:
|
Merge v1 install fields with v1 richness:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct AgentInfo {
|
pub struct AgentInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub installed: bool, // convenience: is fully installed
|
pub installed: bool, // convenience: is fully installed
|
||||||
pub credentials_available: bool, // from credential extraction
|
pub credentials_available: bool, // from credential extraction
|
||||||
pub native_required: bool, // keep from v2
|
pub native_required: bool, // keep from v1
|
||||||
pub native_installed: bool, // keep from v2
|
pub native_installed: bool, // keep from v1
|
||||||
pub native_version: Option<String>, // keep from v2
|
pub native_version: Option<String>, // keep from v1
|
||||||
pub agent_process_installed: bool, // keep from v2
|
pub agent_process_installed: bool, // keep from v1
|
||||||
pub agent_process_source: Option<String>, // keep from v2
|
pub agent_process_source: Option<String>, // keep from v1
|
||||||
pub agent_process_version: Option<String>, // keep from v2
|
pub agent_process_version: Option<String>, // keep from v1
|
||||||
pub path: Option<String>, // from resolve_binary()
|
pub path: Option<String>, // from resolve_binary()
|
||||||
pub capabilities: AgentCapabilities, // full v1 capability set
|
pub capabilities: AgentCapabilities, // full v1 capability set
|
||||||
pub server_status: Option<AgentServerStatus>, // from Feature #6
|
pub server_status: Option<AgentServerStatus>, // from Feature #6
|
||||||
|
|
@ -212,11 +212,11 @@ pub struct AgentInfo {
|
||||||
| `server/packages/sandbox-agent/src/router.rs` | Enrich `AgentInfo` and `AgentCapabilities` structs; add `agent_capabilities_for()` static mapping; add credential check; add convenience `installed` field; add optional `models`/`modes` for installed agents |
|
| `server/packages/sandbox-agent/src/router.rs` | Enrich `AgentInfo` and `AgentCapabilities` structs; add `agent_capabilities_for()` static mapping; add credential check; add convenience `installed` field; add optional `models`/`modes` for installed agents |
|
||||||
| `server/packages/agent-management/src/agents.rs` | Expose credential availability check and `resolve_binary()` if not already present |
|
| `server/packages/agent-management/src/agents.rs` | Expose credential availability check and `resolve_binary()` if not already present |
|
||||||
| `sdks/typescript/src/client.ts` | Update `AgentInfo` and `AgentCapabilities` types |
|
| `sdks/typescript/src/client.ts` | Update `AgentInfo` and `AgentCapabilities` types |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Update agent listing test assertions |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Update agent listing test assertions |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
| Doc | Change |
|
| Doc | Change |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `docs/openapi.json` | Update `/v2/agents` response schema with full `AgentCapabilities` |
|
| `docs/openapi.json` | Update `/v1/agents` response schema with full `AgentCapabilities` |
|
||||||
| `docs/sdks/typescript.mdx` | Document enriched agent listing |
|
| `docs/sdks/typescript.mdx` | Document enriched agent listing |
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 exposed pre-session model/mode discovery via separate endpoints. For v2, models and modes should be optional fields on the agent response payload (only when the agent is installed), with lazy population for dynamic agents.
|
v1 exposed pre-session model/mode discovery via separate endpoints. For v1, models and modes should be optional fields on the agent response payload (only when the agent is installed), with lazy population for dynamic agents.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- `_sandboxagent/session/list_models` works but requires an active ACP connection and session
|
- `_sandboxagent/session/list_models` works but requires an active ACP connection and session
|
||||||
- `GET /v2/agents` does not include pre-session model/mode metadata
|
- `GET /v1/agents` does not include pre-session model/mode metadata
|
||||||
- v1 had static per-agent mode definitions (`agent_modes_for()` in `router.rs`)
|
- v1 had static per-agent mode definitions (`agent_modes_for()` in `router.rs`)
|
||||||
- v1 had dynamic model fetching (Claude/Codex/OpenCode), plus static model lists for Amp/Mock
|
- v1 had dynamic model fetching (Claude/Codex/OpenCode), plus static model lists for Amp/Mock
|
||||||
|
|
||||||
|
|
@ -57,11 +57,11 @@ Model variants are explicitly out of scope for this implementation pass.
|
||||||
| `server/packages/sandbox-agent/src/router.rs` | Enrich agent response type/handlers to optionally include models + modes |
|
| `server/packages/sandbox-agent/src/router.rs` | Enrich agent response type/handlers to optionally include models + modes |
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Expose model query support for control-plane enrichment without requiring an active session |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Expose model query support for control-plane enrichment without requiring an active session |
|
||||||
| `sdks/typescript/src/client.ts` | Extend `AgentInfo` type with optional `models`, `defaultModel`, `modes` |
|
| `sdks/typescript/src/client.ts` | Extend `AgentInfo` type with optional `models`, `defaultModel`, `modes` |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add assertions for installed vs non-installed agent response shapes |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add assertions for installed vs non-installed agent response shapes |
|
||||||
|
|
||||||
## Docs to Update
|
## Docs to Update
|
||||||
|
|
||||||
| Doc | Change |
|
| Doc | Change |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `docs/openapi.json` | Update `/v2/agents` (and agent detail endpoint if present) schema with optional `models`/`modes` |
|
| `docs/openapi.json` | Update `/v1/agents` (and agent detail endpoint if present) schema with optional `models`/`modes` |
|
||||||
| `docs/sdks/typescript.mdx` | Document optional model/mode fields on agent response |
|
| `docs/sdks/typescript.mdx` | Document optional model/mode fields on agent response |
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 `MessageRequest.attachments` allowed sending file attachments (path, mime, filename) with prompts. v2 ACP `embeddedContext` is only partial. Need to support file attachments in prompt messages.
|
v1 `MessageRequest.attachments` allowed sending file attachments (path, mime, filename) with prompts. v1 ACP `embeddedContext` is only partial. Need to support file attachments in prompt messages.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- ACP `session/prompt` accepts `params.content` as the prompt text
|
- ACP `session/prompt` accepts `params.content` as the prompt text
|
||||||
- No attachment mechanism in the current ACP prompt flow
|
- No attachment mechanism in the current ACP prompt flow
|
||||||
|
|
@ -122,7 +122,7 @@ The runtime extracts attachments from `_meta` and transforms them per agent:
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Extract `attachments` from `session/prompt` `_meta`; transform per agent before forwarding |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Extract `attachments` from `session/prompt` `_meta`; transform per agent before forwarding |
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock handling for attachments |
|
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock handling for attachments |
|
||||||
| `sdks/typescript/src/client.ts` | Add `attachments` option to prompt method |
|
| `sdks/typescript/src/client.ts` | Add `attachments` option to prompt method |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add attachment prompt test |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add attachment prompt test |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 `CreateSessionRequest` had `mcp` (full MCP server config with OAuth, env headers, bearer tokens), `skills` (sources with git refs), `agent_version`, `directory`. v2 needs to support these at session creation time.
|
v1 `CreateSessionRequest` had `mcp` (full MCP server config with OAuth, env headers, bearer tokens), `skills` (sources with git refs), `agent_version`, `directory`. v1 needs to support these at session creation time.
|
||||||
|
|
||||||
## Current v2 State — MOSTLY IMPLEMENTED
|
## Current v1 State — MOSTLY IMPLEMENTED
|
||||||
|
|
||||||
Investigation shows that **most of these are already supported** via `_meta.sandboxagent.dev` passthrough in `session/new`:
|
Investigation shows that **most of these are already supported** via `_meta.sandboxagent.dev` passthrough in `session/new`:
|
||||||
|
|
||||||
| Field | v1 | v2 Status | v2 Mechanism |
|
| Field | v1 | v1 Status | v1 Mechanism |
|
||||||
|-------|-----|-----------|-------------|
|
|-------|-----|-----------|-------------|
|
||||||
| `directory` | `CreateSessionRequest.directory` | **Implemented** | `cwd` parameter extracted from payload |
|
| `directory` | `CreateSessionRequest.directory` | **Implemented** | `cwd` parameter extracted from payload |
|
||||||
| `agent_version` | `CreateSessionRequest.agent_version` | **Implemented** | `_meta.sandboxagent.dev.agentVersionRequested` (stored, forwarded) |
|
| `agent_version` | `CreateSessionRequest.agent_version` | **Implemented** | `_meta.sandboxagent.dev.agentVersionRequested` (stored, forwarded) |
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# Feature 16: Session Info
|
# Feature 16: Session Info
|
||||||
|
|
||||||
**Implementation approach:** New HTTP endpoints (`GET /v2/sessions`, `GET /v2/sessions/{id}`)
|
**Implementation approach:** New HTTP endpoints (`GET /v1/sessions`, `GET /v1/sessions/{id}`)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 `SessionInfo` tracked `event_count`, `created_at`, `updated_at`, and full `mcp` config. v2 has session data in the ACP runtime's `MetaSession` struct but no HTTP endpoints to query it. Add REST endpoints for session listing and detail.
|
v1 `SessionInfo` tracked `event_count`, `created_at`, `updated_at`, and full `mcp` config. v1 has session data in the ACP runtime's `MetaSession` struct but no HTTP endpoints to query it. Add REST endpoints for session listing and detail.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
### Internal Session Tracking
|
### Internal Session Tracking
|
||||||
|
|
||||||
|
|
@ -117,15 +117,15 @@ fn build_session_info(state: &SessionState) -> SessionInfo {
|
||||||
### New HTTP Endpoints
|
### New HTTP Endpoints
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /v2/sessions -> SessionListResponse
|
GET /v1/sessions -> SessionListResponse
|
||||||
GET /v2/sessions/{id} -> SessionInfo
|
GET /v1/sessions/{id} -> SessionInfo
|
||||||
```
|
```
|
||||||
|
|
||||||
These are control-plane HTTP endpoints (not ACP), providing session visibility without requiring an active ACP connection.
|
These are control-plane HTTP endpoints (not ACP), providing session visibility without requiring an active ACP connection.
|
||||||
|
|
||||||
### Response Types
|
### Response Types
|
||||||
|
|
||||||
The v2 `SessionInfo` should be a superset of v1 fields, adapted for ACP:
|
The v1 `SessionInfo` should be a superset of v1 fields, adapted for ACP:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Debug, Serialize, JsonSchema, ToSchema)]
|
#[derive(Debug, Serialize, JsonSchema, ToSchema)]
|
||||||
|
|
@ -156,15 +156,15 @@ Need to add:
|
||||||
|
|
||||||
| File | Change |
|
| File | Change |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `server/packages/sandbox-agent/src/router.rs` | Add `GET /v2/sessions` and `GET /v2/sessions/{id}` handlers; add response types |
|
| `server/packages/sandbox-agent/src/router.rs` | Add `GET /v1/sessions` and `GET /v1/sessions/{id}` handlers; add response types |
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add `created_at` to `MetaSession`; add `ended` tracking; expose `list_sessions()` and `get_session()` public methods |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add `created_at` to `MetaSession`; add `ended` tracking; expose `list_sessions()` and `get_session()` public methods |
|
||||||
| `sdks/typescript/src/client.ts` | Add `listSessions()` and `getSession(id)` methods |
|
| `sdks/typescript/src/client.ts` | Add `listSessions()` and `getSession(id)` methods |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add session listing and detail tests |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add session listing and detail tests |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
| Doc | Change |
|
| Doc | Change |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `docs/openapi.json` | Add `/v2/sessions` and `/v2/sessions/{id}` endpoint specs |
|
| `docs/openapi.json` | Add `/v1/sessions` and `/v1/sessions/{id}` endpoint specs |
|
||||||
| `docs/cli.mdx` | Add CLI `sessions list` and `sessions info` commands |
|
| `docs/cli.mdx` | Add CLI `sessions list` and `sessions info` commands |
|
||||||
| `docs/sdks/typescript.mdx` | Document session listing SDK methods |
|
| `docs/sdks/typescript.mdx` | Document session listing SDK methods |
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated) when a session ended due to error. v2 loses this metadata. Need to capture and expose process termination details.
|
v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated) when a session ended due to error. v1 loses this metadata. Need to capture and expose process termination details.
|
||||||
|
|
||||||
## Current v2 State
|
## Current v1 State
|
||||||
|
|
||||||
- Agent process lifecycle is managed in `acp_runtime/mod.rs`
|
- Agent process lifecycle is managed in `acp_runtime/mod.rs`
|
||||||
- Process exit is detected but error metadata (exit code, stderr) is not captured or forwarded
|
- Process exit is detected but error metadata (exit code, stderr) is not captured or forwarded
|
||||||
|
|
@ -171,7 +171,7 @@ When an agent process terminates with an error:
|
||||||
### Session Info Integration
|
### Session Info Integration
|
||||||
|
|
||||||
Termination metadata should be accessible via:
|
Termination metadata should be accessible via:
|
||||||
- `GET /v2/sessions/{id}` (Feature #16) — include `terminationInfo` in response when session has ended
|
- `GET /v1/sessions/{id}` (Feature #16) — include `terminationInfo` in response when session has ended
|
||||||
- `session/list` ACP response — include termination status in session entries
|
- `session/list` ACP response — include termination status in session entries
|
||||||
|
|
||||||
### Files to Modify
|
### Files to Modify
|
||||||
|
|
@ -181,7 +181,7 @@ Termination metadata should be accessible via:
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add stderr capture (head/tail buffer) on agent process; capture exit code; emit `_sandboxagent/session/ended`; store v1-shaped termination info in `MetaSession` |
|
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add stderr capture (head/tail buffer) on agent process; capture exit code; emit `_sandboxagent/session/ended`; store v1-shaped termination info in `MetaSession` |
|
||||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock error termination scenario (e.g., when prompt contains "crash") |
|
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock error termination scenario (e.g., when prompt contains "crash") |
|
||||||
| `sdks/typescript/src/client.ts` | Add `TerminationInfo` type; expose on session events and session info |
|
| `sdks/typescript/src/client.ts` | Add `TerminationInfo` type; expose on session events and session info |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add error termination metadata test |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | Add error termination metadata test |
|
||||||
|
|
||||||
### Docs to Update
|
### Docs to Update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Missing Features Index
|
# Missing Features Index
|
||||||
|
|
||||||
Features selected for implementation from the v1-to-v2 gap analysis.
|
Features selected for implementation from the v1-to-v1 gap analysis.
|
||||||
|
|
||||||
## Completely UNIMPLEMENTED in v2
|
## Completely UNIMPLEMENTED in v1
|
||||||
|
|
||||||
| # | Feature | Implementation notes |
|
| # | Feature | Implementation notes |
|
||||||
|---|---------|---------------------|
|
|---|---------|---------------------|
|
||||||
|
|
@ -12,19 +12,19 @@ Features selected for implementation from the v1-to-v2 gap analysis.
|
||||||
| 4 | **Filesystem API** -- all 8 endpoints (list, read, write, delete, mkdir, move, stat, upload-batch). ACP only has text-only `fs/read_text_file` + `fs/write_text_file` (agent->client direction). | |
|
| 4 | **Filesystem API** -- all 8 endpoints (list, read, write, delete, mkdir, move, stat, upload-batch). ACP only has text-only `fs/read_text_file` + `fs/write_text_file` (agent->client direction). | |
|
||||||
| 5 | **Health endpoint** -- typed `HealthResponse` with status. | |
|
| 5 | **Health endpoint** -- typed `HealthResponse` with status. | |
|
||||||
| 6 | **Server status** -- `ServerStatus` (Running/Stopped/Error), `ServerStatusInfo` (baseUrl, lastError, restartCount, uptimeMs). | |
|
| 6 | **Server status** -- `ServerStatus` (Running/Stopped/Error), `ServerStatusInfo` (baseUrl, lastError, restartCount, uptimeMs). | |
|
||||||
| 7 | **Session termination** -- v1 had full `terminate`. v2 only has `session/cancel` (turn cancellation, not session kill). No explicit close/delete. | See existing ACP RFD |
|
| 7 | **Session termination** -- v1 had full `terminate`. v1 only has `session/cancel` (turn cancellation, not session kill). No explicit close/delete. | See existing ACP RFD |
|
||||||
| 8 | ~~Model variants~~ -- deferred for now. | Out of scope |
|
| 8 | ~~Model variants~~ -- deferred for now. | Out of scope |
|
||||||
| 9 | ~~Agent capability flags~~ | Not selected |
|
| 9 | ~~Agent capability flags~~ | Not selected |
|
||||||
| 10 | ~~`include_raw`~~ -- deferred for now. | Out of scope |
|
| 10 | ~~`include_raw`~~ -- deferred for now. | Out of scope |
|
||||||
|
|
||||||
## Downgraded / Partial in v2
|
## Downgraded / Partial in v1
|
||||||
|
|
||||||
| # | Feature | Implementation notes |
|
| # | Feature | Implementation notes |
|
||||||
|---|---------|---------------------|
|
|---|---------|---------------------|
|
||||||
| 11 | ~~Permission reply granularity~~ | Not selected |
|
| 11 | ~~Permission reply granularity~~ | Not selected |
|
||||||
| 12 | **Agent listing** -- v1 `GET /v1/agents` returned typed `AgentListResponse` with `installed`, `credentialsAvailable`, `path`, `capabilities`, `serverStatus`. v2 returns generic JSON. | |
|
| 12 | **Agent listing** -- v1 `GET /v1/agents` returned typed `AgentListResponse` with `installed`, `credentialsAvailable`, `path`, `capabilities`, `serverStatus`. v1 returns generic JSON. | |
|
||||||
| 13 | **Models/modes listing** -- expose as optional `models`/`modes` fields on agent response payloads (installed agents only), lazily populated. | No separate `/models` or `/modes` endpoints |
|
| 13 | **Models/modes listing** -- expose as optional `models`/`modes` fields on agent response payloads (installed agents only), lazily populated. | No separate `/models` or `/modes` endpoints |
|
||||||
| 14 | **Message attachments** -- v1 `MessageRequest.attachments` (path, mime, filename). v2 ACP `embeddedContext` is only partial. | |
|
| 14 | **Message attachments** -- v1 `MessageRequest.attachments` (path, mime, filename). v1 ACP `embeddedContext` is only partial. | |
|
||||||
| 15 | **Session creation richness** -- v1 `CreateSessionRequest` had `mcp` (full MCP server config with OAuth, env headers, bearer tokens), `skills` (sources with git refs), `agent_version`, `directory`. Most have no ACP equivalent. | Check with our extensions, do not implement if already done |
|
| 15 | **Session creation richness** -- v1 `CreateSessionRequest` had `mcp` (full MCP server config with OAuth, env headers, bearer tokens), `skills` (sources with git refs), `agent_version`, `directory`. Most have no ACP equivalent. | Check with our extensions, do not implement if already done |
|
||||||
| 16 | **Session info** -- v1 `SessionInfo` tracked `event_count`, `created_at`, `updated_at`, full `mcp` config. Mostly lost. | Add as sessions HTTP endpoint |
|
| 16 | **Session info** -- v1 `SessionInfo` tracked `event_count`, `created_at`, `updated_at`, full `mcp` config. Mostly lost. | Add as sessions HTTP endpoint |
|
||||||
| 17 | **Error termination metadata** -- v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated). Gone. | |
|
| 17 | **Error termination metadata** -- v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated). Gone. | |
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
# Missing Features Implementation Plan
|
# Missing Features Implementation Plan
|
||||||
|
|
||||||
Features selected from the v1-to-v2 gap analysis, ordered for implementation.
|
Features selected from the v1-to-v1 gap analysis, ordered for implementation.
|
||||||
|
|
||||||
## Confirmed Decisions (Locked)
|
## Confirmed Decisions (Locked)
|
||||||
|
|
||||||
- Canonical extension naming is `_sandboxagent/...` and `_meta["sandboxagent.dev"]`; remove/ignore `_sandboxagent/*`.
|
- Canonical extension naming is `_sandboxagent/...` and `_meta["sandboxagent.dev"]`; remove/ignore `_sandboxagent/*`.
|
||||||
- Control-plane discovery/status/session APIs are HTTP-only under `/v2/*` (no ACP control-plane equivalents).
|
- Control-plane discovery/status/session APIs are HTTP-only under `/v1/*` (no ACP control-plane equivalents).
|
||||||
- For Health, Filesystem, and Attachments, implementation should port behavior from v1 using commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
- For Health, Filesystem, and Attachments, implementation should port behavior from v1 using commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
||||||
- Session termination via `_sandboxagent/session/terminate` is idempotent.
|
- Session termination via `_sandboxagent/session/terminate` is idempotent.
|
||||||
- `DELETE /v2/rpc` is transport detach only; it must not replace explicit termination semantics.
|
- `DELETE /v1/rpc` is transport detach only; it must not replace explicit termination semantics.
|
||||||
- Model variants (#8) are removed from current scope.
|
- Model variants (#8) are removed from current scope.
|
||||||
- `include_raw` (#10) is removed from current scope.
|
- `include_raw` (#10) is removed from current scope.
|
||||||
- Models/modes should be optional properties on agent response payloads (only when the agent is installed) and lazily populated.
|
- Models/modes should be optional properties on agent response payloads (only when the agent is installed) and lazily populated.
|
||||||
|
|
@ -24,9 +24,9 @@ These features enrich existing endpoints and have no dependencies on each other.
|
||||||
|
|
||||||
| Order | Feature | Spec | Approach | Effort |
|
| Order | Feature | Spec | Approach | Effort |
|
||||||
|:-----:|----------------------------------------------|:----:|--------------------------------------------|:------:|
|
|:-----:|----------------------------------------------|:----:|--------------------------------------------|:------:|
|
||||||
| A1 | [Health Endpoint](./05-health-endpoint.md) | #5 | Port v1 health behavior to `GET /v2/health` | Small |
|
| A1 | [Health Endpoint](./05-health-endpoint.md) | #5 | Port v1 health behavior to `GET /v1/health` | Small |
|
||||||
| A2 | [Server Status](./06-server-status.md) | #6 | Add process tracking to ACP runtime | Medium |
|
| A2 | [Server Status](./06-server-status.md) | #6 | Add process tracking to ACP runtime | Medium |
|
||||||
| A3 | [Agent Listing](./12-agent-listing.md) | #12 | Enrich `GET /v2/agents` with v1-parity data | Medium |
|
| A3 | [Agent Listing](./12-agent-listing.md) | #12 | Enrich `GET /v1/agents` with v1-parity data | Medium |
|
||||||
|
|
||||||
**A2 blocks A3** — agent listing includes server status from Feature #6.
|
**A2 blocks A3** — agent listing includes server status from Feature #6.
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ Session-level features that build on Phase A runtime tracking.
|
||||||
|
|
||||||
| Order | Feature | Spec | Approach | Effort |
|
| Order | Feature | Spec | Approach | Effort |
|
||||||
|:-----:|--------------------------------------------------------------|:----:|------------------------------------------------------|:------:|
|
|:-----:|--------------------------------------------------------------|:----:|------------------------------------------------------|:------:|
|
||||||
| B1 | [Session Info](./16-session-info.md) | #16 | New `GET /v2/sessions` and `GET /v2/sessions/{id}` | Medium |
|
| B1 | [Session Info](./16-session-info.md) | #16 | New `GET /v1/sessions` and `GET /v1/sessions/{id}` | Medium |
|
||||||
| B2 | [Session Termination](./07-session-termination.md) | #7 | Idempotent `_sandboxagent/session/terminate` | Medium |
|
| B2 | [Session Termination](./07-session-termination.md) | #7 | Idempotent `_sandboxagent/session/terminate` | Medium |
|
||||||
| B3 | [Error Termination Metadata](./17-error-termination-metadata.md) | #17 | Stderr capture + `_sandboxagent/session/ended` event | Medium |
|
| B3 | [Error Termination Metadata](./17-error-termination-metadata.md) | #17 | Stderr capture + `_sandboxagent/session/ended` event | Medium |
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ Standalone platform-level API.
|
||||||
|
|
||||||
| Order | Feature | Spec | Approach | Effort |
|
| Order | Feature | Spec | Approach | Effort |
|
||||||
|:-----:|---------------------------------------|:----:|----------------------------------|:------:|
|
|:-----:|---------------------------------------|:----:|----------------------------------|:------:|
|
||||||
| E1 | [Filesystem API](./04-filesystem-api.md) | #4 | Port v1 behavior to `/v2/fs/*` | Large |
|
| E1 | [Filesystem API](./04-filesystem-api.md) | #4 | Port v1 behavior to `/v1/fs/*` | Large |
|
||||||
|
|
||||||
No dependencies on other features. Can be implemented at any time but is the largest single feature.
|
No dependencies on other features. Can be implemented at any time but is the largest single feature.
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ E1 (Filesystem) [independent]
|
||||||
| # | Feature | Spec File | Status | Approach |
|
| # | Feature | Spec File | Status | Approach |
|
||||||
|:--:|---------------------------------|-------------------------------------------------------|---------------------------------|-------------------------------------------------|
|
|:--:|---------------------------------|-------------------------------------------------------|---------------------------------|-------------------------------------------------|
|
||||||
| 1 | ~~Questions~~ | [01-questions.md](./01-questions.md) | Deferred ([#156](https://github.com/rivet-dev/sandbox-agent/issues/156)) | Agent process side |
|
| 1 | ~~Questions~~ | [01-questions.md](./01-questions.md) | Deferred ([#156](https://github.com/rivet-dev/sandbox-agent/issues/156)) | Agent process side |
|
||||||
| 4 | Filesystem API | [04-filesystem-api.md](./04-filesystem-api.md) | Not implemented | Port v1 behavior onto `/v2/fs/*` |
|
| 4 | Filesystem API | [04-filesystem-api.md](./04-filesystem-api.md) | Not implemented | Port v1 behavior onto `/v1/fs/*` |
|
||||||
| 5 | Health Endpoint | [05-health-endpoint.md](./05-health-endpoint.md) | Partial (basic only) | Port v1 health behavior |
|
| 5 | Health Endpoint | [05-health-endpoint.md](./05-health-endpoint.md) | Partial (basic only) | Port v1 health behavior |
|
||||||
| 6 | Server Status | [06-server-status.md](./06-server-status.md) | Not implemented | Runtime tracking |
|
| 6 | Server Status | [06-server-status.md](./06-server-status.md) | Not implemented | Runtime tracking |
|
||||||
| 7 | Session Termination | [07-session-termination.md](./07-session-termination.md) | Not implemented | Idempotent ACP extension |
|
| 7 | Session Termination | [07-session-termination.md](./07-session-termination.md) | Not implemented | Idempotent ACP extension |
|
||||||
|
|
@ -121,7 +121,7 @@ E1 (Filesystem) [independent]
|
||||||
| `sdks/typescript/src/client.ts` | All in-scope features |
|
| `sdks/typescript/src/client.ts` | All in-scope features |
|
||||||
| `docs/openapi.json` | #4, #5, #6, #12, #13, #16 |
|
| `docs/openapi.json` | #4, #5, #6, #12, #13, #16 |
|
||||||
| `docs/sdks/typescript.mdx` | All in-scope features |
|
| `docs/sdks/typescript.mdx` | All in-scope features |
|
||||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | All in-scope features |
|
| `server/packages/sandbox-agent/tests/v1_api.rs` | All in-scope features |
|
||||||
|
|
||||||
### Docs update checklist
|
### Docs update checklist
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
- Old OpenAPI path: `docs/openapi.json` at git ref `8ecd27b`
|
- Old OpenAPI path: `docs/openapi.json` at git ref `8ecd27b`
|
||||||
- ACP v2 API path: `~/misc/acp-docs/schema/schema.json` (`~/misc/acp-docs/schema/schema.unstable.json` for unstable methods)
|
- ACP v1 API path: `~/misc/acp-docs/schema/schema.json` (`~/misc/acp-docs/schema/schema.unstable.json` for unstable methods)
|
||||||
|
|
||||||
| Path / Schema Property | ACP Equivalent |
|
| Path / Schema Property | ACP Equivalent |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue