mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
feat: expand api snapshots and schema tooling
This commit is contained in:
parent
ee014b0838
commit
011ca27287
72 changed files with 29480 additions and 1081 deletions
99
.github/workflows/release.yaml
vendored
99
.github/workflows/release.yaml
vendored
|
|
@ -1,14 +1,17 @@
|
||||||
name: release
|
name: release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: "Version (e.g. 0.1.0 or v0.1.0)"
|
description: "Version (e.g. 0.1.0 or v0.1.0)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
latest:
|
||||||
|
description: "Latest"
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|
@ -18,44 +21,33 @@ env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checks:
|
|
||||||
uses: ./.github/workflows/ci.yaml
|
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
name: "Setup"
|
name: "Setup"
|
||||||
needs: [checks]
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
|
||||||
version: ${{ steps.vars.outputs.version }}
|
|
||||||
latest: ${{ steps.latest.outputs.latest }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install tsx
|
- name: Install tsx
|
||||||
run: npm install -g tsx
|
run: npm install -g tsx
|
||||||
|
|
||||||
- name: Resolve version
|
- name: Run setup phase
|
||||||
id: vars
|
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" = "release" ]; then
|
CMD="./scripts/release/main.ts --version '${{ inputs.version }}' --phase setup-ci"
|
||||||
VERSION="${{ github.event.release.tag_name }}"
|
if [ "${{ inputs.latest }}" != "true" ]; then
|
||||||
else
|
CMD="$CMD --no-latest"
|
||||||
VERSION="${{ inputs.version }}"
|
|
||||||
fi
|
fi
|
||||||
|
eval "$CMD"
|
||||||
VERSION="${VERSION#v}"
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Determine latest
|
|
||||||
id: latest
|
|
||||||
run: |
|
|
||||||
./scripts/release/main.ts --version "${{ steps.vars.outputs.version }}" --print-latest --output "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
binaries:
|
binaries:
|
||||||
name: "Build Binaries"
|
name: "Build Binaries"
|
||||||
|
|
@ -99,8 +91,8 @@ jobs:
|
||||||
name: binary-${{ matrix.target }}
|
name: binary-${{ matrix.target }}
|
||||||
path: dist/sandbox-agent-${{ matrix.target }}${{ matrix.binary_ext }}
|
path: dist/sandbox-agent-${{ matrix.target }}${{ matrix.binary_ext }}
|
||||||
|
|
||||||
publish:
|
complete:
|
||||||
name: "Publish Packages"
|
name: "Complete"
|
||||||
needs: [setup, binaries]
|
needs: [setup, binaries]
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -115,49 +107,12 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: "https://registry.npmjs.org"
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install tsx
|
- name: Install tsx
|
||||||
run: npm install -g tsx
|
run: npm install -g tsx
|
||||||
|
|
||||||
- name: Download binaries
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: dist/
|
|
||||||
pattern: binary-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: List downloaded binaries
|
|
||||||
run: ls -la dist/
|
|
||||||
|
|
||||||
- name: Publish all
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
run: |
|
|
||||||
VERSION="${{ needs.setup.outputs.version }}"
|
|
||||||
./scripts/release/main.ts --version "$VERSION" \
|
|
||||||
--publish-crates \
|
|
||||||
--publish-npm-sdk \
|
|
||||||
--publish-npm-cli
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
name: "Upload Artifacts"
|
|
||||||
needs: [setup, binaries]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install tsx
|
|
||||||
run: npm install -g tsx
|
|
||||||
|
|
||||||
- name: Install AWS CLI
|
- name: Install AWS CLI
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
@ -177,19 +132,15 @@ jobs:
|
||||||
- name: List downloaded binaries
|
- name: List downloaded binaries
|
||||||
run: ls -la dist/
|
run: ls -la dist/
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Publish & upload artifacts
|
||||||
env:
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
R2_RELEASES_ACCESS_KEY_ID: ${{ secrets.R2_RELEASES_ACCESS_KEY_ID }}
|
R2_RELEASES_ACCESS_KEY_ID: ${{ secrets.R2_RELEASES_ACCESS_KEY_ID }}
|
||||||
R2_RELEASES_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
|
R2_RELEASES_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.setup.outputs.version }}"
|
CMD="./scripts/release/main.ts --version '${{ inputs.version }}' --phase complete-ci --no-validate-git"
|
||||||
if [ "${{ needs.setup.outputs.latest }}" = "true" ]; then
|
if [ "${{ inputs.latest }}" != "true" ]; then
|
||||||
LATEST_FLAG="--latest"
|
CMD="$CMD --no-latest"
|
||||||
else
|
|
||||||
LATEST_FLAG="--no-latest"
|
|
||||||
fi
|
fi
|
||||||
|
eval "$CMD"
|
||||||
./scripts/release/main.ts --version "$VERSION" $LATEST_FLAG \
|
|
||||||
--upload-typescript \
|
|
||||||
--upload-install \
|
|
||||||
--upload-binaries
|
|
||||||
|
|
|
||||||
174
ARCHITECTURE.md
Normal file
174
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
# 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
|
||||||
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -9,7 +9,15 @@ There are two ways to work with the SDKs:
|
||||||
|
|
||||||
## Agent Schemas
|
## Agent Schemas
|
||||||
|
|
||||||
Agent schemas (Claude Code, Codex, OpenCode, Amp) are available for reference in `resources/agent-schemas/dist/`.
|
Agent schemas (Claude Code, Codex, OpenCode, Amp) are available for reference in `resources/agent-schemas/artifacts/json-schema/`.
|
||||||
|
|
||||||
|
Extraction methods:
|
||||||
|
- **Claude**: Uses `claude --output-format json --json-schema` CLI command
|
||||||
|
- **Codex**: Uses `codex app-server generate-json-schema` CLI command
|
||||||
|
- **OpenCode**: Fetches from GitHub OpenAPI spec
|
||||||
|
- **Amp**: Scrapes from `https://ampcode.com/manual/appendix?preview#message-schema`
|
||||||
|
|
||||||
|
All extractors have fallback schemas for when CLI/URL is unavailable.
|
||||||
|
|
||||||
Research on how different agents operate (CLI flags, streaming formats, HITL patterns, etc.) is in `research/agents/`. When adding or making changes to agent docs, follow the same structure as existing files.
|
Research on how different agents operate (CLI flags, streaming formats, HITL patterns, etc.) is in `research/agents/`. When adding or making changes to agent docs, follow the same structure as existing files.
|
||||||
|
|
||||||
|
|
@ -24,6 +32,8 @@ Universal schema guidance:
|
||||||
- Update `CLAUDE.md` to keep CLI endpoints in sync with HTTP API changes.
|
- Update `CLAUDE.md` to keep CLI endpoints in sync with HTTP API changes.
|
||||||
- When changing the HTTP API, update the TypeScript SDK and CLI together.
|
- When changing the HTTP API, update the TypeScript SDK and CLI together.
|
||||||
- Do not make breaking changes to API endpoints.
|
- Do not make breaking changes to API endpoints.
|
||||||
|
- When changing API routes, ensure the HTTP/SSE test suite has full coverage of every route.
|
||||||
|
- When agent schema changes, ensure API tests cover the new schema and event shapes end-to-end.
|
||||||
|
|
||||||
### CLI ⇄ HTTP endpoint map (keep in sync)
|
### CLI ⇄ HTTP endpoint map (keep in sync)
|
||||||
|
|
||||||
|
|
|
||||||
67
Cargo.toml
67
Cargo.toml
|
|
@ -9,3 +9,70 @@ authors = ["Sandbox Agent Contributors"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/rivet-dev/sandbox-agent"
|
repository = "https://github.com/rivet-dev/sandbox-agent"
|
||||||
description = "Universal agent API for AI coding assistants"
|
description = "Universal agent API for AI coding assistants"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
# Internal crates
|
||||||
|
sandbox-agent-core = { path = "server/packages/sandbox-agent" }
|
||||||
|
sandbox-agent-error = { path = "server/packages/error" }
|
||||||
|
sandbox-agent-agent-management = { path = "server/packages/agent-management" }
|
||||||
|
sandbox-agent-agent-credentials = { path = "server/packages/agent-credentials" }
|
||||||
|
sandbox-agent-universal-agent-schema = { path = "server/packages/universal-agent-schema" }
|
||||||
|
sandbox-agent-extracted-agent-schemas = { path = "server/packages/extracted-agent-schemas" }
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# Schema generation
|
||||||
|
schemars = "0.8"
|
||||||
|
utoipa = { version = "4.2", features = ["axum_extras"] }
|
||||||
|
|
||||||
|
# Web framework
|
||||||
|
axum = "0.7"
|
||||||
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||||
|
|
||||||
|
# Async runtime
|
||||||
|
tokio = { version = "1.36", features = ["macros", "rt-multi-thread", "signal", "time"] }
|
||||||
|
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# HTTP client
|
||||||
|
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] }
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-logfmt = "0.3"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# Time/date
|
||||||
|
time = { version = "0.3", features = ["parsing", "formatting"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
# Filesystem/paths
|
||||||
|
dirs = "5.0"
|
||||||
|
tempfile = "3.10"
|
||||||
|
|
||||||
|
# Archive handling
|
||||||
|
flate2 = "1.0"
|
||||||
|
tar = "0.4"
|
||||||
|
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
url = "2.5"
|
||||||
|
regress = "0.10"
|
||||||
|
|
||||||
|
# Code generation (build deps)
|
||||||
|
typify = "0.4"
|
||||||
|
prettyplease = "0.2"
|
||||||
|
syn = "2.0"
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
http-body-util = "0.1"
|
||||||
|
insta = { version = "1.41", features = ["yaml"] }
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -7,6 +7,29 @@ Universal API for running Claude Code, Codex, OpenCode, and Amp inside sandboxes
|
||||||
- **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`
|
||||||
|
|
||||||
|
## Agent Support
|
||||||
|
|
||||||
|
| Feature | [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) | [Codex](https://github.com/openai/codex) | [OpenCode](https://github.com/opencode-ai/opencode) | [Amp](https://ampcode.com) |
|
||||||
|
|---------|:-----------:|:-----:|:--------:|:---:|
|
||||||
|
| Stability | Stable | Stable | Experimental | Experimental |
|
||||||
|
| Text Messages | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Tool Calls | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Tool Results | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Questions (HITL) | ✓ | | ✓ | |
|
||||||
|
| Permissions (HITL) | | | ✓ | |
|
||||||
|
| Images | | ✓ | ✓ | |
|
||||||
|
| File Attachments | | ✓ | ✓ | |
|
||||||
|
| Session Lifecycle | | ✓ | ✓ | |
|
||||||
|
| Error Events | | ✓ | ✓ | ✓ |
|
||||||
|
| Reasoning/Thinking | | ✓ | | |
|
||||||
|
| Command Execution | | ✓ | | |
|
||||||
|
| File Changes | | ✓ | | |
|
||||||
|
| MCP Tools | | ✓ | | |
|
||||||
|
| Streaming Deltas | | ✓ | ✓ | |
|
||||||
|
|
||||||
|
Want support for another agent? [Open an issue](https://github.com/anthropics/sandbox-agent/issues/new) to request it.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
|
||||||
2
bugs.md
Normal file
2
bugs.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- openai exteacted credentials do not work
|
||||||
|
|
||||||
|
|
@ -14,106 +14,19 @@ import {
|
||||||
Zap
|
Zap
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
const API_PREFIX = "/v1";
|
SandboxDaemonError,
|
||||||
|
createSandboxDaemonClient,
|
||||||
type AgentInfo = {
|
type SandboxDaemonClient,
|
||||||
id: string;
|
type AgentInfo,
|
||||||
installed: boolean;
|
type AgentModeInfo,
|
||||||
version?: string;
|
type PermissionRequest,
|
||||||
path?: string;
|
type QuestionRequest,
|
||||||
};
|
type SessionInfo,
|
||||||
|
type UniversalEvent,
|
||||||
type SessionInfo = {
|
type UniversalMessage,
|
||||||
sessionId: string;
|
type UniversalMessagePart
|
||||||
agent: string;
|
} from "sandbox-agent";
|
||||||
agentMode: string;
|
|
||||||
permissionMode: string;
|
|
||||||
model?: string;
|
|
||||||
variant?: string;
|
|
||||||
agentSessionId?: string;
|
|
||||||
ended: boolean;
|
|
||||||
eventCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AgentMode = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UniversalEvent = {
|
|
||||||
id: number;
|
|
||||||
timestamp: string;
|
|
||||||
sessionId: string;
|
|
||||||
agent: string;
|
|
||||||
agentSessionId?: string;
|
|
||||||
data: UniversalEventData;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UniversalEventData =
|
|
||||||
| { message: UniversalMessage }
|
|
||||||
| { started: StartedInfo }
|
|
||||||
| { error: CrashInfo }
|
|
||||||
| { questionAsked: QuestionRequest }
|
|
||||||
| { permissionAsked: PermissionRequest };
|
|
||||||
|
|
||||||
type UniversalMessagePart = {
|
|
||||||
type: string;
|
|
||||||
text?: string;
|
|
||||||
name?: string;
|
|
||||||
input?: unknown;
|
|
||||||
output?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UniversalMessage = {
|
|
||||||
role?: string;
|
|
||||||
parts?: UniversalMessagePart[];
|
|
||||||
raw?: unknown;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StartedInfo = {
|
|
||||||
message?: string;
|
|
||||||
pid?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CrashInfo = {
|
|
||||||
message?: string;
|
|
||||||
code?: string;
|
|
||||||
detail?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type QuestionOption = {
|
|
||||||
label: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type QuestionItem = {
|
|
||||||
header?: string;
|
|
||||||
question: string;
|
|
||||||
options: QuestionOption[];
|
|
||||||
multiSelect?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type QuestionRequest = {
|
|
||||||
id: string;
|
|
||||||
sessionID?: string;
|
|
||||||
questions: QuestionItem[];
|
|
||||||
tool?: { messageID?: string; callID?: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type PermissionRequest = {
|
|
||||||
id: string;
|
|
||||||
sessionID?: string;
|
|
||||||
permission: string;
|
|
||||||
patterns?: string[];
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
always?: string[];
|
|
||||||
tool?: { messageID?: string; callID?: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type RequestLog = {
|
type RequestLog = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -130,29 +43,6 @@ type DebugTab = "log" | "events" | "approvals" | "agents";
|
||||||
|
|
||||||
const defaultAgents = ["claude", "codex", "opencode", "amp"];
|
const defaultAgents = ["claude", "codex", "opencode", "amp"];
|
||||||
|
|
||||||
const buildUrl = (endpoint: string, path: string, query?: Record<string, string>) => {
|
|
||||||
const base = endpoint.replace(/\/$/, "");
|
|
||||||
const fullPath = path.startsWith("/") ? path : `/${path}`;
|
|
||||||
const url = new URL(`${base}${fullPath}`);
|
|
||||||
if (query) {
|
|
||||||
Object.entries(query).forEach(([key, value]) => {
|
|
||||||
if (value !== "") {
|
|
||||||
url.searchParams.set(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return url.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const safeJson = (text: string) => {
|
|
||||||
if (!text) return null;
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatJson = (value: unknown) => {
|
const formatJson = (value: unknown) => {
|
||||||
if (value === null || value === undefined) return "";
|
if (value === null || value === undefined) return "";
|
||||||
if (typeof value === "string") return value;
|
if (typeof value === "string") return value;
|
||||||
|
|
@ -203,7 +93,7 @@ export default function App() {
|
||||||
const [connectError, setConnectError] = useState<string | null>(null);
|
const [connectError, setConnectError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
||||||
const [modesByAgent, setModesByAgent] = useState<Record<string, AgentMode[]>>({});
|
const [modesByAgent, setModesByAgent] = useState<Record<string, AgentModeInfo[]>>({});
|
||||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||||
|
|
||||||
const [agentId, setAgentId] = useState("claude");
|
const [agentId, setAgentId] = useState("claude");
|
||||||
|
|
@ -222,7 +112,6 @@ export default function App() {
|
||||||
const [polling, setPolling] = useState(false);
|
const [polling, setPolling] = useState(false);
|
||||||
const pollTimerRef = useRef<number | null>(null);
|
const pollTimerRef = useRef<number | null>(null);
|
||||||
const [streamMode, setStreamMode] = useState<"poll" | "sse">("poll");
|
const [streamMode, setStreamMode] = useState<"poll" | "sse">("poll");
|
||||||
const eventSourceRef = useRef<EventSource | null>(null);
|
|
||||||
const [eventError, setEventError] = useState<string | null>(null);
|
const [eventError, setEventError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [questionSelections, setQuestionSelections] = useState<Record<string, string[][]>>({});
|
const [questionSelections, setQuestionSelections] = useState<Record<string, string[][]>>({});
|
||||||
|
|
@ -237,6 +126,9 @@ export default function App() {
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const clientRef = useRef<SandboxDaemonClient | null>(null);
|
||||||
|
const sseAbortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const logRequest = useCallback((entry: RequestLog) => {
|
const logRequest = useCallback((entry: RequestLog) => {
|
||||||
setRequestLog((prev) => {
|
setRequestLog((prev) => {
|
||||||
const next = [entry, ...prev];
|
const next = [entry, ...prev];
|
||||||
|
|
@ -244,25 +136,16 @@ export default function App() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const apiFetch = useCallback(
|
const createClient = useCallback(() => {
|
||||||
async (
|
const fetchWithLog: typeof fetch = async (input, init) => {
|
||||||
path: string,
|
const method = init?.method ?? "GET";
|
||||||
options?: {
|
const url =
|
||||||
method?: string;
|
typeof input === "string"
|
||||||
body?: unknown;
|
? input
|
||||||
query?: Record<string, string>;
|
: input instanceof URL
|
||||||
}
|
? input.toString()
|
||||||
) => {
|
: input.url;
|
||||||
const method = options?.method ?? "GET";
|
const bodyText = typeof init?.body === "string" ? init.body : undefined;
|
||||||
const url = buildUrl(endpoint, path, options?.query);
|
|
||||||
const bodyText = options?.body ? JSON.stringify(options.body) : undefined;
|
|
||||||
const headers: Record<string, string> = {};
|
|
||||||
if (bodyText) {
|
|
||||||
headers["Content-Type"] = "application/json";
|
|
||||||
}
|
|
||||||
if (token) {
|
|
||||||
headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
const curl = buildCurl(method, url, bodyText, token);
|
const curl = buildCurl(method, url, bodyText, token);
|
||||||
const logId = logIdRef.current++;
|
const logId = logIdRef.current++;
|
||||||
const entry: RequestLog = {
|
const entry: RequestLog = {
|
||||||
|
|
@ -276,23 +159,10 @@ export default function App() {
|
||||||
let logged = false;
|
let logged = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(input, init);
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: bodyText
|
|
||||||
});
|
|
||||||
const text = await response.text();
|
|
||||||
const data = safeJson(text);
|
|
||||||
logRequest({ ...entry, status: response.status });
|
logRequest({ ...entry, status: response.status });
|
||||||
logged = true;
|
logged = true;
|
||||||
if (!response.ok) {
|
return response;
|
||||||
const errorMessage =
|
|
||||||
(typeof data === "object" && data && "detail" in data && data.detail) ||
|
|
||||||
(typeof data === "object" && data && "title" in data && data.title) ||
|
|
||||||
(typeof data === "string" ? data : `Request failed with ${response.status}`);
|
|
||||||
throw new Error(String(errorMessage));
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Request failed";
|
const message = error instanceof Error ? error.message : "Request failed";
|
||||||
if (!logged) {
|
if (!logged) {
|
||||||
|
|
@ -300,22 +170,45 @@ export default function App() {
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[endpoint, token, logRequest]
|
|
||||||
);
|
const client = createSandboxDaemonClient({
|
||||||
|
baseUrl: endpoint,
|
||||||
|
token: token || undefined,
|
||||||
|
fetch: fetchWithLog
|
||||||
|
});
|
||||||
|
clientRef.current = client;
|
||||||
|
return client;
|
||||||
|
}, [endpoint, token, logRequest]);
|
||||||
|
|
||||||
|
const getClient = useCallback((): SandboxDaemonClient => {
|
||||||
|
if (!clientRef.current) {
|
||||||
|
throw new Error("Not connected");
|
||||||
|
}
|
||||||
|
return clientRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
|
if (error instanceof SandboxDaemonError) {
|
||||||
|
return error.problem?.detail ?? error.problem?.title ?? error.message;
|
||||||
|
}
|
||||||
|
return error instanceof Error ? error.message : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
setConnectError(null);
|
setConnectError(null);
|
||||||
try {
|
try {
|
||||||
await apiFetch(`${API_PREFIX}/health`);
|
const client = createClient();
|
||||||
|
await client.getHealth();
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
await fetchSessions();
|
await fetchSessions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unable to connect";
|
const message = getErrorMessage(error, "Unable to connect");
|
||||||
setConnectError(message);
|
setConnectError(message);
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
|
clientRef.current = null;
|
||||||
} finally {
|
} finally {
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -323,6 +216,7 @@ export default function App() {
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
|
clientRef.current = null;
|
||||||
setSessionError(null);
|
setSessionError(null);
|
||||||
setEvents([]);
|
setEvents([]);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
|
|
@ -334,8 +228,8 @@ export default function App() {
|
||||||
|
|
||||||
const refreshAgents = async () => {
|
const refreshAgents = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch(`${API_PREFIX}/agents`);
|
const data = await getClient().listAgents();
|
||||||
const agentList = (data as { agents?: AgentInfo[] })?.agents ?? [];
|
const agentList = data.agents ?? [];
|
||||||
setAgents(agentList);
|
setAgents(agentList);
|
||||||
// Auto-load modes for installed agents
|
// Auto-load modes for installed agents
|
||||||
for (const agent of agentList) {
|
for (const agent of agentList) {
|
||||||
|
|
@ -344,14 +238,14 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setConnectError(error instanceof Error ? error.message : "Unable to refresh agents");
|
setConnectError(getErrorMessage(error, "Unable to refresh agents"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSessions = async () => {
|
const fetchSessions = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch(`${API_PREFIX}/sessions`);
|
const data = await getClient().listSessions();
|
||||||
const sessionList = (data as { sessions?: SessionInfo[] })?.sessions ?? [];
|
const sessionList = data.sessions ?? [];
|
||||||
setSessions(sessionList);
|
setSessions(sessionList);
|
||||||
} catch {
|
} catch {
|
||||||
// Silently fail - sessions list is supplementary
|
// Silently fail - sessions list is supplementary
|
||||||
|
|
@ -360,20 +254,17 @@ export default function App() {
|
||||||
|
|
||||||
const installAgent = async (targetId: string, reinstall: boolean) => {
|
const installAgent = async (targetId: string, reinstall: boolean) => {
|
||||||
try {
|
try {
|
||||||
await apiFetch(`${API_PREFIX}/agents/${targetId}/install`, {
|
await getClient().installAgent(targetId, { reinstall });
|
||||||
method: "POST",
|
|
||||||
body: { reinstall }
|
|
||||||
});
|
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setConnectError(error instanceof Error ? error.message : "Install failed");
|
setConnectError(getErrorMessage(error, "Install failed"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadModes = async (targetId: string) => {
|
const loadModes = async (targetId: string) => {
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch(`${API_PREFIX}/agents/${targetId}/modes`);
|
const data = await getClient().getAgentModes(targetId);
|
||||||
const modes = (data as { modes?: AgentMode[] })?.modes ?? [];
|
const modes = data.modes ?? [];
|
||||||
setModesByAgent((prev) => ({ ...prev, [targetId]: modes }));
|
setModesByAgent((prev) => ({ ...prev, [targetId]: modes }));
|
||||||
} catch {
|
} catch {
|
||||||
// Silently fail - modes are optional
|
// Silently fail - modes are optional
|
||||||
|
|
@ -384,37 +275,41 @@ export default function App() {
|
||||||
if (!message.trim()) return;
|
if (!message.trim()) return;
|
||||||
setSessionError(null);
|
setSessionError(null);
|
||||||
try {
|
try {
|
||||||
await apiFetch(`${API_PREFIX}/sessions/${sessionId}/messages`, {
|
await getClient().postMessage(sessionId, { message });
|
||||||
method: "POST",
|
|
||||||
body: { message }
|
|
||||||
});
|
|
||||||
setMessage("");
|
setMessage("");
|
||||||
|
|
||||||
// Auto-start polling if not already
|
// Auto-start polling if not already
|
||||||
if (!polling && streamMode === "poll") {
|
if (!polling) {
|
||||||
|
if (streamMode === "poll") {
|
||||||
startPolling();
|
startPolling();
|
||||||
|
} else {
|
||||||
|
startSse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSessionError(error instanceof Error ? error.message : "Unable to send message");
|
setSessionError(getErrorMessage(error, "Unable to send message"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSession = async () => {
|
const createSession = async () => {
|
||||||
setSessionError(null);
|
setSessionError(null);
|
||||||
try {
|
try {
|
||||||
const body: Record<string, string> = { agent: agentId };
|
const body: {
|
||||||
|
agent: string;
|
||||||
|
agentMode?: string;
|
||||||
|
permissionMode?: string;
|
||||||
|
model?: string;
|
||||||
|
variant?: string;
|
||||||
|
} = { agent: agentId };
|
||||||
if (agentMode) body.agentMode = agentMode;
|
if (agentMode) body.agentMode = agentMode;
|
||||||
if (permissionMode) body.permissionMode = permissionMode;
|
if (permissionMode) body.permissionMode = permissionMode;
|
||||||
if (model) body.model = model;
|
if (model) body.model = model;
|
||||||
if (variant) body.variant = variant;
|
if (variant) body.variant = variant;
|
||||||
|
|
||||||
await apiFetch(`${API_PREFIX}/sessions/${sessionId}`, {
|
await getClient().createSession(sessionId, body);
|
||||||
method: "POST",
|
|
||||||
body
|
|
||||||
});
|
|
||||||
await fetchSessions();
|
await fetchSessions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSessionError(error instanceof Error ? error.message : "Unable to create session");
|
setSessionError(getErrorMessage(error, "Unable to create session"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -446,19 +341,22 @@ export default function App() {
|
||||||
|
|
||||||
// Create the session
|
// Create the session
|
||||||
try {
|
try {
|
||||||
const body: Record<string, string> = { agent: agentId };
|
const body: {
|
||||||
|
agent: string;
|
||||||
|
agentMode?: string;
|
||||||
|
permissionMode?: string;
|
||||||
|
model?: string;
|
||||||
|
variant?: string;
|
||||||
|
} = { agent: agentId };
|
||||||
if (agentMode) body.agentMode = agentMode;
|
if (agentMode) body.agentMode = agentMode;
|
||||||
if (permissionMode) body.permissionMode = permissionMode;
|
if (permissionMode) body.permissionMode = permissionMode;
|
||||||
if (model) body.model = model;
|
if (model) body.model = model;
|
||||||
if (variant) body.variant = variant;
|
if (variant) body.variant = variant;
|
||||||
|
|
||||||
await apiFetch(`${API_PREFIX}/sessions/${id}`, {
|
await getClient().createSession(id, body);
|
||||||
method: "POST",
|
|
||||||
body
|
|
||||||
});
|
|
||||||
await fetchSessions();
|
await fetchSessions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSessionError(error instanceof Error ? error.message : "Unable to create session");
|
setSessionError(getErrorMessage(error, "Unable to create session"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -473,20 +371,17 @@ export default function App() {
|
||||||
const fetchEvents = useCallback(async () => {
|
const fetchEvents = useCallback(async () => {
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch(`${API_PREFIX}/sessions/${sessionId}/events`, {
|
const response = await getClient().getEvents(sessionId, {
|
||||||
query: {
|
offset: offsetRef.current,
|
||||||
offset: String(offsetRef.current),
|
limit: 200
|
||||||
limit: "200"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const response = data as { events?: UniversalEvent[]; hasMore?: boolean };
|
|
||||||
const newEvents = response.events ?? [];
|
const newEvents = response.events ?? [];
|
||||||
appendEvents(newEvents);
|
appendEvents(newEvents);
|
||||||
setEventError(null);
|
setEventError(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setEventError(error instanceof Error ? error.message : "Unable to fetch events");
|
setEventError(getErrorMessage(error, "Unable to fetch events"));
|
||||||
}
|
}
|
||||||
}, [apiFetch, appendEvents, sessionId]);
|
}, [appendEvents, getClient, sessionId]);
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
stopSse();
|
stopSse();
|
||||||
|
|
@ -506,39 +401,47 @@ export default function App() {
|
||||||
|
|
||||||
const startSse = () => {
|
const startSse = () => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
if (eventSourceRef.current) return;
|
if (sseAbortRef.current) return;
|
||||||
if (token) {
|
if (!sessionId) {
|
||||||
setEventError("SSE streams cannot send auth headers. Use polling or run daemon with --no-token.");
|
setEventError("Select or create a session first.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = buildUrl(endpoint, `${API_PREFIX}/sessions/${sessionId}/events/sse`, {
|
setEventError(null);
|
||||||
offset: String(offsetRef.current)
|
setPolling(true);
|
||||||
});
|
const controller = new AbortController();
|
||||||
const source = new EventSource(url);
|
sseAbortRef.current = controller;
|
||||||
eventSourceRef.current = source;
|
const start = async () => {
|
||||||
source.onmessage = (event) => {
|
|
||||||
try {
|
try {
|
||||||
const parsed = safeJson(event.data);
|
for await (const event of getClient().streamEvents(
|
||||||
if (Array.isArray(parsed)) {
|
sessionId,
|
||||||
appendEvents(parsed as UniversalEvent[]);
|
{ offset: offsetRef.current },
|
||||||
} else if (parsed && typeof parsed === "object") {
|
controller.signal
|
||||||
appendEvents([parsed as UniversalEvent]);
|
)) {
|
||||||
|
appendEvents([event]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setEventError(error instanceof Error ? error.message : "SSE parse error");
|
if (controller.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEventError(getErrorMessage(error, "SSE connection error. Falling back to polling."));
|
||||||
|
stopSse();
|
||||||
|
startPolling();
|
||||||
|
} finally {
|
||||||
|
if (sseAbortRef.current === controller) {
|
||||||
|
sseAbortRef.current = null;
|
||||||
|
setPolling(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
source.onerror = () => {
|
void start();
|
||||||
setEventError("SSE connection error. Falling back to polling.");
|
|
||||||
stopSse();
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopSse = () => {
|
const stopSse = () => {
|
||||||
if (eventSourceRef.current) {
|
if (sseAbortRef.current) {
|
||||||
eventSourceRef.current.close();
|
sseAbortRef.current.abort();
|
||||||
eventSourceRef.current = null;
|
sseAbortRef.current = null;
|
||||||
}
|
}
|
||||||
|
setPolling(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetEvents = () => {
|
const resetEvents = () => {
|
||||||
|
|
@ -584,37 +487,28 @@ export default function App() {
|
||||||
const answerQuestion = async (request: QuestionRequest) => {
|
const answerQuestion = async (request: QuestionRequest) => {
|
||||||
const answers = questionSelections[request.id] ?? [];
|
const answers = questionSelections[request.id] ?? [];
|
||||||
try {
|
try {
|
||||||
await apiFetch(`${API_PREFIX}/sessions/${sessionId}/questions/${request.id}/reply`, {
|
await getClient().replyQuestion(sessionId, request.id, { answers });
|
||||||
method: "POST",
|
|
||||||
body: { answers }
|
|
||||||
});
|
|
||||||
setQuestionStatus((prev) => ({ ...prev, [request.id]: "replied" }));
|
setQuestionStatus((prev) => ({ ...prev, [request.id]: "replied" }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setEventError(error instanceof Error ? error.message : "Unable to reply");
|
setEventError(getErrorMessage(error, "Unable to reply"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectQuestion = async (requestId: string) => {
|
const rejectQuestion = async (requestId: string) => {
|
||||||
try {
|
try {
|
||||||
await apiFetch(`${API_PREFIX}/sessions/${sessionId}/questions/${requestId}/reject`, {
|
await getClient().rejectQuestion(sessionId, requestId);
|
||||||
method: "POST",
|
|
||||||
body: {}
|
|
||||||
});
|
|
||||||
setQuestionStatus((prev) => ({ ...prev, [requestId]: "rejected" }));
|
setQuestionStatus((prev) => ({ ...prev, [requestId]: "rejected" }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setEventError(error instanceof Error ? error.message : "Unable to reject");
|
setEventError(getErrorMessage(error, "Unable to reject"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const replyPermission = async (requestId: string, reply: "once" | "always" | "reject") => {
|
const replyPermission = async (requestId: string, reply: "once" | "always" | "reject") => {
|
||||||
try {
|
try {
|
||||||
await apiFetch(`${API_PREFIX}/sessions/${sessionId}/permissions/${requestId}/reply`, {
|
await getClient().replyPermission(sessionId, requestId, { reply });
|
||||||
method: "POST",
|
|
||||||
body: { reply }
|
|
||||||
});
|
|
||||||
setPermissionStatus((prev) => ({ ...prev, [requestId]: "replied" }));
|
setPermissionStatus((prev) => ({ ...prev, [requestId]: "replied" }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setEventError(error instanceof Error ? error.message : "Unable to reply");
|
setEventError(getErrorMessage(error, "Unable to reply"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -637,14 +531,14 @@ 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;
|
||||||
// Extract text from parts array
|
const parts = "parts" in msg ? msg.parts : [];
|
||||||
const content = msg?.parts
|
const content = parts
|
||||||
?.filter((part) => part.type === "text" && part.text)
|
.filter((part: UniversalMessagePart) => part.type === "text" && part.text)
|
||||||
.map((part) => part.text)
|
.map((part: UniversalMessagePart) => part.text)
|
||||||
.join("\n") ?? "";
|
.join("\n");
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
role: msg?.role ?? "assistant",
|
role: "role" in msg ? msg.role : "assistant",
|
||||||
content,
|
content,
|
||||||
timestamp: event.timestamp
|
timestamp: event.timestamp
|
||||||
};
|
};
|
||||||
|
|
@ -697,7 +591,11 @@ export default function App() {
|
||||||
|
|
||||||
const toggleStream = () => {
|
const toggleStream = () => {
|
||||||
if (polling) {
|
if (polling) {
|
||||||
|
if (streamMode === "poll") {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
} else {
|
||||||
|
stopSse();
|
||||||
|
}
|
||||||
} else if (streamMode === "poll") {
|
} else if (streamMode === "poll") {
|
||||||
startPolling();
|
startPolling();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
153
resources/agent-schemas/artifacts/json-schema/amp.json
Normal file
153
resources/agent-schemas/artifacts/json-schema/amp.json
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://sandbox-agent/schemas/amp.json",
|
||||||
|
"title": "AMP Code SDK Schema",
|
||||||
|
"definitions": {
|
||||||
|
"StreamJSONMessage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"message",
|
||||||
|
"tool_call",
|
||||||
|
"tool_result",
|
||||||
|
"error",
|
||||||
|
"done"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tool_call": {
|
||||||
|
"$ref": "#/definitions/ToolCall"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"AmpOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"baseURL": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"maxTokens": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"systemPrompt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workingDirectory": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"permissionRules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/PermissionRule"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PermissionRule": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tool": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"allow",
|
||||||
|
"deny",
|
||||||
|
"ask"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pattern": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"tool",
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Message": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user",
|
||||||
|
"assistant",
|
||||||
|
"system"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tool_calls": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ToolCall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"role",
|
||||||
|
"content"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ToolCall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"arguments": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"arguments"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
resources/agent-schemas/artifacts/json-schema/claude.json
Normal file
182
resources/agent-schemas/artifacts/json-schema/claude.json
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://sandbox-agent/schemas/claude.json",
|
||||||
|
"title": "Claude Code SDK Schema",
|
||||||
|
"definitions": {
|
||||||
|
"SDKMessage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user",
|
||||||
|
"assistant",
|
||||||
|
"result"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SDKResultMessage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "result"
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"duration_ms": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Options": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"maxTokens": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"systemPrompt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedTools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workingDirectory": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BashInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"workingDirectory": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"command"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FileEditInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"oldText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"newText": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"oldText",
|
||||||
|
"newText"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FileReadInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"startLine": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"endLine": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FileWriteInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"content"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GlobInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pattern": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"pattern"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GrepInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pattern": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"include": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"pattern"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18260
resources/agent-schemas/artifacts/json-schema/codex.json
Normal file
18260
resources/agent-schemas/artifacts/json-schema/codex.json
Normal file
File diff suppressed because it is too large
Load diff
5921
resources/agent-schemas/artifacts/json-schema/opencode.json
Normal file
5921
resources/agent-schemas/artifacts/json-schema/opencode.json
Normal file
File diff suppressed because it is too large
Load diff
695
resources/agent-schemas/deno.lock
generated
Normal file
695
resources/agent-schemas/deno.lock
generated
Normal file
|
|
@ -0,0 +1,695 @@
|
||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"specifiers": {
|
||||||
|
"npm:@anthropic-ai/claude-code@latest": "2.1.19",
|
||||||
|
"npm:@openai/codex@latest": "0.91.0",
|
||||||
|
"npm:@types/node@22": "22.19.7",
|
||||||
|
"npm:cheerio@1": "1.2.0",
|
||||||
|
"npm:ts-json-schema-generator@^2.4.0": "2.4.0",
|
||||||
|
"npm:tsx@^4.19.0": "4.21.0",
|
||||||
|
"npm:typescript@^5.7.0": "5.9.3"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"@anthropic-ai/claude-code@2.1.19": {
|
||||||
|
"integrity": "sha512-/bUlQuX/6nKr1Zqfi/9Q6xf7WonUBk72ZfKKENU4WVrIFWqTv/0JJsoW/dHol9QBNHvyfKIeBbYu4avHNRAnuQ==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-darwin-arm64",
|
||||||
|
"@img/sharp-darwin-x64",
|
||||||
|
"@img/sharp-linux-arm",
|
||||||
|
"@img/sharp-linux-arm64",
|
||||||
|
"@img/sharp-linux-x64",
|
||||||
|
"@img/sharp-linuxmusl-arm64",
|
||||||
|
"@img/sharp-linuxmusl-x64",
|
||||||
|
"@img/sharp-win32-x64"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"@esbuild/aix-ppc64@0.27.2": {
|
||||||
|
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||||
|
"os": ["aix"],
|
||||||
|
"cpu": ["ppc64"]
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm@0.27.2": {
|
||||||
|
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@esbuild/android-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||||
|
"os": ["freebsd"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||||
|
"os": ["freebsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm@0.27.2": {
|
||||||
|
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ia32@0.27.2": {
|
||||||
|
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["ia32"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-loong64@0.27.2": {
|
||||||
|
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["loong64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-mips64el@0.27.2": {
|
||||||
|
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["mips64el"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ppc64@0.27.2": {
|
||||||
|
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["ppc64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-riscv64@0.27.2": {
|
||||||
|
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["riscv64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-s390x@0.27.2": {
|
||||||
|
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["s390x"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||||
|
"os": ["netbsd"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||||
|
"os": ["netbsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||||
|
"os": ["openbsd"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||||
|
"os": ["openbsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/openharmony-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||||
|
"os": ["openharmony"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/sunos-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||||
|
"os": ["sunos"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/win32-arm64@0.27.2": {
|
||||||
|
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/win32-ia32@0.27.2": {
|
||||||
|
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["ia32"]
|
||||||
|
},
|
||||||
|
"@esbuild/win32-x64@0.27.2": {
|
||||||
|
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-darwin-arm64@0.33.5": {
|
||||||
|
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-darwin-arm64"
|
||||||
|
],
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-darwin-x64@0.33.5": {
|
||||||
|
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-darwin-x64"
|
||||||
|
],
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-darwin-arm64@1.0.4": {
|
||||||
|
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-darwin-x64@1.0.4": {
|
||||||
|
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-linux-arm64@1.0.4": {
|
||||||
|
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-linux-arm@1.0.5": {
|
||||||
|
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-linux-x64@1.0.4": {
|
||||||
|
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64@1.0.4": {
|
||||||
|
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64@1.0.4": {
|
||||||
|
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-linux-arm64@0.33.5": {
|
||||||
|
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-linux-arm64"
|
||||||
|
],
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-linux-arm@0.33.5": {
|
||||||
|
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-linux-arm"
|
||||||
|
],
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@img/sharp-linux-x64@0.33.5": {
|
||||||
|
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-linux-x64"
|
||||||
|
],
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-linuxmusl-arm64@0.33.5": {
|
||||||
|
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64"
|
||||||
|
],
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-linuxmusl-x64@0.33.5": {
|
||||||
|
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64"
|
||||||
|
],
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@img/sharp-win32-x64@0.33.5": {
|
||||||
|
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@isaacs/balanced-match@4.0.1": {
|
||||||
|
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="
|
||||||
|
},
|
||||||
|
"@isaacs/brace-expansion@5.0.0": {
|
||||||
|
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@isaacs/balanced-match"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@isaacs/cliui@8.0.2": {
|
||||||
|
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||||
|
"dependencies": [
|
||||||
|
"string-width@5.1.2",
|
||||||
|
"string-width-cjs@npm:string-width@4.2.3",
|
||||||
|
"strip-ansi@7.1.2",
|
||||||
|
"strip-ansi-cjs@npm:strip-ansi@6.0.1",
|
||||||
|
"wrap-ansi@8.1.0",
|
||||||
|
"wrap-ansi-cjs@npm:wrap-ansi@7.0.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@openai/codex@0.91.0": {
|
||||||
|
"integrity": "sha512-eRLRg0+uM0g0iW+Ca5VedBk+laslLcq93Hf6rbFtv+gLb4+aMib2UPdvlDlvvCVkBMbvE8ckY/cju+iOOuKCNA==",
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"@types/json-schema@7.0.15": {
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||||
|
},
|
||||||
|
"@types/node@22.19.7": {
|
||||||
|
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
||||||
|
"dependencies": [
|
||||||
|
"undici-types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ansi-regex@5.0.1": {
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||||
|
},
|
||||||
|
"ansi-regex@6.2.2": {
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="
|
||||||
|
},
|
||||||
|
"ansi-styles@4.3.0": {
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": [
|
||||||
|
"color-convert"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ansi-styles@6.2.3": {
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="
|
||||||
|
},
|
||||||
|
"boolbase@1.0.0": {
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||||
|
},
|
||||||
|
"cheerio-select@2.1.0": {
|
||||||
|
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||||
|
"dependencies": [
|
||||||
|
"boolbase",
|
||||||
|
"css-select",
|
||||||
|
"css-what",
|
||||||
|
"domelementtype",
|
||||||
|
"domhandler",
|
||||||
|
"domutils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cheerio@1.2.0": {
|
||||||
|
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
|
||||||
|
"dependencies": [
|
||||||
|
"cheerio-select",
|
||||||
|
"dom-serializer",
|
||||||
|
"domhandler",
|
||||||
|
"domutils",
|
||||||
|
"encoding-sniffer",
|
||||||
|
"htmlparser2",
|
||||||
|
"parse5",
|
||||||
|
"parse5-htmlparser2-tree-adapter",
|
||||||
|
"parse5-parser-stream",
|
||||||
|
"undici",
|
||||||
|
"whatwg-mimetype"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color-convert@2.0.1": {
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"color-name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color-name@1.1.4": {
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"commander@13.1.0": {
|
||||||
|
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="
|
||||||
|
},
|
||||||
|
"cross-spawn@7.0.6": {
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"dependencies": [
|
||||||
|
"path-key",
|
||||||
|
"shebang-command",
|
||||||
|
"which"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"css-select@5.2.2": {
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"dependencies": [
|
||||||
|
"boolbase",
|
||||||
|
"css-what",
|
||||||
|
"domhandler",
|
||||||
|
"domutils",
|
||||||
|
"nth-check"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"css-what@6.2.2": {
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="
|
||||||
|
},
|
||||||
|
"dom-serializer@2.0.0": {
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": [
|
||||||
|
"domelementtype",
|
||||||
|
"domhandler",
|
||||||
|
"entities@4.5.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domelementtype@2.3.0": {
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||||
|
},
|
||||||
|
"domhandler@5.0.3": {
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": [
|
||||||
|
"domelementtype"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domutils@3.2.2": {
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"dependencies": [
|
||||||
|
"dom-serializer",
|
||||||
|
"domelementtype",
|
||||||
|
"domhandler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eastasianwidth@0.2.0": {
|
||||||
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||||
|
},
|
||||||
|
"emoji-regex@8.0.0": {
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
|
},
|
||||||
|
"emoji-regex@9.2.2": {
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||||
|
},
|
||||||
|
"encoding-sniffer@0.2.1": {
|
||||||
|
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
|
||||||
|
"dependencies": [
|
||||||
|
"iconv-lite",
|
||||||
|
"whatwg-encoding"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entities@4.5.0": {
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
|
},
|
||||||
|
"entities@6.0.1": {
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="
|
||||||
|
},
|
||||||
|
"entities@7.0.1": {
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="
|
||||||
|
},
|
||||||
|
"esbuild@0.27.2": {
|
||||||
|
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@esbuild/aix-ppc64",
|
||||||
|
"@esbuild/android-arm",
|
||||||
|
"@esbuild/android-arm64",
|
||||||
|
"@esbuild/android-x64",
|
||||||
|
"@esbuild/darwin-arm64",
|
||||||
|
"@esbuild/darwin-x64",
|
||||||
|
"@esbuild/freebsd-arm64",
|
||||||
|
"@esbuild/freebsd-x64",
|
||||||
|
"@esbuild/linux-arm",
|
||||||
|
"@esbuild/linux-arm64",
|
||||||
|
"@esbuild/linux-ia32",
|
||||||
|
"@esbuild/linux-loong64",
|
||||||
|
"@esbuild/linux-mips64el",
|
||||||
|
"@esbuild/linux-ppc64",
|
||||||
|
"@esbuild/linux-riscv64",
|
||||||
|
"@esbuild/linux-s390x",
|
||||||
|
"@esbuild/linux-x64",
|
||||||
|
"@esbuild/netbsd-arm64",
|
||||||
|
"@esbuild/netbsd-x64",
|
||||||
|
"@esbuild/openbsd-arm64",
|
||||||
|
"@esbuild/openbsd-x64",
|
||||||
|
"@esbuild/openharmony-arm64",
|
||||||
|
"@esbuild/sunos-x64",
|
||||||
|
"@esbuild/win32-arm64",
|
||||||
|
"@esbuild/win32-ia32",
|
||||||
|
"@esbuild/win32-x64"
|
||||||
|
],
|
||||||
|
"scripts": true,
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"foreground-child@3.3.1": {
|
||||||
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
|
"dependencies": [
|
||||||
|
"cross-spawn",
|
||||||
|
"signal-exit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fsevents@2.3.3": {
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"scripts": true
|
||||||
|
},
|
||||||
|
"get-tsconfig@4.13.0": {
|
||||||
|
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"resolve-pkg-maps"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glob@11.1.0": {
|
||||||
|
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||||
|
"dependencies": [
|
||||||
|
"foreground-child",
|
||||||
|
"jackspeak",
|
||||||
|
"minimatch",
|
||||||
|
"minipass",
|
||||||
|
"package-json-from-dist",
|
||||||
|
"path-scurry"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"htmlparser2@10.1.0": {
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"domelementtype",
|
||||||
|
"domhandler",
|
||||||
|
"domutils",
|
||||||
|
"entities@7.0.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"iconv-lite@0.6.3": {
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dependencies": [
|
||||||
|
"safer-buffer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is-fullwidth-code-point@3.0.0": {
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||||
|
},
|
||||||
|
"isexe@2.0.0": {
|
||||||
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||||
|
},
|
||||||
|
"jackspeak@4.1.1": {
|
||||||
|
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"@isaacs/cliui"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"json5@2.2.3": {
|
||||||
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"lru-cache@11.2.5": {
|
||||||
|
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="
|
||||||
|
},
|
||||||
|
"minimatch@10.1.1": {
|
||||||
|
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"@isaacs/brace-expansion"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minipass@7.1.2": {
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
||||||
|
},
|
||||||
|
"normalize-path@3.0.0": {
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||||
|
},
|
||||||
|
"nth-check@2.1.1": {
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"dependencies": [
|
||||||
|
"boolbase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"package-json-from-dist@1.0.1": {
|
||||||
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||||
|
},
|
||||||
|
"parse5-htmlparser2-tree-adapter@7.1.0": {
|
||||||
|
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||||
|
"dependencies": [
|
||||||
|
"domhandler",
|
||||||
|
"parse5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parse5-parser-stream@7.1.2": {
|
||||||
|
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||||
|
"dependencies": [
|
||||||
|
"parse5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parse5@7.3.0": {
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dependencies": [
|
||||||
|
"entities@6.0.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"path-key@3.1.1": {
|
||||||
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
||||||
|
},
|
||||||
|
"path-scurry@2.0.1": {
|
||||||
|
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||||
|
"dependencies": [
|
||||||
|
"lru-cache",
|
||||||
|
"minipass"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolve-pkg-maps@1.0.0": {
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
|
||||||
|
},
|
||||||
|
"safe-stable-stringify@2.5.0": {
|
||||||
|
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
|
||||||
|
},
|
||||||
|
"safer-buffer@2.1.2": {
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"shebang-command@2.0.0": {
|
||||||
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"dependencies": [
|
||||||
|
"shebang-regex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shebang-regex@3.0.0": {
|
||||||
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||||
|
},
|
||||||
|
"signal-exit@4.1.0": {
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
||||||
|
},
|
||||||
|
"string-width@4.2.3": {
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dependencies": [
|
||||||
|
"emoji-regex@8.0.0",
|
||||||
|
"is-fullwidth-code-point",
|
||||||
|
"strip-ansi@6.0.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"string-width@5.1.2": {
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"dependencies": [
|
||||||
|
"eastasianwidth",
|
||||||
|
"emoji-regex@9.2.2",
|
||||||
|
"strip-ansi@7.1.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strip-ansi@6.0.1": {
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dependencies": [
|
||||||
|
"ansi-regex@5.0.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strip-ansi@7.1.2": {
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"dependencies": [
|
||||||
|
"ansi-regex@6.2.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ts-json-schema-generator@2.4.0": {
|
||||||
|
"integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/json-schema",
|
||||||
|
"commander",
|
||||||
|
"glob",
|
||||||
|
"json5",
|
||||||
|
"normalize-path",
|
||||||
|
"safe-stable-stringify",
|
||||||
|
"tslib",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"tslib@2.8.1": {
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
|
},
|
||||||
|
"tsx@4.21.0": {
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dependencies": [
|
||||||
|
"esbuild",
|
||||||
|
"get-tsconfig"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"fsevents"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"typescript@5.9.3": {
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"undici-types@6.21.0": {
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||||
|
},
|
||||||
|
"undici@7.19.1": {
|
||||||
|
"integrity": "sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg=="
|
||||||
|
},
|
||||||
|
"whatwg-encoding@3.1.1": {
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"iconv-lite"
|
||||||
|
],
|
||||||
|
"deprecated": true
|
||||||
|
},
|
||||||
|
"whatwg-mimetype@4.0.0": {
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="
|
||||||
|
},
|
||||||
|
"which@2.0.2": {
|
||||||
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"dependencies": [
|
||||||
|
"isexe"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"wrap-ansi@7.0.0": {
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dependencies": [
|
||||||
|
"ansi-styles@4.3.0",
|
||||||
|
"string-width@4.2.3",
|
||||||
|
"strip-ansi@6.0.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wrap-ansi@8.1.0": {
|
||||||
|
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"ansi-styles@6.2.3",
|
||||||
|
"string-width@5.1.2",
|
||||||
|
"strip-ansi@7.1.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@anthropic-ai/claude-code@latest",
|
||||||
|
"npm:@openai/codex@latest",
|
||||||
|
"npm:@types/node@22",
|
||||||
|
"npm:cheerio@1",
|
||||||
|
"npm:ts-json-schema-generator@^2.4.0",
|
||||||
|
"npm:tsx@^4.19.0",
|
||||||
|
"npm:typescript@^5.7.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,11 @@
|
||||||
"extract:opencode": "tsx src/index.ts --agent=opencode",
|
"extract:opencode": "tsx src/index.ts --agent=opencode",
|
||||||
"extract:claude": "tsx src/index.ts --agent=claude",
|
"extract:claude": "tsx src/index.ts --agent=claude",
|
||||||
"extract:codex": "tsx src/index.ts --agent=codex",
|
"extract:codex": "tsx src/index.ts --agent=codex",
|
||||||
"extract:amp": "tsx src/index.ts --agent=amp"
|
"extract:amp": "tsx src/index.ts --agent=amp",
|
||||||
|
"extract:claude-events": "tsx src/claude-event-types.ts",
|
||||||
|
"extract:claude-events:sdk": "tsx src/claude-event-types-sdk.ts",
|
||||||
|
"extract:claude-events:cli": "tsx src/claude-event-types-cli.ts",
|
||||||
|
"extract:claude-events:docs": "tsx src/claude-event-types-docs.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ts-json-schema-generator": "^2.4.0",
|
"ts-json-schema-generator": "^2.4.0",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { fetchWithCache } from "./cache.js";
|
||||||
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
||||||
import type { JSONSchema7 } from "json-schema";
|
import type { JSONSchema7 } from "json-schema";
|
||||||
|
|
||||||
const AMP_DOCS_URL = "https://ampcode.com/manual/appendix";
|
const AMP_DOCS_URL = "https://ampcode.com/manual/appendix?preview#message-schema";
|
||||||
|
|
||||||
// Key types we want to extract
|
// Key types we want to extract
|
||||||
const TARGET_TYPES = ["StreamJSONMessage", "AmpOptions", "PermissionRule", "Message", "ToolCall"];
|
const TARGET_TYPES = ["StreamJSONMessage", "AmpOptions", "PermissionRule", "Message", "ToolCall"];
|
||||||
|
|
|
||||||
11
resources/agent-schemas/src/claude-event-types-cli.ts
Normal file
11
resources/agent-schemas/src/claude-event-types-cli.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { collectFromCli } from "./claude-event-types.js";
|
||||||
|
|
||||||
|
const promptArg = process.argv.slice(2).find((arg) => arg.startsWith("--prompt="));
|
||||||
|
const timeoutArg = process.argv.slice(2).find((arg) => arg.startsWith("--timeoutMs="));
|
||||||
|
|
||||||
|
const prompt = promptArg?.split("=")[1] ?? "Reply with exactly OK.";
|
||||||
|
const timeoutMs = timeoutArg ? Number(timeoutArg.split("=")[1]) : 20000;
|
||||||
|
|
||||||
|
collectFromCli(prompt, timeoutMs).then((result) => {
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
});
|
||||||
8
resources/agent-schemas/src/claude-event-types-docs.ts
Normal file
8
resources/agent-schemas/src/claude-event-types-docs.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { collectFromDocs } from "./claude-event-types.js";
|
||||||
|
|
||||||
|
const urlsArg = process.argv.slice(2).find((arg) => arg.startsWith("--urls="));
|
||||||
|
const urls = urlsArg ? urlsArg.split("=")[1]!.split(",") : undefined;
|
||||||
|
|
||||||
|
collectFromDocs(urls ?? []).then((result) => {
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
});
|
||||||
4
resources/agent-schemas/src/claude-event-types-sdk.ts
Normal file
4
resources/agent-schemas/src/claude-event-types-sdk.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { collectFromSdkTypes } from "./claude-event-types.js";
|
||||||
|
|
||||||
|
const result = collectFromSdkTypes();
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
338
resources/agent-schemas/src/claude-event-types.ts
Normal file
338
resources/agent-schemas/src/claude-event-types.ts
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import ts from "typescript";
|
||||||
|
import { load } from "cheerio";
|
||||||
|
|
||||||
|
type SourceResult = {
|
||||||
|
source: string;
|
||||||
|
types: string[];
|
||||||
|
details?: Record<string, string[]>;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SDK_POSSIBLE_PATHS = [
|
||||||
|
"node_modules/@anthropic-ai/claude-code/sdk-tools.d.ts",
|
||||||
|
"node_modules/@anthropic-ai/claude-code/dist/index.d.ts",
|
||||||
|
"node_modules/@anthropic-ai/claude-code/dist/types.d.ts",
|
||||||
|
"node_modules/@anthropic-ai/claude-code/index.d.ts",
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_DOC_URLS = [
|
||||||
|
"https://platform.claude.com/docs/en/messages-streaming",
|
||||||
|
"https://platform.claude.com/docs/en/api/messages-streaming",
|
||||||
|
"https://docs.anthropic.com/claude/reference/messages-streaming",
|
||||||
|
"https://docs.anthropic.com/claude/reference/messages-streaming#events",
|
||||||
|
"https://docs.anthropic.com/claude/docs/messages-streaming",
|
||||||
|
];
|
||||||
|
|
||||||
|
function moduleDir(): string {
|
||||||
|
const metaDir = (import.meta as { dirname?: string }).dirname;
|
||||||
|
if (typeof metaDir === "string") {
|
||||||
|
return metaDir;
|
||||||
|
}
|
||||||
|
return dirname(fileURLToPath(import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSdkTypesPath(): string | null {
|
||||||
|
const resourceDir = join(moduleDir(), "..");
|
||||||
|
const repoRoot = join(moduleDir(), "..", "..", "..");
|
||||||
|
const searchRoots = [resourceDir, repoRoot];
|
||||||
|
|
||||||
|
for (const root of searchRoots) {
|
||||||
|
for (const relativePath of SDK_POSSIBLE_PATHS) {
|
||||||
|
const fullPath = join(root, relativePath);
|
||||||
|
if (existsSync(fullPath)) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractStringLiterals(node: ts.TypeNode): string[] {
|
||||||
|
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) {
|
||||||
|
return [node.literal.text];
|
||||||
|
}
|
||||||
|
if (ts.isUnionTypeNode(node)) {
|
||||||
|
return node.types.flatMap((typeNode) => extractStringLiterals(typeNode));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function containerName(node: ts.Node): string | null {
|
||||||
|
let current: ts.Node | undefined = node;
|
||||||
|
while (current) {
|
||||||
|
if (ts.isInterfaceDeclaration(current) && current.name) {
|
||||||
|
return current.name.text;
|
||||||
|
}
|
||||||
|
if (ts.isTypeAliasDeclaration(current) && current.name) {
|
||||||
|
return current.name.text;
|
||||||
|
}
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFromSdkTypes(): SourceResult {
|
||||||
|
const path = findSdkTypesPath();
|
||||||
|
if (!path) {
|
||||||
|
return { source: "sdk", types: [], error: "Claude SDK types not found" };
|
||||||
|
}
|
||||||
|
const content = readFileSync(path, "utf8");
|
||||||
|
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
|
||||||
|
const types = new Set<string>();
|
||||||
|
const details: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
function visit(node: ts.Node): void {
|
||||||
|
if (ts.isPropertySignature(node)) {
|
||||||
|
const name = node.name && ts.isIdentifier(node.name) ? node.name.text : null;
|
||||||
|
if (name === "type" && node.type) {
|
||||||
|
const literals = extractStringLiterals(node.type);
|
||||||
|
if (literals.length > 0) {
|
||||||
|
const parentName = containerName(node) ?? "anonymous";
|
||||||
|
if (/Event|Stream|Message/i.test(parentName)) {
|
||||||
|
literals.forEach((value) => types.add(value));
|
||||||
|
details[parentName] = (details[parentName] ?? []).concat(literals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(sourceFile);
|
||||||
|
return { source: "sdk", types: Array.from(types).sort(), details };
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFromCli(prompt: string, timeoutMs: number): Promise<SourceResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const result: SourceResult = { source: "cli", types: [] };
|
||||||
|
const types = new Set<string>();
|
||||||
|
const denoGlobal = (globalThis as {
|
||||||
|
Deno?: {
|
||||||
|
which?: (cmd: string) => string | null;
|
||||||
|
Command?: new (
|
||||||
|
cmd: string,
|
||||||
|
options: { args: string[]; stdout: "piped"; stderr: "piped" },
|
||||||
|
) => { output: () => Promise<{ stdout: Uint8Array; stderr: Uint8Array; code: number }> };
|
||||||
|
};
|
||||||
|
}).Deno;
|
||||||
|
|
||||||
|
if (denoGlobal?.which && !denoGlobal.which("claude")) {
|
||||||
|
result.error = "claude binary not found in PATH";
|
||||||
|
resolve(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denoGlobal?.Command) {
|
||||||
|
const command = new denoGlobal.Command("claude", {
|
||||||
|
args: ["--print", "--output-format", "stream-json", "--verbose", prompt],
|
||||||
|
stdout: "piped",
|
||||||
|
stderr: "piped",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
command
|
||||||
|
.output()
|
||||||
|
.then(({ stdout, stderr, code }) => {
|
||||||
|
const text = new TextDecoder().decode(stdout);
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(trimmed);
|
||||||
|
if (value && typeof value.type === "string") {
|
||||||
|
types.add(value.type);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore non-json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.types = Array.from(types).sort();
|
||||||
|
if (code !== 0) {
|
||||||
|
result.error =
|
||||||
|
new TextDecoder().decode(stderr).trim() ||
|
||||||
|
`claude exited with code ${code}`;
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
result.error = error instanceof Error ? error.message : String(error);
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
result.error = error instanceof Error ? error.message : String(error);
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let child;
|
||||||
|
try {
|
||||||
|
child = spawn(
|
||||||
|
"claude",
|
||||||
|
["--print", "--output-format", "stream-json", "--verbose", prompt],
|
||||||
|
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
result.error = error instanceof Error ? error.message : String(error);
|
||||||
|
resolve(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!child.stdout || !child.stderr) {
|
||||||
|
result.error = "claude stdout/stderr not available";
|
||||||
|
resolve(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stderr = "";
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
const text = chunk.toString("utf8");
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(trimmed);
|
||||||
|
if (value && typeof value.type === "string") {
|
||||||
|
types.add(value.type);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore non-json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
result.types = Array.from(types).sort();
|
||||||
|
if (code !== 0) {
|
||||||
|
result.error = stderr.trim() || `claude exited with code ${code}`;
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectFromDocs(urls: string[]): Promise<SourceResult> {
|
||||||
|
if (typeof fetch !== "function") {
|
||||||
|
return { source: "docs", types: [], error: "fetch is not available in this runtime" };
|
||||||
|
}
|
||||||
|
const effectiveUrls = urls.length > 0 ? urls : DEFAULT_DOC_URLS;
|
||||||
|
const types = new Set<string>();
|
||||||
|
const extractFromText = (text: string) => {
|
||||||
|
const typeMatches = text.match(/\"type\"\\s*:\\s*\"([^\"]+)\"/g) ?? [];
|
||||||
|
for (const match of typeMatches) {
|
||||||
|
const value = match.split(":")[1]?.trim().replace(/^\"|\"$/g, "");
|
||||||
|
if (value) types.add(value);
|
||||||
|
}
|
||||||
|
const eventMatches = text.match(/event\\s*:\\s*([a-z_]+)/gi) ?? [];
|
||||||
|
for (const match of eventMatches) {
|
||||||
|
const value = match.split(":")[1]?.trim();
|
||||||
|
if (value) types.add(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const url of effectiveUrls) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const html = await res.text();
|
||||||
|
const $ = load(html);
|
||||||
|
const blocks = $("pre, code")
|
||||||
|
.map((_, el) => $(el).text())
|
||||||
|
.get();
|
||||||
|
for (const block of blocks) {
|
||||||
|
extractFromText(block);
|
||||||
|
}
|
||||||
|
const nextData = $("#__NEXT_DATA__").text();
|
||||||
|
if (nextData) {
|
||||||
|
extractFromText(nextData);
|
||||||
|
}
|
||||||
|
extractFromText(html);
|
||||||
|
} catch {
|
||||||
|
// ignore per-url errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { source: "docs", types: Array.from(types).sort() };
|
||||||
|
}
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
source: "all" | "sdk" | "cli" | "docs";
|
||||||
|
prompt: string;
|
||||||
|
timeoutMs: number;
|
||||||
|
urls: string[];
|
||||||
|
json: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseArgs(): Args {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const sourceArg = args.find((arg) => arg.startsWith("--source="));
|
||||||
|
const promptArg = args.find((arg) => arg.startsWith("--prompt="));
|
||||||
|
const timeoutArg = args.find((arg) => arg.startsWith("--timeoutMs="));
|
||||||
|
const urlsArg = args.find((arg) => arg.startsWith("--urls="));
|
||||||
|
const json = args.includes("--json");
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: (sourceArg?.split("=")[1] as Args["source"]) ?? "all",
|
||||||
|
prompt: promptArg?.split("=")[1] ?? "Reply with exactly OK.",
|
||||||
|
timeoutMs: timeoutArg ? Number(timeoutArg.split("=")[1]) : 20000,
|
||||||
|
urls: urlsArg ? urlsArg.split("=")[1]!.split(",") : DEFAULT_DOC_URLS,
|
||||||
|
json,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(results: SourceResult[]): void {
|
||||||
|
const counts = results.map((r) => ({ source: r.source, count: r.types.length }));
|
||||||
|
const max = Math.max(...counts.map((c) => c.count), 0);
|
||||||
|
const best = counts.filter((c) => c.count === max).map((c) => c.source);
|
||||||
|
const union = Array.from(
|
||||||
|
new Set(results.flatMap((r) => r.types))
|
||||||
|
).sort();
|
||||||
|
|
||||||
|
console.log("Claude event type extraction");
|
||||||
|
console.log("============================");
|
||||||
|
for (const result of results) {
|
||||||
|
console.log(`- ${result.source}: ${result.types.length} types${result.error ? " (error)" : ""}`);
|
||||||
|
}
|
||||||
|
console.log(`\nMost comprehensive: ${best.join(", ") || "none"}`);
|
||||||
|
console.log(`Union (${union.length}): ${union.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const args = parseArgs();
|
||||||
|
const results: SourceResult[] = [];
|
||||||
|
|
||||||
|
if (args.source === "all" || args.source === "sdk") {
|
||||||
|
results.push(collectFromSdkTypes());
|
||||||
|
}
|
||||||
|
if (args.source === "all" || args.source === "cli") {
|
||||||
|
results.push(await collectFromCli(args.prompt, args.timeoutMs));
|
||||||
|
}
|
||||||
|
if (args.source === "all" || args.source === "docs") {
|
||||||
|
results.push(await collectFromDocs(args.urls));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.json) {
|
||||||
|
console.log(JSON.stringify({ results }, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summarize(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { collectFromCli, collectFromDocs, collectFromSdkTypes };
|
||||||
|
|
@ -1,92 +1,43 @@
|
||||||
import { createGenerator, type Config } from "ts-json-schema-generator";
|
import { execSync } from "child_process";
|
||||||
import { existsSync, readFileSync } from "fs";
|
|
||||||
import { join, dirname } from "path";
|
|
||||||
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
||||||
import type { JSONSchema7 } from "json-schema";
|
import type { JSONSchema7 } from "json-schema";
|
||||||
|
|
||||||
// Try multiple possible paths for the SDK types
|
|
||||||
const POSSIBLE_PATHS = [
|
|
||||||
"node_modules/@anthropic-ai/claude-code/sdk-tools.d.ts",
|
|
||||||
"node_modules/@anthropic-ai/claude-code/dist/index.d.ts",
|
|
||||||
"node_modules/@anthropic-ai/claude-code/dist/types.d.ts",
|
|
||||||
"node_modules/@anthropic-ai/claude-code/index.d.ts",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Key types we want to extract
|
|
||||||
const TARGET_TYPES = [
|
|
||||||
"ToolInputSchemas",
|
|
||||||
"AgentInput",
|
|
||||||
"BashInput",
|
|
||||||
"FileEditInput",
|
|
||||||
"FileReadInput",
|
|
||||||
"FileWriteInput",
|
|
||||||
"GlobInput",
|
|
||||||
"GrepInput",
|
|
||||||
"WebFetchInput",
|
|
||||||
"WebSearchInput",
|
|
||||||
"AskUserQuestionInput",
|
|
||||||
];
|
|
||||||
|
|
||||||
function findTypesPath(): string | null {
|
|
||||||
const baseDir = join(import.meta.dirname, "..", "..", "resources", "agent-schemas");
|
|
||||||
|
|
||||||
for (const relativePath of POSSIBLE_PATHS) {
|
|
||||||
const fullPath = join(baseDir, relativePath);
|
|
||||||
if (existsSync(fullPath)) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function extractClaudeSchema(): Promise<NormalizedSchema> {
|
export async function extractClaudeSchema(): Promise<NormalizedSchema> {
|
||||||
console.log("Extracting Claude Code SDK schema...");
|
console.log("Extracting Claude Code schema via CLI...");
|
||||||
|
|
||||||
const typesPath = findTypesPath();
|
|
||||||
|
|
||||||
if (!typesPath) {
|
|
||||||
console.log(" [warn] Claude Code SDK types not found, using fallback schema");
|
|
||||||
return createFallbackSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` [found] ${typesPath}`);
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
path: typesPath,
|
|
||||||
tsconfig: join(import.meta.dirname, "..", "..", "resources", "agent-schemas", "tsconfig.json"),
|
|
||||||
type: "*",
|
|
||||||
skipTypeCheck: true,
|
|
||||||
topRef: false,
|
|
||||||
expose: "export",
|
|
||||||
jsDoc: "extended",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const generator = createGenerator(config);
|
// Run claude CLI with --json-schema flag to get the schema
|
||||||
const schema = generator.createSchema(config.type);
|
const output = execSync("claude --output-format json --json-schema", {
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 30000,
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse the JSON output
|
||||||
|
const parsed = JSON.parse(output);
|
||||||
|
|
||||||
|
// Extract definitions from the schema
|
||||||
const definitions: Record<string, JSONSchema7> = {};
|
const definitions: Record<string, JSONSchema7> = {};
|
||||||
|
|
||||||
if (schema.definitions) {
|
if (parsed.definitions) {
|
||||||
for (const [name, def] of Object.entries(schema.definitions)) {
|
for (const [name, def] of Object.entries(parsed.definitions)) {
|
||||||
definitions[name] = def as JSONSchema7;
|
definitions[name] = def as JSONSchema7;
|
||||||
}
|
}
|
||||||
|
} else if (parsed.$defs) {
|
||||||
|
for (const [name, def] of Object.entries(parsed.$defs)) {
|
||||||
|
definitions[name] = def as JSONSchema7;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The output might be a single schema, use it as the root
|
||||||
|
definitions["Schema"] = parsed as JSONSchema7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify target types exist
|
console.log(` [ok] Extracted ${Object.keys(definitions).length} types from CLI`);
|
||||||
const found = TARGET_TYPES.filter((name) => definitions[name]);
|
|
||||||
const missing = TARGET_TYPES.filter((name) => !definitions[name]);
|
|
||||||
|
|
||||||
if (missing.length > 0) {
|
|
||||||
console.log(` [warn] Missing expected types: ${missing.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` [ok] Extracted ${Object.keys(definitions).length} types (${found.length} target types)`);
|
|
||||||
|
|
||||||
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
|
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [error] Schema generation failed: ${error}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.log(` [warn] CLI extraction failed: ${errorMessage}`);
|
||||||
console.log(" [fallback] Using embedded schema definitions");
|
console.log(" [fallback] Using embedded schema definitions");
|
||||||
return createFallbackSchema();
|
return createFallbackSchema();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,69 @@
|
||||||
import { createGenerator, type Config } from "ts-json-schema-generator";
|
import { execSync } from "child_process";
|
||||||
import { existsSync } from "fs";
|
import { existsSync, readFileSync, rmSync, readdirSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
||||||
import type { JSONSchema7 } from "json-schema";
|
import type { JSONSchema7 } from "json-schema";
|
||||||
|
|
||||||
// Try multiple possible paths for the SDK types
|
|
||||||
const POSSIBLE_PATHS = [
|
|
||||||
"node_modules/@openai/codex/dist/index.d.ts",
|
|
||||||
"node_modules/@openai/codex/dist/types.d.ts",
|
|
||||||
"node_modules/@openai/codex/index.d.ts",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Key types we want to extract
|
|
||||||
const TARGET_TYPES = [
|
|
||||||
"ThreadEvent",
|
|
||||||
"ThreadItem",
|
|
||||||
"CodexOptions",
|
|
||||||
"ThreadOptions",
|
|
||||||
"Input",
|
|
||||||
"ResponseItem",
|
|
||||||
"FunctionCall",
|
|
||||||
"Message",
|
|
||||||
];
|
|
||||||
|
|
||||||
function findTypesPath(): string | null {
|
|
||||||
const baseDir = join(import.meta.dirname, "..", "..", "resources", "agent-schemas");
|
|
||||||
|
|
||||||
for (const relativePath of POSSIBLE_PATHS) {
|
|
||||||
const fullPath = join(baseDir, relativePath);
|
|
||||||
if (existsSync(fullPath)) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function extractCodexSchema(): Promise<NormalizedSchema> {
|
export async function extractCodexSchema(): Promise<NormalizedSchema> {
|
||||||
console.log("Extracting Codex SDK schema...");
|
console.log("Extracting Codex schema via CLI...");
|
||||||
|
|
||||||
const typesPath = findTypesPath();
|
const tempDir = join(import.meta.dirname, "..", ".temp-codex-schemas");
|
||||||
|
|
||||||
if (!typesPath) {
|
try {
|
||||||
console.log(" [warn] Codex SDK types not found, using fallback schema");
|
// Run codex CLI to generate JSON schema
|
||||||
|
execSync(`codex app-server generate-json-schema --out "${tempDir}"`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 30000,
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read generated schema files from temp directory
|
||||||
|
const definitions: Record<string, JSONSchema7> = {};
|
||||||
|
|
||||||
|
if (existsSync(tempDir)) {
|
||||||
|
const files = readdirSync(tempDir).filter((f) => f.endsWith(".json"));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = join(tempDir, file);
|
||||||
|
const content = readFileSync(filePath, "utf-8");
|
||||||
|
const schema = JSON.parse(content);
|
||||||
|
|
||||||
|
// Extract the name from the file (e.g., "ThreadEvent.json" -> "ThreadEvent")
|
||||||
|
const name = file.replace(".json", "");
|
||||||
|
|
||||||
|
if (schema.definitions) {
|
||||||
|
for (const [defName, def] of Object.entries(schema.definitions)) {
|
||||||
|
definitions[defName] = def as JSONSchema7;
|
||||||
|
}
|
||||||
|
} else if (schema.$defs) {
|
||||||
|
for (const [defName, def] of Object.entries(schema.$defs)) {
|
||||||
|
definitions[defName] = def as JSONSchema7;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
definitions[name] = schema as JSONSchema7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp directory
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(definitions).length === 0) {
|
||||||
|
console.log(" [warn] No schemas extracted from CLI, using fallback");
|
||||||
return createFallbackSchema();
|
return createFallbackSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` [found] ${typesPath}`);
|
console.log(` [ok] Extracted ${Object.keys(definitions).length} types from CLI`);
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
path: typesPath,
|
|
||||||
tsconfig: join(import.meta.dirname, "..", "..", "resources", "agent-schemas", "tsconfig.json"),
|
|
||||||
type: "*",
|
|
||||||
skipTypeCheck: true,
|
|
||||||
topRef: false,
|
|
||||||
expose: "export",
|
|
||||||
jsDoc: "extended",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const generator = createGenerator(config);
|
|
||||||
const schema = generator.createSchema(config.type);
|
|
||||||
|
|
||||||
const definitions: Record<string, JSONSchema7> = {};
|
|
||||||
|
|
||||||
if (schema.definitions) {
|
|
||||||
for (const [name, def] of Object.entries(schema.definitions)) {
|
|
||||||
definitions[name] = def as JSONSchema7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify target types exist
|
|
||||||
const found = TARGET_TYPES.filter((name) => definitions[name]);
|
|
||||||
const missing = TARGET_TYPES.filter((name) => !definitions[name]);
|
|
||||||
|
|
||||||
if (missing.length > 0) {
|
|
||||||
console.log(` [warn] Missing expected types: ${missing.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` [ok] Extracted ${Object.keys(definitions).length} types (${found.length} target types)`);
|
|
||||||
|
|
||||||
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
|
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [error] Schema generation failed: ${error}`);
|
// Clean up temp directory on error
|
||||||
|
if (existsSync(tempDir)) {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.log(` [warn] CLI extraction failed: ${errorMessage}`);
|
||||||
console.log(" [fallback] Using embedded schema definitions");
|
console.log(" [fallback] Using embedded schema definitions");
|
||||||
return createFallbackSchema();
|
return createFallbackSchema();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { extractCodexSchema } from "./codex.js";
|
||||||
import { extractAmpSchema } from "./amp.js";
|
import { extractAmpSchema } from "./amp.js";
|
||||||
import { validateSchema, type NormalizedSchema } from "./normalize.js";
|
import { validateSchema, type NormalizedSchema } from "./normalize.js";
|
||||||
|
|
||||||
const RESOURCE_DIR = join(import.meta.dirname, "..", "..", "resources", "agent-schemas");
|
const RESOURCE_DIR = join(import.meta.dirname, "..");
|
||||||
const DIST_DIR = join(RESOURCE_DIR, "dist");
|
const DIST_DIR = join(RESOURCE_DIR, "artifacts", "json-schema");
|
||||||
|
|
||||||
type AgentName = "opencode" | "claude" | "codex" | "amp";
|
type AgentName = "opencode" | "claude" | "codex" | "amp";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { spawnSync, execFileSync } from "node:child_process";
|
import { execFileSync, spawnSync } from "node:child_process";
|
||||||
|
import readline from "node:readline";
|
||||||
|
|
||||||
const ENDPOINT_URL =
|
const ENDPOINT_URL =
|
||||||
"https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com";
|
"https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com";
|
||||||
|
|
@ -32,6 +33,47 @@ const PLATFORM_MAP: Record<string, { pkg: string; os: string; cpu: string; ext:
|
||||||
"aarch64-apple-darwin": { pkg: "darwin-arm64", os: "darwin", cpu: "arm64", ext: "" },
|
"aarch64-apple-darwin": { pkg: "darwin-arm64", os: "darwin", cpu: "arm64", ext: "" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
"confirm-release",
|
||||||
|
"update-version",
|
||||||
|
"generate-artifacts",
|
||||||
|
"git-commit",
|
||||||
|
"git-push",
|
||||||
|
"trigger-workflow",
|
||||||
|
"run-checks",
|
||||||
|
"publish-crates",
|
||||||
|
"publish-npm-sdk",
|
||||||
|
"publish-npm-cli",
|
||||||
|
"upload-typescript",
|
||||||
|
"upload-install",
|
||||||
|
"upload-binaries",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const PHASES = ["setup-local", "setup-ci", "complete-ci"] as const;
|
||||||
|
|
||||||
|
type Step = (typeof STEPS)[number];
|
||||||
|
type Phase = (typeof PHASES)[number];
|
||||||
|
|
||||||
|
const PHASE_MAP: Record<Phase, Step[]> = {
|
||||||
|
"setup-local": [
|
||||||
|
"confirm-release",
|
||||||
|
"update-version",
|
||||||
|
"generate-artifacts",
|
||||||
|
"git-commit",
|
||||||
|
"git-push",
|
||||||
|
"trigger-workflow",
|
||||||
|
],
|
||||||
|
"setup-ci": ["run-checks"],
|
||||||
|
"complete-ci": [
|
||||||
|
"publish-crates",
|
||||||
|
"publish-npm-sdk",
|
||||||
|
"publish-npm-cli",
|
||||||
|
"upload-typescript",
|
||||||
|
"upload-install",
|
||||||
|
"upload-binaries",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
function parseArgs(argv: string[]) {
|
function parseArgs(argv: string[]) {
|
||||||
const args = new Map<string, string>();
|
const args = new Map<string, string>();
|
||||||
const flags = new Set<string>();
|
const flags = new Set<string>();
|
||||||
|
|
@ -61,11 +103,7 @@ function run(cmd: string, cmdArgs: string[], options: Record<string, any> = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runCapture(
|
function runCapture(cmd: string, cmdArgs: string[], options: Record<string, any> = {}) {
|
||||||
cmd: string,
|
|
||||||
cmdArgs: string[],
|
|
||||||
options: Record<string, any> = {},
|
|
||||||
) {
|
|
||||||
const result = spawnSync(cmd, cmdArgs, {
|
const result = spawnSync(cmd, cmdArgs, {
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
|
|
@ -234,16 +272,55 @@ function uploadContent(content: string, remotePath: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePackageJson(filePath: string, version: string, updateOptionalDeps = false) {
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||||
|
pkg.version = version;
|
||||||
|
if (updateOptionalDeps && pkg.optionalDependencies) {
|
||||||
|
for (const dep of Object.keys(pkg.optionalDependencies)) {
|
||||||
|
pkg.optionalDependencies[dep] = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVersion(rootDir: string, version: string) {
|
||||||
|
const cargoPath = path.join(rootDir, "Cargo.toml");
|
||||||
|
let cargoContent = fs.readFileSync(cargoPath, "utf8");
|
||||||
|
cargoContent = cargoContent.replace(/^version = ".*"/m, `version = "${version}"`);
|
||||||
|
fs.writeFileSync(cargoPath, cargoContent);
|
||||||
|
|
||||||
|
updatePackageJson(path.join(rootDir, "sdks", "typescript", "package.json"), version, true);
|
||||||
|
updatePackageJson(path.join(rootDir, "sdks", "cli", "package.json"), version, true);
|
||||||
|
|
||||||
|
const platformsDir = path.join(rootDir, "sdks", "cli", "platforms");
|
||||||
|
for (const entry of fs.readdirSync(platformsDir, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const pkgPath = path.join(platformsDir, entry.name, "package.json");
|
||||||
|
if (fs.existsSync(pkgPath)) {
|
||||||
|
updatePackageJson(pkgPath, version, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildTypescript(rootDir: string) {
|
function buildTypescript(rootDir: string) {
|
||||||
const sdkDir = path.join(rootDir, "sdks", "typescript");
|
const sdkDir = path.join(rootDir, "sdks", "typescript");
|
||||||
if (!fs.existsSync(sdkDir)) {
|
if (!fs.existsSync(sdkDir)) {
|
||||||
throw new Error(`TypeScript SDK not found at ${sdkDir}`);
|
throw new Error(`TypeScript SDK not found at ${sdkDir}`);
|
||||||
}
|
}
|
||||||
run("npm", ["install"], { cwd: sdkDir });
|
run("pnpm", ["install"], { cwd: sdkDir });
|
||||||
run("npm", ["run", "build"], { cwd: sdkDir });
|
run("pnpm", ["run", "build"], { cwd: sdkDir });
|
||||||
return path.join(sdkDir, "dist");
|
return path.join(sdkDir, "dist");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateArtifacts(rootDir: string) {
|
||||||
|
const sdkDir = path.join(rootDir, "sdks", "typescript");
|
||||||
|
run("pnpm", ["run", "generate"], { cwd: sdkDir });
|
||||||
|
run("cargo", ["check", "-p", "sandbox-agent-universal-schema-gen"], { cwd: rootDir });
|
||||||
|
run("cargo", ["run", "-p", "sandbox-agent-openapi-gen", "--", "--out", "sdks/openapi/openapi.json"], {
|
||||||
|
cwd: rootDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function uploadTypescriptArtifacts(rootDir: string, version: string, latest: boolean) {
|
function uploadTypescriptArtifacts(rootDir: string, version: string, latest: boolean) {
|
||||||
console.log("==> Building TypeScript SDK");
|
console.log("==> Building TypeScript SDK");
|
||||||
const distPath = buildTypescript(rootDir);
|
const distPath = buildTypescript(rootDir);
|
||||||
|
|
@ -256,13 +333,7 @@ function uploadTypescriptArtifacts(rootDir: string, version: string, latest: boo
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadInstallScript(rootDir: string, version: string, latest: boolean) {
|
function uploadInstallScript(rootDir: string, version: string, latest: boolean) {
|
||||||
const installPath = path.join(
|
const installPath = path.join(rootDir, "scripts", "release", "static", "install.sh");
|
||||||
rootDir,
|
|
||||||
"scripts",
|
|
||||||
"release",
|
|
||||||
"static",
|
|
||||||
"install.sh",
|
|
||||||
);
|
|
||||||
let installContent = fs.readFileSync(installPath, "utf8");
|
let installContent = fs.readFileSync(installPath, "utf8");
|
||||||
|
|
||||||
const uploadForVersion = (versionValue: string, remoteVersion: string) => {
|
const uploadForVersion = (versionValue: string, remoteVersion: string) => {
|
||||||
|
|
@ -295,7 +366,6 @@ function uploadBinaries(rootDir: string, version: string, latest: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-release checks
|
|
||||||
function runChecks(rootDir: string) {
|
function runChecks(rootDir: string) {
|
||||||
console.log("==> Running Rust checks");
|
console.log("==> Running Rust checks");
|
||||||
run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir });
|
run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir });
|
||||||
|
|
@ -307,58 +377,46 @@ function runChecks(rootDir: string) {
|
||||||
run("pnpm", ["run", "build"], { cwd: rootDir });
|
run("pnpm", ["run", "build"], { cwd: rootDir });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crates.io publishing
|
|
||||||
function publishCrates(rootDir: string, version: string) {
|
function publishCrates(rootDir: string, version: string) {
|
||||||
// Update workspace version
|
updateVersion(rootDir, version);
|
||||||
const cargoPath = path.join(rootDir, "Cargo.toml");
|
|
||||||
let cargoContent = fs.readFileSync(cargoPath, "utf8");
|
|
||||||
cargoContent = cargoContent.replace(/^version = ".*"/m, `version = "${version}"`);
|
|
||||||
fs.writeFileSync(cargoPath, cargoContent);
|
|
||||||
|
|
||||||
for (const crate of CRATE_ORDER) {
|
for (const crate of CRATE_ORDER) {
|
||||||
console.log(`==> Publishing sandbox-agent-${crate}`);
|
console.log(`==> Publishing sandbox-agent-${crate}`);
|
||||||
const crateDir = path.join(rootDir, "server", "packages", crate);
|
const crateDir = path.join(rootDir, "server", "packages", crate);
|
||||||
run("cargo", ["publish", "--allow-dirty"], { cwd: crateDir });
|
run("cargo", ["publish", "--allow-dirty"], { cwd: crateDir });
|
||||||
// Wait for crates.io index propagation
|
|
||||||
console.log("Waiting 30s for index...");
|
console.log("Waiting 30s for index...");
|
||||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30000);
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// npm SDK publishing
|
|
||||||
function publishNpmSdk(rootDir: string, version: string) {
|
function publishNpmSdk(rootDir: string, version: string) {
|
||||||
const sdkDir = path.join(rootDir, "sdks", "typescript");
|
const sdkDir = path.join(rootDir, "sdks", "typescript");
|
||||||
console.log("==> Publishing TypeScript SDK to npm");
|
console.log("==> Publishing TypeScript SDK to npm");
|
||||||
run("npm", ["version", version, "--no-git-tag-version"], { cwd: sdkDir });
|
run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"], { cwd: sdkDir });
|
||||||
run("pnpm", ["install"], { cwd: sdkDir });
|
run("pnpm", ["install"], { cwd: sdkDir });
|
||||||
run("pnpm", ["run", "build"], { cwd: sdkDir });
|
run("pnpm", ["run", "build"], { cwd: sdkDir });
|
||||||
run("npm", ["publish", "--access", "public"], { cwd: sdkDir });
|
run("npm", ["publish", "--access", "public"], { cwd: sdkDir });
|
||||||
}
|
}
|
||||||
|
|
||||||
// npm CLI publishing
|
|
||||||
function publishNpmCli(rootDir: string, version: string) {
|
function publishNpmCli(rootDir: string, version: string) {
|
||||||
const cliDir = path.join(rootDir, "sdks", "cli");
|
const cliDir = path.join(rootDir, "sdks", "cli");
|
||||||
const distDir = path.join(rootDir, "dist");
|
const distDir = path.join(rootDir, "dist");
|
||||||
|
|
||||||
// Publish platform packages first
|
|
||||||
for (const [target, info] of Object.entries(PLATFORM_MAP)) {
|
for (const [target, info] of Object.entries(PLATFORM_MAP)) {
|
||||||
const platformDir = path.join(cliDir, "platforms", info.pkg);
|
const platformDir = path.join(cliDir, "platforms", info.pkg);
|
||||||
const binDir = path.join(platformDir, "bin");
|
const binDir = path.join(platformDir, "bin");
|
||||||
fs.mkdirSync(binDir, { recursive: true });
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
|
||||||
// Copy binary
|
|
||||||
const srcBinary = path.join(distDir, `sandbox-agent-${target}${info.ext}`);
|
const srcBinary = path.join(distDir, `sandbox-agent-${target}${info.ext}`);
|
||||||
const dstBinary = path.join(binDir, `sandbox-agent${info.ext}`);
|
const dstBinary = path.join(binDir, `sandbox-agent${info.ext}`);
|
||||||
fs.copyFileSync(srcBinary, dstBinary);
|
fs.copyFileSync(srcBinary, dstBinary);
|
||||||
if (info.ext !== ".exe") fs.chmodSync(dstBinary, 0o755);
|
if (info.ext !== ".exe") fs.chmodSync(dstBinary, 0o755);
|
||||||
|
|
||||||
// Update version and publish
|
|
||||||
console.log(`==> Publishing @sandbox-agent/cli-${info.pkg}`);
|
console.log(`==> Publishing @sandbox-agent/cli-${info.pkg}`);
|
||||||
run("npm", ["version", version, "--no-git-tag-version"], { cwd: platformDir });
|
run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"], { cwd: platformDir });
|
||||||
run("npm", ["publish", "--access", "public"], { cwd: platformDir });
|
run("npm", ["publish", "--access", "public"], { cwd: platformDir });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish main package (update optionalDeps versions)
|
|
||||||
console.log("==> Publishing @sandbox-agent/cli");
|
console.log("==> Publishing @sandbox-agent/cli");
|
||||||
const pkgPath = path.join(cliDir, "package.json");
|
const pkgPath = path.join(cliDir, "package.json");
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||||
|
|
@ -370,7 +428,26 @@ function publishNpmCli(rootDir: string, version: string) {
|
||||||
run("npm", ["publish", "--access", "public"], { cwd: cliDir });
|
run("npm", ["publish", "--access", "public"], { cwd: cliDir });
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function validateGit(rootDir: string) {
|
||||||
|
const status = runCapture("git", ["status", "--porcelain"], { cwd: rootDir });
|
||||||
|
if (status.trim()) {
|
||||||
|
throw new Error("Working tree is dirty; commit or stash changes before release.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRelease(version: string, latest: boolean) {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(`Release ${version} (latest=${latest})? (yes/no): `, resolve);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "yes" && answer.toLowerCase() !== "y") {
|
||||||
|
console.log("Release cancelled");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
const { args, flags } = parseArgs(process.argv.slice(2));
|
const { args, flags } = parseArgs(process.argv.slice(2));
|
||||||
const versionArg = args.get("--version");
|
const versionArg = args.get("--version");
|
||||||
if (!versionArg) {
|
if (!versionArg) {
|
||||||
|
|
@ -399,33 +476,155 @@ function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const phaseArg = args.get("--phase");
|
||||||
|
const stepsArg = args.get("--only-steps");
|
||||||
|
const requestedSteps = new Set<Step>();
|
||||||
|
|
||||||
|
if (phaseArg || stepsArg) {
|
||||||
|
if (phaseArg && stepsArg) {
|
||||||
|
throw new Error("Cannot use both --phase and --only-steps");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phaseArg) {
|
||||||
|
const phases = phaseArg.split(",").map((value) => value.trim());
|
||||||
|
for (const phase of phases) {
|
||||||
|
if (!PHASES.includes(phase as Phase)) {
|
||||||
|
throw new Error(`Invalid phase: ${phase}`);
|
||||||
|
}
|
||||||
|
for (const step of PHASE_MAP[phase as Phase]) {
|
||||||
|
requestedSteps.add(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepsArg) {
|
||||||
|
const steps = stepsArg.split(",").map((value) => value.trim());
|
||||||
|
for (const step of steps) {
|
||||||
|
if (!STEPS.includes(step as Step)) {
|
||||||
|
throw new Error(`Invalid step: ${step}`);
|
||||||
|
}
|
||||||
|
requestedSteps.add(step as Step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const shouldRun = (step: Step) => requestedSteps.has(step);
|
||||||
|
const hasPhases = requestedSteps.size > 0;
|
||||||
|
|
||||||
|
if (!hasPhases) {
|
||||||
if (flags.has("--check")) {
|
if (flags.has("--check")) {
|
||||||
runChecks(process.cwd());
|
runChecks(rootDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.has("--publish-crates")) {
|
if (flags.has("--publish-crates")) {
|
||||||
publishCrates(process.cwd(), version);
|
publishCrates(rootDir, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.has("--publish-npm-sdk")) {
|
if (flags.has("--publish-npm-sdk")) {
|
||||||
publishNpmSdk(process.cwd(), version);
|
publishNpmSdk(rootDir, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.has("--publish-npm-cli")) {
|
if (flags.has("--publish-npm-cli")) {
|
||||||
publishNpmCli(process.cwd(), version);
|
publishNpmCli(rootDir, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.has("--upload-typescript")) {
|
if (flags.has("--upload-typescript")) {
|
||||||
uploadTypescriptArtifacts(process.cwd(), version, latest);
|
uploadTypescriptArtifacts(rootDir, version, latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.has("--upload-install")) {
|
if (flags.has("--upload-install")) {
|
||||||
uploadInstallScript(process.cwd(), version, latest);
|
uploadInstallScript(rootDir, version, latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.has("--upload-binaries")) {
|
if (flags.has("--upload-binaries")) {
|
||||||
uploadBinaries(process.cwd(), version, latest);
|
uploadBinaries(rootDir, version, latest);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("confirm-release") && !flags.has("--no-confirm")) {
|
||||||
|
await confirmRelease(version, latest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateGitEnabled = !flags.has("--no-validate-git");
|
||||||
|
if ((shouldRun("git-commit") || shouldRun("git-push")) && validateGitEnabled) {
|
||||||
|
validateGit(rootDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("update-version")) {
|
||||||
|
console.log("==> Updating versions");
|
||||||
|
updateVersion(rootDir, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("generate-artifacts")) {
|
||||||
|
console.log("==> Generating OpenAPI and universal schemas");
|
||||||
|
generateArtifacts(rootDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("git-commit")) {
|
||||||
|
console.log("==> Committing changes");
|
||||||
|
run("git", ["add", "."], { cwd: rootDir });
|
||||||
|
run("git", ["commit", "--allow-empty", "-m", `chore(release): update version to ${version}`], {
|
||||||
|
cwd: rootDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("git-push")) {
|
||||||
|
console.log("==> Pushing changes");
|
||||||
|
const branch = runCapture("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: rootDir });
|
||||||
|
if (branch === "main") {
|
||||||
|
run("git", ["push"], { cwd: rootDir });
|
||||||
|
} else {
|
||||||
|
run("git", ["push", "-u", "origin", "HEAD"], { cwd: rootDir });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
if (shouldRun("trigger-workflow")) {
|
||||||
|
console.log("==> Triggering release workflow");
|
||||||
|
const branch = runCapture("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: rootDir });
|
||||||
|
const latestFlag = latest ? "true" : "false";
|
||||||
|
run(
|
||||||
|
"gh",
|
||||||
|
[
|
||||||
|
"workflow",
|
||||||
|
"run",
|
||||||
|
".github/workflows/release.yaml",
|
||||||
|
"-f",
|
||||||
|
`version=${version}`,
|
||||||
|
"-f",
|
||||||
|
`latest=${latestFlag}`,
|
||||||
|
"--ref",
|
||||||
|
branch,
|
||||||
|
],
|
||||||
|
{ cwd: rootDir },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("run-checks")) {
|
||||||
|
runChecks(rootDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("publish-crates")) {
|
||||||
|
publishCrates(rootDir, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("publish-npm-sdk")) {
|
||||||
|
publishNpmSdk(rootDir, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("publish-npm-cli")) {
|
||||||
|
publishNpmCli(rootDir, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("upload-typescript")) {
|
||||||
|
uploadTypescriptArtifacts(rootDir, version, latest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("upload-install")) {
|
||||||
|
uploadInstallScript(rootDir, version, latest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun("upload-binaries")) {
|
||||||
|
uploadBinaries(rootDir, version, latest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/sessions": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"sessions"
|
||||||
|
],
|
||||||
|
"operationId": "list_sessions",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SessionListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/sessions/{session_id}": {
|
"/v1/sessions/{session_id}": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -1047,6 +1067,65 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SessionInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"sessionId",
|
||||||
|
"agent",
|
||||||
|
"agentMode",
|
||||||
|
"permissionMode",
|
||||||
|
"ended",
|
||||||
|
"eventCount"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"agent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"agentMode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"agentSessionId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"ended": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"eventCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"permissionMode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SessionListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"sessions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"sessions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/SessionInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Started": {
|
"Started": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out src/generated/openapi.json",
|
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../openapi/openapi.json",
|
||||||
"generate:types": "openapi-typescript src/generated/openapi.json -o src/generated/openapi.ts",
|
"generate:types": "openapi-typescript ../openapi/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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,21 @@ export type AgentInfo = components["schemas"]["AgentInfo"];
|
||||||
export type AgentListResponse = components["schemas"]["AgentListResponse"];
|
export type AgentListResponse = components["schemas"]["AgentListResponse"];
|
||||||
export type CreateSessionRequest = components["schemas"]["CreateSessionRequest"];
|
export type CreateSessionRequest = components["schemas"]["CreateSessionRequest"];
|
||||||
export type CreateSessionResponse = components["schemas"]["CreateSessionResponse"];
|
export type CreateSessionResponse = components["schemas"]["CreateSessionResponse"];
|
||||||
|
export type HealthResponse = components["schemas"]["HealthResponse"];
|
||||||
export type MessageRequest = components["schemas"]["MessageRequest"];
|
export type MessageRequest = components["schemas"]["MessageRequest"];
|
||||||
export type EventsQuery = components["schemas"]["EventsQuery"];
|
export type EventsQuery = components["schemas"]["EventsQuery"];
|
||||||
export type EventsResponse = components["schemas"]["EventsResponse"];
|
export type EventsResponse = components["schemas"]["EventsResponse"];
|
||||||
|
export type PermissionRequest = components["schemas"]["PermissionRequest"];
|
||||||
export type QuestionReplyRequest = components["schemas"]["QuestionReplyRequest"];
|
export type QuestionReplyRequest = components["schemas"]["QuestionReplyRequest"];
|
||||||
|
export type QuestionRequest = components["schemas"]["QuestionRequest"];
|
||||||
export type PermissionReplyRequest = components["schemas"]["PermissionReplyRequest"];
|
export type PermissionReplyRequest = components["schemas"]["PermissionReplyRequest"];
|
||||||
export type PermissionReply = components["schemas"]["PermissionReply"];
|
export type PermissionReply = components["schemas"]["PermissionReply"];
|
||||||
export type ProblemDetails = components["schemas"]["ProblemDetails"];
|
export type ProblemDetails = components["schemas"]["ProblemDetails"];
|
||||||
|
export type SessionInfo = components["schemas"]["SessionInfo"];
|
||||||
|
export type SessionListResponse = components["schemas"]["SessionListResponse"];
|
||||||
export type UniversalEvent = components["schemas"]["UniversalEvent"];
|
export type UniversalEvent = components["schemas"]["UniversalEvent"];
|
||||||
|
export type UniversalMessage = components["schemas"]["UniversalMessage"];
|
||||||
|
export type UniversalMessagePart = components["schemas"]["UniversalMessagePart"];
|
||||||
|
|
||||||
const API_PREFIX = "/v1";
|
const API_PREFIX = "/v1";
|
||||||
|
|
||||||
|
|
@ -58,6 +65,7 @@ type RequestOptions = {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
headers?: HeadersInit;
|
headers?: HeadersInit;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SandboxDaemonClient {
|
export class SandboxDaemonClient {
|
||||||
|
|
@ -108,6 +116,10 @@ export class SandboxDaemonClient {
|
||||||
return this.requestJson("GET", `${API_PREFIX}/agents`);
|
return this.requestJson("GET", `${API_PREFIX}/agents`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getHealth(): Promise<HealthResponse> {
|
||||||
|
return this.requestJson("GET", `${API_PREFIX}/health`);
|
||||||
|
}
|
||||||
|
|
||||||
async installAgent(agent: string, request: AgentInstallRequest = {}): Promise<void> {
|
async installAgent(agent: string, request: AgentInstallRequest = {}): Promise<void> {
|
||||||
await this.requestJson("POST", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/install`, {
|
await this.requestJson("POST", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/install`, {
|
||||||
body: request,
|
body: request,
|
||||||
|
|
@ -124,6 +136,10 @@ export class SandboxDaemonClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listSessions(): Promise<SessionListResponse> {
|
||||||
|
return this.requestJson("GET", `${API_PREFIX}/sessions`);
|
||||||
|
}
|
||||||
|
|
||||||
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
|
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
|
||||||
await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
||||||
body: request,
|
body: request,
|
||||||
|
|
@ -136,15 +152,20 @@ export class SandboxDaemonClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventsSse(sessionId: string, query?: EventsQuery): Promise<Response> {
|
async getEventsSse(sessionId: string, query?: EventsQuery, signal?: AbortSignal): Promise<Response> {
|
||||||
return this.requestRaw("GET", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/events/sse`, {
|
return this.requestRaw("GET", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/events/sse`, {
|
||||||
query,
|
query,
|
||||||
accept: "text/event-stream",
|
accept: "text/event-stream",
|
||||||
|
signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async *streamEvents(sessionId: string, query?: EventsQuery): AsyncGenerator<UniversalEvent, void, void> {
|
async *streamEvents(
|
||||||
const response = await this.getEventsSse(sessionId, query);
|
sessionId: string,
|
||||||
|
query?: EventsQuery,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): AsyncGenerator<UniversalEvent, void, void> {
|
||||||
|
const response = await this.getEventsSse(sessionId, query, signal);
|
||||||
if (!response.body) {
|
if (!response.body) {
|
||||||
throw new Error("SSE stream is not readable in this environment.");
|
throw new Error("SSE stream is not readable in this environment.");
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +270,7 @@ export class SandboxDaemonClient {
|
||||||
headers.set("Accept", options.accept);
|
headers.set("Accept", options.accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
const init: RequestInit = { method, headers };
|
const init: RequestInit = { method, headers, signal: options.signal };
|
||||||
if (options.body !== undefined) {
|
if (options.body !== undefined) {
|
||||||
headers.set("Content-Type", "application/json");
|
headers.set("Content-Type", "application/json");
|
||||||
init.body = JSON.stringify(options.body);
|
init.body = JSON.stringify(options.body);
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,19 @@ export type {
|
||||||
CreateSessionResponse,
|
CreateSessionResponse,
|
||||||
EventsQuery,
|
EventsQuery,
|
||||||
EventsResponse,
|
EventsResponse,
|
||||||
|
HealthResponse,
|
||||||
MessageRequest,
|
MessageRequest,
|
||||||
|
PermissionRequest,
|
||||||
PermissionReply,
|
PermissionReply,
|
||||||
PermissionReplyRequest,
|
PermissionReplyRequest,
|
||||||
ProblemDetails,
|
ProblemDetails,
|
||||||
|
QuestionRequest,
|
||||||
QuestionReplyRequest,
|
QuestionReplyRequest,
|
||||||
|
SessionInfo,
|
||||||
|
SessionListResponse,
|
||||||
UniversalEvent,
|
UniversalEvent,
|
||||||
|
UniversalMessage,
|
||||||
|
UniversalMessagePart,
|
||||||
SandboxDaemonClientOptions,
|
SandboxDaemonClientOptions,
|
||||||
SandboxDaemonConnectOptions,
|
SandboxDaemonConnectOptions,
|
||||||
} from "./client.js";
|
} from "./client.js";
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,9 @@ export async function spawnSandboxDaemon(
|
||||||
const net = await import("node:net");
|
const net = await import("node:net");
|
||||||
const { createRequire } = await import("node:module");
|
const { createRequire } = await import("node:module");
|
||||||
|
|
||||||
const host = options.host ?? "127.0.0.1";
|
const bindHost = options.host ?? "127.0.0.1";
|
||||||
const port = options.port ?? (await getFreePort(net, host));
|
const port = options.port ?? (await getFreePort(net, bindHost));
|
||||||
|
const connectHost = bindHost === "0.0.0.0" || bindHost === "::" ? "127.0.0.1" : bindHost;
|
||||||
const token = options.token ?? crypto.randomBytes(24).toString("hex");
|
const token = options.token ?? crypto.randomBytes(24).toString("hex");
|
||||||
const timeoutMs = options.timeoutMs ?? 15_000;
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
||||||
const logMode: SandboxDaemonSpawnLogMode = options.log ?? "inherit";
|
const logMode: SandboxDaemonSpawnLogMode = options.log ?? "inherit";
|
||||||
|
|
@ -67,7 +68,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", host, "--port", String(port), "--token", token];
|
const args = ["--host", bindHost, "--port", String(port), "--token", token];
|
||||||
const child = spawn(binaryPath, args, {
|
const child = spawn(binaryPath, args, {
|
||||||
stdio,
|
stdio,
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -77,8 +78,8 @@ export async function spawnSandboxDaemon(
|
||||||
});
|
});
|
||||||
const cleanup = registerProcessCleanup(child);
|
const cleanup = registerProcessCleanup(child);
|
||||||
|
|
||||||
const baseUrl = `http://${host}:${port}`;
|
const baseUrl = `http://${connectHost}:${port}`;
|
||||||
const ready = waitForHealth(baseUrl, fetcher ?? globalThis.fetch, timeoutMs, child);
|
const ready = waitForHealth(baseUrl, fetcher ?? globalThis.fetch, timeoutMs, child, token);
|
||||||
|
|
||||||
await ready;
|
await ready;
|
||||||
|
|
||||||
|
|
@ -161,6 +162,7 @@ async function waitForHealth(
|
||||||
fetcher: typeof fetch | undefined,
|
fetcher: typeof fetch | undefined,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
child: ChildProcess,
|
child: ChildProcess,
|
||||||
|
token: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!fetcher) {
|
if (!fetcher) {
|
||||||
throw new Error("Fetch API is not available; provide a fetch implementation.");
|
throw new Error("Fetch API is not available; provide a fetch implementation.");
|
||||||
|
|
@ -173,7 +175,9 @@ async function waitForHealth(
|
||||||
throw new Error("sandbox-agent exited before becoming healthy.");
|
throw new Error("sandbox-agent exited before becoming healthy.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetcher(`${baseUrl}/v1/health`);
|
const response = await fetcher(`${baseUrl}/v1/health`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
server/AGENTS.md
Symbolic link
1
server/AGENTS.md
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
CLAUDE.md
|
||||||
68
server/CLAUDE.md
Normal file
68
server/CLAUDE.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Server Testing
|
||||||
|
|
||||||
|
## Snapshot tests
|
||||||
|
|
||||||
|
The HTTP/SSE snapshot suite lives in:
|
||||||
|
- `server/packages/sandbox-agent/tests/http_sse_snapshots.rs`
|
||||||
|
|
||||||
|
Snapshots are written to:
|
||||||
|
- `server/packages/sandbox-agent/tests/snapshots/`
|
||||||
|
|
||||||
|
## Agent selection
|
||||||
|
|
||||||
|
`SANDBOX_TEST_AGENTS` controls which agents run. It accepts a comma-separated list or `all`.
|
||||||
|
If it is **not set**, tests will auto-detect installed agents by checking:
|
||||||
|
- binaries on `PATH`, and
|
||||||
|
- the default install dir (`$XDG_DATA_HOME/sandbox-agent/bin` or `./.sandbox-agent/bin`)
|
||||||
|
|
||||||
|
If no agents are found, tests fail with a clear error.
|
||||||
|
|
||||||
|
## Credential handling
|
||||||
|
|
||||||
|
Credentials are pulled from the host by default via `extract_all_credentials`:
|
||||||
|
- environment variables (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`)
|
||||||
|
- local CLI configs (Claude/Codex/Amp/OpenCode)
|
||||||
|
|
||||||
|
You can override host credentials for tests with:
|
||||||
|
- `SANDBOX_TEST_ANTHROPIC_API_KEY`
|
||||||
|
- `SANDBOX_TEST_OPENAI_API_KEY`
|
||||||
|
|
||||||
|
If `SANDBOX_TEST_AGENTS` includes an agent that requires a provider credential and it is missing,
|
||||||
|
tests fail before starting.
|
||||||
|
|
||||||
|
## Credential health checks
|
||||||
|
|
||||||
|
Before running agent tests, credentials are validated with minimal API calls:
|
||||||
|
- Anthropic: `GET https://api.anthropic.com/v1/models`
|
||||||
|
- `x-api-key` for API keys
|
||||||
|
- `Authorization: Bearer` for OAuth tokens
|
||||||
|
- `anthropic-version: 2023-06-01`
|
||||||
|
- OpenAI: `GET https://api.openai.com/v1/models` with `Authorization: Bearer`
|
||||||
|
|
||||||
|
401/403 yields a hard failure (`invalid credentials`). Other non-2xx responses or network
|
||||||
|
errors fail with a health-check error.
|
||||||
|
|
||||||
|
Health checks run in a blocking thread to avoid Tokio runtime drop errors inside async tests.
|
||||||
|
|
||||||
|
## Snapshot stability
|
||||||
|
|
||||||
|
To keep snapshots deterministic:
|
||||||
|
- Event streams are truncated after the first assistant or error event.
|
||||||
|
- Permission flow snapshots are truncated after the permission request (or first assistant) event.
|
||||||
|
- Unknown events are preserved as `kind: unknown` (raw payload in universal schema).
|
||||||
|
|
||||||
|
## Typical commands
|
||||||
|
|
||||||
|
Run only Claude snapshots:
|
||||||
|
```
|
||||||
|
SANDBOX_TEST_AGENTS=claude cargo test -p sandbox-agent-core --test http_sse_snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Run all detected agents:
|
||||||
|
```
|
||||||
|
cargo test -p sandbox-agent-core --test http_sse_snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Universal Schema
|
||||||
|
|
||||||
|
When modifying agent conversion code in `server/packages/universal-agent-schema/src/agents/` or adding/changing properties on the universal schema, update the feature matrix in `README.md` to reflect which agents support which features.
|
||||||
|
|
@ -6,7 +6,7 @@ authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
dirs = "5.0"
|
dirs.workspace = true
|
||||||
time = { version = "0.3", features = ["parsing", "formatting"] }
|
time.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0"
|
sandbox-agent-agent-credentials.workspace = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
thiserror.workspace = true
|
||||||
serde_json = "1.0"
|
serde.workspace = true
|
||||||
sandbox-agent-agent-credentials = { path = "../agent-credentials" }
|
serde_json.workspace = true
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }
|
reqwest.workspace = true
|
||||||
flate2 = "1.0"
|
flate2.workspace = true
|
||||||
tar = "0.4"
|
tar.workspace = true
|
||||||
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
zip.workspace = true
|
||||||
url = "2.5"
|
url.workspace = true
|
||||||
dirs = "5.0"
|
dirs.workspace = true
|
||||||
tempfile = "3.10"
|
tempfile.workspace = true
|
||||||
time = { version = "0.3", features = ["parsing", "formatting"] }
|
time.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
|
||||||
|
use reqwest::StatusCode;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::agents::AgentId;
|
use crate::agents::AgentId;
|
||||||
use crate::credentials::{AuthType, ExtractedCredentials, ProviderCredentials};
|
use crate::credentials::{
|
||||||
|
extract_all_credentials, AuthType, CredentialExtractionOptions, ExtractedCredentials,
|
||||||
|
ProviderCredentials,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TestAgentConfig {
|
pub struct TestAgentConfig {
|
||||||
|
|
@ -13,20 +21,36 @@ pub struct TestAgentConfig {
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum TestAgentConfigError {
|
pub enum TestAgentConfigError {
|
||||||
#[error("no test agents configured (set SANDBOX_TEST_AGENTS)")]
|
#[error("no test agents detected (install agents or set SANDBOX_TEST_AGENTS)")]
|
||||||
NoAgentsConfigured,
|
NoAgentsConfigured,
|
||||||
#[error("unknown agent name: {0}")]
|
#[error("unknown agent name: {0}")]
|
||||||
UnknownAgent(String),
|
UnknownAgent(String),
|
||||||
#[error("missing credentials for {agent}: {missing}")]
|
#[error("missing credentials for {agent}: {missing}")]
|
||||||
MissingCredentials { agent: AgentId, missing: String },
|
MissingCredentials { agent: AgentId, missing: String },
|
||||||
|
#[error("invalid credentials for {provider} (status {status})")]
|
||||||
|
InvalidCredentials { provider: String, status: u16 },
|
||||||
|
#[error("credential health check failed for {provider}: {message}")]
|
||||||
|
HealthCheckFailed { provider: String, message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
const AGENTS_ENV: &str = "SANDBOX_TEST_AGENTS";
|
const AGENTS_ENV: &str = "SANDBOX_TEST_AGENTS";
|
||||||
const ANTHROPIC_ENV: &str = "SANDBOX_TEST_ANTHROPIC_API_KEY";
|
const ANTHROPIC_ENV: &str = "SANDBOX_TEST_ANTHROPIC_API_KEY";
|
||||||
const OPENAI_ENV: &str = "SANDBOX_TEST_OPENAI_API_KEY";
|
const OPENAI_ENV: &str = "SANDBOX_TEST_OPENAI_API_KEY";
|
||||||
|
const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models";
|
||||||
|
const OPENAI_MODELS_URL: &str = "https://api.openai.com/v1/models";
|
||||||
|
const ANTHROPIC_VERSION: &str = "2023-06-01";
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct HealthCheckCache {
|
||||||
|
anthropic_ok: bool,
|
||||||
|
openai_ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigError> {
|
pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigError> {
|
||||||
let raw_agents = env::var(AGENTS_ENV).unwrap_or_default();
|
let raw_agents = env::var(AGENTS_ENV).unwrap_or_default();
|
||||||
|
let mut agents = if raw_agents.trim().is_empty() {
|
||||||
|
detect_system_agents()
|
||||||
|
} else {
|
||||||
let mut agents = Vec::new();
|
let mut agents = Vec::new();
|
||||||
for entry in raw_agents.split(',') {
|
for entry in raw_agents.split(',') {
|
||||||
let trimmed = entry.trim();
|
let trimmed = entry.trim();
|
||||||
|
|
@ -46,43 +70,72 @@ pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigErr
|
||||||
.ok_or_else(|| TestAgentConfigError::UnknownAgent(trimmed.to_string()))?;
|
.ok_or_else(|| TestAgentConfigError::UnknownAgent(trimmed.to_string()))?;
|
||||||
agents.push(agent);
|
agents.push(agent);
|
||||||
}
|
}
|
||||||
|
agents
|
||||||
|
};
|
||||||
|
|
||||||
|
agents.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
||||||
|
agents.dedup();
|
||||||
|
|
||||||
if agents.is_empty() {
|
if agents.is_empty() {
|
||||||
return Err(TestAgentConfigError::NoAgentsConfigured);
|
return Err(TestAgentConfigError::NoAgentsConfigured);
|
||||||
}
|
}
|
||||||
|
|
||||||
let anthropic_key = read_env_key(ANTHROPIC_ENV);
|
let extracted = extract_all_credentials(&CredentialExtractionOptions::new());
|
||||||
let openai_key = read_env_key(OPENAI_ENV);
|
let anthropic_cred = read_env_key(ANTHROPIC_ENV)
|
||||||
|
.map(|key| ProviderCredentials {
|
||||||
|
api_key: key,
|
||||||
|
source: "sandbox-test-env".to_string(),
|
||||||
|
auth_type: AuthType::ApiKey,
|
||||||
|
provider: "anthropic".to_string(),
|
||||||
|
})
|
||||||
|
.or_else(|| extracted.anthropic.clone());
|
||||||
|
let openai_cred = read_env_key(OPENAI_ENV)
|
||||||
|
.map(|key| ProviderCredentials {
|
||||||
|
api_key: key,
|
||||||
|
source: "sandbox-test-env".to_string(),
|
||||||
|
auth_type: AuthType::ApiKey,
|
||||||
|
provider: "openai".to_string(),
|
||||||
|
})
|
||||||
|
.or_else(|| extracted.openai.clone());
|
||||||
|
let mut health_cache = HealthCheckCache::default();
|
||||||
|
|
||||||
let mut configs = Vec::new();
|
let mut configs = Vec::new();
|
||||||
for agent in agents {
|
for agent in agents {
|
||||||
let credentials = match agent {
|
let credentials = match agent {
|
||||||
AgentId::Claude | AgentId::Amp => {
|
AgentId::Claude | AgentId::Amp => {
|
||||||
let anthropic_key = anthropic_key.clone().ok_or_else(|| {
|
let anthropic_cred = anthropic_cred.clone().ok_or_else(|| {
|
||||||
TestAgentConfigError::MissingCredentials {
|
TestAgentConfigError::MissingCredentials {
|
||||||
agent,
|
agent,
|
||||||
missing: ANTHROPIC_ENV.to_string(),
|
missing: ANTHROPIC_ENV.to_string(),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
credentials_with(anthropic_key, None)
|
ensure_anthropic_ok(&mut health_cache, &anthropic_cred)?;
|
||||||
|
credentials_with(Some(anthropic_cred), None)
|
||||||
}
|
}
|
||||||
AgentId::Codex => {
|
AgentId::Codex => {
|
||||||
let openai_key = openai_key.clone().ok_or_else(|| {
|
let openai_cred = openai_cred.clone().ok_or_else(|| {
|
||||||
TestAgentConfigError::MissingCredentials {
|
TestAgentConfigError::MissingCredentials {
|
||||||
agent,
|
agent,
|
||||||
missing: OPENAI_ENV.to_string(),
|
missing: OPENAI_ENV.to_string(),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
credentials_with(None, Some(openai_key))
|
ensure_openai_ok(&mut health_cache, &openai_cred)?;
|
||||||
|
credentials_with(None, Some(openai_cred))
|
||||||
}
|
}
|
||||||
AgentId::Opencode => {
|
AgentId::Opencode => {
|
||||||
if anthropic_key.is_none() && openai_key.is_none() {
|
if anthropic_cred.is_none() && openai_cred.is_none() {
|
||||||
return Err(TestAgentConfigError::MissingCredentials {
|
return Err(TestAgentConfigError::MissingCredentials {
|
||||||
agent,
|
agent,
|
||||||
missing: format!("{ANTHROPIC_ENV} or {OPENAI_ENV}"),
|
missing: format!("{ANTHROPIC_ENV} or {OPENAI_ENV}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
credentials_with(anthropic_key.clone(), openai_key.clone())
|
if let Some(cred) = anthropic_cred.as_ref() {
|
||||||
|
ensure_anthropic_ok(&mut health_cache, cred)?;
|
||||||
|
}
|
||||||
|
if let Some(cred) = openai_cred.as_ref() {
|
||||||
|
ensure_openai_ok(&mut health_cache, cred)?;
|
||||||
|
}
|
||||||
|
credentials_with(anthropic_cred.clone(), openai_cred.clone())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
configs.push(TestAgentConfig { agent, credentials });
|
configs.push(TestAgentConfig { agent, credentials });
|
||||||
|
|
@ -91,6 +144,178 @@ pub fn test_agents_from_env() -> Result<Vec<TestAgentConfig>, TestAgentConfigErr
|
||||||
Ok(configs)
|
Ok(configs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_anthropic_ok(
|
||||||
|
cache: &mut HealthCheckCache,
|
||||||
|
credentials: &ProviderCredentials,
|
||||||
|
) -> Result<(), TestAgentConfigError> {
|
||||||
|
if cache.anthropic_ok {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
health_check_anthropic(credentials)?;
|
||||||
|
cache.anthropic_ok = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_openai_ok(
|
||||||
|
cache: &mut HealthCheckCache,
|
||||||
|
credentials: &ProviderCredentials,
|
||||||
|
) -> Result<(), TestAgentConfigError> {
|
||||||
|
if cache.openai_ok {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
health_check_openai(credentials)?;
|
||||||
|
cache.openai_ok = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn health_check_anthropic(credentials: &ProviderCredentials) -> Result<(), TestAgentConfigError> {
|
||||||
|
let credentials = credentials.clone();
|
||||||
|
run_blocking_check("anthropic", move || {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: "anthropic".to_string(),
|
||||||
|
message: err.to_string(),
|
||||||
|
})?;
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
match credentials.auth_type {
|
||||||
|
AuthType::ApiKey => {
|
||||||
|
headers.insert(
|
||||||
|
"x-api-key",
|
||||||
|
HeaderValue::from_str(&credentials.api_key).map_err(|_| {
|
||||||
|
TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: "anthropic".to_string(),
|
||||||
|
message: "invalid anthropic api key header value".to_string(),
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AuthType::Oauth => {
|
||||||
|
let value = format!("Bearer {}", credentials.api_key);
|
||||||
|
headers.insert(
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_str(&value).map_err(|_| {
|
||||||
|
TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: "anthropic".to_string(),
|
||||||
|
message: "invalid anthropic oauth header value".to_string(),
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers.insert(
|
||||||
|
"anthropic-version",
|
||||||
|
HeaderValue::from_static(ANTHROPIC_VERSION),
|
||||||
|
);
|
||||||
|
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(ANTHROPIC_MODELS_URL)
|
||||||
|
.headers(headers)
|
||||||
|
.send()
|
||||||
|
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: "anthropic".to_string(),
|
||||||
|
message: err.to_string(),
|
||||||
|
})?;
|
||||||
|
handle_health_response("anthropic", response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn health_check_openai(credentials: &ProviderCredentials) -> Result<(), TestAgentConfigError> {
|
||||||
|
let credentials = credentials.clone();
|
||||||
|
run_blocking_check("openai", move || {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: "openai".to_string(),
|
||||||
|
message: err.to_string(),
|
||||||
|
})?;
|
||||||
|
let response = client
|
||||||
|
.get(OPENAI_MODELS_URL)
|
||||||
|
.bearer_auth(&credentials.api_key)
|
||||||
|
.send()
|
||||||
|
.map_err(|err| TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: "openai".to_string(),
|
||||||
|
message: err.to_string(),
|
||||||
|
})?;
|
||||||
|
handle_health_response("openai", response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_health_response(
|
||||||
|
provider: &str,
|
||||||
|
response: reqwest::blocking::Response,
|
||||||
|
) -> Result<(), TestAgentConfigError> {
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
|
||||||
|
return Err(TestAgentConfigError::InvalidCredentials {
|
||||||
|
provider: provider.to_string(),
|
||||||
|
status: status.as_u16(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let body = response.text().unwrap_or_default();
|
||||||
|
let mut summary = body.trim().to_string();
|
||||||
|
if summary.len() > 200 {
|
||||||
|
summary.truncate(200);
|
||||||
|
}
|
||||||
|
Err(TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: provider.to_string(),
|
||||||
|
message: format!("status {}: {}", status.as_u16(), summary),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_blocking_check<F>(
|
||||||
|
provider: &str,
|
||||||
|
check: F,
|
||||||
|
) -> Result<(), TestAgentConfigError>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<(), TestAgentConfigError> + Send + 'static,
|
||||||
|
{
|
||||||
|
std::thread::spawn(check).join().unwrap_or_else(|_| {
|
||||||
|
Err(TestAgentConfigError::HealthCheckFailed {
|
||||||
|
provider: provider.to_string(),
|
||||||
|
message: "health check panicked".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_system_agents() -> Vec<AgentId> {
|
||||||
|
let candidates = [AgentId::Claude, AgentId::Codex, AgentId::Opencode, AgentId::Amp];
|
||||||
|
let install_dir = default_install_dir();
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.filter(|agent| {
|
||||||
|
let binary = agent.binary_name();
|
||||||
|
find_in_path(binary) || install_dir.join(binary).exists()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_install_dir() -> PathBuf {
|
||||||
|
dirs::data_dir()
|
||||||
|
.map(|dir| dir.join("sandbox-agent").join("bin"))
|
||||||
|
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("bin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_in_path(binary_name: &str) -> bool {
|
||||||
|
let path_var = match env::var_os("PATH") {
|
||||||
|
Some(path) => path,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
for path in env::split_paths(&path_var) {
|
||||||
|
let candidate = path.join(binary_name);
|
||||||
|
if candidate.exists() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn read_env_key(name: &str) -> Option<String> {
|
fn read_env_key(name: &str) -> Option<String> {
|
||||||
env::var(name).ok().and_then(|value| {
|
env::var(name).ok().and_then(|value| {
|
||||||
let trimmed = value.trim().to_string();
|
let trimmed = value.trim().to_string();
|
||||||
|
|
@ -103,25 +328,11 @@ fn read_env_key(name: &str) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn credentials_with(
|
fn credentials_with(
|
||||||
anthropic_key: Option<String>,
|
anthropic_cred: Option<ProviderCredentials>,
|
||||||
openai_key: Option<String>,
|
openai_cred: Option<ProviderCredentials>,
|
||||||
) -> ExtractedCredentials {
|
) -> ExtractedCredentials {
|
||||||
let mut credentials = ExtractedCredentials::default();
|
let mut credentials = ExtractedCredentials::default();
|
||||||
if let Some(key) = anthropic_key {
|
credentials.anthropic = anthropic_cred;
|
||||||
credentials.anthropic = Some(ProviderCredentials {
|
credentials.openai = openai_cred;
|
||||||
api_key: key,
|
|
||||||
source: "sandbox-test-env".to_string(),
|
|
||||||
auth_type: AuthType::ApiKey,
|
|
||||||
provider: "anthropic".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(key) = openai_key {
|
|
||||||
credentials.openai = Some(ProviderCredentials {
|
|
||||||
api_key: key,
|
|
||||||
source: "sandbox-test-env".to_string(),
|
|
||||||
auth_type: AuthType::ApiKey,
|
|
||||||
provider: "openai".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
credentials
|
credentials
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "sandbox-agent-agent-schema"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
regress = "0.10"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
typify = "0.4"
|
|
||||||
serde_json = "1.0"
|
|
||||||
schemars = "0.8"
|
|
||||||
prettyplease = "0.2"
|
|
||||||
syn = "2.0"
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
//! Generated types from AI coding agent JSON schemas.
|
|
||||||
//!
|
|
||||||
//! This crate provides Rust types for:
|
|
||||||
//! - OpenCode SDK
|
|
||||||
//! - Claude Code SDK
|
|
||||||
//! - Codex SDK
|
|
||||||
//! - AMP Code SDK
|
|
||||||
|
|
||||||
pub mod opencode {
|
|
||||||
//! OpenCode SDK types extracted from OpenAPI 3.1.1 spec.
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/opencode.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod claude {
|
|
||||||
//! Claude Code SDK types extracted from TypeScript definitions.
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/claude.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod codex {
|
|
||||||
//! Codex SDK types.
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/codex.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod amp {
|
|
||||||
//! AMP Code SDK types.
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/amp.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_claude_bash_input() {
|
|
||||||
let input = claude::BashInput {
|
|
||||||
command: "ls -la".to_string(),
|
|
||||||
timeout: Some(5000.0),
|
|
||||||
description: Some("List files".to_string()),
|
|
||||||
run_in_background: None,
|
|
||||||
simulated_sed_edit: None,
|
|
||||||
dangerously_disable_sandbox: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&input).unwrap();
|
|
||||||
assert!(json.contains("ls -la"));
|
|
||||||
|
|
||||||
let parsed: claude::BashInput = serde_json::from_str(&json).unwrap();
|
|
||||||
assert_eq!(parsed.command, "ls -la");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_codex_thread_event() {
|
|
||||||
let event = codex::ThreadEvent {
|
|
||||||
type_: codex::ThreadEventType::ThreadCreated,
|
|
||||||
thread_id: Some("thread-123".to_string()),
|
|
||||||
item: None,
|
|
||||||
error: serde_json::Map::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(json.contains("thread.created"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_amp_message() {
|
|
||||||
let msg = amp::Message {
|
|
||||||
role: amp::MessageRole::User,
|
|
||||||
content: "Hello".to_string(),
|
|
||||||
tool_calls: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&msg).unwrap();
|
|
||||||
assert!(json.contains("user"));
|
|
||||||
assert!(json.contains("Hello"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,8 +6,8 @@ authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
thiserror = "1.0"
|
thiserror.workspace = true
|
||||||
schemars = "0.8"
|
schemars.workspace = true
|
||||||
utoipa = "4.2"
|
utoipa.workspace = true
|
||||||
|
|
|
||||||
19
server/packages/extracted-agent-schemas/Cargo.toml
Normal file
19
server/packages/extracted-agent-schemas/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "sandbox-agent-extracted-agent-schemas"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
regress.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
typify.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
prettyplease.workspace = true
|
||||||
|
syn.workspace = true
|
||||||
|
|
@ -4,7 +4,7 @@ use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
let schema_dir = Path::new("../../../resources/agent-schemas/dist");
|
let schema_dir = Path::new("../../../resources/agent-schemas/artifacts/json-schema");
|
||||||
|
|
||||||
let schemas = [
|
let schemas = [
|
||||||
("opencode", "opencode.json"),
|
("opencode", "opencode.json"),
|
||||||
111
server/packages/extracted-agent-schemas/src/lib.rs
Normal file
111
server/packages/extracted-agent-schemas/src/lib.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
//! Generated types from AI coding agent JSON schemas.
|
||||||
|
//!
|
||||||
|
//! This crate provides Rust types for:
|
||||||
|
//! - OpenCode SDK
|
||||||
|
//! - Claude Code SDK
|
||||||
|
//! - Codex SDK
|
||||||
|
//! - AMP Code SDK
|
||||||
|
|
||||||
|
pub mod opencode {
|
||||||
|
//! OpenCode SDK types extracted from OpenAPI 3.1.1 spec.
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/opencode.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod claude {
|
||||||
|
//! Claude Code SDK types extracted from TypeScript definitions.
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/claude.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod codex {
|
||||||
|
//! Codex SDK types.
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/codex.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod amp {
|
||||||
|
//! AMP Code SDK types.
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/amp.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_claude_bash_input() {
|
||||||
|
let input = claude::BashInput {
|
||||||
|
command: "ls -la".to_string(),
|
||||||
|
timeout: Some(5000.0),
|
||||||
|
working_directory: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&input).unwrap();
|
||||||
|
assert!(json.contains("ls -la"));
|
||||||
|
|
||||||
|
let parsed: claude::BashInput = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(parsed.command, "ls -la");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_codex_server_notification() {
|
||||||
|
// Test ItemCompletedNotification with AgentMessage
|
||||||
|
let notification = codex::ServerNotification::ItemCompleted(
|
||||||
|
codex::ItemCompletedNotification {
|
||||||
|
item: codex::ThreadItem::AgentMessage {
|
||||||
|
id: "msg-123".to_string(),
|
||||||
|
text: "Hello from Codex".to_string(),
|
||||||
|
},
|
||||||
|
thread_id: "thread-123".to_string(),
|
||||||
|
turn_id: "turn-456".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::to_string(¬ification).unwrap();
|
||||||
|
assert!(json.contains("item/completed"));
|
||||||
|
assert!(json.contains("Hello from Codex"));
|
||||||
|
assert!(json.contains("agentMessage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_codex_thread_item_variants() {
|
||||||
|
// Test UserMessage variant
|
||||||
|
let user_msg = codex::ThreadItem::UserMessage {
|
||||||
|
content: vec![codex::UserInput::Text {
|
||||||
|
text: "Hello".to_string(),
|
||||||
|
text_elements: vec![],
|
||||||
|
}],
|
||||||
|
id: "user-1".to_string(),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&user_msg).unwrap();
|
||||||
|
assert!(json.contains("userMessage"));
|
||||||
|
assert!(json.contains("Hello"));
|
||||||
|
|
||||||
|
// Test CommandExecution variant
|
||||||
|
let cmd = codex::ThreadItem::CommandExecution {
|
||||||
|
aggregated_output: Some("output".to_string()),
|
||||||
|
command: "ls -la".to_string(),
|
||||||
|
command_actions: vec![],
|
||||||
|
cwd: "/tmp".to_string(),
|
||||||
|
duration_ms: Some(100),
|
||||||
|
exit_code: Some(0),
|
||||||
|
id: "cmd-1".to_string(),
|
||||||
|
process_id: None,
|
||||||
|
status: codex::CommandExecutionStatus::Completed,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&cmd).unwrap();
|
||||||
|
assert!(json.contains("commandExecution"));
|
||||||
|
assert!(json.contains("ls -la"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_amp_message() {
|
||||||
|
let msg = amp::Message {
|
||||||
|
role: amp::MessageRole::User,
|
||||||
|
content: "Hello".to_string(),
|
||||||
|
tool_calls: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
assert!(json.contains("user"));
|
||||||
|
assert!(json.contains("Hello"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,11 +7,11 @@ license.workspace = true
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
tracing-logfmt = "0.3"
|
tracing-logfmt.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
sandbox-agent-core = { path = "../sandbox-agent" }
|
sandbox-agent-core.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
utoipa = "4.2"
|
utoipa.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -10,30 +10,30 @@ name = "sandbox-agent"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0"
|
sandbox-agent-error.workspace = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
sandbox-agent-agent-management.workspace = true
|
||||||
serde_json = "1.0"
|
sandbox-agent-agent-credentials.workspace = true
|
||||||
axum = "0.7"
|
sandbox-agent-universal-agent-schema.workspace = true
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
thiserror.workspace = true
|
||||||
futures = "0.3"
|
serde.workspace = true
|
||||||
sandbox-agent-error = { path = "../error" }
|
serde_json.workspace = true
|
||||||
sandbox-agent-agent-management = { path = "../agent-management" }
|
axum.workspace = true
|
||||||
sandbox-agent-agent-credentials = { path = "../agent-credentials" }
|
clap.workspace = true
|
||||||
sandbox-agent-universal-agent-schema = { path = "../universal-agent-schema" }
|
futures.workspace = true
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] }
|
reqwest.workspace = true
|
||||||
dirs = "5.0"
|
dirs.workspace = true
|
||||||
time = { version = "0.3", features = ["parsing", "formatting"] }
|
time.workspace = true
|
||||||
tokio = { version = "1.36", features = ["macros", "rt-multi-thread", "signal", "time"] }
|
tokio.workspace = true
|
||||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
tokio-stream.workspace = true
|
||||||
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
tower-http.workspace = true
|
||||||
utoipa = { version = "4.2", features = ["axum_extras"] }
|
utoipa.workspace = true
|
||||||
schemars = "0.8"
|
schemars.workspace = true
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
tracing-logfmt = "0.3"
|
tracing-logfmt.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
http-body-util = "0.1"
|
http-body-util.workspace = true
|
||||||
insta = "1.41"
|
insta.workspace = true
|
||||||
tempfile = "3.10"
|
tempfile.workspace = true
|
||||||
tower = "0.4"
|
tower.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1202,6 +1202,11 @@ async fn require_token(
|
||||||
req: Request<axum::body::Body>,
|
req: Request<axum::body::Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
|
let path = req.uri().path();
|
||||||
|
if path == "/v1/health" || path == "/health" {
|
||||||
|
return Ok(next.run(req).await);
|
||||||
|
}
|
||||||
|
|
||||||
let expected = match &state.auth.token {
|
let expected = match &state.auth.token {
|
||||||
Some(token) => token.as_str(),
|
Some(token) => token.as_str(),
|
||||||
None => return Ok(next.run(req).await),
|
None => return Ok(next.run(req).await),
|
||||||
|
|
@ -1946,7 +1951,7 @@ fn parse_agent_line(agent: AgentId, line: &str, session_id: &str) -> Option<Even
|
||||||
convert_claude::event_to_universal_with_session(&value, session_id.to_string())
|
convert_claude::event_to_universal_with_session(&value, session_id.to_string())
|
||||||
}
|
}
|
||||||
AgentId::Codex => match serde_json::from_value(value.clone()) {
|
AgentId::Codex => match serde_json::from_value(value.clone()) {
|
||||||
Ok(event) => convert_codex::event_to_universal(&event),
|
Ok(notification) => convert_codex::notification_to_universal(¬ification),
|
||||||
Err(err) => EventConversion::new(unparsed_message(
|
Err(err) => EventConversion::new(unparsed_message(
|
||||||
&value.to_string(),
|
&value.to_string(),
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::{Body, Bytes};
|
||||||
use axum::http::{Method, Request, StatusCode};
|
use axum::http::{header, HeaderMap, HeaderValue, Method, Request, StatusCode};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
|
|
@ -13,9 +13,13 @@ use sandbox_agent_agent_management::agents::{AgentId, AgentManager};
|
||||||
use sandbox_agent_agent_management::testing::{test_agents_from_env, TestAgentConfig};
|
use sandbox_agent_agent_management::testing::{test_agents_from_env, TestAgentConfig};
|
||||||
use sandbox_agent_agent_credentials::ExtractedCredentials;
|
use sandbox_agent_agent_credentials::ExtractedCredentials;
|
||||||
use sandbox_agent_core::router::{build_router, AppState, AuthConfig};
|
use sandbox_agent_core::router::{build_router, AppState, AuthConfig};
|
||||||
use tower::ServiceExt;
|
use tower::util::ServiceExt;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
const PROMPT: &str = "Reply with exactly the single word OK.";
|
const PROMPT: &str = "Reply with exactly the single word OK.";
|
||||||
|
const PERMISSION_PROMPT: &str = "List files in the current directory using available tools.";
|
||||||
|
const QUESTION_PROMPT: &str =
|
||||||
|
"Ask the user a multiple-choice question with options yes/no using any built-in AskUserQuestion tool, then wait.";
|
||||||
|
|
||||||
struct TestApp {
|
struct TestApp {
|
||||||
app: Router,
|
app: Router,
|
||||||
|
|
@ -24,11 +28,22 @@ struct TestApp {
|
||||||
|
|
||||||
impl TestApp {
|
impl TestApp {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
Self::new_with_auth(AuthConfig::disabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_with_auth(auth: AuthConfig) -> Self {
|
||||||
|
Self::new_with_auth_and_cors(auth, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_with_auth_and_cors(auth: AuthConfig, cors: Option<CorsLayer>) -> Self {
|
||||||
let install_dir = tempfile::tempdir().expect("create temp install dir");
|
let install_dir = tempfile::tempdir().expect("create temp install dir");
|
||||||
let manager = AgentManager::new(install_dir.path())
|
let manager = AgentManager::new(install_dir.path())
|
||||||
.expect("create agent manager");
|
.expect("create agent manager");
|
||||||
let state = AppState::new(AuthConfig::disabled(), manager);
|
let state = AppState::new(auth, manager);
|
||||||
let app = build_router(state);
|
let mut app = build_router(state);
|
||||||
|
if let Some(cors) = cors {
|
||||||
|
app = app.layer(cors);
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
app,
|
app,
|
||||||
_install_dir: install_dir,
|
_install_dir: install_dir,
|
||||||
|
|
@ -112,6 +127,37 @@ async fn send_json(app: &Router, method: Method, path: &str, body: Option<Value>
|
||||||
(status, value)
|
(status, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_request(app: &Router, request: Request<Body>) -> (StatusCode, HeaderMap, Bytes) {
|
||||||
|
let response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(request)
|
||||||
|
.await
|
||||||
|
.expect("request handled");
|
||||||
|
let status = response.status();
|
||||||
|
let headers = response.headers().clone();
|
||||||
|
let bytes = response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("read body")
|
||||||
|
.to_bytes();
|
||||||
|
(status, headers, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_json_request(
|
||||||
|
app: &Router,
|
||||||
|
request: Request<Body>,
|
||||||
|
) -> (StatusCode, HeaderMap, Value) {
|
||||||
|
let (status, headers, bytes) = send_request(app, request).await;
|
||||||
|
let value = if bytes.is_empty() {
|
||||||
|
Value::Null
|
||||||
|
} else {
|
||||||
|
serde_json::from_slice(&bytes)
|
||||||
|
.unwrap_or(Value::String(String::from_utf8_lossy(&bytes).to_string()))
|
||||||
|
};
|
||||||
|
(status, headers, value)
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_status(app: &Router, method: Method, path: &str, body: Option<Value>) -> StatusCode {
|
async fn send_status(app: &Router, method: Method, path: &str, body: Option<Value>) -> StatusCode {
|
||||||
let (status, _) = send_json(app, method, path, body).await;
|
let (status, _) = send_json(app, method, path, body).await;
|
||||||
status
|
status
|
||||||
|
|
@ -128,14 +174,14 @@ async fn install_agent(app: &Router, agent: AgentId) {
|
||||||
assert_eq!(status, StatusCode::NO_CONTENT, "install {agent}");
|
assert_eq!(status, StatusCode::NO_CONTENT, "install {agent}");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_session(app: &Router, agent: AgentId, session_id: &str) {
|
async fn create_session(app: &Router, agent: AgentId, session_id: &str, permission_mode: &str) {
|
||||||
let status = send_status(
|
let status = send_status(
|
||||||
app,
|
app,
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("/v1/sessions/{session_id}"),
|
&format!("/v1/sessions/{session_id}"),
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"agent": agent.as_str(),
|
"agent": agent.as_str(),
|
||||||
"permissionMode": "bypass"
|
"permissionMode": permission_mode
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -211,7 +257,7 @@ async fn read_sse_events(
|
||||||
_ => break,
|
_ => break,
|
||||||
};
|
};
|
||||||
let next = tokio::time::timeout(remaining, stream.next()).await;
|
let next = tokio::time::timeout(remaining, stream.next()).await;
|
||||||
let chunk = match next {
|
let chunk: Bytes = match next {
|
||||||
Ok(Some(Ok(chunk))) => chunk,
|
Ok(Some(Ok(chunk))) => chunk,
|
||||||
Ok(Some(Err(_))) => break,
|
Ok(Some(Err(_))) => break,
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
|
|
@ -267,6 +313,23 @@ fn is_error_event(event: &Value) -> bool {
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_permission_event(event: &Value) -> bool {
|
||||||
|
event
|
||||||
|
.get("data")
|
||||||
|
.and_then(|data| data.get("permissionAsked"))
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_permission_events(events: &[Value]) -> Vec<Value> {
|
||||||
|
if let Some(idx) = events.iter().position(is_permission_event) {
|
||||||
|
return events[..=idx].to_vec();
|
||||||
|
}
|
||||||
|
if let Some(idx) = events.iter().position(is_assistant_message) {
|
||||||
|
return events[..=idx].to_vec();
|
||||||
|
}
|
||||||
|
events.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_events(events: &[Value]) -> Value {
|
fn normalize_events(events: &[Value]) -> Value {
|
||||||
let normalized = events
|
let normalized = events
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -276,6 +339,16 @@ fn normalize_events(events: &[Value]) -> Value {
|
||||||
Value::Array(normalized)
|
Value::Array(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn truncate_after_first_stop(events: &[Value]) -> Vec<Value> {
|
||||||
|
if let Some(idx) = events
|
||||||
|
.iter()
|
||||||
|
.position(|event| is_assistant_message(event) || is_error_event(event))
|
||||||
|
{
|
||||||
|
return events[..=idx].to_vec();
|
||||||
|
}
|
||||||
|
events.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_event(event: &Value, seq: usize) -> Value {
|
fn normalize_event(event: &Value, seq: usize) -> Value {
|
||||||
let mut map = Map::new();
|
let mut map = Map::new();
|
||||||
map.insert("seq".to_string(), Value::Number(seq.into()));
|
map.insert("seq".to_string(), Value::Number(seq.into()));
|
||||||
|
|
@ -379,8 +452,239 @@ fn normalize_permission(permission: &Value) -> Value {
|
||||||
Value::Object(map)
|
Value::Object(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_name(prefix: &str, agent: AgentId) -> String {
|
fn normalize_agent_list(value: &Value) -> Value {
|
||||||
format!("{prefix}_{}", agent.as_str())
|
let agents = value
|
||||||
|
.get("agents")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
for agent in agents {
|
||||||
|
let mut map = Map::new();
|
||||||
|
if let Some(id) = agent.get("id").and_then(Value::as_str) {
|
||||||
|
map.insert("id".to_string(), Value::String(id.to_string()));
|
||||||
|
}
|
||||||
|
// Skip installed/version/path fields - they depend on local environment
|
||||||
|
// and make snapshots non-deterministic
|
||||||
|
normalized.push(Value::Object(map));
|
||||||
|
}
|
||||||
|
normalized.sort_by(|a, b| {
|
||||||
|
a.get("id")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.cmp(&b.get("id").and_then(Value::as_str))
|
||||||
|
});
|
||||||
|
json!({ "agents": normalized })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_agent_modes(value: &Value) -> Value {
|
||||||
|
let modes = value
|
||||||
|
.get("modes")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
for mode in modes {
|
||||||
|
let mut map = Map::new();
|
||||||
|
if let Some(id) = mode.get("id").and_then(Value::as_str) {
|
||||||
|
map.insert("id".to_string(), Value::String(id.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(name) = mode.get("name").and_then(Value::as_str) {
|
||||||
|
map.insert("name".to_string(), Value::String(name.to_string()));
|
||||||
|
}
|
||||||
|
if mode.get("description").is_some() {
|
||||||
|
map.insert("description".to_string(), Value::Bool(true));
|
||||||
|
}
|
||||||
|
normalized.push(Value::Object(map));
|
||||||
|
}
|
||||||
|
normalized.sort_by(|a, b| {
|
||||||
|
a.get("id")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.cmp(&b.get("id").and_then(Value::as_str))
|
||||||
|
});
|
||||||
|
json!({ "modes": normalized })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_sessions(value: &Value) -> Value {
|
||||||
|
let sessions = value
|
||||||
|
.get("sessions")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut normalized = Vec::new();
|
||||||
|
for session in sessions {
|
||||||
|
let mut map = Map::new();
|
||||||
|
if let Some(session_id) = session.get("sessionId").and_then(Value::as_str) {
|
||||||
|
map.insert("sessionId".to_string(), Value::String(session_id.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(agent) = session.get("agent").and_then(Value::as_str) {
|
||||||
|
map.insert("agent".to_string(), Value::String(agent.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(agent_mode) = session.get("agentMode").and_then(Value::as_str) {
|
||||||
|
map.insert("agentMode".to_string(), Value::String(agent_mode.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(permission_mode) = session.get("permissionMode").and_then(Value::as_str) {
|
||||||
|
map.insert("permissionMode".to_string(), Value::String(permission_mode.to_string()));
|
||||||
|
}
|
||||||
|
if session.get("model").is_some() {
|
||||||
|
map.insert("model".to_string(), Value::String("<redacted>".to_string()));
|
||||||
|
}
|
||||||
|
if session.get("variant").is_some() {
|
||||||
|
map.insert("variant".to_string(), Value::String("<redacted>".to_string()));
|
||||||
|
}
|
||||||
|
if session.get("agentSessionId").is_some() {
|
||||||
|
map.insert("agentSessionId".to_string(), Value::String("<redacted>".to_string()));
|
||||||
|
}
|
||||||
|
if let Some(ended) = session.get("ended").and_then(Value::as_bool) {
|
||||||
|
map.insert("ended".to_string(), Value::Bool(ended));
|
||||||
|
}
|
||||||
|
if session.get("eventCount").is_some() {
|
||||||
|
map.insert("eventCount".to_string(), Value::String("<redacted>".to_string()));
|
||||||
|
}
|
||||||
|
normalized.push(Value::Object(map));
|
||||||
|
}
|
||||||
|
normalized.sort_by(|a, b| {
|
||||||
|
a.get("sessionId")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.cmp(&b.get("sessionId").and_then(Value::as_str))
|
||||||
|
});
|
||||||
|
json!({ "sessions": normalized })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_create_session(value: &Value) -> Value {
|
||||||
|
let mut map = Map::new();
|
||||||
|
if let Some(healthy) = value.get("healthy").and_then(Value::as_bool) {
|
||||||
|
map.insert("healthy".to_string(), Value::Bool(healthy));
|
||||||
|
}
|
||||||
|
if value.get("agentSessionId").is_some() {
|
||||||
|
map.insert("agentSessionId".to_string(), Value::String("<redacted>".to_string()));
|
||||||
|
}
|
||||||
|
if let Some(error) = value.get("error") {
|
||||||
|
map.insert("error".to_string(), error.clone());
|
||||||
|
}
|
||||||
|
Value::Object(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_health(value: &Value) -> Value {
|
||||||
|
let mut map = Map::new();
|
||||||
|
if let Some(status) = value.get("status").and_then(Value::as_str) {
|
||||||
|
map.insert("status".to_string(), Value::String(status.to_string()));
|
||||||
|
}
|
||||||
|
Value::Object(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_status(status: StatusCode) -> Value {
|
||||||
|
json!({ "status": status.as_u16() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_cors(status: StatusCode, headers: &HeaderMap) -> Value {
|
||||||
|
let mut map = Map::new();
|
||||||
|
map.insert("status".to_string(), Value::Number(status.as_u16().into()));
|
||||||
|
for name in [
|
||||||
|
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
|
header::ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
|
header::ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
|
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||||
|
header::VARY,
|
||||||
|
] {
|
||||||
|
if let Some(value) = headers.get(&name) {
|
||||||
|
map.insert(
|
||||||
|
name.as_str().to_string(),
|
||||||
|
Value::String(value.to_str().unwrap_or("<invalid>").to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_name(prefix: &str, agent: Option<AgentId>) -> String {
|
||||||
|
match agent {
|
||||||
|
Some(agent) => format!("{prefix}_{}", agent.as_str()),
|
||||||
|
None => format!("{prefix}_global"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn poll_events_until_match<F>(
|
||||||
|
app: &Router,
|
||||||
|
session_id: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
stop: F,
|
||||||
|
) -> Vec<Value>
|
||||||
|
where
|
||||||
|
F: Fn(&[Value]) -> bool,
|
||||||
|
{
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut offset = 0u64;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
while start.elapsed() < timeout {
|
||||||
|
let path = format!("/v1/sessions/{session_id}/events?offset={offset}&limit=200");
|
||||||
|
let (status, payload) = send_json(app, Method::GET, &path, None).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "poll events");
|
||||||
|
let new_events = payload
|
||||||
|
.get("events")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
if !new_events.is_empty() {
|
||||||
|
if let Some(last) = new_events
|
||||||
|
.last()
|
||||||
|
.and_then(|event| event.get("id"))
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
{
|
||||||
|
offset = last;
|
||||||
|
}
|
||||||
|
events.extend(new_events);
|
||||||
|
if stop(&events) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(800)).await;
|
||||||
|
}
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_permission_id(events: &[Value]) -> Option<String> {
|
||||||
|
events
|
||||||
|
.iter()
|
||||||
|
.find_map(|event| {
|
||||||
|
event
|
||||||
|
.get("data")
|
||||||
|
.and_then(|data| data.get("permissionAsked"))
|
||||||
|
.and_then(|permission| permission.get("id"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_question_id_and_answers(events: &[Value]) -> Option<(String, Vec<Vec<String>>)> {
|
||||||
|
let question = events.iter().find_map(|event| {
|
||||||
|
event
|
||||||
|
.get("data")
|
||||||
|
.and_then(|data| data.get("questionAsked"))
|
||||||
|
.cloned()
|
||||||
|
})?;
|
||||||
|
let id = question.get("id").and_then(Value::as_str)?.to_string();
|
||||||
|
let questions = question
|
||||||
|
.get("questions")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut answers = Vec::new();
|
||||||
|
for question in questions {
|
||||||
|
let option = question
|
||||||
|
.get("options")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.and_then(|options| options.first())
|
||||||
|
.and_then(|option| option.get("label"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|label| label.to_string());
|
||||||
|
if let Some(label) = option {
|
||||||
|
answers.push(vec![label]);
|
||||||
|
} else {
|
||||||
|
answers.push(Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((id, answers))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
|
|
@ -388,10 +692,11 @@ async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
install_agent(app, config.agent).await;
|
install_agent(app, config.agent).await;
|
||||||
|
|
||||||
let session_id = format!("session-{}", config.agent.as_str());
|
let session_id = format!("session-{}", config.agent.as_str());
|
||||||
create_session(app, config.agent, &session_id).await;
|
create_session(app, config.agent, &session_id, "bypass").await;
|
||||||
send_message(app, &session_id).await;
|
send_message(app, &session_id).await;
|
||||||
|
|
||||||
let events = poll_events_until(app, &session_id, Duration::from_secs(120)).await;
|
let events = poll_events_until(app, &session_id, Duration::from_secs(120)).await;
|
||||||
|
let events = truncate_after_first_stop(&events);
|
||||||
assert!(
|
assert!(
|
||||||
!events.is_empty(),
|
!events.is_empty(),
|
||||||
"no events collected for {}",
|
"no events collected for {}",
|
||||||
|
|
@ -404,7 +709,7 @@ async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
);
|
);
|
||||||
let normalized = normalize_events(&events);
|
let normalized = normalize_events(&events);
|
||||||
insta::with_settings!({
|
insta::with_settings!({
|
||||||
snapshot_suffix => snapshot_name("http_events", config.agent),
|
snapshot_suffix => snapshot_name("http_events", Some(config.agent)),
|
||||||
}, {
|
}, {
|
||||||
insta::assert_yaml_snapshot!(normalized);
|
insta::assert_yaml_snapshot!(normalized);
|
||||||
});
|
});
|
||||||
|
|
@ -415,7 +720,7 @@ async fn run_sse_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
install_agent(app, config.agent).await;
|
install_agent(app, config.agent).await;
|
||||||
|
|
||||||
let session_id = format!("sse-{}", config.agent.as_str());
|
let session_id = format!("sse-{}", config.agent.as_str());
|
||||||
create_session(app, config.agent, &session_id).await;
|
create_session(app, config.agent, &session_id, "bypass").await;
|
||||||
|
|
||||||
let sse_task = {
|
let sse_task = {
|
||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
|
|
@ -428,6 +733,7 @@ async fn run_sse_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
send_message(app, &session_id).await;
|
send_message(app, &session_id).await;
|
||||||
|
|
||||||
let events = sse_task.await.expect("sse task");
|
let events = sse_task.await.expect("sse task");
|
||||||
|
let events = truncate_after_first_stop(&events);
|
||||||
assert!(
|
assert!(
|
||||||
!events.is_empty(),
|
!events.is_empty(),
|
||||||
"no sse events collected for {}",
|
"no sse events collected for {}",
|
||||||
|
|
@ -440,26 +746,494 @@ async fn run_sse_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
);
|
);
|
||||||
let normalized = normalize_events(&events);
|
let normalized = normalize_events(&events);
|
||||||
insta::with_settings!({
|
insta::with_settings!({
|
||||||
snapshot_suffix => snapshot_name("sse_events", config.agent),
|
snapshot_suffix => snapshot_name("sse_events", Some(config.agent)),
|
||||||
}, {
|
}, {
|
||||||
insta::assert_yaml_snapshot!(normalized);
|
insta::assert_yaml_snapshot!(normalized);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn auth_snapshots() {
|
||||||
|
let token = "test-token";
|
||||||
|
let app = TestApp::new_with_auth(AuthConfig::with_token(token.to_string()));
|
||||||
|
|
||||||
|
let (status, payload) = send_json(&app.app, Method::GET, "/v1/health", None).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "health should be public");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("auth_health_public", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": normalize_health(&payload),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
let (status, payload) = send_json(&app.app, Method::GET, "/v1/agents", None).await;
|
||||||
|
assert_eq!(status, StatusCode::UNAUTHORIZED, "missing token should 401");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("auth_missing_token", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": payload,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri("/v1/agents")
|
||||||
|
.header(header::AUTHORIZATION, "Bearer wrong-token")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("auth invalid request");
|
||||||
|
let (status, _headers, payload) = send_json_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::UNAUTHORIZED, "invalid token should 401");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("auth_invalid_token", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": payload,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri("/v1/agents")
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("auth valid request");
|
||||||
|
let (status, _headers, payload) = send_json_request(&app.app, request).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "valid token should allow request");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("auth_valid_token", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": normalize_agent_list(&payload),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn cors_snapshots() {
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_origin(vec![HeaderValue::from_static("http://example.com")])
|
||||||
|
.allow_methods([Method::GET, Method::POST])
|
||||||
|
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
|
||||||
|
.allow_credentials(true);
|
||||||
|
let app = TestApp::new_with_auth_and_cors(AuthConfig::disabled(), Some(cors));
|
||||||
|
|
||||||
|
let preflight = Request::builder()
|
||||||
|
.method(Method::OPTIONS)
|
||||||
|
.uri("/v1/health")
|
||||||
|
.header(header::ORIGIN, "http://example.com")
|
||||||
|
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||||
|
.header(
|
||||||
|
header::ACCESS_CONTROL_REQUEST_HEADERS,
|
||||||
|
"authorization,content-type",
|
||||||
|
)
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("cors preflight request");
|
||||||
|
let (status, headers, _payload) = send_request(&app.app, preflight).await;
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("cors_preflight", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot_cors(status, &headers));
|
||||||
|
});
|
||||||
|
|
||||||
|
let actual = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri("/v1/health")
|
||||||
|
.header(header::ORIGIN, "http://example.com")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("cors actual request");
|
||||||
|
let (status, headers, payload) = send_json_request(&app.app, actual).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "cors actual request should succeed");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("cors_actual", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"cors": snapshot_cors(status, &headers),
|
||||||
|
"payload": normalize_health(&payload),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn api_endpoints_snapshots() {
|
||||||
|
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
let (status, health) = send_json(&app.app, Method::GET, "/v1/health", None).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "health status");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("health", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_health(&health));
|
||||||
|
});
|
||||||
|
|
||||||
|
// List agents (just verify the API returns correct agent IDs, not install state)
|
||||||
|
let (status, agents) = send_json(&app.app, Method::GET, "/v1/agents", None).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "agents list");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("agents_list", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_agent_list(&agents));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install agents (ensure they're available for subsequent tests)
|
||||||
|
for config in &configs {
|
||||||
|
let _guard = apply_credentials(&config.credentials);
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/agents/{}/install", config.agent.as_str()),
|
||||||
|
Some(json!({})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "install agent");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("agent_install", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut session_ids = Vec::new();
|
||||||
|
for config in &configs {
|
||||||
|
let _guard = apply_credentials(&config.credentials);
|
||||||
|
let (status, modes) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::GET,
|
||||||
|
&format!("/v1/agents/{}/modes", config.agent.as_str()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "agent modes");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("agent_modes", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_agent_modes(&modes));
|
||||||
|
});
|
||||||
|
|
||||||
|
let session_id = format!("snapshot-{}", config.agent.as_str());
|
||||||
|
let (status, created) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/sessions/{session_id}"),
|
||||||
|
Some(json!({
|
||||||
|
"agent": config.agent.as_str(),
|
||||||
|
"permissionMode": "bypass"
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "create session");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("create_session", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_create_session(&created));
|
||||||
|
});
|
||||||
|
session_ids.push((config.agent, session_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (status, sessions) = send_json(&app.app, Method::GET, "/v1/sessions", None).await;
|
||||||
|
assert_eq!(status, StatusCode::OK, "list sessions");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("sessions_list", None),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_sessions(&sessions));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (agent, session_id) in &session_ids {
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/sessions/{session_id}/messages"),
|
||||||
|
Some(json!({ "message": PROMPT })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "send message");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("send_message", Some(*agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn approval_flow_snapshots() {
|
||||||
|
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||||
|
let app = TestApp::new();
|
||||||
|
|
||||||
|
for config in &configs {
|
||||||
|
let _guard = apply_credentials(&config.credentials);
|
||||||
|
install_agent(&app.app, config.agent).await;
|
||||||
|
|
||||||
|
let permission_session = format!("perm-{}", config.agent.as_str());
|
||||||
|
create_session(&app.app, config.agent, &permission_session, "plan").await;
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/sessions/{permission_session}/messages"),
|
||||||
|
Some(json!({ "message": PERMISSION_PROMPT })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "send permission prompt");
|
||||||
|
|
||||||
|
let permission_events = poll_events_until_match(
|
||||||
|
&app.app,
|
||||||
|
&permission_session,
|
||||||
|
Duration::from_secs(120),
|
||||||
|
|events| find_permission_id(events).is_some() || should_stop(events),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let permission_events = truncate_permission_events(&permission_events);
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("permission_events", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_events(&permission_events));
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(permission_id) = find_permission_id(&permission_events) {
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!(
|
||||||
|
"/v1/sessions/{permission_session}/permissions/{permission_id}/reply"
|
||||||
|
),
|
||||||
|
Some(json!({ "reply": "once" })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "reply permission");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("permission_reply", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let (status, payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!(
|
||||||
|
"/v1/sessions/{permission_session}/permissions/missing-permission/reply"
|
||||||
|
),
|
||||||
|
Some(json!({ "reply": "once" })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(!status.is_success(), "missing permission id should error");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("permission_reply_missing", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": payload,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let question_reply_session = format!("question-reply-{}", config.agent.as_str());
|
||||||
|
create_session(&app.app, config.agent, &question_reply_session, "bypass").await;
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/sessions/{question_reply_session}/messages"),
|
||||||
|
Some(json!({ "message": QUESTION_PROMPT })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "send question prompt");
|
||||||
|
|
||||||
|
let question_events = poll_events_until_match(
|
||||||
|
&app.app,
|
||||||
|
&question_reply_session,
|
||||||
|
Duration::from_secs(120),
|
||||||
|
|events| find_question_id_and_answers(events).is_some() || should_stop(events),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("question_reply_events", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_events(&question_events));
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((question_id, answers)) = find_question_id_and_answers(&question_events) {
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!(
|
||||||
|
"/v1/sessions/{question_reply_session}/questions/{question_id}/reply"
|
||||||
|
),
|
||||||
|
Some(json!({ "answers": answers })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "reply question");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("question_reply", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let (status, payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!(
|
||||||
|
"/v1/sessions/{question_reply_session}/questions/missing-question/reply"
|
||||||
|
),
|
||||||
|
Some(json!({ "answers": [] })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(!status.is_success(), "missing question id should error");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("question_reply_missing", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": payload,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let question_reject_session = format!("question-reject-{}", config.agent.as_str());
|
||||||
|
create_session(&app.app, config.agent, &question_reject_session, "bypass").await;
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/sessions/{question_reject_session}/messages"),
|
||||||
|
Some(json!({ "message": QUESTION_PROMPT })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "send question prompt reject");
|
||||||
|
|
||||||
|
let reject_events = poll_events_until_match(
|
||||||
|
&app.app,
|
||||||
|
&question_reject_session,
|
||||||
|
Duration::from_secs(120),
|
||||||
|
|events| find_question_id_and_answers(events).is_some() || should_stop(events),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("question_reject_events", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(normalize_events(&reject_events));
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((question_id, _)) = find_question_id_and_answers(&reject_events) {
|
||||||
|
let status = send_status(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!(
|
||||||
|
"/v1/sessions/{question_reject_session}/questions/{question_id}/reject"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::NO_CONTENT, "reject question");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("question_reject", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let (status, payload) = send_json(
|
||||||
|
&app.app,
|
||||||
|
Method::POST,
|
||||||
|
&format!(
|
||||||
|
"/v1/sessions/{question_reject_session}/questions/missing-question/reject"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(!status.is_success(), "missing question id reject should error");
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("question_reject_missing", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(json!({
|
||||||
|
"status": status.as_u16(),
|
||||||
|
"payload": payload,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn http_events_snapshots() {
|
async fn http_events_snapshots() {
|
||||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS");
|
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||||
let app = TestApp::new();
|
let app = TestApp::new();
|
||||||
for config in &configs {
|
for config in &configs {
|
||||||
run_http_events_snapshot(&app.app, config).await;
|
run_http_events_snapshot(&app.app, config).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_concurrency_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||||
|
let _guard = apply_credentials(&config.credentials);
|
||||||
|
install_agent(app, config.agent).await;
|
||||||
|
|
||||||
|
let session_a = format!("concurrent-a-{}", config.agent.as_str());
|
||||||
|
let session_b = format!("concurrent-b-{}", config.agent.as_str());
|
||||||
|
create_session(app, config.agent, &session_a, "bypass").await;
|
||||||
|
create_session(app, config.agent, &session_b, "bypass").await;
|
||||||
|
|
||||||
|
let app_a = app.clone();
|
||||||
|
let app_b = app.clone();
|
||||||
|
let send_a = send_message(&app_a, &session_a);
|
||||||
|
let send_b = send_message(&app_b, &session_b);
|
||||||
|
tokio::join!(send_a, send_b);
|
||||||
|
|
||||||
|
let app_a = app.clone();
|
||||||
|
let app_b = app.clone();
|
||||||
|
let poll_a = poll_events_until(&app_a, &session_a, Duration::from_secs(120));
|
||||||
|
let poll_b = poll_events_until(&app_b, &session_b, Duration::from_secs(120));
|
||||||
|
let (events_a, events_b) = tokio::join!(poll_a, poll_b);
|
||||||
|
let events_a = truncate_after_first_stop(&events_a);
|
||||||
|
let events_b = truncate_after_first_stop(&events_b);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!events_a.is_empty(),
|
||||||
|
"no events collected for concurrent session a {}",
|
||||||
|
config.agent
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!events_b.is_empty(),
|
||||||
|
"no events collected for concurrent session b {}",
|
||||||
|
config.agent
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
should_stop(&events_a),
|
||||||
|
"timed out waiting for assistant/error event for concurrent session a {}",
|
||||||
|
config.agent
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
should_stop(&events_b),
|
||||||
|
"timed out waiting for assistant/error event for concurrent session b {}",
|
||||||
|
config.agent
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = json!({
|
||||||
|
"session_a": normalize_events(&events_a),
|
||||||
|
"session_b": normalize_events(&events_b),
|
||||||
|
});
|
||||||
|
insta::with_settings!({
|
||||||
|
snapshot_suffix => snapshot_name("concurrency_events", Some(config.agent)),
|
||||||
|
}, {
|
||||||
|
insta::assert_yaml_snapshot!(snapshot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn sse_events_snapshots() {
|
async fn sse_events_snapshots() {
|
||||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS");
|
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||||
let app = TestApp::new();
|
let app = TestApp::new();
|
||||||
for config in &configs {
|
for config in &configs {
|
||||||
run_sse_events_snapshot(&app.app, config).await;
|
run_sse_events_snapshot(&app.app, config).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn concurrency_snapshots() {
|
||||||
|
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||||
|
let app = TestApp::new();
|
||||||
|
for config in &configs {
|
||||||
|
run_concurrency_snapshot(&app.app, config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 874
|
||||||
|
expression: snapshot_status(status)
|
||||||
|
---
|
||||||
|
status: 204
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 900
|
||||||
|
expression: normalize_agent_modes(&modes)
|
||||||
|
---
|
||||||
|
modes:
|
||||||
|
- description: true
|
||||||
|
id: build
|
||||||
|
name: Build
|
||||||
|
- description: true
|
||||||
|
id: plan
|
||||||
|
name: Plan
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 881
|
||||||
|
expression: normalize_agent_list(&agents)
|
||||||
|
---
|
||||||
|
agents:
|
||||||
|
- id: amp
|
||||||
|
- id: claude
|
||||||
|
- id: codex
|
||||||
|
- id: opencode
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 918
|
||||||
|
expression: normalize_create_session(&created)
|
||||||
|
---
|
||||||
|
healthy: true
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 850
|
||||||
|
expression: normalize_health(&health)
|
||||||
|
---
|
||||||
|
status: ok
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 943
|
||||||
|
expression: snapshot_status(status)
|
||||||
|
---
|
||||||
|
status: 204
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 928
|
||||||
|
expression: normalize_sessions(&sessions)
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
- agent: claude
|
||||||
|
agentMode: build
|
||||||
|
agentSessionId: "<redacted>"
|
||||||
|
ended: false
|
||||||
|
eventCount: "<redacted>"
|
||||||
|
model: "<redacted>"
|
||||||
|
permissionMode: bypass
|
||||||
|
sessionId: snapshot-claude
|
||||||
|
variant: "<redacted>"
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 978
|
||||||
|
expression: normalize_events(&permission_events)
|
||||||
|
---
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 1011
|
||||||
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
detail: "invalid request: unknown permission id: missing-permission"
|
||||||
|
status: 400
|
||||||
|
title: Invalid Request
|
||||||
|
type: "urn:sandbox-agent:error:invalid_request"
|
||||||
|
status: 400
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 1100
|
||||||
|
expression: normalize_events(&reject_events)
|
||||||
|
---
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 1151
|
||||||
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
detail: "invalid request: unknown question id: missing-question"
|
||||||
|
status: 400
|
||||||
|
title: Invalid Request
|
||||||
|
type: "urn:sandbox-agent:error:invalid_request"
|
||||||
|
status: 400
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 1039
|
||||||
|
expression: normalize_events(&question_events)
|
||||||
|
---
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 1072
|
||||||
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
detail: "invalid request: unknown question id: missing-question"
|
||||||
|
status: 400
|
||||||
|
title: Invalid Request
|
||||||
|
type: "urn:sandbox-agent:error:invalid_request"
|
||||||
|
status: 400
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 765
|
||||||
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": normalize_health(&payload), })"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
status: ok
|
||||||
|
status: 200
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 793
|
||||||
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
detail: token invalid
|
||||||
|
details:
|
||||||
|
message: missing or invalid token
|
||||||
|
status: 401
|
||||||
|
title: Token Invalid
|
||||||
|
type: "urn:sandbox-agent:error:token_invalid"
|
||||||
|
status: 401
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 776
|
||||||
|
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
detail: token invalid
|
||||||
|
details:
|
||||||
|
message: missing or invalid token
|
||||||
|
status: 401
|
||||||
|
title: Token Invalid
|
||||||
|
type: "urn:sandbox-agent:error:token_invalid"
|
||||||
|
status: 401
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 810
|
||||||
|
expression: "json!({\n \"status\": status.as_u16(), \"payload\": normalize_agent_list(&payload),\n})"
|
||||||
|
---
|
||||||
|
payload:
|
||||||
|
agents:
|
||||||
|
- id: amp
|
||||||
|
- id: claude
|
||||||
|
- id: codex
|
||||||
|
- id: opencode
|
||||||
|
status: 200
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 842
|
||||||
|
expression: "json!({\n \"cors\": snapshot_cors(status, &headers), \"payload\":\n normalize_health(&payload),\n})"
|
||||||
|
---
|
||||||
|
cors:
|
||||||
|
access-control-allow-credentials: "true"
|
||||||
|
access-control-allow-origin: "http://example.com"
|
||||||
|
status: 200
|
||||||
|
vary: "origin, access-control-request-method, access-control-request-headers"
|
||||||
|
payload:
|
||||||
|
status: ok
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 818
|
||||||
|
expression: "snapshot_cors(status, &headers)"
|
||||||
|
---
|
||||||
|
access-control-allow-credentials: "true"
|
||||||
|
access-control-allow-headers: "content-type,authorization"
|
||||||
|
access-control-allow-methods: "GET,POST"
|
||||||
|
access-control-allow-origin: "http://example.com"
|
||||||
|
status: 200
|
||||||
|
vary: "origin, access-control-request-method, access-control-request-headers"
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 1232
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
session_a:
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
session_b:
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 721
|
||||||
|
expression: normalized
|
||||||
|
---
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||||
|
assertion_line: 729
|
||||||
|
expression: normalized
|
||||||
|
---
|
||||||
|
- agent: claude
|
||||||
|
kind: started
|
||||||
|
seq: 1
|
||||||
|
started:
|
||||||
|
message: session.created
|
||||||
|
- agent: claude
|
||||||
|
kind: unknown
|
||||||
|
seq: 2
|
||||||
|
- agent: claude
|
||||||
|
kind: message
|
||||||
|
message:
|
||||||
|
parts:
|
||||||
|
- text: "<redacted>"
|
||||||
|
type: text
|
||||||
|
role: assistant
|
||||||
|
seq: 3
|
||||||
|
|
@ -6,9 +6,9 @@ authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sandbox-agent-agent-schema = { path = "../agent-schema" }
|
sandbox-agent-extracted-agent-schemas.workspace = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
schemars = "0.8"
|
schemars.workspace = true
|
||||||
thiserror = "1.0"
|
thiserror.workspace = true
|
||||||
utoipa = { version = "4.2", features = ["axum_extras"] }
|
utoipa.workspace = true
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@ use schemars::JsonSchema;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
pub use sandbox_agent_agent_schema::{amp, claude, codex, opencode};
|
pub use sandbox_agent_extracted_agent_schemas::{amp, claude, codex, opencode};
|
||||||
|
|
||||||
pub mod agents;
|
pub mod agents;
|
||||||
|
|
||||||
|
|
|
||||||
12
server/packages/universal-schema-gen/Cargo.toml
Normal file
12
server/packages/universal-schema-gen/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "sandbox-agent-universal-schema-gen"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
sandbox-agent-universal-agent-schema.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
26
server/packages/universal-schema-gen/build.rs
Normal file
26
server/packages/universal-schema-gen/build.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=../universal-agent-schema/src/lib.rs");
|
||||||
|
|
||||||
|
let schema = schemars::schema_for!(sandbox_agent_universal_agent_schema::UniversalEvent);
|
||||||
|
|
||||||
|
let workspace_root = std::env::var("CARGO_MANIFEST_DIR")
|
||||||
|
.map(|dir| {
|
||||||
|
Path::new(&dir)
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let out_dir = workspace_root.join("spec");
|
||||||
|
fs::create_dir_all(&out_dir).unwrap();
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&schema).expect("Failed to serialize JSON schema");
|
||||||
|
fs::write(out_dir.join("universal-schema.json"), json)
|
||||||
|
.expect("Failed to write universal-schema.json");
|
||||||
|
}
|
||||||
2
server/packages/universal-schema-gen/src/lib.rs
Normal file
2
server/packages/universal-schema-gen/src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// This crate exists only to trigger the build.rs script
|
||||||
|
// which generates the universal JSON schema at build time.
|
||||||
655
spec/universal-schema.json
Normal file
655
spec/universal-schema.json
Normal file
|
|
@ -0,0 +1,655 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "UniversalEvent",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"agent",
|
||||||
|
"data",
|
||||||
|
"id",
|
||||||
|
"sessionId",
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"agent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"agentSessionId": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/UniversalEventData"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"AttachmentSource": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"data",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"encoding": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CrashInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"details": true,
|
||||||
|
"kind": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PermissionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"always",
|
||||||
|
"id",
|
||||||
|
"patterns",
|
||||||
|
"permission",
|
||||||
|
"sessionId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"always": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tool": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/PermissionToolRef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PermissionToolRef": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"callId",
|
||||||
|
"messageId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"callId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"QuestionInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"options",
|
||||||
|
"question"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"multiSelect": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/QuestionOption"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"QuestionOption": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"label"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"QuestionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"questions",
|
||||||
|
"sessionId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"questions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/QuestionInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tool": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/QuestionToolRef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"QuestionToolRef": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"callId",
|
||||||
|
"messageId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"callId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Started": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"details": true,
|
||||||
|
"message": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UniversalEventData": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"$ref": "#/definitions/UniversalMessage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"started"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"started": {
|
||||||
|
"$ref": "#/definitions/Started"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"$ref": "#/definitions/CrashInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"questionAsked"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"questionAsked": {
|
||||||
|
"$ref": "#/definitions/QuestionRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permissionAsked"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"permissionAsked": {
|
||||||
|
"$ref": "#/definitions/PermissionRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"raw"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"raw": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UniversalMessage": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/UniversalMessageParsed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"raw"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raw": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UniversalMessageParsed": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"parts",
|
||||||
|
"role"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"parts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/UniversalMessagePart"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UniversalMessagePart": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"text",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"input",
|
||||||
|
"name",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"input": true,
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"tool_call"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"output",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is_error": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"output": true,
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"tool_result"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"arguments",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"arguments": true,
|
||||||
|
"id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raw": true,
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"function_call"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"result",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is_error": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raw": true,
|
||||||
|
"result": true,
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"function_result"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"source",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raw": true,
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/definitions/AttachmentSource"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"file"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"source",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"alt": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raw": true,
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/definitions/AttachmentSource"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"message",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"raw",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"raw": true,
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"unknown"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
todo.md
1
todo.md
|
|
@ -71,6 +71,7 @@
|
||||||
- [x] Add OpenCode server-mode tests (session create, prompt, SSE)
|
- [x] Add OpenCode server-mode tests (session create, prompt, SSE)
|
||||||
- [ ] Add tests for question/permission flows using deterministic prompts
|
- [ ] Add tests for question/permission flows using deterministic prompts
|
||||||
- [x] Add HTTP/SSE snapshot tests for real agents (env-configured)
|
- [x] Add HTTP/SSE snapshot tests for real agents (env-configured)
|
||||||
|
- [x] Add snapshot coverage for auth, CORS, and concurrent sessions
|
||||||
|
|
||||||
## Frontend (frontend/packages/inspector)
|
## Frontend (frontend/packages/inspector)
|
||||||
- [x] Build Vite + React app with connect screen (endpoint + optional token)
|
- [x] Build Vite + React app with connect screen (endpoint + optional token)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue