mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 04:03:31 +00:00
chore: sync workspace changes
This commit is contained in:
parent
4b5b390b7f
commit
4083baa1c1
55 changed files with 2431 additions and 840 deletions
12
.github/workflows/release.yaml
vendored
12
.github/workflows/release.yaml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
174
ARCHITECTURE.md
174
ARCHITECTURE.md
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -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.
|
||||
|
|
|
|||
22
ROADMAP.md
22
ROADMAP.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
24
docs/cli.mdx
24
docs/cli.mdx
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@
|
|||
"http-api",
|
||||
"typescript-sdk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "API",
|
||||
"openapi": "openapi.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@
|
|||
},
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:2468"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/agents": {
|
||||
"get": {
|
||||
|
|
@ -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"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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
83
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ packages:
|
|||
- "sdks/cli"
|
||||
- "sdks/cli/platforms/*"
|
||||
- "resources/agent-schemas"
|
||||
- "resources/vercel-ai-sdk-schemas"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
resources/vercel-ai-sdk-schemas/.tmp/log.txt
Normal file
50
resources/vercel-ai-sdk-schemas/.tmp/log.txt
Normal 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
|
||||
23
resources/vercel-ai-sdk-schemas/README.md
Normal file
23
resources/vercel-ai-sdk-schemas/README.md
Normal 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
22
resources/vercel-ai-sdk-schemas/package.json
Normal file
22
resources/vercel-ai-sdk-schemas/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
93
resources/vercel-ai-sdk-schemas/src/cache.ts
Normal file
93
resources/vercel-ai-sdk-schemas/src/cache.ts
Normal 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;
|
||||
}
|
||||
398
resources/vercel-ai-sdk-schemas/src/index.ts
Normal file
398
resources/vercel-ai-sdk-schemas/src/index.ts
Normal 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);
|
||||
});
|
||||
11
resources/vercel-ai-sdk-schemas/tsconfig.json
Normal file
11
resources/vercel-ai-sdk-schemas/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
63
server/packages/sandbox-agent/build.rs
Normal file
63
server/packages/sandbox-agent/build.rs
Normal 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('"', "\\\"")
|
||||
}
|
||||
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
pub mod credentials;
|
||||
pub mod router;
|
||||
pub mod ui;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(¬ification);
|
||||
let conversion = convert_codex::notification_to_universal(¬ification);
|
||||
let should_terminate = matches!(
|
||||
notification,
|
||||
codex_schema::ServerNotification::TurnCompleted(_)
|
||||
| codex_schema::ServerNotification::Error(_)
|
||||
);
|
||||
CodexLineOutcome {
|
||||
conversion: Some(conversion),
|
||||
should_terminate,
|
||||
if codex_should_emit_notification(¬ification) {
|
||||
let conversion = convert_codex::notification_to_universal(¬ification);
|
||||
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 } => {
|
||||
|
|
|
|||
81
server/packages/sandbox-agent/src/ui.rs
Normal file
81
server/packages/sandbox-agent/src/ui.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
40
server/packages/sandbox-agent/tests/inspector_ui.rs
Normal file
40
server/packages/sandbox-agent/tests/inspector_ui.rs
Normal 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"));
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
todo.md
2
todo.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
"dependsOn": ["^generate"],
|
||||
"outputs": ["src/generated/**"]
|
||||
},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^build", "build"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue