mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 05:00:20 +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:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
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
|
# Misc
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
regress = "0.10"
|
regress = "0.10"
|
||||||
|
include_dir = "0.7"
|
||||||
|
|
||||||
# Code generation (build deps)
|
# Code generation (build deps)
|
||||||
typify = "0.4"
|
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.
|
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
|
- **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
|
- **Universal session schema**: Universal schema to store agent transcripts
|
||||||
- **Supports your sandbox provider**: Daytona, E2B, Vercel Sandboxes, and more
|
- **Supports your sandbox provider**: Daytona, E2B, Vercel Sandboxes, and more
|
||||||
- **Lightweight, portable Rust binary**: Install anywhere with 1 curl command
|
- **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`
|
[ ] Python SDK
|
||||||
- **Auto-configure MCP & Skills**: Auto-load MCP servers & skills for your agents
|
[ ] Automatic MCP & skillfile configuration
|
||||||
- **Process & logs manager**: Manage processes, logs, and ports for your agents to run background processes
|
|
||||||
|
|
||||||
## Agent Support
|
## 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?**
|
**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.
|
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
|
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
|
- re-review agent schemas and compare it to ours
|
||||||
- switch sdk to use sdtin/stdout for embedded mdoe
|
- 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
|
- discuss actor arch in readme + give example
|
||||||
- skillfile
|
- skillfile
|
||||||
- specifically include the release checklist
|
- 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
|
## 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
|
- auto-updating for long running job
|
||||||
- persistence
|
- persistence
|
||||||
- system information/cpu/etc
|
- system information/cpu/etc
|
||||||
- git utils
|
|
||||||
- api features
|
- api features
|
||||||
- list agent modes available
|
- list agent modes available
|
||||||
- list models available
|
- list models available
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ description: "Supported agents, install methods, and streaming formats."
|
||||||
|
|
||||||
## Capability notes
|
## Capability notes
|
||||||
|
|
||||||
- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event.
|
- **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.
|
- **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.
|
- **Files and images**: normalized via `UniversalMessagePart` with `File` and `Image` parts.
|
||||||
|
|
||||||
See [Universal API](/universal-api) for feature coverage details.
|
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."
|
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
|
## Server flags
|
||||||
|
|
||||||
```bash
|
```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.
|
- `--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>
|
<summary><strong>agents list</strong></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent agents list --endpoint http://127.0.0.1:2468
|
sandbox-daemon agents list --endpoint http://127.0.0.1:2468
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ sandbox-agent agents list --endpoint http://127.0.0.1:2468
|
||||||
<summary><strong>agents install</strong></summary>
|
<summary><strong>agents install</strong></summary>
|
||||||
|
|
||||||
```bash
|
```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>
|
</details>
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:2468
|
||||||
<summary><strong>agents modes</strong></summary>
|
<summary><strong>agents modes</strong></summary>
|
||||||
|
|
||||||
```bash
|
```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>
|
</details>
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ sandbox-agent agents modes claude --endpoint http://127.0.0.1:2468
|
||||||
<summary><strong>sessions create</strong></summary>
|
<summary><strong>sessions create</strong></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent sessions create my-session \
|
sandbox-daemon sessions create my-session \
|
||||||
--agent claude \
|
--agent claude \
|
||||||
--agent-mode build \
|
--agent-mode build \
|
||||||
--permission-mode default \
|
--permission-mode default \
|
||||||
|
|
@ -60,7 +60,7 @@ sandbox-agent sessions create my-session \
|
||||||
<summary><strong>sessions send-message</strong></summary>
|
<summary><strong>sessions send-message</strong></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent sessions send-message my-session \
|
sandbox-daemon sessions send-message my-session \
|
||||||
--message "Summarize the repository" \
|
--message "Summarize the repository" \
|
||||||
--endpoint http://127.0.0.1:2468
|
--endpoint http://127.0.0.1:2468
|
||||||
```
|
```
|
||||||
|
|
@ -70,7 +70,7 @@ sandbox-agent sessions send-message my-session \
|
||||||
<summary><strong>sessions events</strong></summary>
|
<summary><strong>sessions events</strong></summary>
|
||||||
|
|
||||||
```bash
|
```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>
|
</details>
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http:/
|
||||||
<summary><strong>sessions events-sse</strong></summary>
|
<summary><strong>sessions events-sse</strong></summary>
|
||||||
|
|
||||||
```bash
|
```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>
|
</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>
|
<summary><strong>sessions reply-question</strong></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent sessions reply-question my-session QUESTION_ID \
|
sandbox-daemon sessions reply-question my-session QUESTION_ID \
|
||||||
--answers "yes" \
|
--answers "yes" \
|
||||||
--endpoint http://127.0.0.1:2468
|
--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>
|
<summary><strong>sessions reject-question</strong></summary>
|
||||||
|
|
||||||
```bash
|
```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>
|
</details>
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http://
|
||||||
<summary><strong>sessions reply-permission</strong></summary>
|
<summary><strong>sessions reply-permission</strong></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent sessions reply-permission my-session PERMISSION_ID \
|
sandbox-daemon sessions reply-permission my-session PERMISSION_ID \
|
||||||
--reply once \
|
--reply once \
|
||||||
--endpoint http://127.0.0.1:2468
|
--endpoint http://127.0.0.1:2468
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ description: "Deploy the daemon in Cloudflare Sandboxes."
|
||||||
```bash
|
```bash
|
||||||
export SANDBOX_TOKEN="..."
|
export SANDBOX_TOKEN="..."
|
||||||
|
|
||||||
cargo run -p sandbox-agent -- \
|
cargo run -p sandbox-agent -- server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
--token "$SANDBOX_TOKEN" \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 2468
|
--port 2468
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ description: "Run the daemon in a Daytona workspace."
|
||||||
```bash
|
```bash
|
||||||
export SANDBOX_TOKEN="..."
|
export SANDBOX_TOKEN="..."
|
||||||
|
|
||||||
cargo run -p sandbox-agent -- \
|
cargo run -p sandbox-agent -- server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
--token "$SANDBOX_TOKEN" \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 2468
|
--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 \
|
docker run --rm -p 2468:2468 \
|
||||||
-v "$PWD/artifacts:/artifacts" \
|
-v "$PWD/artifacts:/artifacts" \
|
||||||
debian:bookworm-slim \
|
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`.
|
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)
|
# Install sandbox-agent binary (or build from source)
|
||||||
# TODO: replace with release download once published
|
# TODO: replace with release download once published
|
||||||
cargo run -p sandbox-agent -- \
|
cargo run -p sandbox-agent -- server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
--token "$SANDBOX_TOKEN" \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 2468
|
--port 2468
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ description: "Run the daemon inside Vercel Sandboxes."
|
||||||
```bash
|
```bash
|
||||||
export SANDBOX_TOKEN="..."
|
export SANDBOX_TOKEN="..."
|
||||||
|
|
||||||
cargo run -p sandbox-agent -- \
|
cargo run -p sandbox-agent -- server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
--token "$SANDBOX_TOKEN" \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
--port 2468
|
--port 2468
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@
|
||||||
"http-api",
|
"http-api",
|
||||||
"typescript-sdk"
|
"typescript-sdk"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "API",
|
||||||
|
"openapi": "openapi.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,6 @@ The UI expects:
|
||||||
- Endpoint (e.g. `http://127.0.0.1:2468`)
|
- Endpoint (e.g. `http://127.0.0.1:2468`)
|
||||||
- Optional token
|
- 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:
|
Run the daemon locally:
|
||||||
|
|
||||||
```bash
|
```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:
|
Send a message:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@
|
||||||
},
|
},
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:2468"
|
||||||
|
}
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"/v1/agents": {
|
"/v1/agents": {
|
||||||
"get": {
|
"get": {
|
||||||
|
|
@ -8,13 +8,19 @@ description: "Start the daemon and send your first message."
|
||||||
Use the installed binary, or `cargo run` in development.
|
Use the installed binary, or `cargo run` in development.
|
||||||
|
|
||||||
```bash
|
```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):
|
If you want to run without auth (local dev only):
|
||||||
|
|
||||||
```bash
|
```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)
|
### 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:
|
If you are calling the daemon from a browser, enable CORS explicitly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent \
|
sandbox-daemon server \
|
||||||
--token "$SANDBOX_TOKEN" \
|
--token "$SANDBOX_TOKEN" \
|
||||||
--cors-allow-origin "http://localhost:5173" \
|
--cors-allow-origin "http://localhost:5173" \
|
||||||
--cors-allow-method "GET" \
|
--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:
|
The CLI mirrors the HTTP API:
|
||||||
|
|
||||||
```bash
|
```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:
|
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
|
- `openapi-typescript` to generate types
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "pnpm --filter sandbox-agent build && vite build",
|
"build": "pnpm --filter sandbox-agent build && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sandbox-agent": "workspace:*",
|
"sandbox-agent": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,17 @@ const formatTime = (value: string) => {
|
||||||
return date.toLocaleTimeString();
|
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() {
|
export default function App() {
|
||||||
const [endpoint, setEndpoint] = useState("http://localhost:2468");
|
const [endpoint, setEndpoint] = useState(getDefaultEndpoint);
|
||||||
const [token, setToken] = useState("");
|
const [token, setToken] = useState("");
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
|
|
@ -195,18 +204,25 @@ export default function App() {
|
||||||
return error instanceof Error ? error.message : fallback;
|
return error instanceof Error ? error.message : fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const connect = async () => {
|
const connectToDaemon = async (reportError: boolean) => {
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
setConnectError(null);
|
if (reportError) {
|
||||||
|
setConnectError(null);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const client = createClient();
|
const client = createClient();
|
||||||
await client.getHealth();
|
await client.getHealth();
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
await fetchSessions();
|
await fetchSessions();
|
||||||
|
if (reportError) {
|
||||||
|
setConnectError(null);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error, "Unable to connect");
|
if (reportError) {
|
||||||
setConnectError(message);
|
const message = getErrorMessage(error, "Unable to connect");
|
||||||
|
setConnectError(message);
|
||||||
|
}
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
clientRef.current = null;
|
clientRef.current = null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -214,6 +230,8 @@ export default function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connect = () => connectToDaemon(true);
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
clientRef.current = null;
|
clientRef.current = null;
|
||||||
|
|
@ -531,10 +549,10 @@ export default function App() {
|
||||||
.filter((event): event is UniversalEvent & { data: { message: UniversalMessage } } => "message" in event.data)
|
.filter((event): event is UniversalEvent & { data: { message: UniversalMessage } } => "message" in event.data)
|
||||||
.map((event) => {
|
.map((event) => {
|
||||||
const msg = event.data.message;
|
const msg = event.data.message;
|
||||||
const parts = "parts" in msg ? msg.parts : [];
|
const parts = ("parts" in msg ? msg.parts : []) ?? [];
|
||||||
const content = parts
|
const content = parts
|
||||||
.filter((part: UniversalMessagePart) => part.type === "text" && part.text)
|
.filter((part: UniversalMessagePart): part is UniversalMessagePart & { type: "text"; text: string } => part.type === "text" && "text" in part && typeof part.text === "string")
|
||||||
.map((part: UniversalMessagePart) => part.text)
|
.map((part) => part.text)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
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(() => {
|
useEffect(() => {
|
||||||
if (!connected) return;
|
if (!connected) return;
|
||||||
refreshAgents();
|
refreshAgents();
|
||||||
|
|
@ -672,7 +704,7 @@ export default function App() {
|
||||||
|
|
||||||
<p className="hint">
|
<p className="hint">
|
||||||
Start the daemon with CORS enabled for browser access:<br />
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => ({
|
||||||
|
base: command === "build" ? "/ui/" : "/",
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173
|
port: 5173
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --parallel",
|
"dev": "turbo run dev --parallel",
|
||||||
"generate": "turbo run generate"
|
"generate": "turbo run generate",
|
||||||
|
"typecheck": "turbo run typecheck"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"turbo": "^2.4.0"
|
"turbo": "^2.4.0"
|
||||||
|
|
|
||||||
83
pnpm-lock.yaml
generated
83
pnpm-lock.yaml
generated
|
|
@ -67,6 +67,9 @@ importers:
|
||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/json-schema':
|
||||||
|
specifier: ^7.0.15
|
||||||
|
version: 7.0.15
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.0
|
specifier: ^22.0.0
|
||||||
version: 22.19.7
|
version: 22.19.7
|
||||||
|
|
@ -74,6 +77,34 @@ importers:
|
||||||
specifier: ^4.19.0
|
specifier: ^4.19.0
|
||||||
version: 4.21.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: {}
|
||||||
|
|
||||||
sdks/cli/platforms/darwin-arm64: {}
|
sdks/cli/platforms/darwin-arm64: {}
|
||||||
|
|
@ -579,6 +610,10 @@ packages:
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
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':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
|
@ -772,6 +807,9 @@ packages:
|
||||||
'@types/react@18.3.27':
|
'@types/react@18.3.27':
|
||||||
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
|
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
|
||||||
|
|
||||||
|
'@types/semver@7.7.1':
|
||||||
|
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0':
|
'@vitejs/plugin-react@4.7.0':
|
||||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
@ -827,6 +865,10 @@ packages:
|
||||||
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
|
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
|
||||||
engines: {node: '>=20.18.1'}
|
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:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
@ -1033,6 +1075,10 @@ packages:
|
||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
|
@ -1131,6 +1177,11 @@ packages:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.7.3:
|
||||||
|
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -1167,6 +1218,10 @@ packages:
|
||||||
resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
|
resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
tar@7.5.6:
|
||||||
|
resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
@ -1296,6 +1351,10 @@ packages:
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
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:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -1648,6 +1707,10 @@ snapshots:
|
||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
wrap-ansi-cjs: wrap-ansi@7.0.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':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
@ -1798,6 +1861,8 @@ snapshots:
|
||||||
'@types/prop-types': 15.7.15
|
'@types/prop-types': 15.7.15
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/semver@7.7.1': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.7))':
|
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.7))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.6
|
'@babel/core': 7.28.6
|
||||||
|
|
@ -1865,6 +1930,8 @@ snapshots:
|
||||||
undici: 7.19.1
|
undici: 7.19.1
|
||||||
whatwg-mimetype: 4.0.0
|
whatwg-mimetype: 4.0.0
|
||||||
|
|
||||||
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|
@ -2098,6 +2165,10 @@ snapshots:
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
minizlib@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
@ -2214,6 +2285,8 @@ snapshots:
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
|
semver@7.7.3: {}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
|
|
@ -2246,6 +2319,14 @@ snapshots:
|
||||||
|
|
||||||
supports-color@9.4.0: {}
|
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:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
@ -2346,4 +2427,6 @@ snapshots:
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ packages:
|
||||||
- "sdks/cli"
|
- "sdks/cli"
|
||||||
- "sdks/cli/platforms/*"
|
- "sdks/cli/platforms/*"
|
||||||
- "resources/agent-schemas"
|
- "resources/agent-schemas"
|
||||||
|
- "resources/vercel-ai-sdk-schemas"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"extract:claude-events": "tsx src/claude-event-types.ts",
|
"extract:claude-events": "tsx src/claude-event-types.ts",
|
||||||
"extract:claude-events:sdk": "tsx src/claude-event-types-sdk.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: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": {
|
"dependencies": {
|
||||||
"ts-json-schema-generator": "^2.4.0",
|
"ts-json-schema-generator": "^2.4.0",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsx": "^4.19.0",
|
"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) {
|
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");
|
const sdkDir = path.join(rootDir, "sdks", "typescript");
|
||||||
run("pnpm", ["run", "generate"], { cwd: sdkDir });
|
run("pnpm", ["run", "generate"], { cwd: sdkDir });
|
||||||
run("cargo", ["check", "-p", "sandbox-agent-universal-schema-gen"], { cwd: rootDir });
|
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,
|
cwd: rootDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -367,14 +372,25 @@ function uploadBinaries(rootDir: string, version: string, latest: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function runChecks(rootDir: string) {
|
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");
|
console.log("==> Running Rust checks");
|
||||||
run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir });
|
run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir });
|
||||||
run("cargo", ["clippy", "--all-targets", "--", "-D", "warnings"], { cwd: rootDir });
|
run("cargo", ["clippy", "--all-targets", "--", "-D", "warnings"], { cwd: rootDir });
|
||||||
run("cargo", ["test", "--all-targets"], { cwd: rootDir });
|
run("cargo", ["test", "--all-targets"], { cwd: rootDir });
|
||||||
|
|
||||||
console.log("==> Running TypeScript checks");
|
console.log("==> Running TypeScript checks");
|
||||||
run("pnpm", ["install"], { cwd: rootDir });
|
|
||||||
run("pnpm", ["run", "build"], { 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) {
|
function publishCrates(rootDir: string, version: string) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"url": "https://github.com/rivet-dev/sandbox-agent"
|
"url": "https://github.com/rivet-dev/sandbox-agent"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"sandbox-agent": "bin/sandbox-agent"
|
"sandbox-agent": "bin/sandbox-agent",
|
||||||
|
"sandbox-daemon": "bin/sandbox-agent"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@sandbox-agent/cli-darwin-arm64": "0.1.0",
|
"@sandbox-agent/cli-darwin-arm64": "0.1.0",
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,11 @@
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../openapi/openapi.json",
|
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../../docs/openapi.json",
|
||||||
"generate:types": "openapi-typescript ../openapi/openapi.json -o src/generated/openapi.ts",
|
"generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/openapi.ts",
|
||||||
"generate": "pnpm run generate:openapi && pnpm run generate:types",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ export interface paths {
|
||||||
"/v1/health": {
|
"/v1/health": {
|
||||||
get: operations["get_health"];
|
get: operations["get_health"];
|
||||||
};
|
};
|
||||||
|
"/v1/sessions": {
|
||||||
|
get: operations["list_sessions"];
|
||||||
|
};
|
||||||
"/v1/sessions/{session_id}": {
|
"/v1/sessions/{session_id}": {
|
||||||
post: operations["create_session"];
|
post: operations["create_session"];
|
||||||
};
|
};
|
||||||
|
|
@ -179,6 +182,21 @@ export interface components {
|
||||||
callId: string;
|
callId: string;
|
||||||
messageId: 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: {
|
Started: {
|
||||||
details?: unknown;
|
details?: unknown;
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
|
|
@ -358,6 +376,15 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
list_sessions: {
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SessionListResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
create_session: {
|
create_session: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { ChildProcess } from "node:child_process";
|
import type { ChildProcess } from "node:child_process";
|
||||||
import type { AddressInfo } from "node:net";
|
import type { AddressInfo } from "node:net";
|
||||||
import type { NodeRequire } from "node:module";
|
|
||||||
|
|
||||||
export type SandboxDaemonSpawnLogMode = "inherit" | "pipe" | "silent";
|
export type SandboxDaemonSpawnLogMode = "inherit" | "pipe" | "silent";
|
||||||
|
|
||||||
|
|
@ -68,7 +67,7 @@ export async function spawnSandboxDaemon(
|
||||||
}
|
}
|
||||||
|
|
||||||
const stdio = logMode === "inherit" ? "inherit" : logMode === "silent" ? "ignore" : "pipe";
|
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, {
|
const child = spawn(binaryPath, args, {
|
||||||
stdio,
|
stdio,
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -112,7 +111,7 @@ function resolveBinaryFromEnv(fs: typeof import("node:fs"), path: typeof import(
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBinaryFromCliPackage(
|
function resolveBinaryFromCliPackage(
|
||||||
require: NodeRequire,
|
require: ReturnType<typeof import("node:module").createRequire>,
|
||||||
path: typeof import("node:path"),
|
path: typeof import("node:path"),
|
||||||
fs: typeof import("node:fs"),
|
fs: typeof import("node:fs"),
|
||||||
): string | null {
|
): string | null {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ schemars.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-logfmt.workspace = true
|
tracing-logfmt.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
include_dir.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
http-body-util.workspace = true
|
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 credentials;
|
||||||
pub mod router;
|
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::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse};
|
||||||
use sandbox_agent_core::router::build_router;
|
use sandbox_agent_core::router::build_router;
|
||||||
|
use sandbox_agent_core::ui;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -23,25 +24,42 @@ use tower_http::cors::{Any, CorsLayer};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
const API_PREFIX: &str = "/v1";
|
const API_PREFIX: &str = "/v1";
|
||||||
|
const DEFAULT_HOST: &str = "127.0.0.1";
|
||||||
|
const DEFAULT_PORT: u16 = 2468;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "sandbox-agent")]
|
#[command(name = "sandbox-daemon", bin_name = "sandbox-agent")]
|
||||||
#[command(about = "Sandbox agent for managing coding agents", version)]
|
#[command(about = "Sandbox daemon for managing coding agents", version)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
|
|
||||||
#[arg(long, short = 'H', default_value = "127.0.0.1")]
|
#[arg(long, short = 't', global = true)]
|
||||||
host: String,
|
|
||||||
|
|
||||||
#[arg(long, short = 'p', default_value_t = 2468)]
|
|
||||||
port: u16,
|
|
||||||
|
|
||||||
#[arg(long, short = 't')]
|
|
||||||
token: Option<String>,
|
token: Option<String>,
|
||||||
|
|
||||||
#[arg(long, short = 'n')]
|
#[arg(long, short = 'n', global = true)]
|
||||||
no_token: bool,
|
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')]
|
#[arg(long = "cors-allow-origin", short = 'O')]
|
||||||
cors_allow_origin: Vec<String>,
|
cors_allow_origin: Vec<String>,
|
||||||
|
|
@ -56,16 +74,6 @@ struct Cli {
|
||||||
cors_allow_credentials: bool,
|
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)]
|
#[derive(Args, Debug)]
|
||||||
struct AgentsArgs {
|
struct AgentsArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|
@ -255,6 +263,8 @@ struct CredentialsExtractEnvArgs {
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
enum CliError {
|
enum CliError {
|
||||||
|
#[error("missing command: run `sandbox-daemon server` to start the daemon")]
|
||||||
|
MissingCommand,
|
||||||
#[error("missing --token or --no-token for server mode")]
|
#[error("missing --token or --no-token for server mode")]
|
||||||
MissingToken,
|
MissingToken,
|
||||||
#[error("invalid cors origin: {0}")]
|
#[error("invalid cors origin: {0}")]
|
||||||
|
|
@ -280,8 +290,9 @@ fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let result = match &cli.command {
|
let result = match &cli.command {
|
||||||
|
Some(Command::Server(args)) => run_server(&cli, args),
|
||||||
Some(command) => run_client(command, &cli),
|
Some(command) => run_client(command, &cli),
|
||||||
None => run_server(&cli),
|
None => Err(CliError::MissingCommand),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
|
|
@ -298,7 +309,7 @@ fn init_logging() {
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_server(cli: &Cli) -> Result<(), CliError> {
|
fn run_server(cli: &Cli, server: &ServerArgs) -> Result<(), CliError> {
|
||||||
let auth = if cli.no_token {
|
let auth = if cli.no_token {
|
||||||
AuthConfig::disabled()
|
AuthConfig::disabled()
|
||||||
} else if let Some(token) = cli.token.clone() {
|
} 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 state = AppState::new(auth, agent_manager);
|
||||||
let mut router = build_router(state);
|
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);
|
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()
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -325,6 +341,11 @@ fn run_server(cli: &Cli) -> Result<(), CliError> {
|
||||||
runtime.block_on(async move {
|
runtime.block_on(async move {
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
tracing::info!(addr = %addr, "server listening");
|
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)
|
axum::serve(listener, router)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| CliError::Server(err.to_string()))
|
.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> {
|
fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
|
||||||
match command {
|
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::Agents(subcommand) => run_agents(&subcommand.command, cli),
|
||||||
Command::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
|
Command::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
|
||||||
Command::Credentials(subcommand) => run_credentials(&subcommand.command),
|
Command::Credentials(subcommand) => run_credentials(&subcommand.command),
|
||||||
|
|
@ -663,11 +687,11 @@ fn available_providers(credentials: &ExtractedCredentials) -> Vec<String> {
|
||||||
providers
|
providers
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
|
fn build_cors_layer(server: &ServerArgs) -> Result<Option<CorsLayer>, CliError> {
|
||||||
let has_config = !cli.cors_allow_origin.is_empty()
|
let has_config = !server.cors_allow_origin.is_empty()
|
||||||
|| !cli.cors_allow_method.is_empty()
|
|| !server.cors_allow_method.is_empty()
|
||||||
|| !cli.cors_allow_header.is_empty()
|
|| !server.cors_allow_header.is_empty()
|
||||||
|| cli.cors_allow_credentials;
|
|| server.cors_allow_credentials;
|
||||||
|
|
||||||
if !has_config {
|
if !has_config {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -675,11 +699,11 @@ fn build_cors_layer(cli: &Cli) -> Result<Option<CorsLayer>, CliError> {
|
||||||
|
|
||||||
let mut cors = CorsLayer::new();
|
let mut cors = CorsLayer::new();
|
||||||
|
|
||||||
if cli.cors_allow_origin.is_empty() {
|
if server.cors_allow_origin.is_empty() {
|
||||||
cors = cors.allow_origin(Any);
|
cors = cors.allow_origin(Any);
|
||||||
} else {
|
} else {
|
||||||
let mut origins = Vec::new();
|
let mut origins = Vec::new();
|
||||||
for origin in &cli.cors_allow_origin {
|
for origin in &server.cors_allow_origin {
|
||||||
let value = origin
|
let value = origin
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| CliError::InvalidCorsOrigin(origin.clone()))?;
|
.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);
|
cors = cors.allow_origin(origins);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.cors_allow_method.is_empty() {
|
if server.cors_allow_method.is_empty() {
|
||||||
cors = cors.allow_methods(Any);
|
cors = cors.allow_methods(Any);
|
||||||
} else {
|
} else {
|
||||||
let mut methods = Vec::new();
|
let mut methods = Vec::new();
|
||||||
for method in &cli.cors_allow_method {
|
for method in &server.cors_allow_method {
|
||||||
let parsed = method
|
let parsed = method
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| CliError::InvalidCorsMethod(method.clone()))?;
|
.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);
|
cors = cors.allow_methods(methods);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.cors_allow_header.is_empty() {
|
if server.cors_allow_header.is_empty() {
|
||||||
cors = cors.allow_headers(Any);
|
cors = cors.allow_headers(Any);
|
||||||
} else {
|
} else {
|
||||||
let mut headers = Vec::new();
|
let mut headers = Vec::new();
|
||||||
for header in &cli.cors_allow_header {
|
for header in &server.cors_allow_header {
|
||||||
let parsed = header
|
let parsed = header
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| CliError::InvalidCorsHeader(header.clone()))?;
|
.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);
|
cors = cors.allow_headers(headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.cors_allow_credentials {
|
if server.cors_allow_credentials {
|
||||||
cors = cors.allow_credentials(true);
|
cors = cors.allow_credentials(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -732,7 +756,7 @@ impl ClientContext {
|
||||||
let endpoint = args
|
let endpoint = args
|
||||||
.endpoint
|
.endpoint
|
||||||
.clone()
|
.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 token = if cli.no_token { None } else { cli.token.clone() };
|
||||||
let client = HttpClient::builder().build()?;
|
let client = HttpClient::builder().build()?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ use serde_json::{json, Value};
|
||||||
use tokio::sync::{broadcast, mpsc, Mutex};
|
use tokio::sync::{broadcast, mpsc, Mutex};
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use utoipa::{OpenApi, ToSchema};
|
use utoipa::{Modify, OpenApi, ToSchema};
|
||||||
|
|
||||||
use sandbox_agent_agent_management::agents::{
|
use sandbox_agent_agent_management::agents::{
|
||||||
AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn,
|
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 = "meta", description = "Service metadata"),
|
||||||
(name = "agents", description = "Agent management"),
|
(name = "agents", description = "Agent management"),
|
||||||
(name = "sessions", description = "Session management")
|
(name = "sessions", description = "Session management")
|
||||||
)
|
),
|
||||||
|
modifiers(&ServerAddon)
|
||||||
)]
|
)]
|
||||||
pub struct ApiDoc;
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
@ -594,14 +605,14 @@ impl SessionManager {
|
||||||
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
|
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
|
||||||
session_id: session_id.to_string(),
|
session_id: session_id.to_string(),
|
||||||
})?;
|
})?;
|
||||||
if let Some(err) = session.ended_error() {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
if !session.take_question(question_id) {
|
if !session.take_question(question_id) {
|
||||||
return Err(SandboxError::InvalidRequest {
|
return Err(SandboxError::InvalidRequest {
|
||||||
message: format!("unknown question id: {question_id}"),
|
message: format!("unknown question id: {question_id}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if let Some(err) = session.ended_error() {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
(session.agent, session.agent_session_id.clone())
|
(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 {
|
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
|
||||||
session_id: session_id.to_string(),
|
session_id: session_id.to_string(),
|
||||||
})?;
|
})?;
|
||||||
if let Some(err) = session.ended_error() {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
if !session.take_question(question_id) {
|
if !session.take_question(question_id) {
|
||||||
return Err(SandboxError::InvalidRequest {
|
return Err(SandboxError::InvalidRequest {
|
||||||
message: format!("unknown question id: {question_id}"),
|
message: format!("unknown question id: {question_id}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if let Some(err) = session.ended_error() {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
(session.agent, session.agent_session_id.clone())
|
(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 {
|
let session = sessions.get_mut(session_id).ok_or_else(|| SandboxError::SessionNotFound {
|
||||||
session_id: session_id.to_string(),
|
session_id: session_id.to_string(),
|
||||||
})?;
|
})?;
|
||||||
if let Some(err) = session.ended_error() {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
if !session.take_permission(permission_id) {
|
if !session.take_permission(permission_id) {
|
||||||
return Err(SandboxError::InvalidRequest {
|
return Err(SandboxError::InvalidRequest {
|
||||||
message: format!("unknown permission id: {permission_id}"),
|
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 {
|
let codex_metadata = if session.agent == AgentId::Codex {
|
||||||
session.events.iter().find_map(|event| {
|
session.events.iter().find_map(|event| {
|
||||||
if let UniversalEventData::PermissionAsked { permission_asked } = &event.data {
|
if let UniversalEventData::PermissionAsked { permission_asked } = &event.data {
|
||||||
|
|
@ -858,47 +869,45 @@ impl SessionManager {
|
||||||
Ok(Ok(status)) if status.success() => {}
|
Ok(Ok(status)) if status.success() => {}
|
||||||
Ok(Ok(status)) => {
|
Ok(Ok(status)) => {
|
||||||
let message = format!("agent exited with status {:?}", status);
|
let message = format!("agent exited with status {:?}", status);
|
||||||
self.record_error(
|
if !terminate_early {
|
||||||
&session_id,
|
self.record_error(
|
||||||
message.clone(),
|
&session_id,
|
||||||
Some("process_exit".to_string()),
|
message.clone(),
|
||||||
None,
|
Some("process_exit".to_string()),
|
||||||
)
|
None,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
}
|
||||||
self.mark_session_ended(&session_id, status.code(), &message)
|
self.mark_session_ended(&session_id, status.code(), &message)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
let message = format!("failed to wait for agent: {err}");
|
let message = format!("failed to wait for agent: {err}");
|
||||||
self.record_error(
|
if !terminate_early {
|
||||||
&session_id,
|
self.record_error(
|
||||||
message.clone(),
|
&session_id,
|
||||||
Some("process_wait_failed".to_string()),
|
message.clone(),
|
||||||
None,
|
Some("process_wait_failed".to_string()),
|
||||||
)
|
None,
|
||||||
.await;
|
)
|
||||||
self.mark_session_ended(
|
.await;
|
||||||
&session_id,
|
}
|
||||||
None,
|
self.mark_session_ended(&session_id, None, &message)
|
||||||
&message,
|
.await;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let message = format!("failed to join agent task: {err}");
|
let message = format!("failed to join agent task: {err}");
|
||||||
self.record_error(
|
if !terminate_early {
|
||||||
&session_id,
|
self.record_error(
|
||||||
message.clone(),
|
&session_id,
|
||||||
Some("process_wait_failed".to_string()),
|
message.clone(),
|
||||||
None,
|
Some("process_wait_failed".to_string()),
|
||||||
)
|
None,
|
||||||
.await;
|
)
|
||||||
self.mark_session_ended(
|
.await;
|
||||||
&session_id,
|
}
|
||||||
None,
|
self.mark_session_ended(&session_id, None, &message)
|
||||||
&message,
|
.await;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2179,15 +2188,22 @@ impl CodexAppServerState {
|
||||||
serde_json::from_value::<codex_schema::ServerNotification>(value.clone())
|
serde_json::from_value::<codex_schema::ServerNotification>(value.clone())
|
||||||
{
|
{
|
||||||
self.maybe_capture_thread_id(¬ification);
|
self.maybe_capture_thread_id(¬ification);
|
||||||
let conversion = convert_codex::notification_to_universal(¬ification);
|
|
||||||
let should_terminate = matches!(
|
let should_terminate = matches!(
|
||||||
notification,
|
notification,
|
||||||
codex_schema::ServerNotification::TurnCompleted(_)
|
codex_schema::ServerNotification::TurnCompleted(_)
|
||||||
| codex_schema::ServerNotification::Error(_)
|
| codex_schema::ServerNotification::Error(_)
|
||||||
);
|
);
|
||||||
CodexLineOutcome {
|
if codex_should_emit_notification(¬ification) {
|
||||||
conversion: Some(conversion),
|
let conversion = convert_codex::notification_to_universal(¬ification);
|
||||||
should_terminate,
|
CodexLineOutcome {
|
||||||
|
conversion: Some(conversion),
|
||||||
|
should_terminate,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CodexLineOutcome {
|
||||||
|
conversion: None,
|
||||||
|
should_terminate,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CodexLineOutcome::default()
|
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 {
|
fn codex_request_to_universal(request: &codex_schema::ServerRequest) -> EventConversion {
|
||||||
match request {
|
match request {
|
||||||
codex_schema::ServerRequest::ItemCommandExecutionRequestApproval { id, params } => {
|
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
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 995
|
assertion_line: 984
|
||||||
expression: normalize_events(&permission_events)
|
expression: normalize_events(&permission_events)
|
||||||
---
|
---
|
||||||
- agent: codex
|
- agent: codex
|
||||||
|
|
@ -9,83 +9,28 @@ expression: normalize_events(&permission_events)
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 1028
|
assertion_line: 1017
|
||||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
---
|
---
|
||||||
payload:
|
payload:
|
||||||
agent: codex
|
detail: "invalid request: unknown permission id: missing-permission"
|
||||||
detail: "agent process exited: codex"
|
status: 400
|
||||||
details:
|
title: Invalid Request
|
||||||
exitCode: 1
|
type: "urn:sandbox-agent:error:invalid_request"
|
||||||
stderr: agent exited with status ExitStatus(unix_wait_status(256))
|
status: 400
|
||||||
status: 500
|
|
||||||
title: Agent Process Exited
|
|
||||||
type: "urn:sandbox-agent:error:agent_process_exited"
|
|
||||||
status: 500
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 1117
|
assertion_line: 1106
|
||||||
expression: normalize_events(&reject_events)
|
expression: normalize_events(&reject_events)
|
||||||
---
|
---
|
||||||
- agent: codex
|
- agent: codex
|
||||||
|
|
@ -9,83 +9,28 @@ expression: normalize_events(&reject_events)
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 1150
|
assertion_line: 1139
|
||||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
---
|
---
|
||||||
payload:
|
payload:
|
||||||
agent: codex
|
detail: "invalid request: unknown question id: missing-question"
|
||||||
detail: "agent process exited: codex"
|
status: 400
|
||||||
details:
|
title: Invalid Request
|
||||||
exitCode: 1
|
type: "urn:sandbox-agent:error:invalid_request"
|
||||||
stderr: agent exited with status ExitStatus(unix_wait_status(256))
|
status: 400
|
||||||
status: 500
|
|
||||||
title: Agent Process Exited
|
|
||||||
type: "urn:sandbox-agent:error:agent_process_exited"
|
|
||||||
status: 500
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 1056
|
assertion_line: 1045
|
||||||
expression: normalize_events(&question_events)
|
expression: normalize_events(&question_events)
|
||||||
---
|
---
|
||||||
- agent: codex
|
- agent: codex
|
||||||
|
|
@ -9,83 +9,28 @@ expression: normalize_events(&question_events)
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 1089
|
assertion_line: 1078
|
||||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
---
|
---
|
||||||
payload:
|
payload:
|
||||||
agent: codex
|
detail: "invalid request: unknown question id: missing-question"
|
||||||
detail: "agent process exited: codex"
|
status: 400
|
||||||
details:
|
title: Invalid Request
|
||||||
exitCode: 1
|
type: "urn:sandbox-agent:error:invalid_request"
|
||||||
stderr: agent exited with status ExitStatus(unix_wait_status(256))
|
status: 400
|
||||||
status: 500
|
|
||||||
title: Agent Process Exited
|
|
||||||
type: "urn:sandbox-agent:error:agent_process_exited"
|
|
||||||
status: 500
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 1219
|
assertion_line: 1214
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
session_a:
|
session_a:
|
||||||
|
|
@ -10,86 +10,31 @@ session_a:
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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:
|
session_b:
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: started
|
kind: started
|
||||||
|
|
@ -97,83 +42,28 @@ session_b:
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 714
|
assertion_line: 697
|
||||||
expression: normalized
|
expression: normalized
|
||||||
---
|
---
|
||||||
- agent: codex
|
- agent: codex
|
||||||
|
|
@ -9,83 +9,28 @@ expression: normalized
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
assertion_line: 751
|
assertion_line: 734
|
||||||
expression: normalized
|
expression: normalized
|
||||||
---
|
---
|
||||||
- agent: codex
|
- agent: codex
|
||||||
|
|
@ -9,83 +9,28 @@ expression: normalized
|
||||||
started:
|
started:
|
||||||
message: session.created
|
message: session.created
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 2
|
seq: 2
|
||||||
|
started:
|
||||||
|
message: thread/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: started
|
||||||
message:
|
|
||||||
unparsed: true
|
|
||||||
seq: 3
|
seq: 3
|
||||||
|
started:
|
||||||
|
message: turn/started
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: user
|
||||||
seq: 4
|
seq: 4
|
||||||
- agent: codex
|
- agent: codex
|
||||||
kind: message
|
kind: message
|
||||||
message:
|
message:
|
||||||
unparsed: true
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
seq: 5
|
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.)
|
- [ ] Add universal API feature checklist (questions, approve plan, etc.)
|
||||||
- [ ] Document CLI, HTTP API, frontend app, and TypeScript SDK usage
|
- [ ] Document CLI, HTTP API, frontend app, and TypeScript SDK usage
|
||||||
- [ ] Use collapsible sections for endpoints and SDK methods
|
- [ ] 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 e2b example
|
||||||
- implement typescript "start locally" by pulling form server using version
|
- implement typescript "start locally" by pulling form server using version
|
||||||
- [x] Move agent schema sources to src/agents
|
- [x] Move agent schema sources to src/agents
|
||||||
|
- [x] Add Vercel AI SDK UIMessage schema extractor
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
"dependsOn": ["^generate"],
|
"dependsOn": ["^generate"],
|
||||||
"outputs": ["src/generated/**"]
|
"outputs": ["src/generated/**"]
|
||||||
},
|
},
|
||||||
|
"typecheck": {
|
||||||
|
"dependsOn": ["^build", "build"]
|
||||||
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue