5.7 KiB
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.rsreads JSON schemas and uses thetypifycrate to generate Rust structs- Generated code is written to
$OUT_DIR/{agent}.rs - Types are exposed via
include!()macros insrc/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, dataUniversalEventData- Enum: Message, Started, Error, QuestionAsked, PermissionAsked, UnknownUniversalMessage- 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
UnparsedorUnknownvariants
Session Management
Sessions track agent conversations with in-memory state.
Storage
Sessions are stored in an in-memory HashMap<String, SessionState> inside SessionManager:
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:
send_message()spawns the agent CLI as a subprocessconsume_spawn()reads stdout/stderr line by line- Each JSON line is parsed and converted via
parse_agent_line() - Events are recorded via
record_event()which:- Assigns incrementing event ID
- Appends to
eventsvector - Broadcasts to SSE subscribers
SDK Modes
The TypeScript SDK supports two connection modes.
Embedded Mode
Defined in sdks/typescript/src/spawn.ts:
- Binary resolution: Checks
SANDBOX_AGENT_BINenv, then platform-specific npm package, thenPATH - Port selection: Uses provided port or finds a free one via
net.createServer() - Token generation: Uses provided token or generates random 24-byte hex string
- Spawn: Launches
sandbox-agent --host <host> --port <port> --token <token> - Health wait: Polls
GET /v1/healthuntil server is ready (up to 15s timeout) - Cleanup: On dispose, sends SIGTERM then SIGKILL if needed; also registers process exit handlers
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-agentserver - Uses provided
baseUrland optionaltoken - No subprocess management
const client = new SandboxDaemonClient({
baseUrl: "http://remote-server:8080",
token: "secret",
});
Auto-Detection
SandboxDaemonClient.connect() chooses the mode automatically:
// 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 modeSandboxDaemonSpawnOptions- Fine-grained control over host, port, token, binary path, timeout, logging