Compare commits

...

11 commits

Author SHA1 Message Date
Nathan Flurry
bf484e7c96 docs: clean up orphaned docs and add session event types
Delete orphaned docs not in docs.json navigation (gigacode.mdx,
foundry-self-hosting.mdx, session-transcript-schema.mdx, pi-support-plan.md).
Remove outdated musl/glibc troubleshooting section. Add event types
documentation with example payloads to agent-sessions.mdx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:11:19 -07:00
Nathan Flurry
d55b0dfb88 chore(release): update version to 0.4.2 2026-03-25 18:07:26 -07:00
ABC
251f731232
Merge pull request #284 from rivet-dev/03-25-fix_mock_pass_sandbox_agent_bin_to_mock_agent_launcher
fix(mock): pass SANDBOX_AGENT_BIN to mock agent launcher
2026-03-25 17:00:51 -04:00
abcxff
b45989a082 fix(mock): pass SANDBOX_AGENT_BIN to mock agent launcher 2026-03-25 16:54:40 -04:00
Nathan Flurry
78e84281e8 chore(release): update version to 0.4.1 2026-03-25 13:20:57 -07:00
Nathan Flurry
5da35e6dfa feat: sprites support 2026-03-25 12:23:14 -07:00
ABC
9cd9252725
Merge pull request #283 from rivet-dev/03-25-chore_providers_move_back_to_0.4.x_install_script
chore(providers): sync install script with latest 0.4.x
2026-03-25 14:24:55 -04:00
abcxff
858b9a4d2f chore(providers): move back to 0.4.x install script 2026-03-25 14:22:57 -04:00
Nathan Flurry
4fa28061e9
Merge pull request #279 from rivet-dev/NicholasKissel/docs-dark-theme
fix(docs): restore dark theme styling
2026-03-24 23:26:28 -07:00
Nicholas Kissel
32713ff453 fix(docs): keep dark mode strict appearance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 21:33:10 -07:00
Nicholas Kissel
927e77c7e2 fix(docs): restore dark theme styling with custom CSS
Re-enable theme.css with full custom styling (links, inputs, cards,
code blocks, alerts) and update docs.json color values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 21:25:06 -07:00
106 changed files with 1032 additions and 2151 deletions

View file

@ -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/0.5.x/install.sh | sh && curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh &&
sandbox-agent --version sandbox-agent --version
" "
``` ```

View file

@ -20,7 +20,7 @@
- For HTTP/CLI docs/examples, source of truth is: - For HTTP/CLI docs/examples, source of truth is:
- `server/packages/sandbox-agent/src/router.rs` - `server/packages/sandbox-agent/src/router.rs`
- `server/packages/sandbox-agent/src/cli.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). - Keep docs aligned to implemented endpoints/commands only (for example ACP under `/v1/acp`, not legacy session REST APIs).
## Change Tracking ## Change Tracking
@ -78,4 +78,3 @@
- `scripts/release/main.ts` - `scripts/release/main.ts`
- `scripts/release/promote-artifacts.ts` - `scripts/release/promote-artifacts.ts`
- `scripts/release/sdk.ts` - `scripts/release/sdk.ts`
- `scripts/sandbox-testing/test-sandbox.ts`

View file

@ -4,7 +4,7 @@ members = ["server/packages/*", "gigacode"]
exclude = ["factory/packages/desktop/src-tauri", "foundry/packages/desktop/src-tauri"] exclude = ["factory/packages/desktop/src-tauri", "foundry/packages/desktop/src-tauri"]
[workspace.package] [workspace.package]
version = "0.5.0-rc.2" version = "0.4.2"
edition = "2021" edition = "2021"
authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ] authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ]
license = "Apache-2.0" license = "Apache-2.0"
@ -13,13 +13,13 @@ description = "Universal API for automatic coding agents in sandboxes. Supports
[workspace.dependencies] [workspace.dependencies]
# Internal crates # Internal crates
sandbox-agent = { version = "0.5.0-rc.2", path = "server/packages/sandbox-agent" } sandbox-agent = { version = "0.4.2", path = "server/packages/sandbox-agent" }
sandbox-agent-error = { version = "0.5.0-rc.2", path = "server/packages/error" } sandbox-agent-error = { version = "0.4.2", path = "server/packages/error" }
sandbox-agent-agent-management = { version = "0.5.0-rc.2", path = "server/packages/agent-management" } sandbox-agent-agent-management = { version = "0.4.2", path = "server/packages/agent-management" }
sandbox-agent-agent-credentials = { version = "0.5.0-rc.2", path = "server/packages/agent-credentials" } sandbox-agent-agent-credentials = { version = "0.4.2", path = "server/packages/agent-credentials" }
sandbox-agent-opencode-adapter = { version = "0.5.0-rc.2", path = "server/packages/opencode-adapter" } sandbox-agent-opencode-adapter = { version = "0.4.2", path = "server/packages/opencode-adapter" }
sandbox-agent-opencode-server-manager = { version = "0.5.0-rc.2", path = "server/packages/opencode-server-manager" } sandbox-agent-opencode-server-manager = { version = "0.4.2", path = "server/packages/opencode-server-manager" }
acp-http-adapter = { version = "0.5.0-rc.2", path = "server/packages/acp-http-adapter" } acp-http-adapter = { version = "0.4.2", path = "server/packages/acp-http-adapter" }
# Serialization # Serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -80,11 +80,11 @@ Import the SDK directly into your Node or browser application. Full type safety
**Install** **Install**
```bash ```bash
npm install sandbox-agent@0.5.x npm install sandbox-agent@0.4.x
``` ```
```bash ```bash
bun add sandbox-agent@0.5.x bun add sandbox-agent@0.4.x
# Optional: allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()). # Optional: allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()).
bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
``` ```
@ -135,7 +135,7 @@ Run as an HTTP server and connect from any language. Deploy to E2B, Daytona, Ver
```bash ```bash
# Install it # Install it
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
# Run it # Run it
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
``` ```
@ -159,12 +159,12 @@ sandbox-agent server --no-token --host 127.0.0.1 --port 2468
Install the CLI wrapper (optional but convenient): Install the CLI wrapper (optional but convenient):
```bash ```bash
npm install -g @sandbox-agent/cli@0.5.x npm install -g @sandbox-agent/cli@0.4.x
``` ```
```bash ```bash
# Allow Bun to run postinstall scripts for native binaries. # Allow Bun to run postinstall scripts for native binaries.
bun add -g @sandbox-agent/cli@0.5.x bun add -g @sandbox-agent/cli@0.4.x
bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
``` ```
@ -179,11 +179,11 @@ sandbox-agent api sessions send-message-stream my-session --message "Hello" --en
You can also use npx like: You can also use npx like:
```bash ```bash
npx @sandbox-agent/cli@0.5.x --help npx @sandbox-agent/cli@0.4.x --help
``` ```
```bash ```bash
bunx @sandbox-agent/cli@0.5.x --help bunx @sandbox-agent/cli@0.4.x --help
``` ```
[CLI documentation](https://sandboxagent.dev/docs/cli) [CLI documentation](https://sandboxagent.dev/docs/cli)

View file

@ -51,6 +51,108 @@ await session.prompt([
unsubscribe(); unsubscribe();
``` ```
### Event types
Each event's `payload` contains a session update. The `sessionUpdate` field identifies the type.
<AccordionGroup>
<Accordion title="agent_message_chunk">
Streamed text or content from the agent's response.
```json
{
"sessionUpdate": "agent_message_chunk",
"content": { "type": "text", "text": "Here's how the repository is structured..." }
}
```
</Accordion>
<Accordion title="agent_thought_chunk">
Internal reasoning from the agent (chain-of-thought / extended thinking).
```json
{
"sessionUpdate": "agent_thought_chunk",
"content": { "type": "text", "text": "I should start by looking at the project structure..." }
}
```
</Accordion>
<Accordion title="user_message_chunk">
Echo of the user's prompt being processed.
```json
{
"sessionUpdate": "user_message_chunk",
"content": { "type": "text", "text": "Summarize the repository structure." }
}
```
</Accordion>
<Accordion title="tool_call">
The agent invoked a tool (file edit, terminal command, etc.).
```json
{
"sessionUpdate": "tool_call",
"toolCallId": "tc_abc123",
"title": "Read file",
"status": "in_progress",
"rawInput": { "path": "/src/index.ts" }
}
```
</Accordion>
<Accordion title="tool_call_update">
Progress or result update for an in-progress tool call.
```json
{
"sessionUpdate": "tool_call_update",
"toolCallId": "tc_abc123",
"status": "completed",
"content": [{ "type": "text", "text": "import express from 'express';\n..." }]
}
```
</Accordion>
<Accordion title="plan">
The agent's execution plan for the current task.
```json
{
"sessionUpdate": "plan",
"entries": [
{ "content": "Read the project structure", "status": "completed" },
{ "content": "Identify main entrypoints", "status": "in_progress" },
{ "content": "Write summary", "status": "pending" }
]
}
```
</Accordion>
<Accordion title="usage_update">
Token usage metrics for the current turn.
```json
{
"sessionUpdate": "usage_update"
}
```
</Accordion>
<Accordion title="session_info_update">
Session metadata changed (e.g. agent-generated title).
```json
{
"sessionUpdate": "session_info_update",
"title": "Repository structure analysis"
}
```
</Accordion>
</AccordionGroup>
## Fetch persisted event history ## Fetch persisted event history
```ts ```ts

View file

@ -56,7 +56,7 @@ Agents are installed lazily on first use. To avoid the cold-start delay, pre-ins
sandbox-agent install-agent --all sandbox-agent install-agent --all
``` ```
The `rivetdev/sandbox-agent:0.5.0-rc.2-full` Docker image ships with all agents pre-installed. The `rivetdev/sandbox-agent:0.4.2-full` Docker image ships with all agents pre-installed.
## Production-ready agent orchestration ## Production-ready agent orchestration

View file

@ -20,7 +20,7 @@ that BoxLite can load directly (BoxLite has its own image store separate from Do
```dockerfile ```dockerfile
FROM node:22-bookworm-slim FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
RUN sandbox-agent install-agent claude RUN sandbox-agent install-agent claude
RUN sandbox-agent install-agent codex RUN sandbox-agent install-agent codex
``` ```

View file

@ -25,7 +25,7 @@ cd my-sandbox
```dockerfile ```dockerfile
FROM cloudflare/sandbox:0.7.0 FROM cloudflare/sandbox:0.7.0
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex
EXPOSE 8000 EXPOSE 8000
@ -36,7 +36,7 @@ EXPOSE 8000
For standalone scripts, use the `cloudflare` provider: For standalone scripts, use the `cloudflare` provider:
```bash ```bash
npm install sandbox-agent@0.5.x @cloudflare/sandbox npm install sandbox-agent@0.4.x @cloudflare/sandbox
``` ```
```typescript ```typescript

View file

@ -14,7 +14,7 @@ description: "Deploy Sandbox Agent using ComputeSDK's provider-agnostic sandbox
## TypeScript example ## TypeScript example
```bash ```bash
npm install sandbox-agent@0.5.x computesdk npm install sandbox-agent@0.4.x computesdk
``` ```
```typescript ```typescript

View file

@ -16,7 +16,7 @@ See [Daytona network limits](https://www.daytona.io/docs/en/network-limits/).
## TypeScript example ## TypeScript example
```bash ```bash
npm install sandbox-agent@0.5.x @daytonaio/sdk npm install sandbox-agent@0.4.x @daytonaio/sdk
``` ```
```typescript ```typescript
@ -44,7 +44,7 @@ try {
} }
``` ```
The `daytona` provider uses the `rivetdev/sandbox-agent:0.5.0-rc.2-full` image by default and starts the server automatically. The `daytona` provider uses the `rivetdev/sandbox-agent:0.4.2-full` image by default and starts the server automatically.
## Using snapshots for faster startup ## Using snapshots for faster startup
@ -61,7 +61,7 @@ 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/0.5.x/install.sh | sh", "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh",
"sandbox-agent install-agent claude", "sandbox-agent install-agent claude",
"sandbox-agent install-agent codex", "sandbox-agent install-agent codex",
), ),

View file

@ -15,11 +15,11 @@ Run the published full image with all supported agents pre-installed:
docker run --rm -p 3000:3000 \ docker run --rm -p 3000:3000 \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-e OPENAI_API_KEY="$OPENAI_API_KEY" \ -e OPENAI_API_KEY="$OPENAI_API_KEY" \
rivetdev/sandbox-agent:0.5.0-rc.2-full \ rivetdev/sandbox-agent:0.4.2-full \
server --no-token --host 0.0.0.0 --port 3000 server --no-token --host 0.0.0.0 --port 3000
``` ```
The `0.5.0-rc.2-full` tag pins the exact version. The moving `full` tag is also published for contributors who want the latest full image. The `0.4.2-full` tag pins the exact version. The moving `full` tag is also published for contributors who want the latest full image.
If you also want the desktop API inside the container, install desktop dependencies before starting the server: If you also want the desktop API inside the container, install desktop dependencies before starting the server:
@ -31,7 +31,7 @@ docker run --rm -p 3000:3000 \
apt-get update && \ apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6 && \ DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6 && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh && \ curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && \
sandbox-agent install desktop --yes && \ sandbox-agent install desktop --yes && \
sandbox-agent server --no-token --host 0.0.0.0 --port 3000" sandbox-agent server --no-token --host 0.0.0.0 --port 3000"
``` ```
@ -52,7 +52,7 @@ const docker = new Docker();
const PORT = 3000; const PORT = 3000;
const container = await docker.createContainer({ const container = await docker.createContainer({
Image: "rivetdev/sandbox-agent:0.5.0-rc.2-full", Image: "rivetdev/sandbox-agent:0.4.2-full",
Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`], Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`],
Env: [ Env: [
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`, `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
@ -86,7 +86,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
bash ca-certificates curl git && \ bash ca-certificates curl git && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh && \ RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && \
sandbox-agent install-agent --all sandbox-agent install-agent --all
RUN useradd -m -s /bin/bash sandbox RUN useradd -m -s /bin/bash sandbox

View file

@ -11,7 +11,7 @@ description: "Deploy Sandbox Agent inside an E2B sandbox."
## TypeScript example ## TypeScript example
```bash ```bash
npm install sandbox-agent@0.5.x @e2b/code-interpreter npm install sandbox-agent@0.4.x @e2b/code-interpreter
``` ```
```typescript ```typescript

View file

@ -1,155 +0,0 @@
---
title: "Foundry Self-Hosting"
description: "Environment, credentials, and deployment setup for Sandbox Agent Foundry auth, GitHub, and billing."
---
This guide documents the deployment contract for the Foundry product surface: app auth, GitHub onboarding, repository import, and billing.
It also covers the local-development bootstrap that uses `.env.development` only when `NODE_ENV=development`.
## Local Development
For backend local development, the Foundry backend now supports a development-only dotenv bootstrap:
- It loads `.env.development.local` and `.env.development`
- It does this **only** when `NODE_ENV=development`
- It does **not** load dotenv files in production
The example file lives at [`/.env.development.example`](https://github.com/rivet-dev/sandbox-agent/blob/main/.env.development.example).
To use it locally:
```bash
cp .env.development.example .env.development
```
Run the backend with:
```bash
just foundry-backend-start
```
That recipe sets `NODE_ENV=development`, which enables the dotenv loader.
### Local Defaults
These values can be safely defaulted for local development:
- `APP_URL=http://localhost:4173`
- `BETTER_AUTH_URL=http://localhost:7741`
- `BETTER_AUTH_SECRET=sandbox-agent-foundry-development-only-change-me`
- `GITHUB_REDIRECT_URI=http://localhost:7741/v1/auth/callback/github`
These should be treated as development-only values.
## Production Environment
For production or self-hosting, set these as real environment variables in your deployment platform. Do not rely on dotenv file loading.
### App/Auth
| Variable | Required | Notes |
|---|---:|---|
| `APP_URL` | Yes | Public frontend origin |
| `BETTER_AUTH_URL` | Yes | Public auth base URL |
| `BETTER_AUTH_SECRET` | Yes | Strong random secret for auth/session signing |
### GitHub OAuth
| Variable | Required | Notes |
|---|---:|---|
| `GITHUB_CLIENT_ID` | Yes | GitHub OAuth app client id |
| `GITHUB_CLIENT_SECRET` | Yes | GitHub OAuth app client secret |
| `GITHUB_REDIRECT_URI` | Yes | GitHub OAuth callback URL |
Use GitHub OAuth for:
- user sign-in
- user identity
- org selection
- access to the signed-in users GitHub context
## GitHub App
If your Foundry deployment uses GitHub App-backed organization install and repo import, also configure:
| Variable | Required | Notes |
|---|---:|---|
| `GITHUB_APP_ID` | Yes | GitHub App id |
| `GITHUB_APP_CLIENT_ID` | Yes | GitHub App client id |
| `GITHUB_APP_CLIENT_SECRET` | Yes | GitHub App client secret |
| `GITHUB_APP_PRIVATE_KEY` | Yes | PEM private key for installation auth |
For `.env.development` and `.env.development.local`, store `GITHUB_APP_PRIVATE_KEY` as a quoted single-line value with `\n` escapes instead of raw multi-line PEM text.
Recommended GitHub App permissions:
- Repository `Metadata: Read`
- Repository `Contents: Read & Write`
- Repository `Pull requests: Read & Write`
- Repository `Checks: Read`
- Repository `Commit statuses: Read`
Set the webhook URL to `https://<your-backend-host>/v1/webhooks/github` and generate a webhook secret. Store the secret as `GITHUB_WEBHOOK_SECRET`.
This is required, not optional. Foundry depends on GitHub App webhook delivery for installation lifecycle changes, repo access changes, and ongoing repo / pull request sync. If the GitHub App is not installed for the workspace, or webhook delivery is misconfigured, Foundry will remain in an install / reconnect state and core GitHub-backed functionality will not work correctly.
Recommended webhook subscriptions:
- `installation`
- `installation_repositories`
- `pull_request`
- `pull_request_review`
- `pull_request_review_comment`
- `push`
- `create`
- `delete`
- `check_suite`
- `check_run`
- `status`
Use the GitHub App for:
- installation/reconnect state
- org repo import
- repository sync
- PR creation and updates
Use GitHub OAuth for:
- who the user is
- which orgs they can choose
## Stripe
For live billing, configure:
| Variable | Required | Notes |
|---|---:|---|
| `STRIPE_SECRET_KEY` | Yes | Server-side Stripe secret key |
| `STRIPE_PUBLISHABLE_KEY` | Yes | Client-side Stripe publishable key |
| `STRIPE_WEBHOOK_SECRET` | Yes | Signing secret for billing webhooks |
| `STRIPE_PRICE_TEAM` | Yes | Stripe price id for the Team plan checkout session |
Stripe should own:
- hosted checkout
- billing portal
- subscription status
- invoice history
- webhook-driven state sync
## Mock Invariant
Foundrys mock client path should continue to work end to end even when the real auth/GitHub/Stripe path exists.
That includes:
- sign-in
- org selection/import
- settings
- billing UI
- workspace/task/session flow
- seat accrual
Use mock mode for deterministic UI review and local product development. Use the real env-backed path for integration and self-hosting.

View file

@ -9,7 +9,7 @@ For local development, run Sandbox Agent directly on your machine.
```bash ```bash
# Install # Install
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.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
@ -20,12 +20,12 @@ Or with npm/Bun:
<Tabs> <Tabs>
<Tab title="npx"> <Tab title="npx">
```bash ```bash
npx @sandbox-agent/cli@0.5.x server --no-token --host 127.0.0.1 --port 2468 npx @sandbox-agent/cli@0.4.x server --no-token --host 127.0.0.1 --port 2468
``` ```
</Tab> </Tab>
<Tab title="bunx"> <Tab title="bunx">
```bash ```bash
bunx @sandbox-agent/cli@0.5.x server --no-token --host 127.0.0.1 --port 2468 bunx @sandbox-agent/cli@0.4.x server --no-token --host 127.0.0.1 --port 2468
``` ```
</Tab> </Tab>
</Tabs> </Tabs>

View file

@ -11,7 +11,7 @@ description: "Deploy Sandbox Agent inside a Modal sandbox."
## TypeScript example ## TypeScript example
```bash ```bash
npm install sandbox-agent@0.5.x modal npm install sandbox-agent@0.4.x modal
``` ```
```typescript ```typescript

View file

@ -11,7 +11,7 @@ description: "Deploy Sandbox Agent inside a Vercel Sandbox."
## TypeScript example ## TypeScript example
```bash ```bash
npm install sandbox-agent@0.5.x @vercel/sandbox npm install sandbox-agent@0.4.x @vercel/sandbox
``` ```
```typescript ```typescript

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://mintlify.com/docs.json", "$schema": "https://mintlify.com/docs.json",
"theme": "willow", "theme": "mint",
"name": "Sandbox Agent SDK", "name": "Sandbox Agent SDK",
"appearance": { "appearance": {
"default": "dark", "default": "dark",
@ -8,8 +8,8 @@
}, },
"colors": { "colors": {
"primary": "#ff4f00", "primary": "#ff4f00",
"light": "#ff4f00", "light": "#ff6a2a",
"dark": "#ff4f00" "dark": "#cc3f00"
}, },
"favicon": "/favicon.svg", "favicon": "/favicon.svg",
"logo": { "logo": {
@ -25,17 +25,13 @@
}, },
"navbar": { "navbar": {
"links": [ "links": [
{
"label": "Gigacode",
"icon": "terminal",
"href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode"
},
{ {
"label": "Discord", "label": "Discord",
"icon": "discord", "icon": "discord",
"href": "https://discord.gg/auCecybynK" "href": "https://discord.gg/auCecybynK"
}, },
{ {
"label": "GitHub",
"type": "github", "type": "github",
"href": "https://github.com/rivet-dev/sandbox-agent" "href": "https://github.com/rivet-dev/sandbox-agent"
} }
@ -89,13 +85,10 @@
"group": "System", "group": "System",
"pages": ["file-system", "processes", "computer-use", "common-software"] "pages": ["file-system", "processes", "computer-use", "common-software"]
}, },
{
"group": "Orchestration",
"pages": ["orchestration-architecture", "session-persistence", "observability", "multiplayer", "security"]
},
{ {
"group": "Reference", "group": "Reference",
"pages": [ "pages": [
"troubleshooting",
"architecture", "architecture",
"cli", "cli",
"inspector", "inspector",
@ -127,5 +120,11 @@
] ]
} }
] ]
} },
"__removed": [
{
"group": "Orchestration",
"pages": ["orchestration-architecture", "session-persistence", "observability", "multiplayer", "security"]
}
]
} }

View file

@ -1,6 +0,0 @@
---
title: Gigacode
url: "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode"
---

View file

@ -10,7 +10,7 @@
"license": { "license": {
"name": "Apache-2.0" "name": "Apache-2.0"
}, },
"version": "0.5.0-rc.2" "version": "0.4.2"
}, },
"servers": [ "servers": [
{ {

View file

@ -1,210 +0,0 @@
# Pi Agent Support Plan (pi-mono)
## Implementation Status Update
- Runtime selection now supports two internal modes:
- `PerSession` (default for unknown/non-allowlisted Pi capabilities)
- `Shared` (allowlist-only compatibility path)
- Pi sessions now use per-session process isolation by default, enabling true concurrent Pi sessions in Inspector and API clients.
- Shared Pi server code remains available and is used only when capability checks allow multiplexing.
- Session termination for per-session Pi mode hard-kills the underlying Pi process and clears queued prompts/pending waiters.
- In-session concurrent sends are serialized with an unbounded daemon-side FIFO queue per session.
## Investigation Summary
### Pi CLI modes and RPC protocol
- Pi supports multiple modes including interactive, print/JSON output, RPC, and SDK usage. JSON mode outputs a stream of JSON events suitable for parsing, and RPC mode is intended for programmatic control over stdin/stdout.
- RPC mode is started with `pi --mode rpc` and supports options like `--provider`, `--model`, `--no-session`, and `--session-dir`.
- The RPC protocol is newline-delimited JSON over stdin/stdout:
- Commands are JSON objects written to stdin.
- Responses are JSON objects with `type: "response"` and optional `id`.
- Events are JSON objects without `id`.
- `prompt` can include images using `ImageContent` (base64 or URL) alongside text.
- JSON/print mode (`pi -p` or `pi --print --mode json`) produces JSONL for non-interactive parsing and can resume sessions with a token.
### RPC commands
RPC commands listed in `rpc.md` include:
- `new_session`, `get_state`, `list_sessions`, `delete_session`, `rename_session`, `clear_session`
- `prompt`, `queue_message`, `abort`, `get_queued_messages`
### RPC event types
RPC events listed in `rpc.md` include:
- `agent_start`, `agent_end`
- `turn_start`, `turn_end`
- `message_start`, `message_update`, `message_end`
- `tool_execution_start`, `tool_execution_update`, `tool_execution_end`
- `auto_compaction`, `auto_retry`, `hook_error`
`message_update` uses `assistantMessageEvent` deltas such as:
- `start`, `text_start`, `text_delta`, `text_end`
- `thinking_start`, `thinking_delta`, `thinking_end`
- `toolcall_start`, `toolcall_delta`, `toolcall_end`
- `toolcall_args_start`, `toolcall_args_delta`, `toolcall_args_end`
- `done`, `error`
`tool_execution_update` includes `partialResult`, which is described as accumulated output so far.
### Schema source locations (pi-mono)
RPC types are documented as living in:
- `packages/ai/src/types.ts` (Model types)
- `packages/agent/src/types.ts` (AgentResponse types)
- `packages/coding-agent/src/core/messages.ts` (message types)
- `packages/coding-agent/src/modes/rpc/rpc-types.ts` (RPC protocol types)
### Distribution assets
Pi releases provide platform-specific binaries such as:
- `pi-darwin-arm64`, `pi-darwin-x64`
- `pi-linux-arm64`, `pi-linux-x64`
- `pi-win-x64.zip`
## Integration Decisions
- Follow the OpenCode pattern: a shared long-running process (stdio RPC) with session multiplexing.
- Primary integration path is RPC streaming (`pi --mode rpc`).
- JSON/print mode is a fallback only (diagnostics or non-interactive runs).
- Create sessions via `new_session`; store the returned `sessionId` as `native_session_id`.
- Use `get_state` as a re-sync path after server restarts.
- Use `prompt` for send-message, with optional image content.
- Convert Pi events into universal events; emit daemon synthetic `session.started` on session creation and `session.ended` only on errors/termination.
## Implementation Plan
### 1) Agent Identity + Capabilities
Files:
- `server/packages/agent-management/src/agents.rs`
- `server/packages/sandbox-agent/src/router.rs`
- `docs/cli.mdx`, `docs/conversion.mdx`, `docs/session-transcript-schema.mdx`
- `README.md`, `frontend/packages/website/src/components/FAQ.tsx`
Tasks:
- Add `AgentId::Pi` with string/binary name `"pi"` and parsing rules.
- Add Pi to `all_agents()` and agent lists.
- Define `AgentCapabilities` for Pi:
- `tool_calls=true`, `tool_results=true`
- `text_messages=true`, `streaming_deltas=true`, `item_started=true`
- `reasoning=true` (from `thinking_*` deltas)
- `images=true` (ImageContent in `prompt`)
- `permissions=false`, `questions=false`, `mcp_tools=false`
- `shared_process=true`, `session_lifecycle=false` (no native session events)
- `error_events=true` (hook_error)
- `command_execution=false`, `file_changes=false`, `file_attachments=false`
### 2) Installer and Binary Resolution
Files:
- `server/packages/agent-management/src/agents.rs`
Tasks:
- Add `install_pi()` that:
- Downloads the correct release asset per platform (`pi-<platform>`).
- Handles `.zip` on Windows and raw binaries elsewhere.
- Marks binary executable.
- Add Pi to `AgentManager::install`, `is_installed`, `version`.
- Version detection: try `--version`, `version`, `-V`.
### 3) Schema Extraction for Pi
Files:
- `resources/agent-schemas/src/pi.ts` (new)
- `resources/agent-schemas/src/index.ts`
- `resources/agent-schemas/artifacts/json-schema/pi.json`
- `server/packages/extracted-agent-schemas/build.rs`
- `server/packages/extracted-agent-schemas/src/lib.rs`
Tasks:
- Implement `extractPiSchema()`:
- Download pi-mono sources (zip/tarball) into a temp dir.
- Use `ts-json-schema-generator` against `packages/coding-agent/src/modes/rpc/rpc-types.ts`.
- Include dependent files per `rpc.md` (ai/types, agent/types, core/messages).
- Extract `RpcEvent`, `RpcResponse`, `RpcCommand` unions (exact type names from source).
- Add fallback schema if remote fetch fails (minimal union with event/response fields).
- Wire pi into extractor index and artifact generation.
### 4) Universal Schema Conversion (Pi -> Universal)
Files:
- `server/packages/universal-agent-schema/src/agents/pi.rs` (new)
- `server/packages/universal-agent-schema/src/agents/mod.rs`
- `server/packages/universal-agent-schema/src/lib.rs`
- `server/packages/sandbox-agent/src/router.rs`
Mapping rules:
- `message_start` -> `item.started` (kind=message, role=assistant, native_item_id=messageId)
- `message_update`:
- `text_*` -> `item.delta` (assistant text delta)
- `thinking_*` -> `item.delta` with `ContentPart::Reasoning` (visibility=Private)
- `toolcall_*` and `toolcall_args_*` -> ignore for now (tool_execution_* is authoritative)
- `error` -> `item.completed` with `ItemStatus::Failed` (if no later message_end)
- `message_end` -> `item.completed` (finalize assistant message)
- `tool_execution_start` -> `item.started` (kind=tool_call, ContentPart::ToolCall)
- `tool_execution_update` -> `item.delta` for a synthetic tool_result item:
- Maintain a per-toolCallId buffer to compute delta from accumulated `partialResult`.
- `tool_execution_end` -> `item.completed` (kind=tool_result, output from `result.content`)
- If `isError=true`, set item status to failed.
- `agent_start`, `turn_start`, `turn_end`, `agent_end`, `auto_compaction`, `auto_retry`, `hook_error`:
- Map to `ItemKind::Status` with a label like `pi.agent_start`, `pi.auto_retry`, etc.
- Do not emit `session.ended` for these events.
- If event parsing fails, emit `agent.unparsed` (source=daemon, synthetic=true) and fail tests.
### 5) Shared RPC Server Integration
Files:
- `server/packages/sandbox-agent/src/router.rs`
Tasks:
- Add a new managed stdio server type for Pi, similar to Codex:
- Create `PiServer` struct with:
- stdin sender
- pending request map keyed by request id
- per-session native session id mapping
- Extend `ManagedServerKind` to include Pi.
- Add `ensure_pi_server()` and `spawn_pi_server()` using `pi --mode rpc`.
- Add a `handle_pi_server_output()` loop to parse stdout lines into events/responses.
- Session creation:
- On `create_session`, ensure Pi server is running, send `new_session`, store sessionId.
- Register session with `server_manager.register_session` for native mapping.
- Sending messages:
- Use `prompt` command; include sessionId and optional images.
- Emit synthetic `item.started` only if Pi does not emit `message_start`.
### 6) Router + Streaming Path Changes
Files:
- `server/packages/sandbox-agent/src/router.rs`
Tasks:
- Add Pi handling to:
- `create_session` (new_session)
- `send_message` (prompt)
- `parse_agent_line` (Pi event conversion)
- `agent_modes` (default to `default` unless Pi exposes a mode list)
- `agent_supports_resume` (true if Pi supports session resume)
### 7) Tests
Files:
- `server/packages/sandbox-agent/tests/...`
- `server/packages/universal-agent-schema/tests/...` (if present)
Tasks:
- Unit tests for conversion:
- `message_start/update/end` -> item.started/delta/completed
- `tool_execution_*` -> tool call/result mapping with partialResult delta
- failure -> agent.unparsed
- Integration tests:
- Start Pi RPC server, create session, send prompt, stream events.
- Validate `native_session_id` mapping and event ordering.
- Update HTTP/SSE test coverage to include Pi agent if relevant.
## Risk Areas / Edge Cases
- `tool_execution_update.partialResult` is cumulative; must compute deltas.
- `message_update` may emit `done`/`error` without `message_end`; handle both paths.
- No native session lifecycle events; rely on daemon synthetic events.
- Session recovery after RPC server restart requires `get_state` + re-register sessions.
## Acceptance Criteria
- Pi appears in `/v1/agents`, CLI list, and docs.
- `create_session` returns `native_session_id` from Pi `new_session`.
- Streaming prompt yields universal events with proper ordering:
- message -> item.started/delta/completed
- tool execution -> tool call + tool result
- Tests pass and no synthetic data is used in test fixtures.
## Sources
- https://upd.dev/badlogic/pi-mono/src/commit/d36e0ea07303d8a76d51b4a7bd5f0d6d3c490860/packages/coding-agent/docs/rpc.md
- https://buildwithpi.ai/pi-cli
- https://takopi.dev/docs/pi-cli/
- https://upd.dev/badlogic/pi-mono/releases

View file

@ -64,7 +64,7 @@ icon: "rocket"
docker run -p 2468:2468 \ docker run -p 2468:2468 \
-e ANTHROPIC_API_KEY="sk-ant-..." \ -e ANTHROPIC_API_KEY="sk-ant-..." \
-e OPENAI_API_KEY="sk-..." \ -e OPENAI_API_KEY="sk-..." \
rivetdev/sandbox-agent:0.5.0-rc.2-full \ rivetdev/sandbox-agent:0.4.2-full \
server --no-token --host 0.0.0.0 --port 2468 server --no-token --host 0.0.0.0 --port 2468
``` ```
</Tab> </Tab>
@ -89,7 +89,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/0.5.x/install.sh | sh curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
sandbox-agent server --no-token --host 0.0.0.0 --port 2468 sandbox-agent server --no-token --host 0.0.0.0 --port 2468
``` ```
</Tab> </Tab>
@ -98,7 +98,7 @@ icon: "rocket"
Run without installing globally. Run without installing globally.
```bash ```bash
npx @sandbox-agent/cli@0.5.x server --no-token --host 0.0.0.0 --port 2468 npx @sandbox-agent/cli@0.4.x server --no-token --host 0.0.0.0 --port 2468
``` ```
</Tab> </Tab>
@ -106,7 +106,7 @@ icon: "rocket"
Run without installing globally. Run without installing globally.
```bash ```bash
bunx @sandbox-agent/cli@0.5.x server --no-token --host 0.0.0.0 --port 2468 bunx @sandbox-agent/cli@0.4.x server --no-token --host 0.0.0.0 --port 2468
``` ```
</Tab> </Tab>
@ -114,7 +114,7 @@ icon: "rocket"
Install globally, then run. Install globally, then run.
```bash ```bash
npm install -g @sandbox-agent/cli@0.5.x npm install -g @sandbox-agent/cli@0.4.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>
@ -123,7 +123,7 @@ icon: "rocket"
Install globally, then run. Install globally, then run.
```bash ```bash
bun add -g @sandbox-agent/cli@0.5.x bun add -g @sandbox-agent/cli@0.4.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-linux-arm64 @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-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
sandbox-agent server --no-token --host 0.0.0.0 --port 2468 sandbox-agent server --no-token --host 0.0.0.0 --port 2468
@ -134,7 +134,7 @@ icon: "rocket"
For local development, use `SandboxAgent.start()` to 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@0.5.x npm install sandbox-agent@0.4.x
``` ```
```typescript ```typescript
@ -148,7 +148,7 @@ icon: "rocket"
For local development, use `SandboxAgent.start()` to 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@0.5.x bun add sandbox-agent@0.4.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-linux-arm64 @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-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
``` ```

View file

@ -17,7 +17,7 @@ Current exports:
## Install ## Install
```bash ```bash
npm install @sandbox-agent/react@0.5.x npm install @sandbox-agent/react@0.4.x
``` ```
## Full example ## Full example

View file

@ -11,12 +11,12 @@ The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgent` class.
<Tabs> <Tabs>
<Tab title="npm"> <Tab title="npm">
```bash ```bash
npm install sandbox-agent@0.5.x npm install sandbox-agent@0.4.x
``` ```
</Tab> </Tab>
<Tab title="bun"> <Tab title="bun">
```bash ```bash
bun add sandbox-agent@0.5.x bun add sandbox-agent@0.4.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-linux-arm64 @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-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64
``` ```
@ -26,7 +26,7 @@ The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgent` class.
## Optional React components ## Optional React components
```bash ```bash
npm install @sandbox-agent/react@0.5.x npm install @sandbox-agent/react@0.4.x
``` ```
## Create a client ## Create a client

View file

@ -1,388 +0,0 @@
---
title: "Session Transcript Schema"
description: "Universal event schema for session transcripts across all agents."
---
Each coding agent outputs events in its own native format. The sandbox-agent converts these into a universal event schema, giving you a consistent session transcript regardless of which agent you use.
The schema is defined in [OpenAPI format](https://github.com/rivet-dev/sandbox-agent/blob/main/docs/openapi.json). See the [HTTP API Reference](/api-reference) for endpoint documentation.
## Coverage Matrix
This table shows which agent feature coverage appears in the universal event stream. All agents retain their full native feature coverage—this only reflects what's normalized into the schema.
| Feature | Claude | Codex | OpenCode | Amp | Pi (RPC) |
|--------------------|:------:|:-----:|:------------:|:------------:|:------------:|
| Stability | Stable | Stable| Experimental | Experimental | Experimental |
| Text Messages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Tool Calls | ✓ | ✓ | ✓ | ✓ | ✓ |
| Tool Results | ✓ | ✓ | ✓ | ✓ | ✓ |
| Questions (HITL) | ✓ | | ✓ | | |
| Permissions (HITL) | ✓ | ✓ | ✓ | - | |
| Images | - | ✓ | ✓ | - | ✓ |
| File Attachments | - | ✓ | ✓ | - | |
| Session Lifecycle | - | ✓ | ✓ | - | |
| Error Events | - | ✓ | ✓ | ✓ | ✓ |
| Reasoning/Thinking | - | ✓ | - | - | ✓ |
| Command Execution | - | ✓ | - | - | |
| File Changes | - | ✓ | - | - | |
| MCP Tools | ✓ | ✓ | ✓ | ✓ | |
| Streaming Deltas | ✓ | ✓ | ✓ | - | ✓ |
| Variants | | ✓ | ✓ | ✓ | ✓ |
Agents: [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) · [Codex](https://github.com/openai/codex) · [OpenCode](https://github.com/opencode-ai/opencode) · [Amp](https://ampcode.com) · [Pi](https://buildwithpi.ai/pi-cli)
- ✓ = Appears in session events
- \- = Agent supports natively, schema conversion coming soon
- (blank) = Not supported by agent
- Pi runtime model is router-managed per-session RPC (`pi --mode rpc`); it does not use generic subprocess streaming.
<AccordionGroup>
<Accordion title="Text Messages">
Basic message exchange between user and assistant.
</Accordion>
<Accordion title="Tool Calls & Results">
Visibility into tool invocations (file reads, command execution, etc.) and their results. When not natively supported, tool activity is embedded in message content.
</Accordion>
<Accordion title="Questions (HITL)">
Interactive questions the agent asks the user. Emits `question.requested` and `question.resolved` events.
</Accordion>
<Accordion title="Permissions (HITL)">
Permission requests for sensitive operations. Emits `permission.requested` and `permission.resolved` events.
</Accordion>
<Accordion title="Images">
Support for image attachments in messages.
</Accordion>
<Accordion title="File Attachments">
Support for file attachments in messages.
</Accordion>
<Accordion title="Session Lifecycle">
Native `session.started` and `session.ended` events. When not supported, the daemon emits synthetic lifecycle events.
</Accordion>
<Accordion title="Error Events">
Structured error events for runtime failures.
</Accordion>
<Accordion title="Reasoning/Thinking">
Extended thinking or reasoning content with visibility controls.
</Accordion>
<Accordion title="Command Execution">
Detailed command execution events with stdout/stderr.
</Accordion>
<Accordion title="File Changes">
Structured file modification events with diffs.
</Accordion>
<Accordion title="MCP Tools">
Model Context Protocol tool support.
</Accordion>
<Accordion title="Streaming Deltas">
Native streaming of content deltas. When not supported, the daemon emits a single synthetic delta before `item.completed`.
</Accordion>
<Accordion title="Variants">
Model variants such as reasoning effort or depth. Agents may expose different variant sets per model.
</Accordion>
</AccordionGroup>
Want support for another agent? [Open an issue](https://github.com/rivet-dev/sandbox-agent/issues/new) to request it.
## UniversalEvent
Every event from the API is wrapped in a `UniversalEvent` envelope.
| Field | Type | Description |
|-------|------|-------------|
| `event_id` | string | Unique identifier for this event |
| `sequence` | integer | Monotonic sequence number within the session (starts at 1) |
| `time` | string | RFC3339 timestamp |
| `session_id` | string | Daemon-generated session identifier |
| `native_session_id` | string? | Provider-native session/thread identifier (e.g., Codex `threadId`, OpenCode `sessionID`) |
| `source` | string | Event origin: `agent` (native) or `daemon` (synthetic) |
| `synthetic` | boolean | Whether this event was generated by the daemon to fill gaps |
| `type` | string | Event type (see [Event Types](#event-types)) |
| `data` | object | Event-specific payload |
| `raw` | any? | Original provider payload (only when `include_raw=true`) |
```json
{
"event_id": "evt_abc123",
"sequence": 1,
"time": "2025-01-28T12:00:00Z",
"session_id": "my-session",
"native_session_id": "thread_xyz",
"source": "agent",
"synthetic": false,
"type": "item.completed",
"data": { ... }
}
```
## Event Types
### Session Lifecycle
| Type | Description | Data |
|------|-------------|------|
| `session.started` | Session has started | `{ metadata?: any }` |
| `session.ended` | Session has ended | `{ reason, terminated_by, message?, exit_code? }` |
### Turn Lifecycle
| Type | Description | Data |
|------|-------------|------|
| `turn.started` | Turn has started | `{ phase: "started", turn_id?, metadata? }` |
| `turn.ended` | Turn has ended | `{ phase: "ended", turn_id?, metadata? }` |
**SessionEndedData**
| Field | Type | Values |
|-------|------|--------|
| `reason` | string | `completed`, `error`, `terminated` |
| `terminated_by` | string | `agent`, `daemon` |
| `message` | string? | Error message (only present when reason is `error`) |
| `exit_code` | int? | Process exit code (only present when reason is `error`) |
| `stderr` | StderrOutput? | Structured stderr output (only present when reason is `error`) |
**StderrOutput**
| Field | Type | Description |
|-------|------|-------------|
| `head` | string? | First 20 lines of stderr (if truncated) or full stderr (if not truncated) |
| `tail` | string? | Last 50 lines of stderr (only present if truncated) |
| `truncated` | boolean | Whether the output was truncated |
| `total_lines` | int? | Total number of lines in stderr |
### Item Lifecycle
| Type | Description | Data |
|------|-------------|------|
| `item.started` | Item creation | `{ item }` |
| `item.delta` | Streaming content delta | `{ item_id, native_item_id?, delta }` |
| `item.completed` | Item finalized | `{ item }` |
Items follow a consistent lifecycle: `item.started` → `item.delta` (0 or more) → `item.completed`.
### HITL (Human-in-the-Loop)
| Type | Description | Data |
|------|-------------|------|
| `permission.requested` | Permission request pending | `{ permission_id, action, status, metadata? }` |
| `permission.resolved` | Permission decision recorded | `{ permission_id, action, status, metadata? }` |
| `question.requested` | Question pending user input | `{ question_id, prompt, options, status }` |
| `question.resolved` | Question answered or rejected | `{ question_id, prompt, options, status, response? }` |
**PermissionEventData**
| Field | Type | Description |
|-------|------|-------------|
| `permission_id` | string | Identifier for the permission request |
| `action` | string | What the agent wants to do |
| `status` | string | `requested`, `accept`, `accept_for_session`, `reject` |
| `metadata` | any? | Additional context |
**QuestionEventData**
| Field | Type | Description |
|-------|------|-------------|
| `question_id` | string | Identifier for the question |
| `prompt` | string | Question text |
| `options` | string[] | Available answer options |
| `status` | string | `requested`, `answered`, `rejected` |
| `response` | string? | Selected answer (when resolved) |
### Errors
| Type | Description | Data |
|------|-------------|------|
| `error` | Runtime error | `{ message, code?, details? }` |
| `agent.unparsed` | Parse failure | `{ error, location, raw_hash? }` |
The `agent.unparsed` event indicates the daemon failed to parse an agent payload. This should be treated as a bug.
## UniversalItem
Items represent discrete units of content within a session.
| Field | Type | Description |
|-------|------|-------------|
| `item_id` | string | Daemon-generated identifier |
| `native_item_id` | string? | Provider-native item/message identifier |
| `parent_id` | string? | Parent item ID (e.g., tool call/result parented to a message) |
| `kind` | string | Item category (see below) |
| `role` | string? | Actor role for message items |
| `status` | string | Lifecycle status |
| `content` | ContentPart[] | Ordered list of content parts |
### ItemKind
| Value | Description |
|-------|-------------|
| `message` | User or assistant message |
| `tool_call` | Tool invocation |
| `tool_result` | Tool execution result |
| `system` | System message |
| `status` | Status update |
| `unknown` | Unrecognized item type |
### ItemRole
| Value | Description |
|-------|-------------|
| `user` | User message |
| `assistant` | Assistant response |
| `system` | System prompt |
| `tool` | Tool-related message |
### ItemStatus
| Value | Description |
|-------|-------------|
| `in_progress` | Item is streaming or pending |
| `completed` | Item is finalized |
| `failed` | Item execution failed |
## Content Parts
The `content` array contains typed parts that make up an item's payload.
### text
Plain text content.
```json
{ "type": "text", "text": "Hello, world!" }
```
### json
Structured JSON content.
```json
{ "type": "json", "json": { "key": "value" } }
```
### tool_call
Tool invocation.
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Tool name |
| `arguments` | string | JSON-encoded arguments |
| `call_id` | string | Unique call identifier |
```json
{
"type": "tool_call",
"name": "read_file",
"arguments": "{\"path\": \"/src/main.ts\"}",
"call_id": "call_abc123"
}
```
### tool_result
Tool execution result.
| Field | Type | Description |
|-------|------|-------------|
| `call_id` | string | Matching call identifier |
| `output` | string | Tool output |
```json
{
"type": "tool_result",
"call_id": "call_abc123",
"output": "File contents here..."
}
```
### file_ref
File reference with optional diff.
| Field | Type | Description |
|-------|------|-------------|
| `path` | string | File path |
| `action` | string | `read`, `write`, `patch` |
| `diff` | string? | Unified diff (for patches) |
```json
{
"type": "file_ref",
"path": "/src/main.ts",
"action": "write",
"diff": "@@ -1,3 +1,4 @@\n+import { foo } from 'bar';"
}
```
### image
Image reference.
| Field | Type | Description |
|-------|------|-------------|
| `path` | string | Image file path |
| `mime` | string? | MIME type |
```json
{ "type": "image", "path": "/tmp/screenshot.png", "mime": "image/png" }
```
### reasoning
Model reasoning/thinking content.
| Field | Type | Description |
|-------|------|-------------|
| `text` | string | Reasoning text |
| `visibility` | string | `public` or `private` |
```json
{ "type": "reasoning", "text": "Let me think about this...", "visibility": "public" }
```
### status
Status indicator.
| Field | Type | Description |
|-------|------|-------------|
| `label` | string | Status label |
| `detail` | string? | Additional detail |
```json
{ "type": "status", "label": "Running tests", "detail": "3 of 10 passed" }
```
## Source & Synthetics
### EventSource
The `source` field indicates who emitted the event:
| Value | Description |
|-------|-------------|
| `agent` | Native event from the agent |
| `daemon` | Synthetic event generated by the daemon |
### Synthetic Events
The daemon emits synthetic events (`synthetic: true`, `source: "daemon"`) to provide a consistent event stream across all agents. Common synthetics:
| Synthetic | When |
|-----------|------|
| `session.started` | Agent doesn't emit explicit session start |
| `session.ended` | Agent doesn't emit explicit session end |
| `turn.started` | Agent doesn't emit explicit turn start |
| `turn.ended` | Agent doesn't emit explicit turn end |
| `item.started` | Agent doesn't emit item start events |
| `item.delta` | Agent doesn't stream deltas natively |
| `question.*` | Claude Code plan mode (from ExitPlanMode tool) |
### Raw Payloads
Pass `include_raw=true` to event endpoints to receive the original agent payload in the `raw` field. Useful for debugging or accessing agent-specific data not in the universal schema.
```typescript
const events = await client.getEvents("my-session", { includeRaw: true });
// events[0].raw contains the original agent payload
```

View file

@ -20,7 +20,6 @@ body {
color: var(--sa-text); color: var(--sa-text);
} }
/*
a { a {
color: var(--sa-primary); color: var(--sa-primary);
} }
@ -41,6 +40,13 @@ select {
color: var(--sa-text); color: var(--sa-text);
} }
code,
pre {
background-color: var(--sa-card);
border: 1px solid var(--sa-border);
color: var(--sa-text);
}
.card, .card,
.mintlify-card, .mintlify-card,
.docs-card { .docs-card {
@ -64,4 +70,3 @@ select {
.alert-danger { .alert-danger {
border-color: var(--sa-danger); border-color: var(--sa-danger);
} }
*/

View file

@ -29,25 +29,6 @@ Verify the agent is installed:
ls -la ~/.local/share/sandbox-agent/bin/ ls -la ~/.local/share/sandbox-agent/bin/
``` ```
### 4. Binary libc mismatch (musl vs glibc)
Claude Code binaries are available in both musl and glibc variants. If you see errors like:
```
cannot execute: required file not found
Error loading shared library libstdc++.so.6: No such file or directory
```
This means the wrong binary variant was downloaded.
**For sandbox-agent 0.2.0+**: Platform detection is automatic. The correct binary (musl or glibc) is downloaded based on the runtime environment.
**For sandbox-agent 0.1.x**: Use Alpine Linux which has native musl support:
```dockerfile
FROM alpine:latest
RUN apk add --no-cache curl ca-certificates libstdc++ libgcc bash
```
## Daytona Network Restrictions ## Daytona Network Restrictions

View file

@ -1,5 +1,5 @@
FROM node:22-bookworm-slim FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
RUN sandbox-agent install-agent claude RUN sandbox-agent install-agent claude
RUN sandbox-agent install-agent codex RUN sandbox-agent install-agent codex

View file

@ -1,5 +1,5 @@
FROM node:22-bookworm-slim FROM node:22-bookworm-slim
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
RUN sandbox-agent install-agent claude RUN sandbox-agent install-agent claude
RUN sandbox-agent install-agent codex RUN sandbox-agent install-agent codex

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -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/0.5.x/install.sh | sh RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh
# Pre-install agents # Pre-install agents
RUN sandbox-agent install-agent claude && \ RUN sandbox-agent install-agent claude && \

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"lib": ["ES2022"], "lib": ["ES2022", "DOM"],
"types": ["node"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,

View file

@ -1,13 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true,
"types": ["node"]
}, },
"include": ["src"] "include": ["src"]
} }

View file

@ -1,13 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true,
"types": ["node"]
}, },
"include": ["src"] "include": ["src"]
} }

View file

@ -1,13 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true,
"types": ["node"]
}, },
"include": ["src"] "include": ["src"]
} }

View file

@ -9,7 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
/** Pre-built Docker image with all agents installed. */ /** Pre-built Docker image with all agents installed. */
export const FULL_IMAGE = "rivetdev/sandbox-agent:0.5.0-rc.2-full"; export const FULL_IMAGE = "rivetdev/sandbox-agent:0.4.2-full";
export interface DockerSandboxOptions { export interface DockerSandboxOptions {
/** Container port used by sandbox-agent inside Docker. */ /** Container port used by sandbox-agent inside Docker. */

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -0,0 +1,20 @@
{
"name": "@sandbox-agent/example-sprites",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fly/sprites": "latest",
"@sandbox-agent/example-shared": "workspace:*",
"sandbox-agent": "workspace:*"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest",
"vitest": "^3.0.0"
}
}

View file

@ -0,0 +1,21 @@
import { SandboxAgent } from "sandbox-agent";
import { sprites } from "sandbox-agent/sprites";
const env: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const client = await SandboxAgent.start({
sandbox: sprites({
token: process.env.SPRITES_API_KEY ?? process.env.SPRITE_TOKEN ?? process.env.SPRITES_TOKEN,
env,
}),
});
console.log(`UI: ${client.inspectorUrl}`);
console.log(await client.getHealth());
process.once("SIGINT", async () => {
await client.destroySandbox();
process.exit(0);
});

View file

@ -0,0 +1,34 @@
import { describe, it, expect } from "vitest";
import { SandboxAgent } from "sandbox-agent";
import { sprites } from "sandbox-agent/sprites";
const shouldRun = Boolean(process.env.SPRITES_API_KEY || process.env.SPRITE_TOKEN || process.env.SPRITES_TOKEN);
const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10) || 300_000;
const testFn = shouldRun ? it : it.skip;
describe("sprites provider", () => {
testFn(
"starts sandbox-agent and responds to /v1/health",
async () => {
const env: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const sdk = await SandboxAgent.start({
sandbox: sprites({
token: process.env.SPRITES_API_KEY ?? process.env.SPRITE_TOKEN ?? process.env.SPRITES_TOKEN,
env,
}),
});
try {
const health = await sdk.getHealth();
expect(health.status).toBe("ok");
} finally {
await sdk.destroySandbox();
}
},
timeoutMs,
);
});

View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}

View file

@ -9,7 +9,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]

View file

@ -6,7 +6,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "tsup src/index.ts --format esm --dts", "build": "tsup src/index.ts --format esm --dts --tsconfig tsconfig.build.json",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run", "test": "vitest run",
"test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts", "test:e2e:full": "HF_ENABLE_DAEMON_FULL_E2E=1 vitest run test/e2e/full-integration-e2e.test.ts",

View file

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"ignoreDeprecations": "6.0"
}
}

View file

@ -6,7 +6,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "tsup src/index.ts --format esm --dts", "build": "tsup src/index.ts --format esm --dts --tsconfig tsconfig.build.json",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run" "test": "vitest run"
}, },

View file

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"ignoreDeprecations": "6.0"
}
}

View file

@ -55,7 +55,7 @@ Upgrading backend integration from legacy sandbox-agent session endpoints to `sa
### Friction / Issue ### Friction / Issue
`0.2.0` no longer exposes `/v1/sessions` endpoints used by the backend integration; direct session create/status polling via legacy REST paths returns `404`. `0.2.0` no longer exposes the legacy session REST endpoints used by the backend integration; direct session create/status polling via those paths returns `404`.
### Attempted Fix / Workaround ### Attempted Fix / Workaround
@ -65,5 +65,5 @@ Upgrading backend integration from legacy sandbox-agent session endpoints to `sa
### Outcome ### Outcome
- Backend no longer depends on removed `/v1/sessions` endpoints. - Backend no longer depends on removed legacy session REST endpoints.
- Daytona flow is aligned with `sandbox-agent 0.2.0` runtime and SDK usage. - Daytona flow is aligned with `sandbox-agent 0.2.0` runtime and SDK usage.

View file

@ -103,7 +103,7 @@ function SdkCodeHighlighted() {
); );
} }
const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh`; const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.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
@ -196,7 +196,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/0.5.x/install.sh</span> <span className="text-green-400">https://releases.rivet.dev/sandbox-agent/0.4.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>

96
pnpm-lock.yaml generated
View file

@ -450,6 +450,31 @@ importers:
specifier: latest specifier: latest
version: 5.9.3 version: 5.9.3
examples/sprites:
dependencies:
'@fly/sprites':
specifier: latest
version: 0.0.1
'@sandbox-agent/example-shared':
specifier: workspace:*
version: link:../shared
sandbox-agent:
specifier: workspace:*
version: link:../../sdks/typescript
devDependencies:
'@types/node':
specifier: latest
version: 25.5.0
tsx:
specifier: latest
version: 4.21.0
typescript:
specifier: latest
version: 6.0.2
vitest:
specifier: ^3.0.0
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(jsdom@26.1.0)(tsx@4.21.0)(yaml@2.8.2)
examples/vercel: examples/vercel:
dependencies: dependencies:
'@sandbox-agent/example-shared': '@sandbox-agent/example-shared':
@ -531,7 +556,7 @@ importers:
version: 1.3.10 version: 1.3.10
tsup: tsup:
specifier: ^8.5.0 specifier: ^8.5.0
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.2)
foundry/packages/client: foundry/packages/client:
dependencies: dependencies:
@ -553,7 +578,7 @@ importers:
version: 19.2.14 version: 19.2.14
tsup: tsup:
specifier: ^8.5.0 specifier: ^8.5.0
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.2)
foundry/packages/frontend: foundry/packages/frontend:
dependencies: dependencies:
@ -614,7 +639,7 @@ importers:
version: 0.1.27(@types/react@19.2.14)(react@19.2.4) version: 0.1.27(@types/react@19.2.14)(react@19.2.4)
tsup: tsup:
specifier: ^8.5.0 specifier: ^8.5.0
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.2)
vite: vite:
specifier: ^7.1.3 specifier: ^7.1.3
version: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) version: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)
@ -633,7 +658,7 @@ importers:
devDependencies: devDependencies:
tsup: tsup:
specifier: ^8.5.0 specifier: ^8.5.0
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.2)
frontend/packages/inspector: frontend/packages/inspector:
dependencies: dependencies:
@ -771,25 +796,6 @@ importers:
specifier: ^5.7.0 specifier: ^5.7.0
version: 5.9.3 version: 5.9.3
scripts/sandbox-testing:
dependencies:
'@daytonaio/sdk':
specifier: latest
version: 0.151.0(ws@8.19.0)
'@e2b/code-interpreter':
specifier: latest
version: 2.3.3
devDependencies:
'@types/node':
specifier: latest
version: 25.5.0
tsx:
specifier: latest
version: 4.21.0
typescript:
specifier: latest
version: 5.9.3
sdks/acp-http-client: sdks/acp-http-client:
dependencies: dependencies:
'@agentclientprotocol/sdk': '@agentclientprotocol/sdk':
@ -988,6 +994,9 @@ importers:
'@e2b/code-interpreter': '@e2b/code-interpreter':
specifier: '>=1.0.0' specifier: '>=1.0.0'
version: 2.3.3 version: 2.3.3
'@fly/sprites':
specifier: '>=0.0.1'
version: 0.0.1
'@types/dockerode': '@types/dockerode':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.1 version: 4.0.1
@ -2487,6 +2496,10 @@ packages:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@fly/sprites@0.0.1':
resolution: {integrity: sha512-1s+dIVi/pTMP4Aj4Mkg+4LoZ/+a0Kp6l9piPRxvpgEKm11b/eRiZgJwVytwAHeI/vtg2fuwcFExjtXOEfny/TA==}
engines: {node: '>=24.0.0'}
'@grpc/grpc-js@1.14.3': '@grpc/grpc-js@1.14.3':
resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
engines: {node: '>=12.10.0'} engines: {node: '>=12.10.0'}
@ -6937,6 +6950,11 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
typescript@6.0.2:
resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
engines: {node: '>=14.17'}
hasBin: true
ufo@1.6.3: ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
@ -8975,6 +8993,8 @@ snapshots:
'@fastify/busboy@2.1.1': {} '@fastify/busboy@2.1.1': {}
'@fly/sprites@0.0.1': {}
'@grpc/grpc-js@1.14.3': '@grpc/grpc-js@1.14.3':
dependencies: dependencies:
'@grpc/proto-loader': 0.8.0 '@grpc/proto-loader': 0.8.0
@ -14144,6 +14164,34 @@ snapshots:
- tsx - tsx
- yaml - yaml
tsup@8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.2):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.3)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.3
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2)
resolve-from: 5.0.0
rollup: 4.56.0
source-map: 0.7.6
sucrase: 3.35.1
tinyexec: 0.3.2
tinyglobby: 0.2.15
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.5.6
typescript: 6.0.2
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
tsx@4.21.0: tsx@4.21.0:
dependencies: dependencies:
esbuild: 0.27.3 esbuild: 0.27.3
@ -14194,6 +14242,8 @@ snapshots:
typescript@5.9.3: {} typescript@5.9.3: {}
typescript@6.0.2: {}
ufo@1.6.3: {} ufo@1.6.3: {}
ultrahtml@1.6.0: {} ultrahtml@1.6.0: {}

View file

@ -10,6 +10,5 @@ packages:
- "sdks/gigacode/platforms/*" - "sdks/gigacode/platforms/*"
- "resources/vercel-ai-sdk-schemas" - "resources/vercel-ai-sdk-schemas"
- "scripts/release" - "scripts/release"
- "scripts/sandbox-testing"
- "examples/*" - "examples/*"
- "server/packages/sandbox-agent/tests/opencode-compat" - "server/packages/sandbox-agent/tests/opencode-compat"

View file

@ -81,7 +81,7 @@ Update this file continuously during the migration.
- Date: 2026-02-10 - Date: 2026-02-10
- Area: Session lifecycle surface - Area: Session lifecycle surface
- Issue: ACP stable does not include v1-equivalent methods for session listing, explicit session termination/delete, or event-log polling. - Issue: ACP stable does not include v1-equivalent methods for session listing, explicit session termination/delete, or event-log polling.
- Impact: Direct lift-and-shift of `/v1/sessions`, `/terminate`, and `/events` polling is not possible with ACP core only. - Impact: Direct lift-and-shift of the legacy session REST list, terminate, and event-polling behavior is not possible with ACP core only.
- Proposed direction: Define `_sandboxagent/session/*` extension methods for these control operations, while keeping core prompt flow on standard ACP methods. - Proposed direction: Define `_sandboxagent/session/*` extension methods for these control operations, while keeping core prompt flow on standard ACP methods.
- Decision: Open. - Decision: Open.
- Owner: Unassigned. - Owner: Unassigned.

View file

@ -11,4 +11,4 @@ This tracks legacy inspector behaviors that do not yet have full parity on ACP v
5. TDOO: Agent mode discovery before creating a session is not implemented (inspector currently returns cached-or-empty mode lists). 5. TDOO: Agent mode discovery before creating a session is not implemented (inspector currently returns cached-or-empty mode lists).
6. TDOO: Agent model discovery before creating a session is not implemented (inspector currently returns cached-or-empty model lists). 6. TDOO: Agent model discovery before creating a session is not implemented (inspector currently returns cached-or-empty model lists).
7. TDOO: Session listing only reflects sessions created by this inspector client instance (not full server/global session inventory). 7. TDOO: Session listing only reflects sessions created by this inspector client instance (not full server/global session inventory).
8. TDOO: Event history shown in inspector is synthesized from ACP traffic handled by the inspector compatibility layer, not the old canonical `/v1/sessions/*/events` backend history. 8. TDOO: Event history shown in inspector is synthesized from ACP traffic handled by the inspector compatibility layer, not the old canonical session-events backend history.

View file

@ -20,8 +20,8 @@ Static v1 endpoints today:
- `GET /v1/agents` - `GET /v1/agents`
- `POST /v1/agents/:agent/install` - `POST /v1/agents/:agent/install`
- `GET /v1/sessions` - legacy session list endpoint
- `GET /v1/sessions/:session_id` - legacy session detail endpoint
- `GET /v1/fs/entries` - `GET /v1/fs/entries`
- `GET /v1/fs/file` - `GET /v1/fs/file`
- `PUT /v1/fs/file` - `PUT /v1/fs/file`
@ -76,8 +76,8 @@ Interpretation for clients: all agent/session operations and non-binary filesyst
| --- | --- | --- | | --- | --- | --- |
| `GET /v1/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 /v1/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 /v1/sessions` | `_sandboxagent/session/list` | Return current `SessionListResponse` shape (not ACP unstable list shape). | | legacy session list endpoint | `_sandboxagent/session/list` | Return current `SessionListResponse` shape (not ACP unstable list shape). |
| `GET /v1/sessions/:session_id` | `_sandboxagent/session/get` | Return current `SessionInfo` shape; error on missing session. | | legacy session detail endpoint | `_sandboxagent/session/get` | Return current `SessionInfo` shape; error on missing session. |
| `GET /v1/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 /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. | | `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 /v1/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. |
@ -143,7 +143,7 @@ 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 `/v1/agents*`, `/v1/sessions*`, or non-binary `/v1/fs/*` fetches in SDK runtime code. - No direct legacy agents/session REST fetches 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.
@ -184,17 +184,17 @@ Alternative (optional): introduce a runtime-only control connection mode that do
- 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 `/v1/agents*`, `/v1/sessions*`, and non-binary `/v1/fs/*`. - Remove direct runtime fetch usage for legacy agents/session REST endpoints 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 `/v1/agents*` HTTP calls. - Make `api agents list/install` call ACP extension methods (via ACP post flow), not direct legacy agent HTTP calls.
- Inspector flow/docs: - Inspector flow/docs:
- Stop depending on `GET /v1/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 `/v1/agents*`, `/v1/sessions*`, `/v1/fs/entries`, `/v1/fs/entry`, `/v1/fs/mkdir`, `/v1/fs/move`, `/v1/fs/stat` from `router.rs`. - Remove route registrations for legacy agent/session REST endpoints and `/v1/fs/entries`, `/v1/fs/entry`, `/v1/fs/mkdir`, `/v1/fs/move`, `/v1/fs/stat` from `router.rs`.
- Keep `/v1/health`, `/v1/rpc`, `GET /v1/fs/file`, `PUT /v1/fs/file`, and `POST /v1/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`.
@ -237,6 +237,6 @@ Inspector:
## Open Decisions ## Open Decisions
1. Should removed `/v1/agents*`, `/v1/sessions*`, and non-binary `/v1/fs/*` return `410` for one release or be dropped immediately? 1. Should removed legacy agent/session REST endpoints 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?

View file

@ -59,11 +59,11 @@ struct PendingQuestion {
} }
``` ```
## v1 HTTP Endpoints (from `router.rs`) ## Legacy Session REST Endpoints (from `router.rs`)
``` ```
POST /v1/sessions/{session_id}/questions/{question_id}/reply -> 204 No Content session question reply endpoint -> 204 No Content
POST /v1/sessions/{session_id}/questions/{question_id}/reject -> 204 No Content session question reject endpoint -> 204 No Content
``` ```
### `reply_question` handler ### `reply_question` handler
@ -122,7 +122,7 @@ Key flow:
1. Agent emits `question.requested` event with `QuestionEventData { status: Requested, question_id, prompt, options }` 1. Agent emits `question.requested` event with `QuestionEventData { status: Requested, question_id, prompt, options }`
2. Client renders question UI 2. Client renders question UI
3. Client calls `POST /v1/sessions/{id}/questions/{qid}/reply` with `{ answers: [["selected"]] }` or `POST .../reject` 3. Client calls the legacy session question reply or reject endpoint with `{ answers: [["selected"]] }`
4. System emits `question.resolved` event with `QuestionEventData { status: Answered, response: Some("...") }` or `{ status: Rejected }` 4. System emits `question.resolved` event with `QuestionEventData { status: Answered, response: Some("...") }` or `{ status: Rejected }`
## v1 Agent Capability ## v1 Agent Capability

View file

@ -4,7 +4,7 @@
## Summary ## Summary
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. The legacy session REST API had an explicit terminate endpoint. ACP 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 v1 State ## Current v1 State
@ -20,7 +20,7 @@ v1 had explicit session termination (`POST /v1/sessions/{id}/terminate`). v1 onl
### HTTP Endpoint ### HTTP Endpoint
``` ```
POST /v1/sessions/{id}/terminate legacy session terminate endpoint
``` ```
### Handler (from `router.rs`) ### Handler (from `router.rs`)

View file

@ -49,7 +49,7 @@ Returned `AgentModelsResponse` with full model list including variants.
### Session Creation with Variant ### Session Creation with Variant
``` ```
POST /v1/sessions legacy session create endpoint
``` ```
Body included `variant: Option<String>` to select a specific model variant at session creation time. Body included `variant: Option<String>` to select a specific model variant at session creation time.

View file

@ -34,7 +34,7 @@ pub struct UniversalEvent {
### v1 Usage ### v1 Usage
``` ```
GET /v1/sessions/{id}/events?include_raw=true legacy event polling endpoint with `include_raw=true`
``` ```
When `include_raw=true`, each `UniversalEvent` included the verbatim JSON the agent process emitted before normalization into the universal schema. When `include_raw=true`, each `UniversalEvent` included the verbatim JSON the agent process emitted before normalization into the universal schema.

View file

@ -1,10 +1,10 @@
# Feature 16: Session Info # Feature 16: Session Info
**Implementation approach:** New HTTP endpoints (`GET /v1/sessions`, `GET /v1/sessions/{id}`) **Implementation approach:** New session-info HTTP endpoints
## Summary ## Summary
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. 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 HTTP endpoints for session listing and detail.
## Current v1 State ## Current v1 State
@ -117,8 +117,8 @@ fn build_session_info(state: &SessionState) -> SessionInfo {
### New HTTP Endpoints ### New HTTP Endpoints
``` ```
GET /v1/sessions -> SessionListResponse session list endpoint -> SessionListResponse
GET /v1/sessions/{id} -> SessionInfo session detail endpoint -> 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.
@ -156,7 +156,7 @@ Need to add:
| File | Change | | File | Change |
|------|--------| |------|--------|
| `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/router.rs` | Add session list and session detail 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/v1_api.rs` | Add session listing and detail tests | | `server/packages/sandbox-agent/tests/v1_api.rs` | Add session listing and detail tests |
@ -165,6 +165,6 @@ Need to add:
| Doc | Change | | Doc | Change |
|-----|--------| |-----|--------|
| `docs/openapi.json` | Add `/v1/sessions` and `/v1/sessions/{id}` endpoint specs | | `docs/openapi.json` | Add session list and session detail 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 |

View file

@ -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 /v1/sessions/{id}` (Feature #16) — include `terminationInfo` in response when session has ended - the session info endpoint (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

View file

@ -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 /v1/sessions` and `GET /v1/sessions/{id}` | Medium | | B1 | [Session Info](./16-session-info.md) | #16 | New session info HTTP endpoints | 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 |

View file

@ -17,16 +17,16 @@
| /v1/fs/stat | UNIMPLEMENTED | | /v1/fs/stat | UNIMPLEMENTED |
| /v1/fs/upload-batch | UNIMPLEMENTED | | /v1/fs/upload-batch | UNIMPLEMENTED |
| /v1/health | UNIMPLEMENTED | | /v1/health | UNIMPLEMENTED |
| /v1/sessions | session/list (UNSTABLE) | | legacy session list route | session/list (UNSTABLE) |
| /v1/sessions/{session_id} | session/new \| session/load \| session/resume (UNSTABLE) | | legacy session create/load/resume route | session/new \| session/load \| session/resume (UNSTABLE) |
| /v1/sessions/{session_id}/events | UNIMPLEMENTED | | legacy session events polling route | UNIMPLEMENTED |
| /v1/sessions/{session_id}/events/sse | session/update (notification stream) | | legacy session events SSE route | session/update (notification stream) |
| /v1/sessions/{session_id}/messages | session/prompt | | legacy session prompt route | session/prompt |
| /v1/sessions/{session_id}/messages/stream | session/prompt + session/update notifications | | legacy session prompt + stream route | session/prompt + session/update notifications |
| /v1/sessions/{session_id}/permissions/{permission_id}/reply | session/request_permission response | | legacy permission reply route | session/request_permission response |
| /v1/sessions/{session_id}/questions/{question_id}/reject | UNIMPLEMENTED | | legacy question reject route | UNIMPLEMENTED |
| /v1/sessions/{session_id}/questions/{question_id}/reply | UNIMPLEMENTED | | legacy question reply route | UNIMPLEMENTED |
| /v1/sessions/{session_id}/terminate | session/cancel (turn cancellation only) | | legacy session terminate route | session/cancel (turn cancellation only) |
| AgentCapabilities | initialize.result.agentCapabilities | | AgentCapabilities | initialize.result.agentCapabilities |
| AgentCapabilities.commandExecution | UNIMPLEMENTED | | AgentCapabilities.commandExecution | UNIMPLEMENTED |
| AgentCapabilities.errorEvents | UNIMPLEMENTED | | AgentCapabilities.errorEvents | UNIMPLEMENTED |
@ -427,7 +427,7 @@
- `UNIMPLEMENTED` means there is no ACP-standard field/method with equivalent semantics in `schema.unstable.json`; implementation would require ACP extension methods (`_...`) and/or `_meta` payloads. - `UNIMPLEMENTED` means there is no ACP-standard field/method with equivalent semantics in `schema.unstable.json`; implementation would require ACP extension methods (`_...`) and/or `_meta` payloads.
- Rows mapped to `_meta[...]` are ACP-compatible extensions, not standard interoperable ACP fields; both sides must agree on names and semantics. - Rows mapped to `_meta[...]` are ACP-compatible extensions, not standard interoperable ACP fields; both sides must agree on names and semantics.
- Legacy event polling (`/v1/sessions/{session_id}/events`) has no ACP equivalent; ACP is stream-first via `session/update` notifications over streamable HTTP. - Legacy event polling has no ACP equivalent; ACP is stream-first via `session/update` notifications over streamable HTTP.
- Session lifecycle differs: ACP has `session/new`, `session/load`, `session/resume` (UNSTABLE), and `session/fork` (UNSTABLE), but no standard explicit "close session" method. - Session lifecycle differs: ACP has `session/new`, `session/load`, `session/resume` (UNSTABLE), and `session/fork` (UNSTABLE), but no standard explicit "close session" method.
- Permission handling is request/response (`session/request_permission`) tied to JSON-RPC request IDs; it does not use standalone REST reply endpoints. - Permission handling is request/response (`session/request_permission`) tied to JSON-RPC request IDs; it does not use standalone REST reply endpoints.
- Question/answer HITL flow in the old schema has no standard ACP equivalent today (separate from permission prompts). - Question/answer HITL flow in the old schema has no standard ACP equivalent today (separate from permission prompts).

View file

@ -233,8 +233,6 @@ Non-ACP endpoints retained in v1:
- `GET /v1/health` - `GET /v1/health`
- `GET /v1/agents` (capabilities + install status) - `GET /v1/agents` (capabilities + install status)
- `POST /v1/agents/{agent}/install` - `POST /v1/agents/{agent}/install`
- `GET /v1/sessions`
- `GET /v1/sessions/{id}`
- `GET /v1/fs/file` - `GET /v1/fs/file`
- `PUT /v1/fs/file` - `PUT /v1/fs/file`
- `POST /v1/fs/upload-batch` - `POST /v1/fs/upload-batch`

View file

@ -54,16 +54,16 @@ Extension namespace used in this spec:
| `POST /v1/fs/move` | `POST /v1/fs/move` | HTTP platform API | Port v1 behavior. | | `POST /v1/fs/move` | `POST /v1/fs/move` | HTTP platform API | Port v1 behavior. |
| `GET /v1/fs/stat` | `GET /v1/fs/stat` | HTTP platform API | Port v1 behavior. | | `GET /v1/fs/stat` | `GET /v1/fs/stat` | HTTP platform API | Port v1 behavior. |
| `POST /v1/fs/upload-batch` | `POST /v1/fs/upload-batch` | HTTP platform API | Tar upload/extract behavior from v1. | | `POST /v1/fs/upload-batch` | `POST /v1/fs/upload-batch` | HTTP platform API | Tar upload/extract behavior from v1. |
| `GET /v1/sessions` | `GET /v1/sessions` | HTTP control-plane | Session inventory without ACP connection requirement. | | legacy session list route | session/list | HTTP control-plane | Session inventory without ACP connection requirement. |
| `POST /v1/sessions/{session_id}` | `session/new` | Standard | Path `session_id` becomes alias in `_meta["sandboxagent.dev"].requestedSessionId`. | | legacy session create route | `session/new` | Standard | Path `session_id` becomes alias in `_meta["sandboxagent.dev"].requestedSessionId`. |
| `POST /v1/sessions/{session_id}/messages` | `session/prompt` | Standard | Asynchronous behavior comes from transport (request + stream). | | legacy session prompt route | `session/prompt` | Standard | Asynchronous behavior comes from transport (request + stream). |
| `POST /v1/sessions/{session_id}/messages/stream` | `session/prompt` + consume `session/update` on SSE | Standard | Streaming is transport-level, not a distinct ACP method. | | legacy session prompt + stream route | `session/prompt` + consume `session/update` on SSE | Standard | Streaming is transport-level, not a distinct ACP method. |
| `POST /v1/sessions/{session_id}/terminate` | `_sandboxagent/session/terminate` | Extension | Idempotent termination semantics distinct from `DELETE /v1/rpc`. | | legacy session terminate route | `_sandboxagent/session/terminate` | Extension | Idempotent termination semantics distinct from `DELETE /v1/rpc`. |
| `GET /v1/sessions/{session_id}/events` | `_sandboxagent/session/events` (poll view over ACP stream) | Extension | Optional compatibility helper; canonical v1 is stream consumption. | | legacy event polling route | `_sandboxagent/session/events` (poll view over ACP stream) | Extension | Optional compatibility helper; canonical v1 is stream consumption. |
| `GET /v1/sessions/{session_id}/events/sse` | `GET /v1/rpc` SSE stream | Standard transport | Filter by sessionId client-side or via connection/session binding. | | legacy event SSE route | `GET /v1/rpc` SSE stream | Standard transport | Filter by sessionId client-side or via connection/session binding. |
| `POST /v1/sessions/{session_id}/permissions/{permission_id}/reply` | JSON-RPC response to pending `session/request_permission` request id | Standard | Bridge `permission_id` to request `id` in transport state. | | legacy permission reply route | JSON-RPC response to pending `session/request_permission` request id | Standard | Bridge `permission_id` to request `id` in transport state. |
| `POST /v1/sessions/{session_id}/questions/{question_id}/reply` | JSON-RPC response to pending `_sandboxagent/session/request_question` | Extension | ACP stable has no generic question/HITL request method. | | legacy question reply route | JSON-RPC response to pending `_sandboxagent/session/request_question` | Extension | ACP stable has no generic question/HITL request method. |
| `POST /v1/sessions/{session_id}/questions/{question_id}/reject` | JSON-RPC response to pending `_sandboxagent/session/request_question` | Extension | Encode rejection in response outcome. | | legacy question reject route | JSON-RPC response to pending `_sandboxagent/session/request_question` | Extension | Encode rejection in response outcome. |
### 3.1 `CreateSessionRequest` field mapping ### 3.1 `CreateSessionRequest` field mapping

View file

@ -45,7 +45,6 @@ const VERSION_REFERENCE_FILES = [
"scripts/release/main.ts", "scripts/release/main.ts",
"scripts/release/promote-artifacts.ts", "scripts/release/promote-artifacts.ts",
"scripts/release/sdk.ts", "scripts/release/sdk.ts",
"scripts/sandbox-testing/test-sandbox.ts",
]; ];
export async function updateVersion(opts: ReleaseOpts) { export async function updateVersion(opts: ReleaseOpts) {
@ -181,7 +180,7 @@ async function updateVersionReferences(opts: ReleaseOpts, oldVersion: string, ol
} }
if (modifiedFiles.length > 0) { if (modifiedFiles.length > 0) {
await $({ cwd: opts.root })`git add ${modifiedFiles}`; await $({ cwd: opts.root })`git add -f ${modifiedFiles}`;
console.log(`\nUpdated ${modifiedFiles.length} files with version references.`); console.log(`\nUpdated ${modifiedFiles.length} files with version references.`);
} else { } else {
console.log(`\nNo version reference files needed updates.`); console.log(`\nNo version reference files needed updates.`);

View file

@ -1,21 +0,0 @@
{
"name": "@sandbox-agent/testing",
"private": true,
"type": "module",
"scripts": {
"test": "tsx test-sandbox.ts",
"test:docker": "tsx test-sandbox.ts docker",
"test:daytona": "tsx test-sandbox.ts daytona",
"test:mock": "tsx test-sandbox.ts docker --agent=mock",
"test:verbose": "tsx test-sandbox.ts docker --verbose"
},
"dependencies": {
"@daytonaio/sdk": "latest",
"@e2b/code-interpreter": "latest"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest"
}
}

View file

@ -1,720 +0,0 @@
#!/usr/bin/env npx tsx
/**
* Sandbox Testing Script
*
* Tests sandbox-agent on various cloud sandbox providers.
* Usage: npx tsx test-sandbox.ts [provider] [options]
*
* Providers: daytona, e2b, docker
*
* Options:
* --skip-build Skip cargo build step
* --use-release Use pre-built release binary from releases.rivet.dev
* --agent <name> Test specific agent (claude, codex, mock)
* --skip-agent-install Skip pre-installing agents (tests on-demand install like Daytona example)
* --keep-alive Don't cleanup sandbox after test
* --verbose Show all logs
*/
import { execSync, spawn } from "node:child_process";
import { existsSync, readFileSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, "../..");
const SERVER_DIR = join(ROOT_DIR, "server");
// Parse args
const args = process.argv.slice(2);
const provider = args.find((a) => !a.startsWith("--")) || "docker";
const skipBuild = args.includes("--skip-build");
const useRelease = args.includes("--use-release");
const skipAgentInstall = args.includes("--skip-agent-install");
const keepAlive = args.includes("--keep-alive");
const verbose = args.includes("--verbose");
const agentArg = args.find((a) => a.startsWith("--agent="))?.split("=")[1];
// Colors
const log = {
info: (msg: string) => console.log(`\x1b[34m[INFO]\x1b[0m ${msg}`),
success: (msg: string) => console.log(`\x1b[32m[OK]\x1b[0m ${msg}`),
error: (msg: string) => console.log(`\x1b[31m[ERROR]\x1b[0m ${msg}`),
warn: (msg: string) => console.log(`\x1b[33m[WARN]\x1b[0m ${msg}`),
debug: (msg: string) => verbose && console.log(`\x1b[90m[DEBUG]\x1b[0m ${msg}`),
section: (msg: string) => console.log(`\n\x1b[1m=== ${msg} ===\x1b[0m`),
};
// Credentials extraction using sandbox-agent CLI
function extractCredentials(): { anthropicApiKey?: string; openaiApiKey?: string } {
// First check environment variables
const envCreds = {
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
openaiApiKey: process.env.OPENAI_API_KEY,
};
// If both are set in env, use them
if (envCreds.anthropicApiKey && envCreds.openaiApiKey) {
return envCreds;
}
// Try to extract using sandbox-agent CLI
try {
const binaryPath = join(ROOT_DIR, "target/release/sandbox-agent");
const debugBinaryPath = join(ROOT_DIR, "target/debug/sandbox-agent");
const binary = existsSync(binaryPath) ? binaryPath : existsSync(debugBinaryPath) ? debugBinaryPath : null;
if (binary) {
const output = execSync(`${binary} credentials extract-env --export`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
// Parse export statements: export KEY="value"
for (const line of output.split("\n")) {
const match = line.match(/^export (\w+)="(.*)"/);
if (match) {
const [, key, value] = match;
if (key === "ANTHROPIC_API_KEY" && !envCreds.anthropicApiKey) {
envCreds.anthropicApiKey = value;
} else if (key === "OPENAI_API_KEY" && !envCreds.openaiApiKey) {
envCreds.openaiApiKey = value;
}
}
}
log.debug(`Extracted credentials via sandbox-agent CLI`);
}
} catch (err) {
log.debug(`Failed to extract credentials via CLI: ${err}`);
}
return envCreds;
}
function getAnthropicApiKey(): string | undefined {
return extractCredentials().anthropicApiKey;
}
function getOpenAiApiKey(): string | undefined {
return extractCredentials().openaiApiKey;
}
// Build sandbox-agent
async function buildSandboxAgent(): Promise<string> {
log.section("Building sandbox-agent");
if (useRelease) {
log.info("Using pre-built release from releases.rivet.dev");
return "RELEASE";
}
// Binary is in workspace root target dir, not server target dir
const binaryPath = join(ROOT_DIR, "target/release/sandbox-agent");
if (skipBuild) {
if (!existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}. Run without --skip-build.`);
}
log.info(`Using existing binary: ${binaryPath}`);
return binaryPath;
}
log.info("Running cargo build --release...");
try {
execSync("cargo build --release -p sandbox-agent", {
cwd: ROOT_DIR,
stdio: verbose ? "inherit" : "pipe",
});
log.success(`Built: ${binaryPath}`);
return binaryPath;
} catch (err) {
throw new Error(`Build failed: ${err}`);
}
}
// Provider interface
interface SandboxProvider {
name: string;
requiredEnv: string[];
create(opts: { envVars: Record<string, string> }): Promise<Sandbox>;
}
interface Sandbox {
id: string;
exec(cmd: string): Promise<{ stdout: string; stderr: string; exitCode: number }>;
upload(localPath: string, remotePath: string): Promise<void>;
getBaseUrl(port: number): Promise<string>;
cleanup(): Promise<void>;
}
// Docker provider
// Uses Ubuntu because Claude Code and sandbox-agent are glibc binaries
const dockerProvider: SandboxProvider = {
name: "docker",
requiredEnv: [],
async create({ envVars }) {
const id = `sandbox-test-${Date.now()}`;
const envArgs = Object.entries(envVars)
.map(([k, v]) => `-e ${k}=${v}`)
.join(" ");
log.info(`Creating Docker container: ${id}`);
execSync(`docker run -d --name ${id} ${envArgs} -p 0:3000 ubuntu:22.04 tail -f /dev/null`, { stdio: verbose ? "inherit" : "pipe" });
// Install dependencies
execSync(`docker exec ${id} sh -c "apt-get update && apt-get install -y curl ca-certificates"`, {
stdio: verbose ? "inherit" : "pipe",
});
return {
id,
async exec(cmd) {
try {
const stdout = execSync(`docker exec ${id} sh -c "${cmd.replace(/"/g, '\\"')}"`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return { stdout, stderr: "", exitCode: 0 };
} catch (err: any) {
return { stdout: err.stdout || "", stderr: err.stderr || "", exitCode: err.status || 1 };
}
},
async upload(localPath, remotePath) {
execSync(`docker cp "${localPath}" ${id}:${remotePath}`, { stdio: verbose ? "inherit" : "pipe" });
execSync(`docker exec ${id} chmod +x ${remotePath}`, { stdio: verbose ? "inherit" : "pipe" });
},
async getBaseUrl(port) {
const portMapping = execSync(`docker port ${id} ${port}`, { encoding: "utf-8" }).trim();
const hostPort = portMapping.split(":").pop();
return `http://localhost:${hostPort}`;
},
async cleanup() {
log.info(`Cleaning up container: ${id}`);
execSync(`docker rm -f ${id}`, { stdio: "pipe" });
},
};
},
};
// Daytona provider
const daytonaProvider: SandboxProvider = {
name: "daytona",
requiredEnv: ["DAYTONA_API_KEY"],
async create({ envVars }) {
const { Daytona } = await import("@daytonaio/sdk");
const daytona = new Daytona();
log.info("Creating Daytona sandbox...");
// NOTE: Tier 1/2 sandboxes have restricted network that cannot be overridden
// networkAllowList requires CIDR notation (IP ranges), not domain names
const sandbox = await daytona.create({
image: "ubuntu:22.04",
envVars,
});
const id = sandbox.id;
// Install curl
await sandbox.process.executeCommand("apt-get update && apt-get install -y curl ca-certificates");
return {
id,
async exec(cmd) {
const result = await sandbox.process.executeCommand(cmd);
// Daytona SDK returns: { exitCode, result: string, artifacts: { stdout: string } }
return {
stdout: result.result || "",
stderr: "",
exitCode: result.exitCode ?? 0,
};
},
async upload(localPath, remotePath) {
const content = readFileSync(localPath);
// Daytona SDK signature: uploadFile(Buffer, remotePath)
await sandbox.fs.uploadFile(content, remotePath);
await sandbox.process.executeCommand(`chmod +x ${remotePath}`);
},
async getBaseUrl(port) {
const preview = await sandbox.getSignedPreviewUrl(port, 4 * 60 * 60);
return preview.url;
},
async cleanup() {
log.info(`Cleaning up Daytona sandbox: ${id}`);
await sandbox.delete(60);
},
};
},
};
// E2B provider
const e2bProvider: SandboxProvider = {
name: "e2b",
requiredEnv: ["E2B_API_KEY"],
async create({ envVars }) {
const { Sandbox } = await import("@e2b/code-interpreter");
log.info("Creating E2B sandbox...");
let sandbox;
try {
sandbox = await Sandbox.create({
allowInternetAccess: true,
envs: envVars,
});
} catch (err: any) {
log.error(`E2B sandbox creation failed: ${err.message || err}`);
throw err;
}
const id = sandbox.sandboxId;
// Install curl (E2B uses Debian which has glibc, sandbox-agent will auto-detect)
const installResult = await sandbox.commands.run("sudo apt-get update && sudo apt-get install -y curl ca-certificates");
log.debug(`Install output: ${installResult.stdout} ${installResult.stderr}`);
return {
id,
async exec(cmd) {
const result = await sandbox.commands.run(cmd);
return {
stdout: result.stdout || "",
stderr: result.stderr || "",
exitCode: result.exitCode,
};
},
async upload(localPath, remotePath) {
const content = readFileSync(localPath);
await sandbox.files.write(remotePath, content);
await sandbox.commands.run(`chmod +x ${remotePath}`);
},
async getBaseUrl(port) {
return `https://${sandbox.getHost(port)}`;
},
async cleanup() {
log.info(`Cleaning up E2B sandbox: ${id}`);
await sandbox.kill();
},
};
},
};
// Get provider
function getProvider(name: string): SandboxProvider {
switch (name) {
case "docker":
return dockerProvider;
case "daytona":
return daytonaProvider;
case "e2b":
return e2bProvider;
default:
throw new Error(`Unknown provider: ${name}. Available: docker, daytona, e2b`);
}
}
// Install sandbox-agent in sandbox
async function installSandboxAgent(sandbox: Sandbox, binaryPath: string): Promise<void> {
log.section("Installing sandbox-agent");
if (binaryPath === "RELEASE") {
log.info("Installing from releases.rivet.dev...");
const result = await sandbox.exec("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh | sh");
log.debug(`Install output: ${result.stdout}`);
if (result.exitCode !== 0) {
throw new Error(`Install failed: ${result.stderr}`);
}
} else {
log.info(`Uploading local binary: ${binaryPath}`);
await sandbox.upload(binaryPath, "/usr/local/bin/sandbox-agent");
}
// Verify installation
const version = await sandbox.exec("sandbox-agent --version");
log.success(`Installed: ${version.stdout.trim()}`);
}
// Install agents
async function installAgents(sandbox: Sandbox, agents: string[]): Promise<void> {
log.section("Installing agents");
for (const agent of agents) {
log.info(`Installing ${agent}...`);
if (agent === "claude" || agent === "codex") {
const result = await sandbox.exec(`sandbox-agent install-agent ${agent}`);
if (result.exitCode !== 0) throw new Error(`Failed to install ${agent}: ${result.stderr}`);
log.success(`Installed ${agent}`);
} else if (agent === "mock") {
// Mock agent is built into sandbox-agent, no install needed
log.info("Mock agent is built-in, skipping install");
}
}
}
// Start server and check health
async function startServerAndCheckHealth(sandbox: Sandbox): Promise<string> {
log.section("Starting server");
// Start server in background
await sandbox.exec("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
log.info("Server started in background");
// Get base URL
const baseUrl = await sandbox.getBaseUrl(3000);
log.info(`Base URL: ${baseUrl}`);
// Wait for health
log.info("Waiting for health check...");
for (let i = 0; i < 30; i++) {
try {
const response = await fetch(`${baseUrl}/v1/health`);
if (response.ok) {
const data = await response.json();
if (data.status === "ok") {
log.success("Health check passed!");
return baseUrl;
}
}
} catch {}
await new Promise((r) => setTimeout(r, 1000));
}
// Show logs on failure
const logs = await sandbox.exec("cat /tmp/sandbox-agent.log");
log.error("Server logs:\n" + logs.stdout);
throw new Error("Health check failed after 30 seconds");
}
// Send a message and wait for response, auto-approving permissions
// Returns the response text
async function sendMessage(baseUrl: string, sessionId: string, message: string): Promise<string> {
log.info(`Sending message: "${message.slice(0, 60)}${message.length > 60 ? "..." : ""}"`);
const msgRes = await fetch(`${baseUrl}/v1/sessions/${sessionId}/messages/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
if (!msgRes.ok || !msgRes.body) {
throw new Error(`Failed to send message: ${await msgRes.text()}`);
}
// Process SSE stream
const reader = msgRes.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let responseText = "";
let receivedText = false;
let hasError = false;
let errorMessage = "";
let pendingPermission: string | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6);
if (data === "[DONE]") continue;
try {
const event = JSON.parse(data);
log.debug(`Event: ${event.type}`);
if (event.type === "item.delta") {
const delta = event.data?.delta;
const text = typeof delta === "string" ? delta : delta?.text || "";
if (text) {
if (!receivedText) {
log.info("Receiving response...");
receivedText = true;
}
process.stdout.write(text);
responseText += text;
}
}
// Handle permission requests - auto-approve
if (event.type === "permission.requested") {
const permissionId = event.data?.permission_id;
if (permissionId) {
pendingPermission = permissionId;
log.info(`Permission requested (${permissionId}), auto-approving...`);
}
}
if (event.type === "error") {
hasError = true;
errorMessage = event.data?.message || JSON.stringify(event.data);
log.error(`Error event: ${errorMessage}`);
}
if (event.type === "agent.unparsed") {
hasError = true;
errorMessage = `Agent unparsed: ${JSON.stringify(event.data)}`;
log.error(errorMessage);
}
} catch {}
}
// If we have a pending permission, approve it
if (pendingPermission) {
const permId = pendingPermission;
pendingPermission = null;
try {
const approveRes = await fetch(`${baseUrl}/v1/sessions/${sessionId}/permissions/${permId}/reply`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reply: "once" }),
});
if (approveRes.ok) {
log.success(`Permission ${permId} approved`);
} else {
log.warn(`Failed to approve permission: ${await approveRes.text()}`);
}
} catch (err) {
log.warn(`Error approving permission: ${err}`);
}
}
}
if (receivedText) {
console.log(); // newline after response
}
if (hasError) {
throw new Error(`Agent returned error: ${errorMessage}`);
}
return responseText;
}
// Test agent interaction
async function testAgent(baseUrl: string, agent: string, message: string): Promise<void> {
log.section(`Testing ${agent} agent`);
const sessionId = crypto.randomUUID();
// Create session
log.info(`Creating session ${sessionId}...`);
const createRes = await fetch(`${baseUrl}/v1/sessions/${sessionId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent }),
});
if (!createRes.ok) {
throw new Error(`Failed to create session: ${await createRes.text()}`);
}
log.success("Session created");
const response = await sendMessage(baseUrl, sessionId, message);
if (!response) {
throw new Error("No response received from agent");
}
log.success("Received response from agent");
}
// Test that agent can actually modify files and run commands
async function testAgentActions(baseUrl: string, agent: string, sandbox: Sandbox): Promise<void> {
log.section(`Testing ${agent} agent actions (file + command)`);
const sessionId = crypto.randomUUID();
const testFile = "/tmp/sandbox-test-file.txt";
const expectedContent = "Hello from sandbox test!";
// For Claude running as root in containers, we must use default permission mode
// and handle permissions via the API (bypass mode is not supported as root).
// For other agents, we can use bypass mode.
const permissionMode = agent === "claude" ? "default" : "bypass";
log.info(`Creating session ${sessionId} with permissionMode=${permissionMode}...`);
const createRes = await fetch(`${baseUrl}/v1/sessions/${sessionId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent, permissionMode }),
});
if (!createRes.ok) {
throw new Error(`Failed to create session: ${await createRes.text()}`);
}
log.success("Session created");
// Ask agent to create a file
const fileMessage = `Create a file at ${testFile} with exactly this content (no quotes, no extra text): ${expectedContent}`;
await sendMessage(baseUrl, sessionId, fileMessage);
// Wait for agent to complete action after permission approval
log.info("Waiting for agent to complete action...");
await new Promise((r) => setTimeout(r, 5000));
// Verify file was created
log.info("Verifying file was created...");
const fileCheck = await sandbox.exec(`cat ${testFile} 2>&1`);
if (fileCheck.exitCode !== 0) {
throw new Error(`File was not created: ${fileCheck.stderr || fileCheck.stdout}`);
}
if (!fileCheck.stdout.includes("Hello from sandbox test")) {
throw new Error(`File content mismatch. Expected "${expectedContent}", got "${fileCheck.stdout.trim()}"`);
}
log.success(`File created with correct content: "${fileCheck.stdout.trim()}"`);
// Ask agent to run a command and create output
const cmdMessage = `Run this command and tell me the output: echo "command-test-$(date +%s)" > /tmp/cmd-output.txt && cat /tmp/cmd-output.txt`;
await sendMessage(baseUrl, sessionId, cmdMessage);
// Verify command was executed
log.info("Verifying command was executed...");
const cmdCheck = await sandbox.exec("cat /tmp/cmd-output.txt 2>&1");
if (cmdCheck.exitCode !== 0) {
throw new Error(`Command output file not found: ${cmdCheck.stderr || cmdCheck.stdout}`);
}
if (!cmdCheck.stdout.includes("command-test-")) {
throw new Error(`Command output mismatch. Expected "command-test-*", got "${cmdCheck.stdout.trim()}"`);
}
log.success(`Command executed successfully: "${cmdCheck.stdout.trim()}"`);
}
// Check environment diagnostics
async function checkEnvironment(sandbox: Sandbox): Promise<void> {
log.section("Environment diagnostics");
const checks = [
{ name: "Environment variables", cmd: "env | grep -E 'ANTHROPIC|OPENAI|CLAUDE|CODEX' | sed 's/=.*/=<set>/'" },
// Check both /root (Alpine) and /home/user (E2B/Debian) paths
{
name: "Agent binaries",
cmd: "ls -la ~/.local/share/sandbox-agent/bin/ 2>/dev/null || ls -la /root/.local/share/sandbox-agent/bin/ 2>/dev/null || ls -la /home/user/.local/share/sandbox-agent/bin/ 2>/dev/null || echo 'No agents installed'",
},
{
name: "Claude version",
cmd: "~/.local/share/sandbox-agent/bin/claude --version 2>&1 || /root/.local/share/sandbox-agent/bin/claude --version 2>&1 || echo 'Claude not installed'",
},
{ name: "sandbox-agent version", cmd: "sandbox-agent --version 2>/dev/null || echo 'Not installed'" },
{ name: "Server process", cmd: "pgrep -a sandbox-agent 2>/dev/null || ps aux | grep sandbox-agent | grep -v grep || echo 'Not running'" },
{ name: "Server logs (last 50 lines)", cmd: "tail -50 /tmp/sandbox-agent.log 2>/dev/null || echo 'No logs'" },
{
name: "Network: api.anthropic.com",
cmd: "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 https://api.anthropic.com/v1/messages 2>&1 || echo 'UNREACHABLE'",
},
{
name: "Network: api.openai.com",
cmd: "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 https://api.openai.com/v1/models 2>&1 || echo 'UNREACHABLE'",
},
];
for (const { name, cmd } of checks) {
const result = await sandbox.exec(cmd);
console.log(`\n\x1b[1m${name}:\x1b[0m`);
console.log(result.stdout || "(empty)");
if (result.stderr) console.log(`stderr: ${result.stderr}`);
}
}
// Main
async function main() {
log.section(`Sandbox Testing (provider: ${provider})`);
// Check credentials
const anthropicKey = getAnthropicApiKey();
const openaiKey = getOpenAiApiKey();
log.info(`Anthropic API key: ${anthropicKey ? "found" : "not found"}`);
log.info(`OpenAI API key: ${openaiKey ? "found" : "not found"}`);
// Determine which agents to test
let agents: string[];
if (agentArg) {
agents = [agentArg];
} else if (anthropicKey) {
agents = ["claude"];
} else if (openaiKey) {
agents = ["codex"];
} else {
agents = ["mock"];
log.warn("No API keys found, using mock agent only");
}
log.info(`Agents to test: ${agents.join(", ")}`);
// Get provider
const prov = getProvider(provider);
// Check required env vars
for (const envVar of prov.requiredEnv) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
// Build
const binaryPath = await buildSandboxAgent();
// Create sandbox
log.section(`Creating ${prov.name} sandbox`);
const envVars: Record<string, string> = {};
if (anthropicKey) envVars.ANTHROPIC_API_KEY = anthropicKey;
if (openaiKey) envVars.OPENAI_API_KEY = openaiKey;
const sandbox = await prov.create({ envVars });
log.success(`Created sandbox: ${sandbox.id}`);
try {
// Install sandbox-agent
await installSandboxAgent(sandbox, binaryPath);
// Install agents (unless --skip-agent-install to test on-demand install like Daytona example)
if (skipAgentInstall) {
log.info("Skipping agent pre-install (testing on-demand installation)");
} else {
await installAgents(sandbox, agents);
}
// Check environment
await checkEnvironment(sandbox);
// Start server and check health
const baseUrl = await startServerAndCheckHealth(sandbox);
// Test each agent
for (const agent of agents) {
// Basic response test
const message = agent === "mock" ? "hello" : "Say hello in 10 words or less";
await testAgent(baseUrl, agent, message);
// For real agents, also test file/command actions with permission handling.
// Claude uses default permission mode and we auto-approve via API.
// Other agents can use bypass mode.
if (agent !== "mock") {
await testAgentActions(baseUrl, agent, sandbox);
}
}
log.section("All tests passed!");
if (keepAlive) {
log.info(`Sandbox ${sandbox.id} is still running. Press Ctrl+C to cleanup.`);
log.info(`Base URL: ${await sandbox.getBaseUrl(3000)}`);
await new Promise(() => {}); // Wait forever
}
} catch (err) {
log.error(`Test failed: ${err}`);
// Show diagnostics on failure
try {
await checkEnvironment(sandbox);
} catch {}
if (!keepAlive) {
await sandbox.cleanup();
}
process.exit(1);
}
if (!keepAlive) {
await sandbox.cleanup();
}
}
main().catch((err) => {
log.error(err.message || err);
process.exit(1);
});

View file

@ -1,6 +1,6 @@
{ {
"name": "acp-http-client", "name": "acp-http-client",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "Protocol-faithful ACP JSON-RPC over streamable HTTP client.", "description": "Protocol-faithful ACP JSON-RPC over streamable HTTP client.",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-shared", "name": "@sandbox-agent/cli-shared",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "Shared helpers for sandbox-agent CLI and SDK", "description": "Shared helpers for sandbox-agent CLI and SDK",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli", "name": "@sandbox-agent/cli",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "CLI for sandbox-agent - run AI coding agents in sandboxes", "description": "CLI for sandbox-agent - run AI coding agents in sandboxes",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-darwin-arm64", "name": "@sandbox-agent/cli-darwin-arm64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "sandbox-agent CLI binary for macOS ARM64", "description": "sandbox-agent CLI binary for macOS ARM64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-darwin-x64", "name": "@sandbox-agent/cli-darwin-x64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "sandbox-agent CLI binary for macOS x64", "description": "sandbox-agent CLI binary for macOS x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-linux-arm64", "name": "@sandbox-agent/cli-linux-arm64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "sandbox-agent CLI binary for Linux arm64", "description": "sandbox-agent CLI binary for Linux arm64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-linux-x64", "name": "@sandbox-agent/cli-linux-x64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "sandbox-agent CLI binary for Linux x64", "description": "sandbox-agent CLI binary for Linux x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-win32-x64", "name": "@sandbox-agent/cli-win32-x64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "sandbox-agent CLI binary for Windows x64", "description": "sandbox-agent CLI binary for Windows x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/gigacode", "name": "@sandbox-agent/gigacode",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "Gigacode CLI (sandbox-agent with OpenCode attach by default)", "description": "Gigacode CLI (sandbox-agent with OpenCode attach by default)",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/gigacode-darwin-arm64", "name": "@sandbox-agent/gigacode-darwin-arm64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "gigacode CLI binary for macOS arm64", "description": "gigacode CLI binary for macOS arm64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/gigacode-darwin-x64", "name": "@sandbox-agent/gigacode-darwin-x64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "gigacode CLI binary for macOS x64", "description": "gigacode CLI binary for macOS x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/gigacode-linux-arm64", "name": "@sandbox-agent/gigacode-linux-arm64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "gigacode CLI binary for Linux arm64", "description": "gigacode CLI binary for Linux arm64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/gigacode-linux-x64", "name": "@sandbox-agent/gigacode-linux-x64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "gigacode CLI binary for Linux x64", "description": "gigacode CLI binary for Linux x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/gigacode-win32-x64", "name": "@sandbox-agent/gigacode-win32-x64",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "gigacode CLI binary for Windows x64", "description": "gigacode CLI binary for Windows x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/persist-indexeddb", "name": "@sandbox-agent/persist-indexeddb",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "IndexedDB persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "description": "IndexedDB persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/persist-postgres", "name": "@sandbox-agent/persist-postgres",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "PostgreSQL persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "description": "PostgreSQL persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/persist-rivet", "name": "@sandbox-agent/persist-rivet",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "Rivet Actor persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "description": "Rivet Actor persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/persist-sqlite", "name": "@sandbox-agent/persist-sqlite",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "SQLite persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "description": "SQLite persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/react", "name": "@sandbox-agent/react",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "React components for Sandbox Agent frontend integrations", "description": "React components for Sandbox Agent frontend integrations",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "sandbox-agent", "name": "sandbox-agent",
"version": "0.5.0-rc.2", "version": "0.4.2",
"description": "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp.", "description": "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp.",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
@ -46,12 +46,17 @@
"./computesdk": { "./computesdk": {
"types": "./dist/providers/computesdk.d.ts", "types": "./dist/providers/computesdk.d.ts",
"import": "./dist/providers/computesdk.js" "import": "./dist/providers/computesdk.js"
},
"./sprites": {
"types": "./dist/providers/sprites.d.ts",
"import": "./dist/providers/sprites.js"
} }
}, },
"peerDependencies": { "peerDependencies": {
"@cloudflare/sandbox": ">=0.1.0", "@cloudflare/sandbox": ">=0.1.0",
"@daytonaio/sdk": ">=0.12.0", "@daytonaio/sdk": ">=0.12.0",
"@e2b/code-interpreter": ">=1.0.0", "@e2b/code-interpreter": ">=1.0.0",
"@fly/sprites": ">=0.0.1",
"@vercel/sandbox": ">=0.1.0", "@vercel/sandbox": ">=0.1.0",
"dockerode": ">=4.0.0", "dockerode": ">=4.0.0",
"get-port": ">=7.0.0", "get-port": ">=7.0.0",
@ -68,6 +73,9 @@
"@e2b/code-interpreter": { "@e2b/code-interpreter": {
"optional": true "optional": true
}, },
"@fly/sprites": {
"optional": true
},
"@vercel/sandbox": { "@vercel/sandbox": {
"optional": true "optional": true
}, },
@ -104,6 +112,7 @@
"@cloudflare/sandbox": ">=0.1.0", "@cloudflare/sandbox": ">=0.1.0",
"@daytonaio/sdk": ">=0.12.0", "@daytonaio/sdk": ">=0.12.0",
"@e2b/code-interpreter": ">=1.0.0", "@e2b/code-interpreter": ">=1.0.0",
"@fly/sprites": ">=0.0.1",
"@types/dockerode": "^4.0.0", "@types/dockerode": "^4.0.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",

View file

@ -147,3 +147,9 @@ export type {
SandboxAgentSpawnLogMode, SandboxAgentSpawnLogMode,
SandboxAgentSpawnOptions, SandboxAgentSpawnOptions,
} from "./spawn.ts"; } from "./spawn.ts";
export type {
SpritesProviderOptions,
SpritesCreateOverrides,
SpritesClientOverrides,
} from "./providers/sprites.ts";

View file

@ -4,6 +4,7 @@ import { DEFAULT_SANDBOX_AGENT_IMAGE, buildServerStartCommand } from "./shared.t
const DEFAULT_AGENT_PORT = 3000; const DEFAULT_AGENT_PORT = 3000;
const DEFAULT_PREVIEW_TTL_SECONDS = 4 * 60 * 60; const DEFAULT_PREVIEW_TTL_SECONDS = 4 * 60 * 60;
const DEFAULT_CWD = "/home/sandbox";
type DaytonaCreateParams = NonNullable<Parameters<Daytona["create"]>[0]>; type DaytonaCreateParams = NonNullable<Parameters<Daytona["create"]>[0]>;
@ -13,6 +14,7 @@ export interface DaytonaProviderOptions {
create?: DaytonaCreateOverrides | (() => DaytonaCreateOverrides | Promise<DaytonaCreateOverrides>); create?: DaytonaCreateOverrides | (() => DaytonaCreateOverrides | Promise<DaytonaCreateOverrides>);
image?: DaytonaCreateParams["image"]; image?: DaytonaCreateParams["image"];
agentPort?: number; agentPort?: number;
cwd?: string;
previewTtlSeconds?: number; previewTtlSeconds?: number;
deleteTimeoutSeconds?: number; deleteTimeoutSeconds?: number;
} }
@ -26,12 +28,13 @@ async function resolveCreateOptions(value: DaytonaProviderOptions["create"]): Pr
export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider { export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider {
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT; const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
const image = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE; const image = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE;
const cwd = options.cwd ?? DEFAULT_CWD;
const previewTtlSeconds = options.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS; const previewTtlSeconds = options.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS;
const client = new Daytona(); const client = new Daytona();
return { return {
name: "daytona", name: "daytona",
defaultCwd: "/home/daytona", defaultCwd: cwd,
async create(): Promise<string> { async create(): Promise<string> {
const createOpts = await resolveCreateOptions(options.create); const createOpts = await resolveCreateOptions(options.create);
const sandbox = await client.create({ const sandbox = await client.create({

View file

@ -5,6 +5,7 @@ import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
const DEFAULT_AGENT_PORT = 3000; const DEFAULT_AGENT_PORT = 3000;
const DEFAULT_TIMEOUT_MS = 3_600_000; const DEFAULT_TIMEOUT_MS = 3_600_000;
const SANDBOX_AGENT_PATH_EXPORT = 'export PATH="/usr/local/bin:$HOME/.local/bin:$PATH"';
type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">; type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">;
type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">; type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">;
@ -35,6 +36,11 @@ async function resolveTemplate(value: E2BTemplateOverride | undefined): Promise<
return typeof value === "function" ? await value() : value; return typeof value === "function" ? await value() : value;
} }
function buildShellCommand(command: string, strict = false): string {
const strictPrefix = strict ? "set -euo pipefail; " : "";
return `bash -lc '${strictPrefix}${SANDBOX_AGENT_PATH_EXPORT}; ${command}'`;
}
export function e2b(options: E2BProviderOptions = {}): SandboxProvider { export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT; const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@ -56,15 +62,15 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
: // eslint-disable-next-line @typescript-eslint/no-explicit-any : // eslint-disable-next-line @typescript-eslint/no-explicit-any
await Sandbox.betaCreate({ allowInternetAccess: true, ...restCreateOpts, timeoutMs, autoPause } as any); await Sandbox.betaCreate({ allowInternetAccess: true, ...restCreateOpts, timeoutMs, autoPause } as any);
await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => { await sandbox.commands.run(buildShellCommand(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`, true)).then((r) => {
if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`); if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);
}); });
for (const agent of DEFAULT_AGENTS) { for (const agent of DEFAULT_AGENTS) {
await sandbox.commands.run(`sandbox-agent install-agent ${agent}`).then((r) => { await sandbox.commands.run(buildShellCommand(`sandbox-agent install-agent ${agent}`)).then((r) => {
if (r.exitCode !== 0) throw new Error(`e2b agent install failed: ${agent}\n${r.stderr}`); if (r.exitCode !== 0) throw new Error(`e2b agent install failed: ${agent}\n${r.stderr}`);
}); });
} }
await sandbox.commands.run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, { background: true, timeoutMs: 0 }); await sandbox.commands.run(buildShellCommand(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`), { background: true, timeoutMs: 0 });
return sandbox.sandboxId; return sandbox.sandboxId;
}, },
@ -100,7 +106,7 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
async ensureServer(sandboxId: string): Promise<void> { async ensureServer(sandboxId: string): Promise<void> {
const connectOpts = await resolveOptions(options.connect, sandboxId); const connectOpts = await resolveOptions(options.connect, sandboxId);
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts); const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
await sandbox.commands.run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, { background: true, timeoutMs: 0 }); await sandbox.commands.run(buildShellCommand(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`), { background: true, timeoutMs: 0 });
}, },
}; };
} }

View file

@ -1,5 +1,7 @@
export const DEFAULT_SANDBOX_AGENT_IMAGE = "rivetdev/sandbox-agent:0.5.0-rc.2-full"; export const SANDBOX_AGENT_VERSION = "0.5.0-rc.2";
export const SANDBOX_AGENT_INSTALL_SCRIPT = "https://releases.rivet.dev/sandbox-agent/0.5.x/install.sh"; export const DEFAULT_SANDBOX_AGENT_IMAGE = `rivetdev/sandbox-agent:${SANDBOX_AGENT_VERSION}-full`;
export const SANDBOX_AGENT_INSTALL_SCRIPT = `https://releases.rivet.dev/sandbox-agent/${SANDBOX_AGENT_VERSION}/install.sh`;
export const SANDBOX_AGENT_NPX_SPEC = `@sandbox-agent/cli@${SANDBOX_AGENT_VERSION}`;
export const DEFAULT_AGENTS = ["claude", "codex"] as const; export const DEFAULT_AGENTS = ["claude", "codex"] as const;
export function buildServerStartCommand(port: number): string { export function buildServerStartCommand(port: number): string {

View file

@ -0,0 +1,267 @@
import { ExecError, SpritesClient, type ClientOptions as SpritesClientOptions, type SpriteConfig } from "@fly/sprites";
import { SandboxDestroyedError } from "../client.ts";
import type { SandboxProvider } from "./types.ts";
import { SANDBOX_AGENT_NPX_SPEC } from "./shared.ts";
const DEFAULT_AGENT_PORT = 8080;
const DEFAULT_SERVICE_NAME = "sandbox-agent";
const DEFAULT_NAME_PREFIX = "sandbox-agent";
const DEFAULT_SERVICE_START_DURATION = "10m";
export interface SpritesCreateOverrides {
name?: string;
config?: SpriteConfig;
}
export type SpritesClientOverrides = Partial<SpritesClientOptions>;
export interface SpritesProviderOptions {
token?: string | (() => string | Promise<string>);
client?: SpritesClientOverrides | (() => SpritesClientOverrides | Promise<SpritesClientOverrides>);
create?: SpritesCreateOverrides | (() => SpritesCreateOverrides | Promise<SpritesCreateOverrides>);
env?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
installAgents?: readonly string[];
agentPort?: number;
serviceName?: string;
serviceStartDuration?: string;
namePrefix?: string;
}
type SpritesSandboxProvider = SandboxProvider & {
getToken(sandboxId: string): Promise<string>;
};
interface SpritesService {
cmd?: string;
args?: string[];
http_port?: number | null;
state?: {
status?: string;
};
}
async function resolveValue<T>(value: T | (() => T | Promise<T>) | undefined, fallback: T): Promise<T> {
if (value === undefined) {
return fallback;
}
if (typeof value === "function") {
return await (value as () => T | Promise<T>)();
}
return value;
}
async function resolveToken(value: SpritesProviderOptions["token"]): Promise<string> {
const token = await resolveValue(value, process.env.SPRITES_API_KEY ?? process.env.SPRITE_TOKEN ?? process.env.SPRITES_TOKEN ?? "");
if (!token) {
throw new Error("sprites provider requires a token. Set SPRITES_API_KEY (or SPRITE_TOKEN) or pass `token`.");
}
return token;
}
function createSpritesClient(token: string, options: SpritesClientOverrides): SpritesClient {
return new SpritesClient(token, options);
}
function generateSpriteName(prefix: string): string {
const suffix =
typeof globalThis.crypto?.randomUUID === "function"
? globalThis.crypto.randomUUID().slice(0, 8)
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
return `${prefix}-${suffix}`.toLowerCase();
}
function isSpriteNotFoundError(error: unknown): boolean {
return error instanceof Error && error.message.startsWith("Sprite not found:");
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function buildServiceCommand(env: Record<string, string>, port: number): string {
const exportParts: string[] = [];
for (const [key, value] of Object.entries(env)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
throw new Error(`sprites provider received an invalid environment variable name: ${key}`);
}
exportParts.push(`export ${key}=${shellQuote(value)}`);
}
exportParts.push(`exec npx -y ${SANDBOX_AGENT_NPX_SPEC} server --no-token --host 0.0.0.0 --port ${port}`);
return exportParts.join("; ");
}
async function runSpriteCommand(sprite: ReturnType<SpritesClient["sprite"]>, file: string, args: string[], env?: Record<string, string>): Promise<void> {
try {
const result = await sprite.execFile(file, args, env ? { env } : undefined);
if (result.exitCode !== 0) {
throw new Error(`sprites command failed: ${file} ${args.join(" ")}`);
}
} catch (error) {
if (error instanceof ExecError) {
throw new Error(
`sprites command failed: ${file} ${args.join(" ")} (exit ${error.exitCode})\nstdout:\n${String(error.stdout)}\nstderr:\n${String(error.stderr)}`,
{ cause: error },
);
}
throw error;
}
}
async function fetchService(client: SpritesClient, spriteName: string, serviceName: string): Promise<SpritesService | undefined> {
const response = await fetch(`${client.baseURL}/v1/sprites/${encodeURIComponent(spriteName)}/services/${encodeURIComponent(serviceName)}`, {
method: "GET",
headers: {
Authorization: `Bearer ${client.token}`,
},
});
if (response.status === 404) {
return undefined;
}
if (!response.ok) {
throw new Error(`sprites service lookup failed (status ${response.status}): ${await response.text()}`);
}
return (await response.json()) as SpritesService;
}
async function upsertService(client: SpritesClient, spriteName: string, serviceName: string, port: number, command: string): Promise<void> {
const existing = await fetchService(client, spriteName, serviceName);
const expectedArgs = ["-lc", command];
const isCurrent = existing?.cmd === "bash" && existing.http_port === port && JSON.stringify(existing.args ?? []) === JSON.stringify(expectedArgs);
if (isCurrent) {
return;
}
const response = await fetch(`${client.baseURL}/v1/sprites/${encodeURIComponent(spriteName)}/services/${encodeURIComponent(serviceName)}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${client.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
cmd: "bash",
args: expectedArgs,
http_port: port,
}),
});
if (!response.ok) {
throw new Error(`sprites service upsert failed (status ${response.status}): ${await response.text()}`);
}
}
async function startServiceIfNeeded(client: SpritesClient, spriteName: string, serviceName: string, duration: string): Promise<void> {
const existing = await fetchService(client, spriteName, serviceName);
if (existing?.state?.status === "running" || existing?.state?.status === "starting") {
return;
}
const response = await fetch(
`${client.baseURL}/v1/sprites/${encodeURIComponent(spriteName)}/services/${encodeURIComponent(serviceName)}/start?duration=${encodeURIComponent(duration)}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${client.token}`,
},
},
);
if (!response.ok) {
throw new Error(`sprites service start failed (status ${response.status}): ${await response.text()}`);
}
await response.text();
}
async function ensureService(
client: SpritesClient,
spriteName: string,
serviceName: string,
port: number,
duration: string,
env: Record<string, string>,
): Promise<void> {
const command = buildServiceCommand(env, port);
await upsertService(client, spriteName, serviceName, port, command);
await startServiceIfNeeded(client, spriteName, serviceName, duration);
}
export function sprites(options: SpritesProviderOptions = {}): SandboxProvider {
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
const serviceName = options.serviceName ?? DEFAULT_SERVICE_NAME;
const serviceStartDuration = options.serviceStartDuration ?? DEFAULT_SERVICE_START_DURATION;
const namePrefix = options.namePrefix ?? DEFAULT_NAME_PREFIX;
const installAgents = [...(options.installAgents ?? [])];
const getClient = async (): Promise<SpritesClient> => {
const token = await resolveToken(options.token);
const clientOptions = await resolveValue(options.client, {});
return createSpritesClient(token, clientOptions);
};
const getServerEnv = async (): Promise<Record<string, string>> => {
return await resolveValue(options.env, {});
};
const provider: SpritesSandboxProvider = {
name: "sprites",
defaultCwd: "/home/sprite",
async create(): Promise<string> {
const client = await getClient();
const createOptions = await resolveValue(options.create, {});
const spriteName = createOptions.name ?? generateSpriteName(namePrefix);
const sprite = await client.createSprite(spriteName, createOptions.config);
const serverEnv = await getServerEnv();
for (const agent of installAgents) {
await runSpriteCommand(sprite, "bash", ["-lc", `npx -y ${SANDBOX_AGENT_NPX_SPEC} install-agent ${agent}`], serverEnv);
}
await ensureService(client, spriteName, serviceName, agentPort, serviceStartDuration, serverEnv);
return sprite.name;
},
async destroy(sandboxId: string): Promise<void> {
const client = await getClient();
try {
await client.deleteSprite(sandboxId);
} catch (error) {
if (isSpriteNotFoundError(error) || (error instanceof Error && error.message.includes("status 404"))) {
return;
}
throw error;
}
},
async reconnect(sandboxId: string): Promise<void> {
const client = await getClient();
try {
await client.getSprite(sandboxId);
} catch (error) {
if (isSpriteNotFoundError(error)) {
throw new SandboxDestroyedError(sandboxId, "sprites", { cause: error });
}
throw error;
}
},
async getUrl(sandboxId: string): Promise<string> {
const client = await getClient();
const sprite = await client.getSprite(sandboxId);
const url = (sprite as { url?: string }).url;
if (!url) {
throw new Error(`sprites API did not return a URL for sprite: ${sandboxId}`);
}
return url;
},
async ensureServer(sandboxId: string): Promise<void> {
const client = await getClient();
await ensureService(client, sandboxId, serviceName, agentPort, serviceStartDuration, await getServerEnv());
},
async getToken(): Promise<string> {
return await resolveToken(options.token);
},
};
return provider;
}

View file

@ -29,6 +29,12 @@ const computeSdkMocks = vi.hoisted(() => ({
getById: vi.fn(), getById: vi.fn(),
})); }));
const spritesMocks = vi.hoisted(() => ({
createSprite: vi.fn(),
getSprite: vi.fn(),
deleteSprite: vi.fn(),
}));
vi.mock("@e2b/code-interpreter", () => ({ vi.mock("@e2b/code-interpreter", () => ({
NotFoundError: e2bMocks.MockNotFoundError, NotFoundError: e2bMocks.MockNotFoundError,
Sandbox: { Sandbox: {
@ -58,9 +64,26 @@ vi.mock("computesdk", () => ({
}, },
})); }));
vi.mock("@fly/sprites", () => ({
SpritesClient: class MockSpritesClient {
readonly token: string;
readonly baseURL: string;
constructor(token: string, options: { baseURL?: string } = {}) {
this.token = token;
this.baseURL = options.baseURL ?? "https://api.sprites.dev";
}
createSprite = spritesMocks.createSprite;
getSprite = spritesMocks.getSprite;
deleteSprite = spritesMocks.deleteSprite;
},
}));
import { e2b } from "../src/providers/e2b.ts"; import { e2b } from "../src/providers/e2b.ts";
import { modal } from "../src/providers/modal.ts"; import { modal } from "../src/providers/modal.ts";
import { computesdk } from "../src/providers/computesdk.ts"; import { computesdk } from "../src/providers/computesdk.ts";
import { sprites } from "../src/providers/sprites.ts";
function createFetch(): typeof fetch { function createFetch(): typeof fetch {
return async () => new Response(null, { status: 200 }); return async () => new Response(null, { status: 200 });
@ -110,6 +133,9 @@ beforeEach(() => {
modalMocks.sandboxFromId.mockReset(); modalMocks.sandboxFromId.mockReset();
computeSdkMocks.create.mockReset(); computeSdkMocks.create.mockReset();
computeSdkMocks.getById.mockReset(); computeSdkMocks.getById.mockReset();
spritesMocks.createSprite.mockReset();
spritesMocks.getSprite.mockReset();
spritesMocks.deleteSprite.mockReset();
}); });
describe("SandboxAgent provider lifecycle", () => { describe("SandboxAgent provider lifecycle", () => {
@ -308,7 +334,7 @@ describe("modal provider", () => {
expect(modalMocks.appsFromName).toHaveBeenCalledWith("custom-app", { createIfMissing: true }); expect(modalMocks.appsFromName).toHaveBeenCalledWith("custom-app", { createIfMissing: true });
expect(modalMocks.imageFromRegistry).toHaveBeenCalledWith("python:3.12-slim"); expect(modalMocks.imageFromRegistry).toHaveBeenCalledWith("python:3.12-slim");
expect(image.dockerfileCommands).toHaveBeenCalled(); expect(image.dockerfileCommands).not.toHaveBeenCalled();
expect(modalMocks.sandboxCreate).toHaveBeenCalledWith( expect(modalMocks.sandboxCreate).toHaveBeenCalledWith(
app, app,
image, image,
@ -347,3 +373,139 @@ describe("computesdk provider", () => {
); );
}); });
}); });
describe("sprites provider", () => {
it("creates a sprite, installs sandbox-agent, and configures the managed service", async () => {
const sprite = {
name: "sprite-1",
execFile: vi.fn(async () => ({ stdout: "", stderr: "", exitCode: 0 })),
};
spritesMocks.createSprite.mockResolvedValue(sprite);
const fetchMock = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(new Response(null, { status: 404 }))
.mockResolvedValueOnce(new Response(JSON.stringify({}), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ state: { status: "stopped" } }), { status: 200 }))
.mockResolvedValueOnce(new Response("", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const provider = sprites({
token: "sprite-token",
create: {
name: "sprite-1",
},
env: {
OPENAI_API_KEY: "test'value",
},
});
await expect(provider.create()).resolves.toBe("sprite-1");
expect(spritesMocks.createSprite).toHaveBeenCalledWith("sprite-1", undefined);
expect(sprite.execFile).not.toHaveBeenCalled();
const putCall = fetchMock.mock.calls.find(([url, init]) => String(url).includes("/services/sandbox-agent") && init?.method === "PUT");
expect(putCall).toBeDefined();
expect(String(putCall?.[0])).toContain("/v1/sprites/sprite-1/services/sandbox-agent");
expect(putCall?.[1]?.headers).toMatchObject({
Authorization: "Bearer sprite-token",
"Content-Type": "application/json",
});
const serviceRequest = JSON.parse(String(putCall?.[1]?.body)) as { args: string[] };
expect(serviceRequest.args[1]).toContain("exec npx -y @sandbox-agent/cli@0.5.0-rc.2 server --no-token --host 0.0.0.0 --port 8080");
expect(serviceRequest.args[1]).toContain("OPENAI_API_KEY='test'\\''value'");
});
it("optionally installs agents through npx when requested", async () => {
const sprite = {
name: "sprite-1",
execFile: vi.fn(async () => ({ stdout: "", stderr: "", exitCode: 0 })),
};
spritesMocks.createSprite.mockResolvedValue(sprite);
const fetchMock = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(new Response(null, { status: 404 }))
.mockResolvedValueOnce(new Response(JSON.stringify({}), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ state: { status: "stopped" } }), { status: 200 }))
.mockResolvedValueOnce(new Response("", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const provider = sprites({
token: "sprite-token",
create: { name: "sprite-1" },
env: { OPENAI_API_KEY: "test" },
installAgents: ["claude", "codex"],
});
await provider.create();
expect(sprite.execFile).toHaveBeenCalledWith("bash", ["-lc", "npx -y @sandbox-agent/cli@0.5.0-rc.2 install-agent claude"], {
env: { OPENAI_API_KEY: "test" },
});
expect(sprite.execFile).toHaveBeenCalledWith("bash", ["-lc", "npx -y @sandbox-agent/cli@0.5.0-rc.2 install-agent codex"], {
env: { OPENAI_API_KEY: "test" },
});
});
it("returns the sprite URL and provider token for authenticated access", async () => {
spritesMocks.getSprite.mockResolvedValue({
name: "sprite-1",
url: "https://sprite-1.sprites.app",
});
const provider = sprites({
token: "sprite-token",
});
await expect(provider.getUrl?.("sprite-1")).resolves.toBe("https://sprite-1.sprites.app");
await expect((provider as SandboxProvider & { getToken: (sandboxId: string) => Promise<string> }).getToken("sprite-1")).resolves.toBe("sprite-token");
});
it("maps missing reconnect targets to SandboxDestroyedError", async () => {
spritesMocks.getSprite.mockRejectedValue(new Error("Sprite not found: missing-sprite"));
const provider = sprites({
token: "sprite-token",
});
await expect(provider.reconnect?.("missing-sprite")).rejects.toBeInstanceOf(SandboxDestroyedError);
});
it("skips starting the service when the desired service is already running", async () => {
const fetchMock = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
cmd: "bash",
args: ["-lc", "exec npx -y @sandbox-agent/cli@0.5.0-rc.2 server --no-token --host 0.0.0.0 --port 8080"],
http_port: 8080,
state: { status: "running" },
}),
{ status: 200 },
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
cmd: "bash",
args: ["-lc", "exec npx -y @sandbox-agent/cli@0.5.0-rc.2 server --no-token --host 0.0.0.0 --port 8080"],
http_port: 8080,
state: { status: "running" },
}),
{ status: 200 },
),
);
vi.stubGlobal("fetch", fetchMock);
const provider = sprites({
token: "sprite-token",
});
await provider.ensureServer?.("sprite-1");
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls.every(([, init]) => init?.method === "GET")).toBe(true);
});
});

Some files were not shown because too many files have changed in this diff Show more