chore: sync workspace changes

This commit is contained in:
Nathan Flurry 2026-01-26 22:29:10 -08:00
parent 4b5b390b7f
commit 4083baa1c1
55 changed files with 2431 additions and 840 deletions

View file

@ -77,6 +77,18 @@ jobs:
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Build inspector frontend
run: |
pnpm install
SANDBOX_AGENT_SKIP_INSPECTOR=1 pnpm --filter @sandbox-agent/inspector build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View file

@ -1,174 +0,0 @@
# Architecture
This document covers three key architectural areas of the sandbox-daemon system.
## Agent Schema Pipeline
The schema pipeline extracts type definitions from AI coding agents and converts them to a universal format.
### Schema Extraction
TypeScript extractors in `resources/agent-schemas/src/` pull schemas from each agent:
| Agent | Source | Extractor |
|-------|--------|-----------|
| Claude | `claude --output-format json --json-schema` | `claude.ts` |
| Codex | `codex app-server generate-json-schema` | `codex.ts` |
| OpenCode | GitHub OpenAPI spec | `opencode.ts` |
| Amp | Scrapes ampcode.com docs | `amp.ts` |
All extractors include fallback schemas for when CLIs or URLs are unavailable.
**Output:** JSON schemas written to `resources/agent-schemas/artifacts/json-schema/`
### Rust Type Generation
The `server/packages/extracted-agent-schemas/` package generates Rust types at build time:
- `build.rs` reads JSON schemas and uses the `typify` crate to generate Rust structs
- Generated code is written to `$OUT_DIR/{agent}.rs`
- Types are exposed via `include!()` macros in `src/lib.rs`
```
resources/agent-schemas/artifacts/json-schema/*.json
↓ (build.rs + typify)
$OUT_DIR/{claude,codex,opencode,amp}.rs
↓ (include!)
extracted_agent_schemas::{claude,codex,opencode,amp}::*
```
### Universal Schema
The `server/packages/universal-agent-schema/` package defines agent-agnostic types:
**Core types** (`src/lib.rs`):
- `UniversalEvent` - Wrapper with id, timestamp, session_id, agent, data
- `UniversalEventData` - Enum: Message, Started, Error, QuestionAsked, PermissionAsked, Unknown
- `UniversalMessage` - Parsed (role, parts, metadata) or Unparsed (raw JSON)
- `UniversalMessagePart` - Text, ToolCall, ToolResult, FunctionCall, FunctionResult, File, Image, Error, Unknown
**Converters** (`src/agents/{claude,codex,opencode,amp}.rs`):
- Each agent has a converter module that transforms native events to universal format
- Conversions are best-effort; unparseable data preserved in `Unparsed` or `Unknown` variants
## Session Management
Sessions track agent conversations with in-memory state.
### Storage
Sessions are stored in an in-memory `HashMap<String, SessionState>` inside `SessionManager`:
```rust
struct SessionManager {
sessions: Mutex<HashMap<String, SessionState>>,
// ...
}
```
There is no disk persistence. Sessions are ephemeral and lost on server restart.
### SessionState
Each session tracks:
| Field | Purpose |
|-------|---------|
| `session_id` | Client-provided identifier |
| `agent` | Agent type (Claude, Codex, OpenCode, Amp) |
| `agent_mode` | Operating mode (build, plan, custom) |
| `permission_mode` | Permission handling (default, plan, bypass) |
| `model` | Optional model override |
| `events: Vec<UniversalEvent>` | Full event history |
| `pending_questions` | Question IDs awaiting reply |
| `pending_permissions` | Permission IDs awaiting reply |
| `broadcaster` | Tokio broadcast channel for SSE streaming |
| `ended` | Whether agent process has terminated |
### Lifecycle
```
POST /v1/sessions/{sessionId} Create session, auto-install agent
POST /v1/sessions/{id}/messages Spawn agent subprocess, stream output
GET /v1/sessions/{id}/events Poll for new events (offset-based)
GET /v1/sessions/{id}/events/sse Subscribe to SSE stream
POST .../questions/{id}/reply Answer agent question
POST .../permissions/{id}/reply Grant/deny permission request
(agent process terminates) Session marked as ended
```
### Event Flow
When a message is sent:
1. `send_message()` spawns the agent CLI as a subprocess
2. `consume_spawn()` reads stdout/stderr line by line
3. Each JSON line is parsed and converted via `parse_agent_line()`
4. Events are recorded via `record_event()` which:
- Assigns incrementing event ID
- Appends to `events` vector
- Broadcasts to SSE subscribers
## SDK Modes
The TypeScript SDK supports two connection modes.
### Embedded Mode
Defined in `sdks/typescript/src/spawn.ts`:
1. **Binary resolution**: Checks `SANDBOX_AGENT_BIN` env, then platform-specific npm package, then `PATH`
2. **Port selection**: Uses provided port or finds a free one via `net.createServer()`
3. **Token generation**: Uses provided token or generates random 24-byte hex string
4. **Spawn**: Launches `sandbox-agent --host <host> --port <port> --token <token>`
5. **Health wait**: Polls `GET /v1/health` until server is ready (up to 15s timeout)
6. **Cleanup**: On dispose, sends SIGTERM then SIGKILL if needed; also registers process exit handlers
```typescript
const handle = await spawnSandboxDaemon({ log: "inherit" });
// handle.baseUrl = "http://127.0.0.1:<port>"
// handle.token = "<generated>"
// handle.dispose() to cleanup
```
### Server Mode
Defined in `sdks/typescript/src/client.ts`:
- Direct HTTP client to a remote `sandbox-agent` server
- Uses provided `baseUrl` and optional `token`
- No subprocess management
```typescript
const client = new SandboxDaemonClient({
baseUrl: "http://remote-server:8080",
token: "secret",
});
```
### Auto-Detection
`SandboxDaemonClient.connect()` chooses the mode automatically:
```typescript
// If baseUrl provided → server mode
const client = await SandboxDaemonClient.connect({
baseUrl: "http://remote:8080",
});
// If no baseUrl → embedded mode (spawns subprocess)
const client = await SandboxDaemonClient.connect({});
// Explicit control
const client = await SandboxDaemonClient.connect({
spawn: { enabled: true, port: 9000 },
});
```
The `spawn` option can be:
- `true` / `false` - Enable/disable embedded mode
- `SandboxDaemonSpawnOptions` - Fine-grained control over host, port, token, binary path, timeout, logging

View file

@ -67,6 +67,7 @@ zip = { version = "0.6", default-features = false, features = ["deflate"] }
# Misc
url = "2.5"
regress = "0.10"
include_dir = "0.7"
# Code generation (build deps)
typify = "0.4"

View file

@ -3,17 +3,16 @@
Universal API for running Claude Code, Codex, OpenCode, and Amp inside sandboxes.
- **Any coding agent**: Universal API to interact with all agents with full feature coverage
- **Server, stdin/stdout, or SDK mode**: Run as an HTTP server, CLI using stdin/stdout, or with the SDK
- **Server or SDK mode**: Run as an HTTP server or with the TypeScript SDK
- **Universal session schema**: Universal schema to store agent transcripts
- **Supports your sandbox provider**: Daytona, E2B, Vercel Sandboxes, and more
- **Lightweight, portable Rust binary**: Install anywhere with 1 curl command
- **OpenAPI spec**: Versioned API schema tracked in `sdks/openapi/openapi.json`
- **OpenAPI spec**: Versioned API schema tracked in `docs/openapi.json`
Coming soon:
Roadmap:
- **Vercel AI SDK Compatibility**: Works with existing AI SDK tooling, like `useChat`
- **Auto-configure MCP & Skills**: Auto-load MCP servers & skills for your agents
- **Process & logs manager**: Manage processes, logs, and ports for your agents to run background processes
[ ] Python SDK
[ ] Automatic MCP & skillfile configuration
## Agent Support
@ -85,5 +84,12 @@ The server is a single Rust binary that runs anywhere with a curl install. If yo
**Can I use this with my personal API keys?**
Yes. Use `sandbox-agent credentials extract-env` to extract API keys from your local agent configs (Claude Code, Codex, OpenCode, Amp) and pass them to the sandbox environment.
**Why rust?**
**Why Rust?**
TODO
**Why not use stdio/JSON-RPC?**
- has benefit of not having to listen on a port
- more difficult to interact with, harder to analyze, doesn't support inspector for debugging
- may add at some point
- codex does this. claude sort of does this.

View file

@ -1,18 +1,30 @@
## soon
## launch
- implement stdin/stdout
- switch sdk to use sdtin/stdout for embedded mdoe
- re-review agent schemas and compare it to ours
- auto-serve frontend from cli
- verify embedded sdk works
- fix bugs in ui
- double messages
- user-sent messages
- permissions
- consider migraing our standard to match the vercel ai standard
- discuss actor arch in readme + give example
- skillfile
- specifically include the release checklist
- image/etc input
## soon
- **Vercel AI SDK Compatibility**: Works with existing AI SDK tooling, like `useChat`
- **Auto-configure MCP & Skills**: Auto-load MCP servers & skills for your agents
- **Process & logs manager**: Manage processes, logs, and ports for your agents to run background processes
## later
- review all flags available on coding agents clis
- set up agent to check diffs in versions to recommend updates
- auto-updating for long running job
- persistence
- system information/cpu/etc
- git utils
- api features
- list agent modes available
- list models available

View file

@ -19,8 +19,9 @@ description: "Supported agents, install methods, and streaming formats."
## Capability notes
- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event.
- **Streaming**: all agents stream events; OpenCode uses SSE, Codex uses JSON-RPC over stdio, others use JSONL.
- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event (tests do not currently exercise Claude question/permission flows).
- **Streaming**: all agents stream events; OpenCode uses SSE, Codex uses JSON-RPC over stdio, others use JSONL. Codex is currently normalized to thread/turn starts plus user/assistant completed items (deltas and tool/reasoning items are not emitted yet).
- **User messages**: Claude CLI output does not include explicit user-message events in our snapshots, so only assistant messages are surfaced for Claude today.
- **Files and images**: normalized via `UniversalMessagePart` with `File` and `Image` parts.
See [Universal API](/universal-api) for feature coverage details.

View file

@ -3,12 +3,12 @@ title: "CLI"
description: "CLI reference and server flags."
---
The `sandbox-agent` CLI mirrors the HTTP API so you can script everything without writing client code.
The `sandbox-daemon` CLI mirrors the HTTP API so you can script everything without writing client code.
## Server flags
```bash
sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
sandbox-daemon server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
```
- `--token`: global token for all requests.
@ -22,7 +22,7 @@ sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
<summary><strong>agents list</strong></summary>
```bash
sandbox-agent agents list --endpoint http://127.0.0.1:2468
sandbox-daemon agents list --endpoint http://127.0.0.1:2468
```
</details>
@ -30,7 +30,7 @@ sandbox-agent agents list --endpoint http://127.0.0.1:2468
<summary><strong>agents install</strong></summary>
```bash
sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:2468
sandbox-daemon agents install claude --reinstall --endpoint http://127.0.0.1:2468
```
</details>
@ -38,7 +38,7 @@ sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:2468
<summary><strong>agents modes</strong></summary>
```bash
sandbox-agent agents modes claude --endpoint http://127.0.0.1:2468
sandbox-daemon agents modes claude --endpoint http://127.0.0.1:2468
```
</details>
@ -48,7 +48,7 @@ sandbox-agent agents modes claude --endpoint http://127.0.0.1:2468
<summary><strong>sessions create</strong></summary>
```bash
sandbox-agent sessions create my-session \
sandbox-daemon sessions create my-session \
--agent claude \
--agent-mode build \
--permission-mode default \
@ -60,7 +60,7 @@ sandbox-agent sessions create my-session \
<summary><strong>sessions send-message</strong></summary>
```bash
sandbox-agent sessions send-message my-session \
sandbox-daemon sessions send-message my-session \
--message "Summarize the repository" \
--endpoint http://127.0.0.1:2468
```
@ -70,7 +70,7 @@ sandbox-agent sessions send-message my-session \
<summary><strong>sessions events</strong></summary>
```bash
sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468
sandbox-daemon sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468
```
</details>
@ -78,7 +78,7 @@ sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http:/
<summary><strong>sessions events-sse</strong></summary>
```bash
sandbox-agent sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468
sandbox-daemon sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468
```
</details>
@ -86,7 +86,7 @@ sandbox-agent sessions events-sse my-session --offset 0 --endpoint http://127.0.
<summary><strong>sessions reply-question</strong></summary>
```bash
sandbox-agent sessions reply-question my-session QUESTION_ID \
sandbox-daemon sessions reply-question my-session QUESTION_ID \
--answers "yes" \
--endpoint http://127.0.0.1:2468
```
@ -96,7 +96,7 @@ sandbox-agent sessions reply-question my-session QUESTION_ID \
<summary><strong>sessions reject-question</strong></summary>
```bash
sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468
sandbox-daemon sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468
```
</details>
@ -104,7 +104,7 @@ sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http://
<summary><strong>sessions reply-permission</strong></summary>
```bash
sandbox-agent sessions reply-permission my-session PERMISSION_ID \
sandbox-daemon sessions reply-permission my-session PERMISSION_ID \
--reply once \
--endpoint http://127.0.0.1:2468
```

View file

@ -12,7 +12,7 @@ description: "Deploy the daemon in Cloudflare Sandboxes."
```bash
export SANDBOX_TOKEN="..."
cargo run -p sandbox-agent -- \
cargo run -p sandbox-agent -- server \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 2468

View file

@ -12,7 +12,7 @@ description: "Run the daemon in a Daytona workspace."
```bash
export SANDBOX_TOKEN="..."
cargo run -p sandbox-agent -- \
cargo run -p sandbox-agent -- server \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 2468

View file

@ -21,7 +21,7 @@ The binary will be written to `./artifacts/sandbox-agent-x86_64-unknown-linux-mu
docker run --rm -p 2468:2468 \
-v "$PWD/artifacts:/artifacts" \
debian:bookworm-slim \
/artifacts/sandbox-agent-x86_64-unknown-linux-musl --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468
/artifacts/sandbox-agent-x86_64-unknown-linux-musl server --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468
```
You can now access the API at `http://localhost:2468`.

View file

@ -16,7 +16,7 @@ export SANDBOX_TOKEN="..."
# Install sandbox-agent binary (or build from source)
# TODO: replace with release download once published
cargo run -p sandbox-agent -- \
cargo run -p sandbox-agent -- server \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 2468

View file

@ -12,7 +12,7 @@ description: "Run the daemon inside Vercel Sandboxes."
```bash
export SANDBOX_TOKEN="..."
cargo run -p sandbox-agent -- \
cargo run -p sandbox-agent -- server \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 2468

View file

@ -45,6 +45,10 @@
"http-api",
"typescript-sdk"
]
},
{
"group": "API",
"openapi": "openapi.json"
}
]
},

View file

@ -17,4 +17,6 @@ The UI expects:
- Endpoint (e.g. `http://127.0.0.1:2468`)
- Optional token
If you see CORS errors, enable CORS on the daemon with `--cors-allow-origin` and related flags.
When running the daemon, the inspector is also served automatically at `http://127.0.0.1:2468/ui`.
If you see CORS errors, enable CORS on the daemon with `sandbox-daemon server --cors-allow-origin` and related flags.

View file

@ -18,7 +18,7 @@ Sandbox Agent SDK is a universal API and daemon for running coding agents inside
Run the daemon locally:
```bash
sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
sandbox-daemon server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
```
Send a message:

View file

@ -11,6 +11,11 @@
},
"version": "0.1.0"
},
"servers": [
{
"url": "http://localhost:2468"
}
],
"paths": {
"/v1/agents": {
"get": {

View file

@ -8,13 +8,19 @@ description: "Start the daemon and send your first message."
Use the installed binary, or `cargo run` in development.
```bash
sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
sandbox-daemon server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
```
If you want to run without auth (local dev only):
```bash
sandbox-agent --no-token --host 127.0.0.1 --port 2468
sandbox-daemon server --no-token --host 127.0.0.1 --port 2468
```
If you're running from source instead of the installed CLI:
```bash
cargo run -p sandbox-agent -- server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
```
### CORS (frontend usage)
@ -22,7 +28,7 @@ sandbox-agent --no-token --host 127.0.0.1 --port 2468
If you are calling the daemon from a browser, enable CORS explicitly:
```bash
sandbox-agent \
sandbox-daemon server \
--token "$SANDBOX_TOKEN" \
--cors-allow-origin "http://localhost:5173" \
--cors-allow-method "GET" \
@ -69,7 +75,7 @@ curl "http://127.0.0.1:2468/v1/sessions/my-session/events/sse?offset=0" \
The CLI mirrors the HTTP API:
```bash
sandbox-agent sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
sandbox-daemon sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
sandbox-agent sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
sandbox-daemon sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
```

View file

@ -13,7 +13,7 @@ pnpm --filter sandbox-agent generate
This runs:
- `cargo run -p sandbox-agent-openapi-gen` to emit OpenAPI JSON
- `cargo run -p sandbox-agent-openapi-gen -- --out docs/openapi.json` to emit OpenAPI JSON
- `openapi-typescript` to generate types
## Usage

View file

@ -7,7 +7,8 @@
"scripts": {
"dev": "vite",
"build": "pnpm --filter sandbox-agent build && vite build",
"preview": "vite preview"
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"sandbox-agent": "workspace:*",

View file

@ -85,8 +85,17 @@ const formatTime = (value: string) => {
return date.toLocaleTimeString();
};
const getDefaultEndpoint = () => {
if (typeof window === "undefined") return "http://127.0.0.1:2468";
const { origin, protocol } = window.location;
if (!origin || origin === "null" || protocol === "file:") {
return "http://127.0.0.1:2468";
}
return origin;
};
export default function App() {
const [endpoint, setEndpoint] = useState("http://localhost:2468");
const [endpoint, setEndpoint] = useState(getDefaultEndpoint);
const [token, setToken] = useState("");
const [connected, setConnected] = useState(false);
const [connecting, setConnecting] = useState(false);
@ -195,18 +204,25 @@ export default function App() {
return error instanceof Error ? error.message : fallback;
};
const connect = async () => {
const connectToDaemon = async (reportError: boolean) => {
setConnecting(true);
setConnectError(null);
if (reportError) {
setConnectError(null);
}
try {
const client = createClient();
await client.getHealth();
setConnected(true);
await refreshAgents();
await fetchSessions();
if (reportError) {
setConnectError(null);
}
} catch (error) {
const message = getErrorMessage(error, "Unable to connect");
setConnectError(message);
if (reportError) {
const message = getErrorMessage(error, "Unable to connect");
setConnectError(message);
}
setConnected(false);
clientRef.current = null;
} finally {
@ -214,6 +230,8 @@ export default function App() {
}
};
const connect = () => connectToDaemon(true);
const disconnect = () => {
setConnected(false);
clientRef.current = null;
@ -531,10 +549,10 @@ export default function App() {
.filter((event): event is UniversalEvent & { data: { message: UniversalMessage } } => "message" in event.data)
.map((event) => {
const msg = event.data.message;
const parts = "parts" in msg ? msg.parts : [];
const parts = ("parts" in msg ? msg.parts : []) ?? [];
const content = parts
.filter((part: UniversalMessagePart) => part.type === "text" && part.text)
.map((part: UniversalMessagePart) => part.text)
.filter((part: UniversalMessagePart): part is UniversalMessagePart & { type: "text"; text: string } => part.type === "text" && "text" in part && typeof part.text === "string")
.map((part) => part.text)
.join("\n");
return {
id: event.id,
@ -553,6 +571,20 @@ export default function App() {
};
}, []);
useEffect(() => {
let active = true;
const attempt = async () => {
await connectToDaemon(false);
};
attempt().catch(() => {
if (!active) return;
setConnecting(false);
});
return () => {
active = false;
};
}, []);
useEffect(() => {
if (!connected) return;
refreshAgents();
@ -672,7 +704,7 @@ export default function App() {
<p className="hint">
Start the daemon with CORS enabled for browser access:<br />
<code>sandbox-agent --cors-allow-origin http://localhost:5173</code>
<code>sandbox-daemon server --cors-allow-origin http://localhost:5173</code>
</p>
</div>
</div>

View file

@ -1,9 +1,10 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
export default defineConfig(({ command }) => ({
base: command === "build" ? "/ui/" : "/",
plugins: [react()],
server: {
port: 5173
}
});
}));

View file

@ -6,7 +6,8 @@
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"generate": "turbo run generate"
"generate": "turbo run generate",
"typecheck": "turbo run typecheck"
},
"devDependencies": {
"turbo": "^2.4.0"

83
pnpm-lock.yaml generated
View file

@ -67,6 +67,9 @@ importers:
specifier: ^5.7.0
version: 5.9.3
devDependencies:
'@types/json-schema':
specifier: ^7.0.15
version: 7.0.15
'@types/node':
specifier: ^22.0.0
version: 22.19.7
@ -74,6 +77,34 @@ importers:
specifier: ^4.19.0
version: 4.21.0
resources/vercel-ai-sdk-schemas:
dependencies:
semver:
specifier: ^7.6.3
version: 7.7.3
tar:
specifier: ^7.0.0
version: 7.5.6
ts-json-schema-generator:
specifier: ^2.4.0
version: 2.4.0
typescript:
specifier: ^5.7.0
version: 5.9.3
devDependencies:
'@types/json-schema':
specifier: ^7.0.15
version: 7.0.15
'@types/node':
specifier: ^22.0.0
version: 22.19.7
'@types/semver':
specifier: ^7.5.0
version: 7.7.1
tsx:
specifier: ^4.19.0
version: 4.21.0
sdks/cli: {}
sdks/cli/platforms/darwin-arm64: {}
@ -579,6 +610,10 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@ -772,6 +807,9 @@ packages:
'@types/react@18.3.27':
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@vitejs/plugin-react@4.7.0':
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -827,6 +865,10 @@ packages:
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
engines: {node: '>=20.18.1'}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -1033,6 +1075,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.1.0:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1131,6 +1177,11 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -1167,6 +1218,10 @@ packages:
resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
engines: {node: '>=12'}
tar@7.5.6:
resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==}
engines: {node: '>=18'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -1296,6 +1351,10 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@ -1648,6 +1707,10 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -1798,6 +1861,8 @@ snapshots:
'@types/prop-types': 15.7.15
csstype: 3.2.3
'@types/semver@7.7.1': {}
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.7))':
dependencies:
'@babel/core': 7.28.6
@ -1865,6 +1930,8 @@ snapshots:
undici: 7.19.1
whatwg-mimetype: 4.0.0
chownr@3.0.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -2098,6 +2165,10 @@ snapshots:
minipass@7.1.2: {}
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
ms@2.1.3: {}
nanoid@3.3.11: {}
@ -2214,6 +2285,8 @@ snapshots:
semver@6.3.1: {}
semver@7.7.3: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -2246,6 +2319,14 @@ snapshots:
supports-color@9.4.0: {}
tar@7.5.6:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.1.0
yallist: 5.0.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@ -2346,4 +2427,6 @@ snapshots:
yallist@3.1.1: {}
yallist@5.0.0: {}
yargs-parser@21.1.1: {}

View file

@ -4,3 +4,4 @@ packages:
- "sdks/cli"
- "sdks/cli/platforms/*"
- "resources/agent-schemas"
- "resources/vercel-ai-sdk-schemas"

View file

@ -12,7 +12,8 @@
"extract:claude-events": "tsx src/claude-event-types.ts",
"extract:claude-events:sdk": "tsx src/claude-event-types-sdk.ts",
"extract:claude-events:cli": "tsx src/claude-event-types-cli.ts",
"extract:claude-events:docs": "tsx src/claude-event-types-docs.ts"
"extract:claude-events:docs": "tsx src/claude-event-types-docs.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"ts-json-schema-generator": "^2.4.0",
@ -23,6 +24,7 @@
},
"devDependencies": {
"tsx": "^4.19.0",
"@types/node": "^22.0.0"
"@types/node": "^22.0.0",
"@types/json-schema": "^7.0.15"
}
}

View file

@ -0,0 +1,50 @@
> vercel-ai-sdk-schemas@1.0.0 extract /home/nathan/sandbox-daemon/resources/vercel-ai-sdk-schemas
> tsx src/index.ts
Vercel AI SDK UIMessage Schema Extractor
========================================
[cache hit] https://registry.npmjs.org/ai
Target version: ai@6.0.50
[debug] temp dir: /tmp/vercel-ai-sdk-JnQ1yL
[cache hit] https://registry.npmjs.org/ai
[cache hit] https://registry.npmjs.org/@opentelemetry%2Fapi
[cache hit] https://registry.npmjs.org/@ai-sdk%2Fgateway
[cache hit] https://registry.npmjs.org/@vercel%2Foidc
[cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider
[cache hit] https://registry.npmjs.org/json-schema
[cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider-utils
[cache hit] https://registry.npmjs.org/@standard-schema%2Fspec
[cache hit] https://registry.npmjs.org/eventsource-parser
[cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider
[cache hit] https://registry.npmjs.org/zod
[cache hit] https://registry.npmjs.org/zod
[cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider
[cache hit] https://registry.npmjs.org/@ai-sdk%2Fprovider-utils
[cache hit] https://registry.npmjs.org/zod
[shim] Wrote type-fest ValueOf shim
[debug] DataUIPart alias snippet: type DataUIPart<DATA_TYPES extends UIDataTypes> = ValueOf<{
[NAME in keyof DATA_TYPES & string]: {
type: `data-${NAME}`;
[patch] Simplified DataUIPart to avoid indexed access
[debug] ToolUIPart alias snippet: type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{
[NAME in keyof TOOLS & string]: {
type: `tool-${NAME}`;
[patch] Simplified ToolUIPart to avoid indexed access
[warn] ValueOf alias declaration not found
[warn] ValueOf alias not found in ai types
[debug] ai types path: /tmp/vercel-ai-sdk-JnQ1yL/node_modules/ai/dist/index.d.ts
[debug] preview: ValueOf} from 'type-fest';
import data = require('./data.json');
export function getData(name: string): ValueOf<typeof data> {
return data[name];
}
export function onlyBar(name: string): ValueOf
[debug] entry path: /tmp/vercel-ai-sdk-JnQ1yL/entry.ts
[debug] tsconfig path: /tmp/vercel-ai-sdk-JnQ1yL/tsconfig.json
[debug] entry size: 89
[wrote] /home/nathan/sandbox-daemon/resources/vercel-ai-sdk-schemas/artifacts/json-schema/ui-message.json

View file

@ -0,0 +1,23 @@
# Vercel AI SDK Schemas
This package extracts JSON Schema for `UIMessage` from the Vercel AI SDK v6 TypeScript types.
## Usage
- Install dependencies in this folder.
- Run the extractor:
```
pnpm install
pnpm extract
```
Optional flags:
- `--version=6.x.y` to pin an exact version
- `--major=6` to select the latest version for a major (default: 6)
Output:
- `artifacts/json-schema/ui-message.json`
The registry response is cached under `.cache/` for 24 hours. The extractor downloads the AI SDK package
and the minimal dependency tree needed for TypeScript type resolution into a temporary folder.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
{
"name": "vercel-ai-sdk-schemas",
"version": "1.0.0",
"type": "module",
"license": "Apache-2.0",
"scripts": {
"extract": "tsx src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"ts-json-schema-generator": "^2.4.0",
"typescript": "^5.7.0",
"tar": "^7.0.0",
"semver": "^7.6.3"
},
"devDependencies": {
"tsx": "^4.19.0",
"@types/node": "^22.0.0",
"@types/semver": "^7.5.0",
"@types/json-schema": "^7.0.15"
}
}

View file

@ -0,0 +1,93 @@
import { createHash } from "crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
const CACHE_DIR = join(import.meta.dirname, "..", ".cache");
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
function ensureCacheDir(): void {
if (!existsSync(CACHE_DIR)) {
mkdirSync(CACHE_DIR, { recursive: true });
}
}
function hashKey(key: string): string {
return createHash("sha256").update(key).digest("hex");
}
function getCachePath(key: string): string {
return join(CACHE_DIR, `${hashKey(key)}.json`);
}
export function getCached<T>(key: string): T | null {
const path = getCachePath(key);
if (!existsSync(path)) {
return null;
}
try {
const content = readFileSync(path, "utf-8");
const entry: CacheEntry<T> = JSON.parse(content);
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
return null;
}
return entry.data;
} catch {
return null;
}
}
export function setCache<T>(key: string, data: T, ttl: number = DEFAULT_TTL_MS): void {
ensureCacheDir();
const entry: CacheEntry<T> = {
data,
timestamp: Date.now(),
ttl,
};
const path = getCachePath(key);
writeFileSync(path, JSON.stringify(entry, null, 2));
}
export async function fetchWithCache(url: string, ttl?: number): Promise<string> {
const cached = getCached<string>(url);
if (cached !== null) {
console.log(` [cache hit] ${url}`);
return cached;
}
console.log(` [fetching] ${url}`);
let lastError: Error | null = null;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const text = await response.text();
setCache(url, text, ttl);
return text;
} catch (error) {
lastError = error as Error;
if (attempt < 2) {
const delay = Math.pow(2, attempt) * 1000;
console.log(` [retry ${attempt + 1}] waiting ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}

View file

@ -0,0 +1,398 @@
import {
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
existsSync,
appendFileSync,
statSync,
} from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { createGenerator, type Config } from "ts-json-schema-generator";
import { maxSatisfying, rsort, valid } from "semver";
import { x as extractTar } from "tar";
import type { JSONSchema7 } from "json-schema";
import { fetchWithCache } from "./cache.js";
const REGISTRY_URL = "https://registry.npmjs.org/ai";
const TARGET_TYPE = "UIMessage";
const DEFAULT_MAJOR = 6;
const RESOURCE_DIR = join(import.meta.dirname, "..");
const OUTPUT_DIR = join(RESOURCE_DIR, "artifacts", "json-schema");
const OUTPUT_PATH = join(OUTPUT_DIR, "ui-message.json");
const SCHEMA_ID = "https://sandbox-agent/schemas/vercel-ai-sdk/ui-message.json";
interface RegistryResponse {
versions?: Record<
string,
{
dist?: { tarball?: string };
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
}
>;
"dist-tags"?: Record<string, string>;
}
interface Args {
version: string | null;
major: number;
}
function parseArgs(): Args {
const args = process.argv.slice(2);
const versionArg = args.find((arg) => arg.startsWith("--version="));
const majorArg = args.find((arg) => arg.startsWith("--major="));
const version = versionArg ? versionArg.split("=")[1] : null;
const major = majorArg ? Number(majorArg.split("=")[1]) : DEFAULT_MAJOR;
return {
version,
major: Number.isFinite(major) && major > 0 ? major : DEFAULT_MAJOR,
};
}
function log(message: string): void {
console.log(message);
}
function ensureOutputDir(): void {
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR, { recursive: true });
}
}
async function fetchRegistry(url: string): Promise<RegistryResponse> {
const registry = await fetchWithCache(url);
return JSON.parse(registry) as RegistryResponse;
}
function resolveLatestVersion(registry: RegistryResponse, major: number): string {
const versions = Object.keys(registry.versions ?? {});
const candidates = versions.filter((version) => valid(version) && version.startsWith(`${major}.`));
const sorted = rsort(candidates);
if (sorted.length === 0) {
throw new Error(`No versions found for major ${major}`);
}
return sorted[0];
}
function resolveVersionFromRange(registry: RegistryResponse, range: string): string {
if (registry.versions?.[range]) {
return range;
}
const versions = Object.keys(registry.versions ?? {}).filter((version) => valid(version));
const resolved = maxSatisfying(versions, range);
if (!resolved) {
throw new Error(`No versions satisfy range ${range}`);
}
return resolved;
}
async function downloadTarball(url: string, destination: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync(destination, buffer);
}
async function extractPackage(tarballPath: string, targetDir: string): Promise<void> {
mkdirSync(targetDir, { recursive: true });
await extractTar({
file: tarballPath,
cwd: targetDir,
strip: 1,
});
}
function packageDirFor(name: string, nodeModulesDir: string): string {
const parts = name.split("/");
return join(nodeModulesDir, ...parts);
}
async function installPackage(
name: string,
versionRange: string,
nodeModulesDir: string,
installed: Set<string>
): Promise<void> {
const encodedName = name.startsWith("@")
? `@${encodeURIComponent(name.slice(1))}`
: encodeURIComponent(name);
const registryUrl = `https://registry.npmjs.org/${encodedName}`;
const registry = await fetchRegistry(registryUrl);
const version = resolveVersionFromRange(registry, versionRange);
const installKey = `${name}@${version}`;
if (installed.has(installKey)) {
return;
}
installed.add(installKey);
const tarball = registry.versions?.[version]?.dist?.tarball;
if (!tarball) {
throw new Error(`No tarball found for ${installKey}`);
}
const tempDir = mkdtempSync(join(tmpdir(), "vercel-ai-sdk-dep-"));
const tarballPath = join(tempDir, `${name.replace("/", "-")}-${version}.tgz`);
const packageDir = packageDirFor(name, nodeModulesDir);
try {
await downloadTarball(tarball, tarballPath);
await extractPackage(tarballPath, packageDir);
const packageJsonPath = join(packageDir, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
};
const dependencies = {
...packageJson.dependencies,
...packageJson.peerDependencies,
};
for (const [depName, depRange] of Object.entries(dependencies)) {
await installPackage(depName, depRange, nodeModulesDir, installed);
}
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
}
function writeTempTsconfig(tempDir: string): string {
const tsconfigPath = join(tempDir, "tsconfig.json");
const tsconfig = {
compilerOptions: {
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
strict: true,
skipLibCheck: true,
esModuleInterop: true,
},
};
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
return tsconfigPath;
}
function writeEntryFile(tempDir: string): string {
const entryPath = join(tempDir, "entry.ts");
const contents = `import type { ${TARGET_TYPE} as AI${TARGET_TYPE} } from "ai";\nexport type ${TARGET_TYPE} = AI${TARGET_TYPE};\n`;
writeFileSync(entryPath, contents);
return entryPath;
}
function patchValueOfAlias(nodeModulesDir: string): void {
const aiTypesPath = join(nodeModulesDir, "ai", "dist", "index.d.ts");
if (!existsSync(aiTypesPath)) {
log(" [warn] ai types not found for ValueOf patch");
return;
}
const contents = readFileSync(aiTypesPath, "utf-8");
const valueOfMatch = contents.match(/type ValueOf[\\s\\S]*?;/);
if (valueOfMatch) {
const snippet = valueOfMatch[0].replace(/\\s+/g, " ").slice(0, 200);
log(` [debug] ValueOf alias snippet: ${snippet}`);
} else {
log(" [warn] ValueOf alias declaration not found");
}
let patched = contents.replace(
/ObjectType\\s*\\[\\s*ValueType\\s*\\]/,
"ObjectType[string]"
);
if (patched !== contents) {
writeFileSync(aiTypesPath, patched);
log(" [patch] Adjusted ValueOf alias for schema generation");
return;
}
const valueOfIndex = contents.indexOf("ValueOf");
const preview =
valueOfIndex === -1 ? contents.slice(0, 200) : contents.slice(valueOfIndex, valueOfIndex + 200);
log(" [warn] ValueOf alias not found in ai types");
log(` [debug] ai types path: ${aiTypesPath}`);
log(` [debug] preview: ${preview.replace(/\\s+/g, " ").slice(0, 200)}`);
}
function ensureTypeFestShim(nodeModulesDir: string): void {
const typeFestDir = join(nodeModulesDir, "type-fest");
if (!existsSync(typeFestDir)) {
mkdirSync(typeFestDir, { recursive: true });
}
const packageJsonPath = join(typeFestDir, "package.json");
const typesPath = join(typeFestDir, "index.d.ts");
if (!existsSync(packageJsonPath)) {
const pkg = {
name: "type-fest",
version: "0.0.0",
types: "index.d.ts",
};
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
}
const shim = `export type ValueOf<\n ObjectType,\n ValueType extends keyof ObjectType = keyof ObjectType,\n> = ObjectType[string];\n\nexport type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};\n`;
writeFileSync(typesPath, shim);
log(" [shim] Wrote type-fest ValueOf shim");
}
function generateSchema(entryPath: string, tsconfigPath: string): JSONSchema7 {
const config: Config = {
path: entryPath,
tsconfig: tsconfigPath,
type: TARGET_TYPE,
expose: "export",
skipTypeCheck: true,
};
const generator = createGenerator(config);
return generator.createSchema(TARGET_TYPE) as JSONSchema7;
}
function addSchemaMetadata(schema: JSONSchema7, version: string): JSONSchema7 {
const withMeta: JSONSchema7 = {
...schema,
$schema: schema.$schema ?? "http://json-schema.org/draft-07/schema#",
$id: SCHEMA_ID,
title: schema.title ?? TARGET_TYPE,
description: schema.description ?? `Vercel AI SDK v${version} ${TARGET_TYPE}`,
};
return withMeta;
}
function loadFallback(): JSONSchema7 | null {
if (!existsSync(OUTPUT_PATH)) {
return null;
}
try {
const content = readFileSync(OUTPUT_PATH, "utf-8");
return JSON.parse(content) as JSONSchema7;
} catch {
return null;
}
}
function patchUiMessageTypes(nodeModulesDir: string): void {
const aiTypesPath = join(nodeModulesDir, "ai", "dist", "index.d.ts");
if (!existsSync(aiTypesPath)) {
log(" [warn] ai types not found for UIMessage patch");
return;
}
const contents = readFileSync(aiTypesPath, "utf-8");
let patched = contents;
const replaceAlias = (typeName: string, replacement: string): boolean => {
const start = patched.indexOf(`type ${typeName}`);
if (start === -1) {
log(` [warn] ${typeName} alias not found for patch`);
return false;
}
const end = patched.indexOf(";", start);
if (end === -1) {
log(` [warn] ${typeName} alias not terminated`);
return false;
}
const snippet = patched.slice(start, Math.min(end + 1, start + 400)).replace(/\\s+/g, " ");
log(` [debug] ${typeName} alias snippet: ${snippet}`);
patched = patched.slice(0, start) + replacement + patched.slice(end + 1);
return true;
};
const dataReplaced = replaceAlias(
"DataUIPart",
"type DataUIPart<DATA_TYPES extends UIDataTypes> = {\\n type: `data-${string}`;\\n id?: string;\\n data: unknown;\\n};"
);
if (dataReplaced) {
log(" [patch] Simplified DataUIPart to avoid indexed access");
}
const toolReplaced = replaceAlias(
"ToolUIPart",
"type ToolUIPart<TOOLS extends UITools = UITools> = {\\n type: `tool-${string}`;\\n} & UIToolInvocation<UITool>;"
);
if (toolReplaced) {
log(" [patch] Simplified ToolUIPart to avoid indexed access");
}
if (patched !== contents) {
writeFileSync(aiTypesPath, patched);
}
}
async function main(): Promise<void> {
log("Vercel AI SDK UIMessage Schema Extractor");
log("========================================\n");
const args = parseArgs();
ensureOutputDir();
const registry = await fetchRegistry(REGISTRY_URL);
const version = args.version ?? resolveLatestVersion(registry, args.major);
log(`Target version: ai@${version}`);
const tempDir = mkdtempSync(join(tmpdir(), "vercel-ai-sdk-"));
const nodeModulesDir = join(tempDir, "node_modules");
try {
log(` [debug] temp dir: ${tempDir}`);
await installPackage("ai", version, nodeModulesDir, new Set());
ensureTypeFestShim(nodeModulesDir);
patchUiMessageTypes(nodeModulesDir);
patchValueOfAlias(nodeModulesDir);
const tsconfigPath = writeTempTsconfig(tempDir);
const entryPath = writeEntryFile(tempDir);
log(` [debug] entry path: ${entryPath}`);
log(` [debug] tsconfig path: ${tsconfigPath}`);
if (existsSync(entryPath)) {
const entryStat = statSync(entryPath);
log(` [debug] entry size: ${entryStat.size}`);
}
const schema = generateSchema(entryPath, tsconfigPath);
const schemaWithMeta = addSchemaMetadata(schema, version);
writeFileSync(OUTPUT_PATH, JSON.stringify(schemaWithMeta, null, 2));
log(`\n [wrote] ${OUTPUT_PATH}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log(`\n [error] ${message}`);
if (error instanceof Error && error.stack) {
log(error.stack);
}
const fallback = loadFallback();
if (fallback) {
log(" [fallback] Keeping existing schema artifact");
return;
}
process.exitCode = 1;
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}

View file

@ -313,10 +313,15 @@ function buildTypescript(rootDir: string) {
}
function generateArtifacts(rootDir: string) {
run("pnpm", ["install"], { cwd: rootDir });
run("pnpm", ["--filter", "@sandbox-agent/inspector", "build"], {
cwd: rootDir,
env: { ...process.env, SANDBOX_AGENT_SKIP_INSPECTOR: "1" },
});
const sdkDir = path.join(rootDir, "sdks", "typescript");
run("pnpm", ["run", "generate"], { cwd: sdkDir });
run("cargo", ["check", "-p", "sandbox-agent-universal-schema-gen"], { cwd: rootDir });
run("cargo", ["run", "-p", "sandbox-agent-openapi-gen", "--", "--out", "sdks/openapi/openapi.json"], {
run("cargo", ["run", "-p", "sandbox-agent-openapi-gen", "--", "--out", "docs/openapi.json"], {
cwd: rootDir,
});
}
@ -367,14 +372,25 @@ function uploadBinaries(rootDir: string, version: string, latest: boolean) {
}
function runChecks(rootDir: string) {
console.log("==> Installing Node dependencies");
run("pnpm", ["install"], { cwd: rootDir });
console.log("==> Building inspector frontend");
run("pnpm", ["--filter", "@sandbox-agent/inspector", "build"], {
cwd: rootDir,
env: { ...process.env, SANDBOX_AGENT_SKIP_INSPECTOR: "1" },
});
console.log("==> Running Rust checks");
run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir });
run("cargo", ["clippy", "--all-targets", "--", "-D", "warnings"], { cwd: rootDir });
run("cargo", ["test", "--all-targets"], { cwd: rootDir });
console.log("==> Running TypeScript checks");
run("pnpm", ["install"], { cwd: rootDir });
run("pnpm", ["run", "build"], { cwd: rootDir });
console.log("==> Validating OpenAPI spec for Mintlify");
run("pnpm", ["dlx", "mint", "openapi-check", "docs/openapi.json"], { cwd: rootDir });
}
function publishCrates(rootDir: string, version: string) {

View file

@ -8,7 +8,8 @@
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"bin": {
"sandbox-agent": "bin/sandbox-agent"
"sandbox-agent": "bin/sandbox-agent",
"sandbox-daemon": "bin/sandbox-agent"
},
"optionalDependencies": {
"@sandbox-agent/cli-darwin-arm64": "0.1.0",

View file

@ -20,10 +20,11 @@
"dist"
],
"scripts": {
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../openapi/openapi.json",
"generate:types": "openapi-typescript ../openapi/openapi.json -o src/generated/openapi.ts",
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../../docs/openapi.json",
"generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/openapi.ts",
"generate": "pnpm run generate:openapi && pnpm run generate:types",
"build": "pnpm run generate && tsc -p tsconfig.json"
"build": "pnpm run generate && tsc -p tsconfig.json",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.0.0",

View file

@ -22,6 +22,9 @@ export interface paths {
"/v1/health": {
get: operations["get_health"];
};
"/v1/sessions": {
get: operations["list_sessions"];
};
"/v1/sessions/{session_id}": {
post: operations["create_session"];
};
@ -179,6 +182,21 @@ export interface components {
callId: string;
messageId: string;
};
SessionInfo: {
agent: string;
agentMode: string;
agentSessionId?: string | null;
ended: boolean;
/** Format: int64 */
eventCount: number;
model?: string | null;
permissionMode: string;
sessionId: string;
variant?: string | null;
};
SessionListResponse: {
sessions: components["schemas"]["SessionInfo"][];
};
Started: {
details?: unknown;
message?: string | null;
@ -358,6 +376,15 @@ export interface operations {
};
};
};
list_sessions: {
responses: {
200: {
content: {
"application/json": components["schemas"]["SessionListResponse"];
};
};
};
};
create_session: {
parameters: {
path: {

View file

@ -1,6 +1,5 @@
import type { ChildProcess } from "node:child_process";
import type { AddressInfo } from "node:net";
import type { NodeRequire } from "node:module";
export type SandboxDaemonSpawnLogMode = "inherit" | "pipe" | "silent";
@ -68,7 +67,7 @@ export async function spawnSandboxDaemon(
}
const stdio = logMode === "inherit" ? "inherit" : logMode === "silent" ? "ignore" : "pipe";
const args = ["--host", bindHost, "--port", String(port), "--token", token];
const args = ["server", "--host", bindHost, "--port", String(port), "--token", token];
const child = spawn(binaryPath, args, {
stdio,
env: {
@ -112,7 +111,7 @@ function resolveBinaryFromEnv(fs: typeof import("node:fs"), path: typeof import(
}
function resolveBinaryFromCliPackage(
require: NodeRequire,
require: ReturnType<typeof import("node:module").createRequire>,
path: typeof import("node:path"),
fs: typeof import("node:fs"),
): string | null {

View file

@ -31,6 +31,7 @@ schemars.workspace = true
tracing.workspace = true
tracing-logfmt.workspace = true
tracing-subscriber.workspace = true
include_dir.workspace = true
[dev-dependencies]
http-body-util.workspace = true

View file

@ -0,0 +1,63 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let root_dir = manifest_dir
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.expect("workspace root");
let dist_dir = root_dir
.join("frontend")
.join("packages")
.join("inspector")
.join("dist");
println!("cargo:rerun-if-env-changed=SANDBOX_AGENT_SKIP_INSPECTOR");
println!("cargo:rerun-if-changed={}", dist_dir.display());
let skip = env::var("SANDBOX_AGENT_SKIP_INSPECTOR").is_ok();
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let out_file = out_dir.join("inspector_assets.rs");
if skip {
write_disabled(&out_file);
return;
}
if !dist_dir.exists() {
panic!(
"Inspector frontend missing at {}. Run `pnpm --filter @sandbox-agent/inspector build` (or `pnpm -C frontend/packages/inspector build`) or set SANDBOX_AGENT_SKIP_INSPECTOR=1 to skip embedding.",
dist_dir.display()
);
}
let dist_literal = quote_path(&dist_dir);
let contents = format!(
"pub const INSPECTOR_ENABLED: bool = true;\n\
pub fn inspector_dir() -> Option<&'static include_dir::Dir<'static>> {{\n\
Some(&INSPECTOR_DIR)\n\
}}\n\
static INSPECTOR_DIR: include_dir::Dir<'static> = include_dir::include_dir!(\"{}\");\n",
dist_literal
);
fs::write(&out_file, contents).expect("write inspector_assets.rs");
}
fn write_disabled(out_file: &Path) {
let contents = "pub const INSPECTOR_ENABLED: bool = false;\n\
pub fn inspector_dir() -> Option<&'static include_dir::Dir<'static>> {\n\
None\n\
}\n";
fs::write(out_file, contents).expect("write inspector_assets.rs");
}
fn quote_path(path: &Path) -> String {
path.to_str()
.expect("valid path")
.replace('\\', "\\\\")
.replace('"', "\\\"")
}

View file

@ -2,3 +2,4 @@
pub mod credentials;
pub mod router;
pub mod ui;

View file

@ -16,6 +16,7 @@ use sandbox_agent_core::router::{
};
use sandbox_agent_core::router::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse};
use sandbox_agent_core::router::build_router;
use sandbox_agent_core::ui;
use serde::Serialize;
use serde_json::Value;
use thiserror::Error;
@ -23,25 +24,42 @@ use tower_http::cors::{Any, CorsLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
const API_PREFIX: &str = "/v1";
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 2468;
#[derive(Parser, Debug)]
#[command(name = "sandbox-agent")]
#[command(about = "Sandbox agent for managing coding agents", version)]
#[command(name = "sandbox-daemon", bin_name = "sandbox-agent")]
#[command(about = "Sandbox daemon for managing coding agents", version)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(long, short = 'H', default_value = "127.0.0.1")]
host: String,
#[arg(long, short = 'p', default_value_t = 2468)]
port: u16,
#[arg(long, short = 't')]
#[arg(long, short = 't', global = true)]
token: Option<String>,
#[arg(long, short = 'n')]
#[arg(long, short = 'n', global = true)]
no_token: bool,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Run the sandbox daemon HTTP server.
Server(ServerArgs),
/// Manage installed agents and their modes.
Agents(AgentsArgs),
/// Create sessions and interact with session events.
Sessions(SessionsArgs),
/// Inspect locally discovered credentials.
Credentials(CredentialsArgs),
}
#[derive(Args, Debug)]
struct ServerArgs {
#[arg(long, short = 'H', default_value = DEFAULT_HOST)]
host: String,
#[arg(long, short = 'p', default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long = "cors-allow-origin", short = 'O')]
cors_allow_origin: Vec<String>,
@ -56,16 +74,6 @@ struct Cli {
cors_allow_credentials: bool,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Manage installed agents and their modes.
Agents(AgentsArgs),
/// Create sessions and interact with session events.
Sessions(SessionsArgs),
/// Inspect locally discovered credentials.
Credentials(CredentialsArgs),
}
#[derive(Args, Debug)]
struct AgentsArgs {
#[command(subcommand)]
@ -255,6 +263,8 @@ struct CredentialsExtractEnvArgs {
#[derive(Debug, Error)]
enum CliError {
#[error("missing command: run `sandbox-daemon server` to start the daemon")]
MissingCommand,
#[error("missing --token or --no-token for server mode")]
MissingToken,
#[error("invalid cors origin: {0}")]
@ -280,8 +290,9 @@ fn main() {
let cli = Cli::parse();
let result = match &cli.command {
Some(Command::Server(args)) => run_server(&cli, args),
Some(command) => run_client(command, &cli),
None => run_server(&cli),
None => Err(CliError::MissingCommand),
};
if let Err(err) = result {
@ -298,7 +309,7 @@ fn init_logging() {
.init();
}
fn run_server(cli: &Cli) -> Result<(), CliError> {
fn run_server(cli: &Cli, server: &ServerArgs) -> Result<(), CliError> {
let auth = if cli.no_token {
AuthConfig::disabled()
} else if let Some(token) = cli.token.clone() {
@ -312,11 +323,16 @@ fn run_server(cli: &Cli) -> Result<(), CliError> {
let state = AppState::new(auth, agent_manager);
let mut router = build_router(state);
if let Some(cors) = build_cors_layer(cli)? {
if let Some(cors) = build_cors_layer(server)? {
router = router.layer(cors);
}
let addr = format!("{}:{}", cli.host, cli.port);
let addr = format!("{}:{}", server.host, server.port);
let display_host = match server.host.as_str() {
"0.0.0.0" | "::" => "localhost",
other => other,
};
let inspector_url = format!("http://{}:{}/ui", display_host, server.port);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
@ -325,6 +341,11 @@ fn run_server(cli: &Cli) -> Result<(), CliError> {
runtime.block_on(async move {
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!(addr = %addr, "server listening");
if ui::is_enabled() {
tracing::info!(url = %inspector_url, "inspector ui available");
} else {
tracing::info!("inspector ui not embedded; set SANDBOX_AGENT_SKIP_INSPECTOR=1 to skip embedding during builds");
}
axum::serve(listener, router)
.await
.map_err(|err| CliError::Server(err.to_string()))
@ -339,6 +360,9 @@ fn default_install_dir() -> PathBuf {
fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
match command {
Command::Server(_) => Err(CliError::Server(
"server subcommand must be invoked as `sandbox-daemon server`".to_string(),
)),
Command::Agents(subcommand) => run_agents(&subcommand.command, cli),
Command::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
Command::Credentials(subcommand) => run_credentials(&subcommand.command),
@ -663,11 +687,11 @@ fn available_providers(credentials: &ExtractedCredentials) -> Vec<String> {
providers
}
fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
let has_config = !cli.cors_allow_origin.is_empty()
|| !cli.cors_allow_method.is_empty()
|| !cli.cors_allow_header.is_empty()
|| cli.cors_allow_credentials;
fn build_cors_layer(server: &ServerArgs) -> Result<Option<CorsLayer>, CliError> {
let has_config = !server.cors_allow_origin.is_empty()
|| !server.cors_allow_method.is_empty()
|| !server.cors_allow_header.is_empty()
|| server.cors_allow_credentials;
if !has_config {
return Ok(None);
@ -675,11 +699,11 @@ fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
let mut cors = CorsLayer::new();
if cli.cors_allow_origin.is_empty() {
if server.cors_allow_origin.is_empty() {
cors = cors.allow_origin(Any);
} else {
let mut origins = Vec::new();
for origin in &cli.cors_allow_origin {
for origin in &server.cors_allow_origin {
let value = origin
.parse()
.map_err(|_| CliError::InvalidCorsOrigin(origin.clone()))?;
@ -688,11 +712,11 @@ fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
cors = cors.allow_origin(origins);
}
if cli.cors_allow_method.is_empty() {
if server.cors_allow_method.is_empty() {
cors = cors.allow_methods(Any);
} else {
let mut methods = Vec::new();
for method in &cli.cors_allow_method {
for method in &server.cors_allow_method {
let parsed = method
.parse()
.map_err(|_| CliError::InvalidCorsMethod(method.clone()))?;
@ -701,11 +725,11 @@ fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
cors = cors.allow_methods(methods);
}
if cli.cors_allow_header.is_empty() {
if server.cors_allow_header.is_empty() {
cors = cors.allow_headers(Any);
} else {
let mut headers = Vec::new();
for header in &cli.cors_allow_header {
for header in &server.cors_allow_header {
let parsed = header
.parse()
.map_err(|_| CliError::InvalidCorsHeader(header.clone()))?;
@ -714,7 +738,7 @@ fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
cors = cors.allow_headers(headers);
}
if cli.cors_allow_credentials {
if server.cors_allow_credentials {
cors = cors.allow_credentials(true);
}
@ -732,7 +756,7 @@ impl ClientContext {
let endpoint = args
.endpoint
.clone()
.unwrap_or_else(|| format!("http://{}:{}", cli.host, cli.port));
.unwrap_or_else(|| format!("http://{}:{}", DEFAULT_HOST, DEFAULT_PORT));
let token = if cli.no_token { None } else { cli.token.clone() };
let client = HttpClient::builder().build()?;
Ok(Self {

View file

@ -46,7 +46,7 @@ use serde_json::{json, Value};
use tokio::sync::{broadcast, mpsc, Mutex};
use tokio_stream::wrappers::BroadcastStream;
use tokio::time::sleep;
use utoipa::{OpenApi, ToSchema};
use utoipa::{Modify, OpenApi, ToSchema};
use sandbox_agent_agent_management::agents::{
AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn,
@ -187,10 +187,21 @@ pub fn build_router(state: AppState) -> Router {
(name = "meta", description = "Service metadata"),
(name = "agents", description = "Agent management"),
(name = "sessions", description = "Session management")
)
),
modifiers(&ServerAddon)
)]
pub struct ApiDoc;
struct ServerAddon;
impl Modify for ServerAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
openapi.servers = Some(vec![utoipa::openapi::Server::new(
"http://localhost:2468",
)]);
}
}
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error(transparent)]
@ -594,14 +605,14 @@ impl SessionManager {
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
session_id: session_id.to_string(),
})?;
if let Some(err) = session.ended_error() {
return Err(err);
}
if !session.take_question(question_id) {
return Err(SandboxError::InvalidRequest {
message: format!("unknown question id: {question_id}"),
});
}
if let Some(err) = session.ended_error() {
return Err(err);
}
(session.agent, session.agent_session_id.clone())
};
@ -628,14 +639,14 @@ impl SessionManager {
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
session_id: session_id.to_string(),
})?;
if let Some(err) = session.ended_error() {
return Err(err);
}
if !session.take_question(question_id) {
return Err(SandboxError::InvalidRequest {
message: format!("unknown question id: {question_id}"),
});
}
if let Some(err) = session.ended_error() {
return Err(err);
}
(session.agent, session.agent_session_id.clone())
};
@ -663,14 +674,14 @@ impl SessionManager {
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
session_id: session_id.to_string(),
})?;
if let Some(err) = session.ended_error() {
return Err(err);
}
if !session.take_permission(permission_id) {
return Err(SandboxError::InvalidRequest {
message: format!("unknown permission id: {permission_id}"),
});
}
if let Some(err) = session.ended_error() {
return Err(err);
}
let codex_metadata = if session.agent == AgentId::Codex {
session.events.iter().find_map(|event| {
if let UniversalEventData::PermissionAsked { permission_asked } = &event.data {
@ -858,47 +869,45 @@ impl SessionManager {
Ok(Ok(status)) if status.success() => {}
Ok(Ok(status)) => {
let message = format!("agent exited with status {:?}", status);
self.record_error(
&session_id,
message.clone(),
Some("process_exit".to_string()),
None,
)
if !terminate_early {
self.record_error(
&session_id,
message.clone(),
Some("process_exit".to_string()),
None,
)
.await;
}
self.mark_session_ended(&session_id, status.code(), &message)
.await;
}
Ok(Err(err)) => {
let message = format!("failed to wait for agent: {err}");
self.record_error(
&session_id,
message.clone(),
Some("process_wait_failed".to_string()),
None,
)
.await;
self.mark_session_ended(
&session_id,
None,
&message,
)
.await;
if !terminate_early {
self.record_error(
&session_id,
message.clone(),
Some("process_wait_failed".to_string()),
None,
)
.await;
}
self.mark_session_ended(&session_id, None, &message)
.await;
}
Err(err) => {
let message = format!("failed to join agent task: {err}");
self.record_error(
&session_id,
message.clone(),
Some("process_wait_failed".to_string()),
None,
)
.await;
self.mark_session_ended(
&session_id,
None,
&message,
)
.await;
if !terminate_early {
self.record_error(
&session_id,
message.clone(),
Some("process_wait_failed".to_string()),
None,
)
.await;
}
self.mark_session_ended(&session_id, None, &message)
.await;
}
}
}
@ -2179,15 +2188,22 @@ impl CodexAppServerState {
serde_json::from_value::<codex_schema::ServerNotification>(value.clone())
{
self.maybe_capture_thread_id(&notification);
let conversion = convert_codex::notification_to_universal(&notification);
let should_terminate = matches!(
notification,
codex_schema::ServerNotification::TurnCompleted(_)
| codex_schema::ServerNotification::Error(_)
);
CodexLineOutcome {
conversion: Some(conversion),
should_terminate,
if codex_should_emit_notification(&notification) {
let conversion = convert_codex::notification_to_universal(&notification);
CodexLineOutcome {
conversion: Some(conversion),
should_terminate,
}
} else {
CodexLineOutcome {
conversion: None,
should_terminate,
}
}
} else {
CodexLineOutcome::default()
@ -2369,6 +2385,20 @@ fn codex_sandbox_policy(mode: Option<&str>) -> Option<codex_schema::SandboxPolic
}
}
fn codex_should_emit_notification(notification: &codex_schema::ServerNotification) -> bool {
match notification {
codex_schema::ServerNotification::ThreadStarted(_)
| codex_schema::ServerNotification::TurnStarted(_)
| codex_schema::ServerNotification::Error(_) => true,
codex_schema::ServerNotification::ItemCompleted(params) => matches!(
params.item,
codex_schema::ThreadItem::UserMessage { .. }
| codex_schema::ThreadItem::AgentMessage { .. }
),
_ => false,
}
}
fn codex_request_to_universal(request: &codex_schema::ServerRequest) -> EventConversion {
match request {
codex_schema::ServerRequest::ItemCommandExecutionRequestApproval { id, params } => {

View file

@ -0,0 +1,81 @@
use std::path::Path;
use axum::body::Body;
use axum::extract::Path as AxumPath;
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
include!(concat!(env!("OUT_DIR"), "/inspector_assets.rs"));
pub fn is_enabled() -> bool {
INSPECTOR_ENABLED
}
pub fn router() -> Router {
if !INSPECTOR_ENABLED {
return Router::new();
}
Router::new()
.route("/ui", get(handle_index))
.route("/ui/", get(handle_index))
.route("/ui/*path", get(handle_path))
}
async fn handle_index() -> Response {
serve_path("")
}
async fn handle_path(AxumPath(path): AxumPath<String>) -> Response {
serve_path(&path)
}
fn serve_path(path: &str) -> Response {
let Some(dir) = inspector_dir() else {
return StatusCode::NOT_FOUND.into_response();
};
let trimmed = path.trim_start_matches('/');
let target = if trimmed.is_empty() { "index.html" } else { trimmed };
if let Some(file) = dir.get_file(target) {
return file_response(file);
}
if !target.contains('.') {
if let Some(file) = dir.get_file("index.html") {
return file_response(file);
}
}
StatusCode::NOT_FOUND.into_response()
}
fn file_response(file: &include_dir::File) -> Response {
let mut response = Response::new(Body::from(file.contents().to_vec()));
*response.status_mut() = StatusCode::OK;
let content_type = content_type_for(file.path());
let value = HeaderValue::from_static(content_type);
response.headers_mut().insert(header::CONTENT_TYPE, value);
response
}
fn content_type_for(path: &Path) -> &'static str {
match path.extension().and_then(|ext| ext.to_str()) {
Some("html") => "text/html; charset=utf-8",
Some("js") => "text/javascript; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("svg") => "image/svg+xml",
Some("png") => "image/png",
Some("ico") => "image/x-icon",
Some("json") => "application/json",
Some("map") => "application/json",
Some("txt") => "text/plain; charset=utf-8",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
Some("ttf") => "font/ttf",
Some("eot") => "application/vnd.ms-fontobject",
_ => "application/octet-stream",
}
}

View file

@ -0,0 +1,40 @@
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use sandbox_agent_agent_management::agents::AgentManager;
use sandbox_agent_core::router::{build_router, AppState, AuthConfig};
use sandbox_agent_core::ui;
use tempfile::TempDir;
use tower::util::ServiceExt;
#[tokio::test]
async fn serves_inspector_ui() {
if !ui::is_enabled() {
return;
}
let install_dir = TempDir::new().expect("create temp install dir");
let manager = AgentManager::new(install_dir.path()).expect("create agent manager");
let state = AppState::new(AuthConfig::disabled(), manager);
let app = build_router(state);
let request = Request::builder()
.uri("/ui")
.body(Body::empty())
.expect("build request");
let response = app
.oneshot(request)
.await
.expect("request handled");
assert_eq!(response.status(), StatusCode::OK);
let bytes = response
.into_body()
.collect()
.await
.expect("read body")
.to_bytes();
let body = String::from_utf8_lossy(&bytes);
assert!(body.contains("<!doctype html") || body.contains("<html"));
}

View file

@ -1,6 +1,6 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 995
assertion_line: 984
expression: normalize_events(&permission_events)
---
- agent: codex
@ -9,83 +9,28 @@ expression: normalize_events(&permission_events)
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17

View file

@ -1,15 +1,11 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 1028
assertion_line: 1017
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
---
payload:
agent: codex
detail: "agent process exited: codex"
details:
exitCode: 1
stderr: agent exited with status ExitStatus(unix_wait_status(256))
status: 500
title: Agent Process Exited
type: "urn:sandbox-agent:error:agent_process_exited"
status: 500
detail: "invalid request: unknown permission id: missing-permission"
status: 400
title: Invalid Request
type: "urn:sandbox-agent:error:invalid_request"
status: 400

View file

@ -1,6 +1,6 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 1117
assertion_line: 1106
expression: normalize_events(&reject_events)
---
- agent: codex
@ -9,83 +9,28 @@ expression: normalize_events(&reject_events)
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17

View file

@ -1,15 +1,11 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 1150
assertion_line: 1139
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
---
payload:
agent: codex
detail: "agent process exited: codex"
details:
exitCode: 1
stderr: agent exited with status ExitStatus(unix_wait_status(256))
status: 500
title: Agent Process Exited
type: "urn:sandbox-agent:error:agent_process_exited"
status: 500
detail: "invalid request: unknown question id: missing-question"
status: 400
title: Invalid Request
type: "urn:sandbox-agent:error:invalid_request"
status: 400

View file

@ -1,6 +1,6 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 1056
assertion_line: 1045
expression: normalize_events(&question_events)
---
- agent: codex
@ -9,83 +9,28 @@ expression: normalize_events(&question_events)
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17

View file

@ -1,15 +1,11 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 1089
assertion_line: 1078
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
---
payload:
agent: codex
detail: "agent process exited: codex"
details:
exitCode: 1
stderr: agent exited with status ExitStatus(unix_wait_status(256))
status: 500
title: Agent Process Exited
type: "urn:sandbox-agent:error:agent_process_exited"
status: 500
detail: "invalid request: unknown question id: missing-question"
status: 400
title: Invalid Request
type: "urn:sandbox-agent:error:invalid_request"
status: 400

View file

@ -1,6 +1,6 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 1219
assertion_line: 1214
expression: snapshot
---
session_a:
@ -10,86 +10,31 @@ session_a:
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17
session_b:
- agent: codex
kind: started
@ -97,83 +42,28 @@ session_b:
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17

View file

@ -1,6 +1,6 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 714
assertion_line: 697
expression: normalized
---
- agent: codex
@ -9,83 +9,28 @@ expression: normalized
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17

View file

@ -1,6 +1,6 @@
---
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
assertion_line: 751
assertion_line: 734
expression: normalized
---
- agent: codex
@ -9,83 +9,28 @@ expression: normalized
started:
message: session.created
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 2
started:
message: thread/started
- agent: codex
kind: message
message:
unparsed: true
kind: started
seq: 3
started:
message: turn/started
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: user
seq: 4
- agent: codex
kind: message
message:
unparsed: true
parts:
- text: "<redacted>"
type: text
role: assistant
seq: 5
- agent: codex
kind: message
message:
unparsed: true
seq: 6
- agent: codex
kind: message
message:
unparsed: true
seq: 7
- agent: codex
kind: message
message:
unparsed: true
seq: 8
- agent: codex
kind: message
message:
unparsed: true
seq: 9
- agent: codex
kind: message
message:
unparsed: true
seq: 10
- agent: codex
kind: message
message:
unparsed: true
seq: 11
- agent: codex
kind: message
message:
unparsed: true
seq: 12
- agent: codex
kind: message
message:
unparsed: true
seq: 13
- agent: codex
kind: message
message:
unparsed: true
seq: 14
- agent: codex
kind: message
message:
unparsed: true
seq: 15
- agent: codex
kind: message
message:
unparsed: true
seq: 16
- agent: codex
error:
kind: process_exit
message: agent exited with status ExitStatus(unix_wait_status(256))
kind: error
seq: 17

View file

@ -104,6 +104,7 @@
- [ ] Add universal API feature checklist (questions, approve plan, etc.)
- [ ] Document CLI, HTTP API, frontend app, and TypeScript SDK usage
- [ ] Use collapsible sections for endpoints and SDK methods
- [x] Integrate OpenAPI spec with Mintlify (docs/openapi.json + validation)
---
@ -111,3 +112,4 @@
- implement e2b example
- implement typescript "start locally" by pulling form server using version
- [x] Move agent schema sources to src/agents
- [x] Add Vercel AI SDK UIMessage schema extractor

View file

@ -9,6 +9,9 @@
"dependsOn": ["^generate"],
"outputs": ["src/generated/**"]
},
"typecheck": {
"dependsOn": ["^build", "build"]
},
"dev": {
"cache": false,
"persistent": true