diff --git a/.agents/skills/agent-browser/SKILL.md b/.agents/skills/agent-browser/SKILL.md index ab3ea3c..680828d 100644 --- a/.agents/skills/agent-browser/SKILL.md +++ b/.agents/skills/agent-browser/SKILL.md @@ -1,265 +1,24 @@ --- name: agent-browser -description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages. -allowed-tools: Bash(agent-browser:*) +description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. +allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*) --- # Browser Automation with agent-browser -## Quick start +## Core Workflow -```bash -agent-browser open # Navigate to page -agent-browser snapshot -i # Get interactive elements with refs -agent-browser click @e1 # Click element by ref -agent-browser fill @e2 "text" # Fill input by ref -agent-browser close # Close browser -``` +Every browser automation follows this pattern: -## Core workflow - -1. Navigate: `agent-browser open ` -2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`) -3. Interact using refs from the snapshot -4. Re-snapshot after navigation or significant DOM changes - -## Commands - -### Navigation - -```bash -agent-browser open # Navigate to URL (aliases: goto, navigate) - # Supports: https://, http://, file://, about:, data:// - # Auto-prepends https:// if no protocol given -agent-browser back # Go back -agent-browser forward # Go forward -agent-browser reload # Reload page -agent-browser close # Close browser (aliases: quit, exit) -agent-browser connect 9222 # Connect to browser via CDP port -``` - -### Snapshot (page analysis) - -```bash -agent-browser snapshot # Full accessibility tree -agent-browser snapshot -i # Interactive elements only (recommended) -agent-browser snapshot -c # Compact output -agent-browser snapshot -d 3 # Limit depth to 3 -agent-browser snapshot -s "#main" # Scope to CSS selector -``` - -### Interactions (use @refs from snapshot) - -```bash -agent-browser click @e1 # Click -agent-browser dblclick @e1 # Double-click -agent-browser focus @e1 # Focus element -agent-browser fill @e2 "text" # Clear and type -agent-browser type @e2 "text" # Type without clearing -agent-browser press Enter # Press key (alias: key) -agent-browser press Control+a # Key combination -agent-browser keydown Shift # Hold key down -agent-browser keyup Shift # Release key -agent-browser hover @e1 # Hover -agent-browser check @e1 # Check checkbox -agent-browser uncheck @e1 # Uncheck checkbox -agent-browser select @e1 "value" # Select dropdown option -agent-browser select @e1 "a" "b" # Select multiple options -agent-browser scroll down 500 # Scroll page (default: down 300px) -agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto) -agent-browser drag @e1 @e2 # Drag and drop -agent-browser upload @e1 file.pdf # Upload files -``` - -### Get information - -```bash -agent-browser get text @e1 # Get element text -agent-browser get html @e1 # Get innerHTML -agent-browser get value @e1 # Get input value -agent-browser get attr @e1 href # Get attribute -agent-browser get title # Get page title -agent-browser get url # Get current URL -agent-browser get count ".item" # Count matching elements -agent-browser get box @e1 # Get bounding box -agent-browser get styles @e1 # Get computed styles (font, color, bg, etc.) -``` - -### Check state - -```bash -agent-browser is visible @e1 # Check if visible -agent-browser is enabled @e1 # Check if enabled -agent-browser is checked @e1 # Check if checked -``` - -### Screenshots & PDF - -```bash -agent-browser screenshot # Save to a temporary directory -agent-browser screenshot path.png # Save to a specific path -agent-browser screenshot --full # Full page -agent-browser pdf output.pdf # Save as PDF -``` - -### Video recording - -```bash -agent-browser record start ./demo.webm # Start recording (uses current URL + state) -agent-browser click @e1 # Perform actions -agent-browser record stop # Stop and save video -agent-browser record restart ./take2.webm # Stop current + start new recording -``` - -Recording creates a fresh context but preserves cookies/storage from your session. If no URL is provided, it -automatically returns to your current page. For smooth demos, explore first, then start recording. - -### Wait - -```bash -agent-browser wait @e1 # Wait for element -agent-browser wait 2000 # Wait milliseconds -agent-browser wait --text "Success" # Wait for text (or -t) -agent-browser wait --url "**/dashboard" # Wait for URL pattern (or -u) -agent-browser wait --load networkidle # Wait for network idle (or -l) -agent-browser wait --fn "window.ready" # Wait for JS condition (or -f) -``` - -### Mouse control - -```bash -agent-browser mouse move 100 200 # Move mouse -agent-browser mouse down left # Press button -agent-browser mouse up left # Release button -agent-browser mouse wheel 100 # Scroll wheel -``` - -### Semantic locators (alternative to refs) - -```bash -agent-browser find role button click --name "Submit" -agent-browser find text "Sign In" click -agent-browser find text "Sign In" click --exact # Exact match only -agent-browser find label "Email" fill "user@test.com" -agent-browser find placeholder "Search" type "query" -agent-browser find alt "Logo" click -agent-browser find title "Close" click -agent-browser find testid "submit-btn" click -agent-browser find first ".item" click -agent-browser find last ".item" click -agent-browser find nth 2 "a" hover -``` - -### Browser settings - -```bash -agent-browser set viewport 1920 1080 # Set viewport size -agent-browser set device "iPhone 14" # Emulate device -agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation) -agent-browser set offline on # Toggle offline mode -agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers -agent-browser set credentials user pass # HTTP basic auth (alias: auth) -agent-browser set media dark # Emulate color scheme -agent-browser set media light reduced-motion # Light mode + reduced motion -``` - -### Cookies & Storage - -```bash -agent-browser cookies # Get all cookies -agent-browser cookies set name value # Set cookie -agent-browser cookies clear # Clear cookies -agent-browser storage local # Get all localStorage -agent-browser storage local key # Get specific key -agent-browser storage local set k v # Set value -agent-browser storage local clear # Clear all -``` - -### Network - -```bash -agent-browser network route # Intercept requests -agent-browser network route --abort # Block requests -agent-browser network route --body '{}' # Mock response -agent-browser network unroute [url] # Remove routes -agent-browser network requests # View tracked requests -agent-browser network requests --filter api # Filter requests -``` - -### Tabs & Windows - -```bash -agent-browser tab # List tabs -agent-browser tab new [url] # New tab -agent-browser tab 2 # Switch to tab by index -agent-browser tab close # Close current tab -agent-browser tab close 2 # Close tab by index -agent-browser window new # New window -``` - -### Frames - -```bash -agent-browser frame "#iframe" # Switch to iframe -agent-browser frame main # Back to main frame -``` - -### Dialogs - -```bash -agent-browser dialog accept [text] # Accept dialog -agent-browser dialog dismiss # Dismiss dialog -``` - -### JavaScript - -```bash -agent-browser eval "document.title" # Run JavaScript -``` - -## Global options - -```bash -agent-browser --session ... # Isolated browser session -agent-browser --json ... # JSON output for parsing -agent-browser --headed ... # Show browser window (not headless) -agent-browser --full ... # Full page screenshot (-f) -agent-browser --cdp ... # Connect via Chrome DevTools Protocol -agent-browser -p ... # Cloud browser provider (--provider) -agent-browser --proxy ... # Use proxy server -agent-browser --headers ... # HTTP headers scoped to URL's origin -agent-browser --executable-path

# Custom browser executable -agent-browser --extension ... # Load browser extension (repeatable) -agent-browser --help # Show help (-h) -agent-browser --version # Show version (-V) -agent-browser --help # Show detailed help for a command -``` - -### Proxy support - -```bash -agent-browser --proxy http://proxy.com:8080 open example.com -agent-browser --proxy http://user:pass@proxy.com:8080 open example.com -agent-browser --proxy socks5://proxy.com:1080 open example.com -``` - -## Environment variables - -```bash -AGENT_BROWSER_SESSION="mysession" # Default session name -AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path -AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths -AGENT_BROWSER_PROVIDER="your-cloud-browser-provider" # Cloud browser provider (select browseruse or browserbase) -AGENT_BROWSER_STREAM_PORT="9223" # WebSocket streaming port -AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location (for daemon.js) -``` - -## Example: Form submission +1. **Navigate**: `agent-browser open ` +2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`) +3. **Interact**: Use refs to click, fill, select +4. **Re-snapshot**: After navigation or DOM changes, get fresh refs ```bash agent-browser open https://example.com/form agent-browser snapshot -i -# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3] +# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit" agent-browser fill @e1 "user@example.com" agent-browser fill @e2 "password123" @@ -268,72 +27,504 @@ agent-browser wait --load networkidle agent-browser snapshot -i # Check result ``` -## Example: Authentication with saved state +## Command Chaining + +Commands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls. ```bash -# Login once +# Chain open + wait + snapshot in one call +agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i + +# Chain multiple interactions +agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "password123" && agent-browser click @e3 + +# Navigate and capture +agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png +``` + +**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs). + +## Essential Commands + +```bash +# Navigation +agent-browser open # Navigate (aliases: goto, navigate) +agent-browser close # Close browser + +# Snapshot +agent-browser snapshot -i # Interactive elements with refs (recommended) +agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer) +agent-browser snapshot -s "#selector" # Scope to CSS selector + +# Interaction (use @refs from snapshot) +agent-browser click @e1 # Click element +agent-browser click @e1 --new-tab # Click and open in new tab +agent-browser fill @e2 "text" # Clear and type text +agent-browser type @e2 "text" # Type without clearing +agent-browser select @e1 "option" # Select dropdown option +agent-browser check @e1 # Check checkbox +agent-browser press Enter # Press key +agent-browser keyboard type "text" # Type at current focus (no selector) +agent-browser keyboard inserttext "text" # Insert without key events +agent-browser scroll down 500 # Scroll page +agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container + +# Get information +agent-browser get text @e1 # Get element text +agent-browser get url # Get current URL +agent-browser get title # Get page title + +# Wait +agent-browser wait @e1 # Wait for element +agent-browser wait --load networkidle # Wait for network idle +agent-browser wait --url "**/page" # Wait for URL pattern +agent-browser wait 2000 # Wait milliseconds + +# Downloads +agent-browser download @e1 ./file.pdf # Click element to trigger download +agent-browser wait --download ./output.zip # Wait for any download to complete +agent-browser --download-path ./downloads open # Set default download directory + +# Capture +agent-browser screenshot # Screenshot to temp dir +agent-browser screenshot --full # Full page screenshot +agent-browser screenshot --annotate # Annotated screenshot with numbered element labels +agent-browser pdf output.pdf # Save as PDF + +# Diff (compare page states) +agent-browser diff snapshot # Compare current vs last snapshot +agent-browser diff snapshot --baseline before.txt # Compare current vs saved file +agent-browser diff screenshot --baseline before.png # Visual pixel diff +agent-browser diff url # Compare two pages +agent-browser diff url --wait-until networkidle # Custom wait strategy +agent-browser diff url --selector "#main" # Scope to element +``` + +## Common Patterns + +### Form Submission + +```bash +agent-browser open https://example.com/signup +agent-browser snapshot -i +agent-browser fill @e1 "Jane Doe" +agent-browser fill @e2 "jane@example.com" +agent-browser select @e3 "California" +agent-browser check @e4 +agent-browser click @e5 +agent-browser wait --load networkidle +``` + +### Authentication with Auth Vault (Recommended) + +```bash +# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY) +# Recommended: pipe password via stdin to avoid shell history exposure +echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin + +# Login using saved profile (LLM never sees password) +agent-browser auth login github + +# List/show/delete profiles +agent-browser auth list +agent-browser auth show github +agent-browser auth delete github +``` + +### Authentication with State Persistence + +```bash +# Login once and save state agent-browser open https://app.example.com/login agent-browser snapshot -i -agent-browser fill @e1 "username" -agent-browser fill @e2 "password" +agent-browser fill @e1 "$USERNAME" +agent-browser fill @e2 "$PASSWORD" agent-browser click @e3 agent-browser wait --url "**/dashboard" agent-browser state save auth.json -# Later sessions: load saved state +# Reuse in future sessions agent-browser state load auth.json agent-browser open https://app.example.com/dashboard ``` -## Sessions (parallel browsers) +### Session Persistence ```bash -agent-browser --session test1 open site-a.com -agent-browser --session test2 open site-b.com -agent-browser session list +# Auto-save/restore cookies and localStorage across browser restarts +agent-browser --session-name myapp open https://app.example.com/login +# ... login flow ... +agent-browser close # State auto-saved to ~/.agent-browser/sessions/ + +# Next time, state is auto-loaded +agent-browser --session-name myapp open https://app.example.com/dashboard + +# Encrypt state at rest +export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32) +agent-browser --session-name secure open https://app.example.com + +# Manage saved states +agent-browser state list +agent-browser state show myapp-default.json +agent-browser state clear myapp +agent-browser state clean --older-than 7 ``` -## JSON output (for parsing) - -Add `--json` for machine-readable output: +### Data Extraction ```bash +agent-browser open https://example.com/products +agent-browser snapshot -i +agent-browser get text @e5 # Get specific element text +agent-browser get text body > page.txt # Get all page text + +# JSON output for parsing agent-browser snapshot -i --json agent-browser get text @e1 --json ``` -## Debugging +### Parallel Sessions ```bash -agent-browser --headed open example.com # Show browser window -agent-browser --cdp 9222 snapshot # Connect via CDP port -agent-browser connect 9222 # Alternative: connect command -agent-browser console # View console messages -agent-browser console --clear # Clear console -agent-browser errors # View page errors -agent-browser errors --clear # Clear errors -agent-browser highlight @e1 # Highlight element -agent-browser trace start # Start recording trace -agent-browser trace stop trace.zip # Stop and save trace -agent-browser record start ./debug.webm # Record video from current page -agent-browser record stop # Save recording +agent-browser --session site1 open https://site-a.com +agent-browser --session site2 open https://site-b.com + +agent-browser --session site1 snapshot -i +agent-browser --session site2 snapshot -i + +agent-browser session list ``` -## Deep-dive documentation +### Connect to Existing Chrome -For detailed patterns and best practices, see: +```bash +# Auto-discover running Chrome with remote debugging enabled +agent-browser --auto-connect open https://example.com +agent-browser --auto-connect snapshot -| Reference | Description | +# Or with explicit CDP port +agent-browser --cdp 9222 snapshot +``` + +### Color Scheme (Dark Mode) + +```bash +# Persistent dark mode via flag (applies to all pages and new tabs) +agent-browser --color-scheme dark open https://example.com + +# Or via environment variable +AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com + +# Or set during session (persists for subsequent commands) +agent-browser set media dark +``` + +### Visual Browser (Debugging) + +```bash +agent-browser --headed open https://example.com +agent-browser highlight @e1 # Highlight element +agent-browser record start demo.webm # Record session +agent-browser profiler start # Start Chrome DevTools profiling +agent-browser profiler stop trace.json # Stop and save profile (path optional) +``` + +Use `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode. + +### Local Files (PDFs, HTML) + +```bash +# Open local files with file:// URLs +agent-browser --allow-file-access open file:///path/to/document.pdf +agent-browser --allow-file-access open file:///path/to/page.html +agent-browser screenshot output.png +``` + +### iOS Simulator (Mobile Safari) + +```bash +# List available iOS simulators +agent-browser device list + +# Launch Safari on a specific device +agent-browser -p ios --device "iPhone 16 Pro" open https://example.com + +# Same workflow as desktop - snapshot, interact, re-snapshot +agent-browser -p ios snapshot -i +agent-browser -p ios tap @e1 # Tap (alias for click) +agent-browser -p ios fill @e2 "text" +agent-browser -p ios swipe up # Mobile-specific gesture + +# Take screenshot +agent-browser -p ios screenshot mobile.png + +# Close session (shuts down simulator) +agent-browser -p ios close +``` + +**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`) + +**Real devices:** Works with physical iOS devices if pre-configured. Use `--device ""` where UDID is from `xcrun xctrace list devices`. + +## Security + +All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output. + +### Content Boundaries (Recommended for AI Agents) + +Enable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content: + +```bash +export AGENT_BROWSER_CONTENT_BOUNDARIES=1 +agent-browser snapshot +# Output: +# --- AGENT_BROWSER_PAGE_CONTENT nonce= origin=https://example.com --- +# [accessibility tree] +# --- END_AGENT_BROWSER_PAGE_CONTENT nonce= --- +``` + +### Domain Allowlist + +Restrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on: + +```bash +export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com" +agent-browser open https://example.com # OK +agent-browser open https://malicious.com # Blocked +``` + +### Action Policy + +Use a policy file to gate destructive actions: + +```bash +export AGENT_BROWSER_ACTION_POLICY=./policy.json +``` + +Example `policy.json`: +```json +{"default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"]} +``` + +Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies. + +### Output Limits + +Prevent context flooding from large pages: + +```bash +export AGENT_BROWSER_MAX_OUTPUT=50000 +``` + +## Diffing (Verifying Changes) + +Use `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session. + +```bash +# Typical workflow: snapshot -> action -> diff +agent-browser snapshot -i # Take baseline snapshot +agent-browser click @e2 # Perform action +agent-browser diff snapshot # See what changed (auto-compares to last snapshot) +``` + +For visual regression testing or monitoring: + +```bash +# Save a baseline screenshot, then compare later +agent-browser screenshot baseline.png +# ... time passes or changes are made ... +agent-browser diff screenshot --baseline baseline.png + +# Compare staging vs production +agent-browser diff url https://staging.example.com https://prod.example.com --screenshot +``` + +`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage. + +## Timeouts and Slow Pages + +The default Playwright timeout is 25 seconds for local browsers. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout: + +```bash +# Wait for network activity to settle (best for slow pages) +agent-browser wait --load networkidle + +# Wait for a specific element to appear +agent-browser wait "#content" +agent-browser wait @e1 + +# Wait for a specific URL pattern (useful after redirects) +agent-browser wait --url "**/dashboard" + +# Wait for a JavaScript condition +agent-browser wait --fn "document.readyState === 'complete'" + +# Wait a fixed duration (milliseconds) as a last resort +agent-browser wait 5000 +``` + +When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait ` or `wait @ref`. + +## Session Management and Cleanup + +When running multiple agents or automations concurrently, always use named sessions to avoid conflicts: + +```bash +# Each agent gets its own isolated session +agent-browser --session agent1 open site-a.com +agent-browser --session agent2 open site-b.com + +# Check active sessions +agent-browser session list +``` + +Always close your browser session when done to avoid leaked processes: + +```bash +agent-browser close # Close default session +agent-browser --session agent1 close # Close specific session +``` + +If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work. + +## Ref Lifecycle (Important) + +Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after: + +- Clicking links or buttons that navigate +- Form submissions +- Dynamic content loading (dropdowns, modals) + +```bash +agent-browser click @e5 # Navigates to new page +agent-browser snapshot -i # MUST re-snapshot +agent-browser click @e1 # Use new refs +``` + +## Annotated Screenshots (Vision Mode) + +Use `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot. + +```bash +agent-browser screenshot --annotate +# Output includes the image path and a legend: +# [1] @e1 button "Submit" +# [2] @e2 link "Home" +# [3] @e3 textbox "Email" +agent-browser click @e2 # Click using ref from annotated screenshot +``` + +Use annotated screenshots when: +- The page has unlabeled icon buttons or visual-only elements +- You need to verify visual layout or styling +- Canvas or chart elements are present (invisible to text snapshots) +- You need spatial reasoning about element positions + +## Semantic Locators (Alternative to Refs) + +When refs are unavailable or unreliable, use semantic locators: + +```bash +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find role button click --name "Submit" +agent-browser find placeholder "Search" type "query" +agent-browser find testid "submit-btn" click +``` + +## JavaScript Evaluation (eval) + +Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues. + +```bash +# Simple expressions work with regular quoting +agent-browser eval 'document.title' +agent-browser eval 'document.querySelectorAll("img").length' + +# Complex JS: use --stdin with heredoc (RECOMMENDED) +agent-browser eval --stdin <<'EVALEOF' +JSON.stringify( + Array.from(document.querySelectorAll("img")) + .filter(i => !i.alt) + .map(i => ({ src: i.src.split("/").pop(), width: i.width })) +) +EVALEOF + +# Alternative: base64 encoding (avoids all shell escaping issues) +agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)" +``` + +**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely. + +**Rules of thumb:** +- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine +- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'` +- Programmatic/generated scripts -> use `eval -b` with base64 + +## Configuration File + +Create `agent-browser.json` in the project root for persistent settings: + +```json +{ + "headed": true, + "proxy": "http://localhost:8080", + "profile": "./browser-data" +} +``` + +Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config ` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `"executablePath"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced. + +## Deep-Dive Documentation + +| Reference | When to Use | |-----------|-------------| +| [references/commands.md](references/commands.md) | Full command reference with all options | | [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting | | [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping | | [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse | | [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation | +| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis | | [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies | -## Ready-to-use templates +## Experimental: Native Mode -Executable workflow scripts for common patterns: +agent-browser has an experimental native Rust daemon that communicates with Chrome directly via CDP, bypassing Node.js and Playwright entirely. It is opt-in and not recommended for production use yet. + +```bash +# Enable via flag +agent-browser --native open example.com + +# Enable via environment variable (avoids passing --native every time) +export AGENT_BROWSER_NATIVE=1 +agent-browser open example.com +``` + +The native daemon supports Chromium and Safari (via WebDriver). Firefox and WebKit are not yet supported. All core commands (navigate, snapshot, click, fill, screenshot, cookies, storage, tabs, eval, etc.) work identically in native mode. Use `agent-browser close` before switching between native and default mode within the same session. + +## Browser Engine Selection + +Use `--engine` to choose a local browser engine. The default is `chrome`. + +```bash +# Use Lightpanda (fast headless browser, requires separate install) +agent-browser --engine lightpanda open example.com + +# Via environment variable +export AGENT_BROWSER_ENGINE=lightpanda +agent-browser open example.com + +# With custom binary path +agent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com +``` + +Supported engines: +- `chrome` (default) -- Chrome/Chromium via CDP +- `lightpanda` -- Lightpanda headless browser via CDP (10x faster, 10x less memory than Chrome) + +Lightpanda does not support `--extension`, `--profile`, `--state`, or `--allow-file-access`. Install Lightpanda from https://lightpanda.io/docs/open-source/installation. + +## Ready-to-Use Templates | Template | Description | |----------|-------------| @@ -341,16 +532,8 @@ Executable workflow scripts for common patterns: | [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state | | [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | -Usage: ```bash ./templates/form-automation.sh https://example.com/form ./templates/authenticated-session.sh https://app.example.com/login ./templates/capture-workflow.sh https://example.com ./output ``` - -## HTTPS Certificate Errors - -For sites with self-signed or invalid certificates: -```bash -agent-browser open https://localhost:8443 --ignore-https-errors -``` diff --git a/.agents/skills/agent-browser/references/authentication.md b/.agents/skills/agent-browser/references/authentication.md index 5d801f6..12ef5e4 100644 --- a/.agents/skills/agent-browser/references/authentication.md +++ b/.agents/skills/agent-browser/references/authentication.md @@ -1,6 +1,20 @@ # Authentication Patterns -Patterns for handling login flows, session persistence, and authenticated browsing. +Login flows, session persistence, OAuth, 2FA, and authenticated browsing. + +**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Login Flow](#basic-login-flow) +- [Saving Authentication State](#saving-authentication-state) +- [Restoring Authentication](#restoring-authentication) +- [OAuth / SSO Flows](#oauth--sso-flows) +- [Two-Factor Authentication](#two-factor-authentication) +- [HTTP Basic Auth](#http-basic-auth) +- [Cookie-Based Auth](#cookie-based-auth) +- [Token Refresh Handling](#token-refresh-handling) +- [Security Best Practices](#security-best-practices) ## Basic Login Flow diff --git a/.agents/skills/agent-browser/references/proxy-support.md b/.agents/skills/agent-browser/references/proxy-support.md index 05fcec2..e86a8fe 100644 --- a/.agents/skills/agent-browser/references/proxy-support.md +++ b/.agents/skills/agent-browser/references/proxy-support.md @@ -1,13 +1,29 @@ # Proxy Support -Configure proxy servers for browser automation, useful for geo-testing, rate limiting avoidance, and corporate environments. +Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments. + +**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Proxy Configuration](#basic-proxy-configuration) +- [Authenticated Proxy](#authenticated-proxy) +- [SOCKS Proxy](#socks-proxy) +- [Proxy Bypass](#proxy-bypass) +- [Common Use Cases](#common-use-cases) +- [Verifying Proxy Connection](#verifying-proxy-connection) +- [Troubleshooting](#troubleshooting) +- [Best Practices](#best-practices) ## Basic Proxy Configuration -Set proxy via environment variable before starting: +Use the `--proxy` flag or set proxy via environment variable: ```bash -# HTTP proxy +# Via CLI flag +agent-browser --proxy "http://proxy.example.com:8080" open https://example.com + +# Via environment variable export HTTP_PROXY="http://proxy.example.com:8080" agent-browser open https://example.com @@ -45,10 +61,13 @@ agent-browser open https://example.com ## Proxy Bypass -Skip proxy for specific domains: +Skip proxy for specific domains using `--proxy-bypass` or `NO_PROXY`: ```bash -# Bypass proxy for local addresses +# Via CLI flag +agent-browser --proxy "http://proxy.example.com:8080" --proxy-bypass "localhost,*.internal.com" open https://example.com + +# Via environment variable export NO_PROXY="localhost,127.0.0.1,.internal.company.com" agent-browser open https://internal.company.com # Direct connection agent-browser open https://external.com # Via proxy diff --git a/.agents/skills/agent-browser/references/session-management.md b/.agents/skills/agent-browser/references/session-management.md index cfc3362..bb5312d 100644 --- a/.agents/skills/agent-browser/references/session-management.md +++ b/.agents/skills/agent-browser/references/session-management.md @@ -1,6 +1,18 @@ # Session Management -Run multiple isolated browser sessions concurrently with state persistence. +Multiple isolated browser sessions with state persistence and concurrent browsing. + +**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Named Sessions](#named-sessions) +- [Session Isolation Properties](#session-isolation-properties) +- [Session State Persistence](#session-state-persistence) +- [Common Patterns](#common-patterns) +- [Default Session](#default-session) +- [Session Cleanup](#session-cleanup) +- [Best Practices](#best-practices) ## Named Sessions diff --git a/.agents/skills/agent-browser/references/snapshot-refs.md b/.agents/skills/agent-browser/references/snapshot-refs.md index 0b17a4d..c5868d5 100644 --- a/.agents/skills/agent-browser/references/snapshot-refs.md +++ b/.agents/skills/agent-browser/references/snapshot-refs.md @@ -1,21 +1,29 @@ -# Snapshot + Refs Workflow +# Snapshot and Refs -The core innovation of agent-browser: compact element references that reduce context usage dramatically for AI agents. +Compact element references that reduce context usage dramatically for AI agents. -## How It Works +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. -### The Problem -Traditional browser automation sends full DOM to AI agents: +## Contents + +- [How Refs Work](#how-refs-work) +- [Snapshot Command](#the-snapshot-command) +- [Using Refs](#using-refs) +- [Ref Lifecycle](#ref-lifecycle) +- [Best Practices](#best-practices) +- [Ref Notation Details](#ref-notation-details) +- [Troubleshooting](#troubleshooting) + +## How Refs Work + +Traditional approach: ``` -Full DOM/HTML sent → AI parses → Generates CSS selector → Executes action -~3000-5000 tokens per interaction +Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens) ``` -### The Solution -agent-browser uses compact snapshots with refs: +agent-browser approach: ``` -Compact snapshot → @refs assigned → Direct ref interaction -~200-400 tokens per interaction +Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens) ``` ## The Snapshot Command @@ -166,8 +174,8 @@ agent-browser snapshot -i ### Element Not Visible in Snapshot ```bash -# Scroll to reveal element -agent-browser scroll --bottom +# Scroll down to reveal element +agent-browser scroll down 1000 agent-browser snapshot -i # Or wait for dynamic content diff --git a/.agents/skills/agent-browser/references/video-recording.md b/.agents/skills/agent-browser/references/video-recording.md index 98e6b0a..e6a9fb4 100644 --- a/.agents/skills/agent-browser/references/video-recording.md +++ b/.agents/skills/agent-browser/references/video-recording.md @@ -1,6 +1,17 @@ # Video Recording -Capture browser automation sessions as video for debugging, documentation, or verification. +Capture browser automation as video for debugging, documentation, or verification. + +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Recording](#basic-recording) +- [Recording Commands](#recording-commands) +- [Use Cases](#use-cases) +- [Best Practices](#best-practices) +- [Output Format](#output-format) +- [Limitations](#limitations) ## Basic Recording diff --git a/.agents/skills/agent-browser/templates/authenticated-session.sh b/.agents/skills/agent-browser/templates/authenticated-session.sh index e44aaad..b66c928 100755 --- a/.agents/skills/agent-browser/templates/authenticated-session.sh +++ b/.agents/skills/agent-browser/templates/authenticated-session.sh @@ -1,67 +1,81 @@ #!/bin/bash # Template: Authenticated Session Workflow -# Login once, save state, reuse for subsequent runs +# Purpose: Login once, save state, reuse for subsequent runs +# Usage: ./authenticated-session.sh [state-file] # -# Usage: -# ./authenticated-session.sh [state-file] +# RECOMMENDED: Use the auth vault instead of this template: +# echo "" | agent-browser auth save myapp --url --username --password-stdin +# agent-browser auth login myapp +# The auth vault stores credentials securely and the LLM never sees passwords. # -# Setup: -# 1. Run once to see your form structure -# 2. Note the @refs for your fields -# 3. Uncomment LOGIN FLOW section and update refs +# Environment variables: +# APP_USERNAME - Login username/email +# APP_PASSWORD - Login password +# +# Two modes: +# 1. Discovery mode (default): Shows form structure so you can identify refs +# 2. Login mode: Performs actual login after you update the refs +# +# Setup steps: +# 1. Run once to see form structure (discovery mode) +# 2. Update refs in LOGIN FLOW section below +# 3. Set APP_USERNAME and APP_PASSWORD +# 4. Delete the DISCOVERY section set -euo pipefail LOGIN_URL="${1:?Usage: $0 [state-file]}" STATE_FILE="${2:-./auth-state.json}" -echo "Authentication workflow for: $LOGIN_URL" +echo "Authentication workflow: $LOGIN_URL" -# ══════════════════════════════════════════════════════════════ -# SAVED STATE: Skip login if we have valid saved state -# ══════════════════════════════════════════════════════════════ +# ================================================================ +# SAVED STATE: Skip login if valid saved state exists +# ================================================================ if [[ -f "$STATE_FILE" ]]; then - echo "Loading saved authentication state..." - agent-browser state load "$STATE_FILE" - agent-browser open "$LOGIN_URL" - agent-browser wait --load networkidle + echo "Loading saved state from $STATE_FILE..." + if agent-browser --state "$STATE_FILE" open "$LOGIN_URL" 2>/dev/null; then + agent-browser wait --load networkidle - CURRENT_URL=$(agent-browser get url) - if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then - echo "Session restored successfully!" - agent-browser snapshot -i - exit 0 + CURRENT_URL=$(agent-browser get url) + if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then + echo "Session restored successfully" + agent-browser snapshot -i + exit 0 + fi + echo "Session expired, performing fresh login..." + agent-browser close 2>/dev/null || true + else + echo "Failed to load state, re-authenticating..." fi - echo "Session expired, performing fresh login..." rm -f "$STATE_FILE" fi -# ══════════════════════════════════════════════════════════════ -# DISCOVERY MODE: Show form structure (remove after setup) -# ══════════════════════════════════════════════════════════════ +# ================================================================ +# DISCOVERY MODE: Shows form structure (delete after setup) +# ================================================================ echo "Opening login page..." agent-browser open "$LOGIN_URL" agent-browser wait --load networkidle echo "" -echo "┌─────────────────────────────────────────────────────────┐" -echo "│ LOGIN FORM STRUCTURE │" -echo "├─────────────────────────────────────────────────────────┤" +echo "Login form structure:" +echo "---" agent-browser snapshot -i -echo "└─────────────────────────────────────────────────────────┘" +echo "---" echo "" echo "Next steps:" -echo " 1. Note refs: @e? = username, @e? = password, @e? = submit" -echo " 2. Uncomment LOGIN FLOW section below" -echo " 3. Replace @e1, @e2, @e3 with your refs" +echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?" +echo " 2. Update the LOGIN FLOW section below with your refs" +echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'" echo " 4. Delete this DISCOVERY MODE section" echo "" agent-browser close exit 0 -# ══════════════════════════════════════════════════════════════ +# ================================================================ # LOGIN FLOW: Uncomment and customize after discovery -# ══════════════════════════════════════════════════════════════ +# ================================================================ # : "${APP_USERNAME:?Set APP_USERNAME environment variable}" # : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}" # @@ -78,14 +92,14 @@ exit 0 # # Verify login succeeded # FINAL_URL=$(agent-browser get url) # if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then -# echo "ERROR: Login failed - still on login page" +# echo "Login failed - still on login page" # agent-browser screenshot /tmp/login-failed.png # agent-browser close # exit 1 # fi # # # Save state for future runs -# echo "Saving authentication state to: $STATE_FILE" +# echo "Saving state to $STATE_FILE" # agent-browser state save "$STATE_FILE" -# echo "Login successful!" +# echo "Login successful" # agent-browser snapshot -i diff --git a/.agents/skills/agent-browser/templates/capture-workflow.sh b/.agents/skills/agent-browser/templates/capture-workflow.sh index a4eae75..3bc93ad 100755 --- a/.agents/skills/agent-browser/templates/capture-workflow.sh +++ b/.agents/skills/agent-browser/templates/capture-workflow.sh @@ -1,68 +1,69 @@ #!/bin/bash # Template: Content Capture Workflow -# Extract content from web pages with optional authentication +# Purpose: Extract content from web pages (text, screenshots, PDF) +# Usage: ./capture-workflow.sh [output-dir] +# +# Outputs: +# - page-full.png: Full page screenshot +# - page-structure.txt: Page element structure with refs +# - page-text.txt: All text content +# - page.pdf: PDF version +# +# Optional: Load auth state for protected pages set -euo pipefail TARGET_URL="${1:?Usage: $0 [output-dir]}" OUTPUT_DIR="${2:-.}" -echo "Capturing content from: $TARGET_URL" +echo "Capturing: $TARGET_URL" mkdir -p "$OUTPUT_DIR" -# Optional: Load authentication state if needed +# Optional: Load authentication state # if [[ -f "./auth-state.json" ]]; then +# echo "Loading authentication state..." # agent-browser state load "./auth-state.json" # fi -# Navigate to target page +# Navigate to target agent-browser open "$TARGET_URL" agent-browser wait --load networkidle -# Get page metadata -echo "Page title: $(agent-browser get title)" -echo "Page URL: $(agent-browser get url)" +# Get metadata +TITLE=$(agent-browser get title) +URL=$(agent-browser get url) +echo "Title: $TITLE" +echo "URL: $URL" # Capture full page screenshot agent-browser screenshot --full "$OUTPUT_DIR/page-full.png" -echo "Screenshot saved: $OUTPUT_DIR/page-full.png" +echo "Saved: $OUTPUT_DIR/page-full.png" -# Get page structure +# Get page structure with refs agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt" -echo "Structure saved: $OUTPUT_DIR/page-structure.txt" +echo "Saved: $OUTPUT_DIR/page-structure.txt" -# Extract main content -# Adjust selector based on target site structure -# agent-browser get text @e1 > "$OUTPUT_DIR/main-content.txt" - -# Extract specific elements (uncomment as needed) -# agent-browser get text "article" > "$OUTPUT_DIR/article.txt" -# agent-browser get text "main" > "$OUTPUT_DIR/main.txt" -# agent-browser get text ".content" > "$OUTPUT_DIR/content.txt" - -# Get full page text +# Extract all text content agent-browser get text body > "$OUTPUT_DIR/page-text.txt" -echo "Text content saved: $OUTPUT_DIR/page-text.txt" +echo "Saved: $OUTPUT_DIR/page-text.txt" -# Optional: Save as PDF +# Save as PDF agent-browser pdf "$OUTPUT_DIR/page.pdf" -echo "PDF saved: $OUTPUT_DIR/page.pdf" +echo "Saved: $OUTPUT_DIR/page.pdf" -# Optional: Capture with scrolling for infinite scroll pages -# scroll_and_capture() { -# local count=0 -# while [[ $count -lt 5 ]]; do -# agent-browser scroll down 1000 -# agent-browser wait 1000 -# ((count++)) -# done -# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png" -# } -# scroll_and_capture +# Optional: Extract specific elements using refs from structure +# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt" + +# Optional: Handle infinite scroll pages +# for i in {1..5}; do +# agent-browser scroll down 1000 +# agent-browser wait 1000 +# done +# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png" # Cleanup agent-browser close echo "" -echo "Capture complete! Files saved to: $OUTPUT_DIR" +echo "Capture complete:" ls -la "$OUTPUT_DIR" diff --git a/.agents/skills/agent-browser/templates/form-automation.sh b/.agents/skills/agent-browser/templates/form-automation.sh index 02a7c81..6784fcd 100755 --- a/.agents/skills/agent-browser/templates/form-automation.sh +++ b/.agents/skills/agent-browser/templates/form-automation.sh @@ -1,64 +1,62 @@ #!/bin/bash # Template: Form Automation Workflow -# Fills and submits web forms with validation +# Purpose: Fill and submit web forms with validation +# Usage: ./form-automation.sh +# +# This template demonstrates the snapshot-interact-verify pattern: +# 1. Navigate to form +# 2. Snapshot to get element refs +# 3. Fill fields using refs +# 4. Submit and verify result +# +# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output set -euo pipefail FORM_URL="${1:?Usage: $0 }" -echo "Automating form at: $FORM_URL" +echo "Form automation: $FORM_URL" -# Navigate to form page +# Step 1: Navigate to form agent-browser open "$FORM_URL" agent-browser wait --load networkidle -# Get interactive snapshot to identify form fields -echo "Analyzing form structure..." +# Step 2: Snapshot to discover form elements +echo "" +echo "Form structure:" agent-browser snapshot -i -# Example: Fill common form fields -# Uncomment and modify refs based on snapshot output +# Step 3: Fill form fields (customize these refs based on snapshot output) +# +# Common field types: +# agent-browser fill @e1 "John Doe" # Text input +# agent-browser fill @e2 "user@example.com" # Email input +# agent-browser fill @e3 "SecureP@ss123" # Password input +# agent-browser select @e4 "Option Value" # Dropdown +# agent-browser check @e5 # Checkbox +# agent-browser click @e6 # Radio button +# agent-browser fill @e7 "Multi-line text" # Textarea +# agent-browser upload @e8 /path/to/file.pdf # File upload +# +# Uncomment and modify: +# agent-browser fill @e1 "Test User" +# agent-browser fill @e2 "test@example.com" +# agent-browser click @e3 # Submit button -# Text inputs -# agent-browser fill @e1 "John Doe" # Name field -# agent-browser fill @e2 "user@example.com" # Email field -# agent-browser fill @e3 "+1-555-123-4567" # Phone field - -# Password fields -# agent-browser fill @e4 "SecureP@ssw0rd!" - -# Dropdowns -# agent-browser select @e5 "Option Value" - -# Checkboxes -# agent-browser check @e6 # Check -# agent-browser uncheck @e7 # Uncheck - -# Radio buttons -# agent-browser click @e8 # Select radio option - -# Text areas -# agent-browser fill @e9 "Multi-line text content here" - -# File uploads -# agent-browser upload @e10 /path/to/file.pdf - -# Submit form -# agent-browser click @e11 # Submit button - -# Wait for response +# Step 4: Wait for submission # agent-browser wait --load networkidle -# agent-browser wait --url "**/success" # Or wait for redirect +# agent-browser wait --url "**/success" # Or wait for redirect -# Verify submission -echo "Form submission result:" +# Step 5: Verify result +echo "" +echo "Result:" agent-browser get url agent-browser snapshot -i -# Take screenshot of result +# Optional: Capture evidence agent-browser screenshot /tmp/form-result.png +echo "Screenshot saved: /tmp/form-result.png" # Cleanup agent-browser close - -echo "Form automation complete" +echo "Done" diff --git a/.claude/commands/post-release-testing.md b/.claude/commands/post-release-testing.md index 771a1d8..10cf6ff 100644 --- a/.claude/commands/post-release-testing.md +++ b/.claude/commands/post-release-testing.md @@ -43,7 +43,7 @@ Manually verify the install script works in a fresh environment: ```bash docker run --rm alpine:latest sh -c " apk add --no-cache curl ca-certificates libstdc++ libgcc bash && - curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh && + curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && sandbox-agent --version " ``` diff --git a/.dockerignore b/.dockerignore index 1a4fa41..4ba2cf3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,11 +9,15 @@ build/ # Cache .cache/ .turbo/ +**/.turbo/ *.tsbuildinfo +.pnpm-store/ +coverage/ # Environment .env .env.* +.foundry/ # IDE .idea/ @@ -24,3 +28,7 @@ build/ # Git .git/ + +# Tests +**/test/ +**/tests/ diff --git a/.env.development.example b/.env.development.example new file mode 100644 index 0000000..0ae0f58 --- /dev/null +++ b/.env.development.example @@ -0,0 +1,34 @@ +# Foundry local development environment. +# Copy ~/misc/the-foundry.env to .env in the repo root to populate secrets. +# .env is gitignored — never commit it. The source of truth is ~/misc/the-foundry.env. +# +# Docker Compose (just foundry-dev) and the justfile (set dotenv-load := true) +# both read .env automatically. + +APP_URL=http://localhost:4173 +BETTER_AUTH_URL=http://localhost:4173 +BETTER_AUTH_SECRET=sandbox-agent-foundry-development-only-change-me +GITHUB_REDIRECT_URI=http://localhost:4173/v1/auth/callback/github + +# Fill these in when enabling live GitHub OAuth. +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Fill these in when enabling GitHub App-backed org installation and repo import. +GITHUB_APP_ID= +GITHUB_APP_CLIENT_ID= +GITHUB_APP_CLIENT_SECRET= +# Store PEM material as a quoted single-line value with \n escapes. +GITHUB_APP_PRIVATE_KEY= +# Webhook secret for verifying GitHub webhook payloads. +# Use smee.io for local development: https://smee.io/new +GITHUB_WEBHOOK_SECRET= +# Required for local GitHub webhook forwarding in compose.dev. +SMEE_URL= +SMEE_TARGET=http://backend:7741/v1/webhooks/github + +# Fill these in when enabling live Stripe billing. +STRIPE_SECRET_KEY= +STRIPE_PUBLISHABLE_KEY= +STRIPE_WEBHOOK_SECRET= +STRIPE_PRICE_TEAM= diff --git a/.github/media/inspector.png b/.github/media/inspector.png index 1c16ed2..02e588e 100644 Binary files a/.github/media/inspector.png and b/.github/media/inspector.png differ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e009cad..85f828d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,18 +11,50 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - - uses: Swatinem/rust-cache@main + - uses: Swatinem/rust-cache@v2 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install + - name: Run formatter hooks + shell: bash + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + git fetch origin "${{ github.base_ref }}" --depth=1 + diff_range="origin/${{ github.base_ref }}...HEAD" + elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + diff_range="${{ github.event.before }}...${{ github.sha }}" + else + diff_range="HEAD^...HEAD" + fi + + mapfile -t changed_files < <( + git diff --name-only --diff-filter=ACMR "$diff_range" \ + | grep -E '\.(cjs|cts|js|jsx|json|jsonc|mjs|mts|rs|ts|tsx)$' \ + || true + ) + + if [ ${#changed_files[@]} -eq 0 ]; then + echo "No formatter-managed files changed." + exit 0 + fi + + args=() + for file in "${changed_files[@]}"; do + args+=(--file "$file") + done + + pnpm exec lefthook run pre-commit --no-stage-fixed --fail-on-changes "${args[@]}" + - run: npm install -g tsx - name: Run checks - run: ./scripts/release/main.ts --version 0.0.0 --check + run: ./scripts/release/main.ts --version 0.0.0 --only-steps run-ci-checks - name: Run ACP v1 server tests run: | cargo test -p sandbox-agent-agent-management @@ -31,5 +63,3 @@ jobs: cargo test -p sandbox-agent --lib - name: Run SDK tests run: pnpm --dir sdks/typescript test - - name: Run Inspector browser E2E - run: pnpm --filter @sandbox-agent/inspector test:agent-browser diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 102f612..34fb64a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -180,10 +180,20 @@ jobs: include: - platform: linux/arm64 runner: depot-ubuntu-24.04-arm-8 - arch_suffix: -arm64 + tag_suffix: -arm64 + dockerfile: docker/runtime/Dockerfile - platform: linux/amd64 runner: depot-ubuntu-24.04-8 - arch_suffix: -amd64 + tag_suffix: -amd64 + dockerfile: docker/runtime/Dockerfile + - platform: linux/arm64 + runner: depot-ubuntu-24.04-arm-8 + tag_suffix: -full-arm64 + dockerfile: docker/runtime/Dockerfile.full + - platform: linux/amd64 + runner: depot-ubuntu-24.04-8 + tag_suffix: -full-amd64 + dockerfile: docker/runtime/Dockerfile.full runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 @@ -205,8 +215,8 @@ jobs: with: context: . push: true - tags: rivetdev/sandbox-agent:${{ steps.vars.outputs.sha_short }}${{ matrix.arch_suffix }} - file: docker/runtime/Dockerfile + tags: rivetdev/sandbox-agent:${{ steps.vars.outputs.sha_short }}${{ matrix.tag_suffix }} + file: ${{ matrix.dockerfile }} platforms: ${{ matrix.platform }} build-args: | TARGETARCH=${{ contains(matrix.platform, 'arm64') && 'arm64' || 'amd64' }} diff --git a/.gitignore b/.gitignore index 07d74c2..de4d863 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ yarn.lock .astro/ *.tsbuildinfo .turbo/ +**/.turbo/ +.pnpm-store/ +coverage/ # Environment .env @@ -47,6 +50,13 @@ Cargo.lock # Example temp files .tmp-upload/ +*.db +.foundry/ # CLI binaries (downloaded during npm publish) sdks/cli/platforms/*/bin/ + +# Foundry desktop app build artifacts +foundry/packages/desktop/frontend-dist/ +foundry/packages/desktop/src-tauri/sidecars/ +.context/ diff --git a/.mcp.json b/.mcp.json index cc04a2b..7bae219 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,10 +1,8 @@ { "mcpServers": { "everything": { - "args": [ - "@modelcontextprotocol/server-everything" - ], + "args": ["@modelcontextprotocol/server-everything"], "command": "npx" } } -} \ No newline at end of file +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f301fed --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=false diff --git a/CLAUDE.md b/CLAUDE.md index 2934e3a..248f075 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,38 +1,5 @@ # Instructions -## ACP v1 Baseline - -- v1 is ACP-native. -- `/v1/*` is removed and returns `410 Gone` (`application/problem+json`). -- `/opencode/*` is disabled during ACP core phases and returns `503`. -- Prompt/session traffic is ACP JSON-RPC over streamable HTTP on `/v1/rpc`: - - `POST /v1/rpc` - - `GET /v1/rpc` (SSE) - - `DELETE /v1/rpc` -- Control-plane endpoints: - - `GET /v1/health` - - `GET /v1/agents` - - `POST /v1/agents/{agent}/install` -- Binary filesystem transfer endpoints (intentionally HTTP, not ACP extension methods): - - `GET /v1/fs/file` - - `PUT /v1/fs/file` - - `POST /v1/fs/upload-batch` -- Sandbox Agent ACP extension method naming: - - Custom ACP methods use `_sandboxagent/...` (not `_sandboxagent/v1/...`). - - Session detach method is `_sandboxagent/session/detach`. - -## API Scope - -- ACP is the primary protocol for agent/session behavior and all functionality that talks directly to the agent. -- ACP extensions may be used for gaps (for example `skills`, `models`, and related metadata), but the default is that agent-facing behavior is implemented by the agent through ACP. -- Custom HTTP APIs are for non-agent/session platform services (for example filesystem, terminals, and other host/runtime capabilities). -- Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP. -- Keep `GET /v1/fs/file`, `PUT /v1/fs/file`, and `POST /v1/fs/upload-batch` on HTTP: - - These are Sandbox Agent host/runtime operations with cross-agent-consistent behavior. - - They may involve very large binary transfers that ACP JSON-RPC envelopes are not suited to stream. - - This is intentionally separate from ACP native `fs/read_text_file` and `fs/write_text_file`. - - ACP extension variants may exist in parallel, but SDK defaults should prefer HTTP for these binary transfer operations. - ## Naming and Ownership - This repository/product is **Sandbox Agent**. @@ -41,59 +8,44 @@ - Canonical extension namespace/domain string is `sandboxagent.dev` (no hyphen). - Canonical custom ACP extension method prefix is `_sandboxagent/...` (no hyphen). -## Architecture (Brief) +## Docs Terminology -- HTTP contract and problem/error mapping: `server/packages/sandbox-agent/src/router.rs` -- ACP client runtime and agent process bridge: `server/packages/sandbox-agent/src/acp_runtime/mod.rs` -- Agent/native + ACP agent process install and lazy install: `server/packages/agent-management/` -- Inspector UI served at `/ui/` and bound to ACP over HTTP from `frontend/packages/inspector/` +- Never mention "ACP" in user-facing docs (`docs/**/*.mdx`) except in docs that are specifically about ACP itself (e.g. `docs/acp-http-client.mdx`). +- Never expose underlying protocol method names (e.g. `session/request_permission`, `session/create`, `_sandboxagent/session/detach`) in non-ACP docs. Describe the behavior in user-facing terms instead. +- Do not describe the underlying protocol implementation in docs. Only document the SDK surface (methods, types, options). ACP protocol details belong exclusively in ACP-specific pages. +- Do not use em dashes (`—`) in docs. Use commas, periods, or parentheses instead. -## TypeScript SDK Architecture +### Docs Source Of Truth (HTTP/CLI) -- TypeScript clients are split into: - - `acp-http-client`: protocol-pure ACP-over-HTTP (`/v1/acp`) with no Sandbox-specific HTTP helpers. - - `sandbox-agent`: `SandboxAgent` SDK wrapper that combines ACP session operations with Sandbox control-plane and filesystem helpers. -- `SandboxAgent` entry points are `SandboxAgent.connect(...)` and `SandboxAgent.start(...)`. -- Stable Sandbox session methods are `createSession`, `resumeSession`, `resumeOrCreateSession`, `destroySession`, `sendSessionMethod`, and `onSessionEvent`. -- `Session` helpers are `prompt(...)`, `send(...)`, and `onEvent(...)`. -- Cleanup is `sdk.dispose()`. - -### Docs Source Of Truth - -- For TypeScript docs/examples, source of truth is implementation in: - - `sdks/typescript/src/client.ts` - - `sdks/typescript/src/index.ts` - - `sdks/acp-http-client/src/index.ts` -- Do not document TypeScript APIs unless they are exported and implemented in those files. - For HTTP/CLI docs/examples, source of truth is: - `server/packages/sandbox-agent/src/router.rs` - `server/packages/sandbox-agent/src/cli.rs` -- Keep docs aligned to implemented endpoints/commands only (for example ACP under `/v1/acp`, not legacy `/v1/sessions` APIs). - -## Source Documents - -- `~/misc/acp-docs/schema/schema.json` -- `~/misc/acp-docs/schema/meta.json` -- `research/acp/spec.md` -- `research/acp/v1-schema-to-acp-mapping.md` -- `research/acp/friction.md` -- `research/acp/todo.md` +- Keep docs aligned to implemented endpoints/commands only (for example ACP under `/v1/acp`, not legacy session REST APIs). ## Change Tracking +- If the user asks to "push" changes, treat that as permission to commit and push all current workspace changes, not a hand-picked subset, unless the user explicitly scopes the push. - Keep CLI subcommands and HTTP endpoints in sync. - Update `docs/cli.mdx` when CLI behavior changes. - Regenerate `docs/openapi.json` when HTTP contracts change. - Keep `docs/inspector.mdx` and `docs/sdks/typescript.mdx` aligned with implementation. - Append blockers/decisions to `research/acp/friction.md` during ACP work. -- TypeScript SDK tests should run against a real running server/runtime over real `/v1` HTTP APIs, typically using the real `mock` agent for deterministic behavior. -- Do not use Vitest fetch/transport mocks to simulate server functionality in TypeScript SDK tests. +- `docs/agent-capabilities.mdx` lists models/modes/thought levels per agent. Update it when adding a new agent or changing `fallback_config_options`. If its "Last updated" date is >2 weeks old, re-run `cd scripts/agent-configs && npx tsx dump.ts` and update the doc to match. Source data: `scripts/agent-configs/resources/*.json` and hardcoded entries in `server/packages/sandbox-agent/src/router/support.rs` (`fallback_config_options`). +- Some agent models are gated by subscription (e.g. Claude `opus`). The live report only shows models available to the current credentials. The static doc and JSON resource files should list all known models regardless of subscription tier. -## Docker Examples (Dev Testing) +## Docker Test Image -- When manually testing bleeding-edge (unreleased) versions of sandbox-agent in `examples/`, use `SANDBOX_AGENT_DEV=1` with the Docker-based examples. -- This triggers `examples/shared/Dockerfile.dev` which builds the server binary from local source and packages it into the Docker image. -- Example: `SANDBOX_AGENT_DEV=1 pnpm --filter @sandbox-agent/example-mcp start` +- Docker-backed Rust and TypeScript tests build `docker/test-agent/Dockerfile` directly in-process and cache the image tag only in memory (`OnceLock` in Rust, module-level variable in TypeScript). +- Do not add cross-process image-build scripts unless there is a concrete need for them. + +## Common Software Sync + +- These three files must stay in sync: + - `docs/common-software.mdx` (user-facing documentation) + - `docker/test-common-software/Dockerfile` (packages installed in the test image) + - `server/packages/sandbox-agent/tests/common_software.rs` (test assertions) +- When adding or removing software from `docs/common-software.mdx`, also add/remove the corresponding `apt-get install` line in the Dockerfile and add/remove the test in `common_software.rs`. +- Run `cargo test -p sandbox-agent --test common_software` to verify. ## Install Version References @@ -107,6 +59,7 @@ - `docs/cli.mdx` - `docs/quickstart.mdx` - `docs/sdk-overview.mdx` + - `docs/react-components.mdx` - `docs/session-persistence.mdx` - `docs/deploy/local.mdx` - `docs/deploy/cloudflare.mdx` @@ -118,11 +71,10 @@ - `.claude/commands/post-release-testing.md` - `examples/cloudflare/Dockerfile` - `examples/daytona/src/index.ts` - - `examples/daytona/src/daytona-with-snapshot.ts` + - `examples/shared/src/docker.ts` - `examples/docker/src/index.ts` - `examples/e2b/src/index.ts` - `examples/vercel/src/index.ts` - `scripts/release/main.ts` - `scripts/release/promote-artifacts.ts` - `scripts/release/sdk.ts` - - `scripts/sandbox-testing/test-sandbox.ts` diff --git a/Cargo.toml b/Cargo.toml index 505efb0..0fc4dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [workspace] resolver = "2" members = ["server/packages/*", "gigacode"] +exclude = ["factory/packages/desktop/src-tauri", "foundry/packages/desktop/src-tauri"] [workspace.package] -version = "0.2.0" +version = "0.4.2" edition = "2021" authors = [ "Rivet Gaming, LLC " ] license = "Apache-2.0" @@ -12,13 +13,13 @@ description = "Universal API for automatic coding agents in sandboxes. Supports [workspace.dependencies] # Internal crates -sandbox-agent = { version = "0.2.0", path = "server/packages/sandbox-agent" } -sandbox-agent-error = { version = "0.2.0", path = "server/packages/error" } -sandbox-agent-agent-management = { version = "0.2.0", path = "server/packages/agent-management" } -sandbox-agent-agent-credentials = { version = "0.2.0", path = "server/packages/agent-credentials" } -sandbox-agent-opencode-adapter = { version = "0.2.0", path = "server/packages/opencode-adapter" } -sandbox-agent-opencode-server-manager = { version = "0.2.0", path = "server/packages/opencode-server-manager" } -acp-http-adapter = { version = "0.2.0", path = "server/packages/acp-http-adapter" } +sandbox-agent = { version = "0.4.2", path = "server/packages/sandbox-agent" } +sandbox-agent-error = { version = "0.4.2", path = "server/packages/error" } +sandbox-agent-agent-management = { version = "0.4.2", path = "server/packages/agent-management" } +sandbox-agent-agent-credentials = { version = "0.4.2", path = "server/packages/agent-credentials" } +sandbox-agent-opencode-adapter = { version = "0.4.2", path = "server/packages/opencode-adapter" } +sandbox-agent-opencode-server-manager = { version = "0.4.2", path = "server/packages/opencode-server-manager" } +acp-http-adapter = { version = "0.4.2", path = "server/packages/acp-http-adapter" } # Serialization serde = { version = "1.0", features = ["derive"] } @@ -32,7 +33,7 @@ schemars = "0.8" utoipa = { version = "4.2", features = ["axum_extras"] } # Web framework -axum = "0.7" +axum = { version = "0.7", features = ["ws"] } tower = { version = "0.5", features = ["util"] } tower-http = { version = "0.5", features = ["cors", "trace"] } diff --git a/README.md b/README.md index f57d233..cf9b933 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,13 @@ Import the SDK directly into your Node or browser application. Full type safety **Install** ```bash -npm install sandbox-agent@0.2.x +npm install sandbox-agent@0.4.x ``` ```bash -bun add sandbox-agent@0.2.x +bun add sandbox-agent@0.4.x # Optional: allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()). -bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 +bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 ``` **Setup** @@ -118,7 +118,6 @@ const agents = await client.listAgents(); await client.createSession("demo", { agent: "codex", agentMode: "default", - permissionMode: "plan", }); await client.postMessage("demo", { message: "Hello from the SDK." }); @@ -128,9 +127,7 @@ for await (const event of client.streamEvents("demo", { offset: 0 })) { } ``` -`permissionMode: "acceptEdits"` passes through to Claude, auto-approves file changes for Codex, and is treated as `default` for other agents. - -[SDK documentation](https://sandboxagent.dev/docs/sdks/typescript) — [Building a Chat UI](https://sandboxagent.dev/docs/building-chat-ui) — [Managing Sessions](https://sandboxagent.dev/docs/manage-sessions) +[SDK documentation](https://sandboxagent.dev/docs/sdks/typescript) — [Managing Sessions](https://sandboxagent.dev/docs/manage-sessions) ### HTTP Server @@ -138,7 +135,7 @@ Run as an HTTP server and connect from any language. Deploy to E2B, Daytona, Ver ```bash # Install it -curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh +curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh # Run it sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 ``` @@ -146,10 +143,7 @@ sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468 Optional: preinstall agent binaries (no server required; they will be installed lazily on first use if you skip this): ```bash -sandbox-agent install-agent claude -sandbox-agent install-agent codex -sandbox-agent install-agent opencode -sandbox-agent install-agent amp +sandbox-agent install-agent --all ``` To disable auth locally: @@ -165,13 +159,13 @@ sandbox-agent server --no-token --host 127.0.0.1 --port 2468 Install the CLI wrapper (optional but convenient): ```bash -npm install -g @sandbox-agent/cli@0.2.x +npm install -g @sandbox-agent/cli@0.4.x ``` ```bash # Allow Bun to run postinstall scripts for native binaries. -bun add -g @sandbox-agent/cli@0.2.x -bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 +bun add -g @sandbox-agent/cli@0.4.x +bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 ``` Create a session and send a message: @@ -185,11 +179,11 @@ sandbox-agent api sessions send-message-stream my-session --message "Hello" --en You can also use npx like: ```bash -npx @sandbox-agent/cli@0.2.x --help +npx @sandbox-agent/cli@0.4.x --help ``` ```bash -bunx @sandbox-agent/cli@0.2.x --help +bunx @sandbox-agent/cli@0.4.x --help ``` [CLI documentation](https://sandboxagent.dev/docs/cli) @@ -283,7 +277,7 @@ Coding agents expect interactive terminals with proper TTY handling. SSH with pi - **Storage of sessions on disk**: Sessions are already stored by the respective coding agents on disk. It's assumed that the consumer is streaming data from this machine to an external storage, such as Postgres, ClickHouse, or Rivet. - **Direct LLM wrappers**: Use the [Vercel AI SDK](https://ai-sdk.dev/docs/introduction) if you want to implement your own agent from scratch. - **Git Repo Management**: Just use git commands or the features provided by your sandbox provider of choice. -- **Sandbox Provider API**: Sandbox providers have many nuanced differences in their API, it does not make sense for us to try to provide a custom layer. Instead, we opt to provide guides that let you integrate this project with sandbox providers. +- **Sandbox Provider API**: Sandbox providers have many nuanced differences in their API, it does not make sense for us to try to provide a custom layer. Instead, we opt to provide guides that let you integrate this repository with sandbox providers. ## Roadmap diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4a8bd54 --- /dev/null +++ b/biome.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "formatter": { + "indentStyle": "space", + "lineWidth": 160 + } +} diff --git a/docker/inspector-dev/Dockerfile b/docker/inspector-dev/Dockerfile new file mode 100644 index 0000000..b55923f --- /dev/null +++ b/docker/inspector-dev/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-bookworm-slim + +RUN npm install -g pnpm@10.28.2 + +WORKDIR /app + +CMD ["bash", "-lc", "pnpm install --filter @sandbox-agent/inspector... && cd frontend/packages/inspector && exec pnpm vite --host 0.0.0.0 --port 5173"] diff --git a/docker/release/linux-aarch64.Dockerfile b/docker/release/linux-aarch64.Dockerfile index ef70c14..d5ff208 100644 --- a/docker/release/linux-aarch64.Dockerfile +++ b/docker/release/linux-aarch64.Dockerfile @@ -10,7 +10,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/cli-shared/package.json ./sdks/cli-shared/ COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ -COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/ +COPY sdks/react/package.json ./sdks/react/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies @@ -20,14 +20,14 @@ RUN pnpm install --filter @sandbox-agent/inspector... COPY docs/openapi.json ./docs/ COPY sdks/cli-shared ./sdks/cli-shared COPY sdks/acp-http-client ./sdks/acp-http-client -COPY sdks/persist-indexeddb ./sdks/persist-indexeddb +COPY sdks/react ./sdks/react COPY sdks/typescript ./sdks/typescript -# Build cli-shared, acp-http-client, SDK, then persist-indexeddb (depends on SDK) +# Build cli-shared, acp-http-client, SDK, then react (depends on SDK) RUN cd sdks/cli-shared && pnpm exec tsup RUN cd sdks/acp-http-client && pnpm exec tsup RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup -RUN cd sdks/persist-indexeddb && pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup # Copy inspector source and build COPY frontend/packages/inspector ./frontend/packages/inspector diff --git a/docker/release/linux-x86_64.Dockerfile b/docker/release/linux-x86_64.Dockerfile index 262543b..1c41711 100644 --- a/docker/release/linux-x86_64.Dockerfile +++ b/docker/release/linux-x86_64.Dockerfile @@ -10,7 +10,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/cli-shared/package.json ./sdks/cli-shared/ COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ -COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/ +COPY sdks/react/package.json ./sdks/react/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies @@ -20,14 +20,14 @@ RUN pnpm install --filter @sandbox-agent/inspector... COPY docs/openapi.json ./docs/ COPY sdks/cli-shared ./sdks/cli-shared COPY sdks/acp-http-client ./sdks/acp-http-client -COPY sdks/persist-indexeddb ./sdks/persist-indexeddb +COPY sdks/react ./sdks/react COPY sdks/typescript ./sdks/typescript -# Build cli-shared, acp-http-client, SDK, then persist-indexeddb (depends on SDK) +# Build cli-shared, acp-http-client, SDK, then react (depends on SDK) RUN cd sdks/cli-shared && pnpm exec tsup RUN cd sdks/acp-http-client && pnpm exec tsup RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup -RUN cd sdks/persist-indexeddb && pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup # Copy inspector source and build COPY frontend/packages/inspector ./frontend/packages/inspector diff --git a/docker/release/macos-aarch64.Dockerfile b/docker/release/macos-aarch64.Dockerfile index e93ab15..5d918b2 100644 --- a/docker/release/macos-aarch64.Dockerfile +++ b/docker/release/macos-aarch64.Dockerfile @@ -10,7 +10,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/cli-shared/package.json ./sdks/cli-shared/ COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ -COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/ +COPY sdks/react/package.json ./sdks/react/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies @@ -20,14 +20,14 @@ RUN pnpm install --filter @sandbox-agent/inspector... COPY docs/openapi.json ./docs/ COPY sdks/cli-shared ./sdks/cli-shared COPY sdks/acp-http-client ./sdks/acp-http-client -COPY sdks/persist-indexeddb ./sdks/persist-indexeddb +COPY sdks/react ./sdks/react COPY sdks/typescript ./sdks/typescript -# Build cli-shared, acp-http-client, SDK, then persist-indexeddb (depends on SDK) +# Build cli-shared, acp-http-client, SDK, then react (depends on SDK) RUN cd sdks/cli-shared && pnpm exec tsup RUN cd sdks/acp-http-client && pnpm exec tsup RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup -RUN cd sdks/persist-indexeddb && pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup # Copy inspector source and build COPY frontend/packages/inspector ./frontend/packages/inspector diff --git a/docker/release/macos-x86_64.Dockerfile b/docker/release/macos-x86_64.Dockerfile index af57336..9b52aa6 100644 --- a/docker/release/macos-x86_64.Dockerfile +++ b/docker/release/macos-x86_64.Dockerfile @@ -10,7 +10,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/cli-shared/package.json ./sdks/cli-shared/ COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ -COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/ +COPY sdks/react/package.json ./sdks/react/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies @@ -20,14 +20,14 @@ RUN pnpm install --filter @sandbox-agent/inspector... COPY docs/openapi.json ./docs/ COPY sdks/cli-shared ./sdks/cli-shared COPY sdks/acp-http-client ./sdks/acp-http-client -COPY sdks/persist-indexeddb ./sdks/persist-indexeddb +COPY sdks/react ./sdks/react COPY sdks/typescript ./sdks/typescript -# Build cli-shared, acp-http-client, SDK, then persist-indexeddb (depends on SDK) +# Build cli-shared, acp-http-client, SDK, then react (depends on SDK) RUN cd sdks/cli-shared && pnpm exec tsup RUN cd sdks/acp-http-client && pnpm exec tsup RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup -RUN cd sdks/persist-indexeddb && pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup # Copy inspector source and build COPY frontend/packages/inspector ./frontend/packages/inspector diff --git a/docker/release/windows.Dockerfile b/docker/release/windows.Dockerfile index 83b3f37..92067db 100644 --- a/docker/release/windows.Dockerfile +++ b/docker/release/windows.Dockerfile @@ -10,7 +10,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/cli-shared/package.json ./sdks/cli-shared/ COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ -COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/ +COPY sdks/react/package.json ./sdks/react/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies @@ -20,14 +20,14 @@ RUN pnpm install --filter @sandbox-agent/inspector... COPY docs/openapi.json ./docs/ COPY sdks/cli-shared ./sdks/cli-shared COPY sdks/acp-http-client ./sdks/acp-http-client -COPY sdks/persist-indexeddb ./sdks/persist-indexeddb +COPY sdks/react ./sdks/react COPY sdks/typescript ./sdks/typescript -# Build cli-shared, acp-http-client, SDK, then persist-indexeddb (depends on SDK) +# Build cli-shared, acp-http-client, SDK, then react (depends on SDK) RUN cd sdks/cli-shared && pnpm exec tsup RUN cd sdks/acp-http-client && pnpm exec tsup RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup -RUN cd sdks/persist-indexeddb && pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup # Copy inspector source and build COPY frontend/packages/inspector ./frontend/packages/inspector diff --git a/docker/runtime/Dockerfile b/docker/runtime/Dockerfile index 029a969..85473be 100644 --- a/docker/runtime/Dockerfile +++ b/docker/runtime/Dockerfile @@ -12,7 +12,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/cli-shared/package.json ./sdks/cli-shared/ COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ -COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/ +COPY sdks/react/package.json ./sdks/react/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies @@ -22,14 +22,14 @@ RUN pnpm install --filter @sandbox-agent/inspector... COPY docs/openapi.json ./docs/ COPY sdks/cli-shared ./sdks/cli-shared COPY sdks/acp-http-client ./sdks/acp-http-client -COPY sdks/persist-indexeddb ./sdks/persist-indexeddb +COPY sdks/react ./sdks/react COPY sdks/typescript ./sdks/typescript -# Build cli-shared, acp-http-client, SDK, then persist-indexeddb (depends on SDK) +# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK) RUN cd sdks/cli-shared && pnpm exec tsup RUN cd sdks/acp-http-client && pnpm exec tsup RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup -RUN cd sdks/persist-indexeddb && pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup # Copy inspector source and build COPY frontend/packages/inspector ./frontend/packages/inspector @@ -149,7 +149,8 @@ FROM debian:bookworm-slim RUN apt-get update && apt-get install -y \ ca-certificates \ curl \ - git && \ + git \ + ffmpeg && \ rm -rf /var/lib/apt/lists/* # Copy the binary from builder @@ -164,4 +165,4 @@ WORKDIR /home/sandbox EXPOSE 2468 ENTRYPOINT ["sandbox-agent"] -CMD ["--host", "0.0.0.0", "--port", "2468"] +CMD ["server", "--host", "0.0.0.0", "--port", "2468"] diff --git a/docker/runtime/Dockerfile.full b/docker/runtime/Dockerfile.full new file mode 100644 index 0000000..9ab4c0d --- /dev/null +++ b/docker/runtime/Dockerfile.full @@ -0,0 +1,159 @@ +# syntax=docker/dockerfile:1.10.0 + +# ============================================================================ +# Build inspector frontend +# ============================================================================ +FROM node:22-alpine AS inspector-build +WORKDIR /app +RUN npm install -g pnpm + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ +COPY sdks/cli-shared/package.json ./sdks/cli-shared/ +COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/ +COPY sdks/react/package.json ./sdks/react/ +COPY sdks/typescript/package.json ./sdks/typescript/ + +RUN pnpm install --filter @sandbox-agent/inspector... + +COPY docs/openapi.json ./docs/ +COPY sdks/cli-shared ./sdks/cli-shared +COPY sdks/acp-http-client ./sdks/acp-http-client +COPY sdks/react ./sdks/react +COPY sdks/typescript ./sdks/typescript + +RUN cd sdks/cli-shared && pnpm exec tsup +RUN cd sdks/acp-http-client && pnpm exec tsup +RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup +RUN cd sdks/react && pnpm exec tsup + +COPY frontend/packages/inspector ./frontend/packages/inspector +RUN cd frontend/packages/inspector && pnpm exec vite build + +# ============================================================================ +# AMD64 Builder - Uses cross-tools musl toolchain +# ============================================================================ +FROM --platform=linux/amd64 rust:1.88.0 AS builder-amd64 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + musl-tools \ + musl-dev \ + llvm-14-dev \ + libclang-14-dev \ + clang-14 \ + libssl-dev \ + pkg-config \ + ca-certificates \ + g++ \ + g++-multilib \ + git \ + curl \ + wget && \ + rm -rf /var/lib/apt/lists/* + +RUN wget -q https://github.com/cross-tools/musl-cross/releases/latest/download/x86_64-unknown-linux-musl.tar.xz && \ + tar -xf x86_64-unknown-linux-musl.tar.xz -C /opt/ && \ + rm x86_64-unknown-linux-musl.tar.xz && \ + rustup target add x86_64-unknown-linux-musl + +ENV PATH="/opt/x86_64-unknown-linux-musl/bin:$PATH" \ + LIBCLANG_PATH=/usr/lib/llvm-14/lib \ + CLANG_PATH=/usr/bin/clang-14 \ + CC_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-gcc \ + CXX_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-g++ \ + AR_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-ar \ + CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-unknown-linux-musl-gcc \ + CARGO_INCREMENTAL=0 \ + CARGO_NET_GIT_FETCH_WITH_CLI=true + +ENV SSL_VER=1.1.1w +RUN wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz && \ + tar -xzf openssl-$SSL_VER.tar.gz && \ + cd openssl-$SSL_VER && \ + ./Configure no-shared no-async --prefix=/musl --openssldir=/musl/ssl linux-x86_64 && \ + make -j$(nproc) && \ + make install_sw && \ + cd .. && \ + rm -rf openssl-$SSL_VER* + +ENV OPENSSL_DIR=/musl \ + OPENSSL_INCLUDE_DIR=/musl/include \ + OPENSSL_LIB_DIR=/musl/lib \ + PKG_CONFIG_ALLOW_CROSS=1 \ + RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static-libgcc" + +WORKDIR /build +COPY . . + +COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/build/target \ + cargo build -p sandbox-agent --release --target x86_64-unknown-linux-musl && \ + cp target/x86_64-unknown-linux-musl/release/sandbox-agent /sandbox-agent + +# ============================================================================ +# ARM64 Builder - Uses Alpine with native musl +# ============================================================================ +FROM --platform=linux/arm64 rust:1.88-alpine AS builder-arm64 + +RUN apk add --no-cache \ + musl-dev \ + clang \ + llvm-dev \ + openssl-dev \ + openssl-libs-static \ + pkgconfig \ + git \ + curl \ + build-base + +RUN rustup target add aarch64-unknown-linux-musl + +ENV CARGO_INCREMENTAL=0 \ + CARGO_NET_GIT_FETCH_WITH_CLI=true \ + RUSTFLAGS="-C target-feature=+crt-static" + +WORKDIR /build +COPY . . + +COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/build/target \ + cargo build -p sandbox-agent --release --target aarch64-unknown-linux-musl && \ + cp target/aarch64-unknown-linux-musl/release/sandbox-agent /sandbox-agent + +# ============================================================================ +# Select the appropriate builder based on target architecture +# ============================================================================ +ARG TARGETARCH +FROM builder-${TARGETARCH} AS builder + +# Runtime stage - full image with all supported agents preinstalled +FROM node:22-bookworm-slim + +RUN apt-get update && apt-get install -y \ + bash \ + ca-certificates \ + curl \ + git && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent +RUN chmod +x /usr/local/bin/sandbox-agent + +RUN useradd -m -s /bin/bash sandbox +USER sandbox +WORKDIR /home/sandbox + +RUN sandbox-agent install-agent --all + +EXPOSE 2468 + +ENTRYPOINT ["sandbox-agent"] +CMD ["server", "--host", "0.0.0.0", "--port", "2468"] diff --git a/docker/test-agent/Dockerfile b/docker/test-agent/Dockerfile new file mode 100644 index 0000000..67888b3 --- /dev/null +++ b/docker/test-agent/Dockerfile @@ -0,0 +1,61 @@ +FROM rust:1.88.0-bookworm AS builder +WORKDIR /build + +COPY Cargo.toml Cargo.lock ./ +COPY server/ ./server/ +COPY gigacode/ ./gigacode/ +COPY resources/agent-schemas/artifacts/ ./resources/agent-schemas/artifacts/ +COPY scripts/agent-configs/ ./scripts/agent-configs/ +COPY scripts/audit-acp-deps/ ./scripts/audit-acp-deps/ + +ENV SANDBOX_AGENT_SKIP_INSPECTOR=1 + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/build/target \ + cargo build -p sandbox-agent --release && \ + cp target/release/sandbox-agent /sandbox-agent + +# Extract neko binary from the official image for WebRTC desktop streaming. +# Using neko v3 base image from GHCR which provides multi-arch support (amd64, arm64). +# Pinned by digest to prevent breaking changes from upstream. +# Reference client: https://github.com/demodesk/neko-client/blob/37f93eae6bd55b333c94bd009d7f2b079075a026/src/component/internal/webrtc.ts +FROM ghcr.io/m1k1o/neko/base@sha256:0c384afa56268aaa2d5570211d284763d0840dcdd1a7d9a24be3081d94d3dfce AS neko-base + +FROM node:22-bookworm-slim +RUN apt-get update -qq && \ + apt-get install -y -qq --no-install-recommends \ + ca-certificates \ + bash \ + libstdc++6 \ + xvfb \ + openbox \ + xdotool \ + imagemagick \ + ffmpeg \ + gstreamer1.0-tools \ + gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly \ + gstreamer1.0-nice \ + gstreamer1.0-x \ + gstreamer1.0-pulseaudio \ + libxcvt0 \ + x11-xserver-utils \ + dbus-x11 \ + xauth \ + fonts-dejavu-core \ + xterm \ + > /dev/null 2>&1 && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent +COPY --from=neko-base /usr/bin/neko /usr/local/bin/neko + +EXPOSE 3000 +# Expose UDP port range for WebRTC media transport +EXPOSE 59050-59070/udp + +ENTRYPOINT ["/usr/local/bin/sandbox-agent"] +CMD ["server", "--host", "0.0.0.0", "--port", "3000", "--no-token"] diff --git a/docker/test-common-software/Dockerfile b/docker/test-common-software/Dockerfile new file mode 100644 index 0000000..7a03abc --- /dev/null +++ b/docker/test-common-software/Dockerfile @@ -0,0 +1,37 @@ +# Extends the base test-agent image with common software pre-installed. +# Used by the common_software integration test to verify that all documented +# software in docs/common-software.mdx works correctly inside the sandbox. +# +# KEEP IN SYNC with docs/common-software.mdx + +ARG BASE_IMAGE=sandbox-agent-test:dev +FROM ${BASE_IMAGE} + +USER root + +RUN apt-get update -qq && \ + apt-get install -y -qq --no-install-recommends \ + # Browsers + chromium \ + firefox-esr \ + # Languages + python3 python3-pip python3-venv \ + default-jdk \ + ruby-full \ + # Databases + sqlite3 \ + redis-server \ + # Build tools + build-essential cmake pkg-config \ + # CLI tools + git jq tmux \ + # Media and graphics + imagemagick \ + poppler-utils \ + # Desktop apps + gimp \ + > /dev/null 2>&1 && \ + rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["/usr/local/bin/sandbox-agent"] +CMD ["server", "--host", "0.0.0.0", "--port", "3000", "--no-token"] diff --git a/docs/advanced/acp-http-client.mdx b/docs/advanced/acp-http-client.mdx deleted file mode 100644 index 4d93363..0000000 --- a/docs/advanced/acp-http-client.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "ACP HTTP Client" -description: "Protocol-pure ACP JSON-RPC over streamable HTTP client." ---- - -`acp-http-client` is a standalone, low-level package for ACP over HTTP (`/v2/rpc`). - -Use it when you want strict ACP protocol behavior with no Sandbox-specific metadata or extension adaptation. - -## Install - -```bash -npm install acp-http-client -``` - -## Usage - -```ts -import { AcpHttpClient } from "acp-http-client"; - -const client = new AcpHttpClient({ - baseUrl: "http://127.0.0.1:2468", - token: process.env.SANDBOX_TOKEN, -}); - -await client.initialize({ - _meta: { - "sandboxagent.dev": { - agent: "mock", - }, - }, -}); - -const session = await client.newSession({ - cwd: "/", - mcpServers: [], - _meta: { - "sandboxagent.dev": { - agent: "mock", - }, - }, -}); - -const result = await client.prompt({ - sessionId: session.sessionId, - prompt: [{ type: "text", text: "hello" }], -}); - -console.log(result.stopReason); -await client.disconnect(); -``` - -## Scope - -- Implements ACP HTTP transport and connection lifecycle. -- Supports ACP requests/notifications and session streaming. -- Does not inject `_meta["sandboxagent.dev"]`. -- Does not wrap `_sandboxagent/*` extension methods/events. - -## Transport Contract - -- `POST /v2/rpc` is JSON-only. Send `Content-Type: application/json` and `Accept: application/json`. -- `GET /v2/rpc` is SSE-only. Send `Accept: text/event-stream`. -- Keep one active SSE stream per ACP connection id. -- `x-acp-agent` is removed. Provide agent via `_meta["sandboxagent.dev"].agent` on `initialize` and `session/new`. - -If you want Sandbox Agent metadata/extensions and higher-level helpers, use `sandbox-agent` and `SandboxAgentClient` instead. diff --git a/docs/agent-sessions.mdx b/docs/agent-sessions.mdx index ac29b9f..0154537 100644 --- a/docs/agent-sessions.mdx +++ b/docs/agent-sessions.mdx @@ -21,10 +21,7 @@ const sdk = await SandboxAgent.connect({ const session = await sdk.createSession({ agent: "codex", - sessionInit: { - cwd: "/", - mcpServers: [], - }, + cwd: "/", }); console.log(session.id, session.agentSessionId); @@ -54,6 +51,108 @@ await session.prompt([ unsubscribe(); ``` +### Event types + +Each event's `payload` contains a session update. The `sessionUpdate` field identifies the type. + + + +Streamed text or content from the agent's response. + +```json +{ + "sessionUpdate": "agent_message_chunk", + "content": { "type": "text", "text": "Here's how the repository is structured..." } +} +``` + + + +Internal reasoning from the agent (chain-of-thought / extended thinking). + +```json +{ + "sessionUpdate": "agent_thought_chunk", + "content": { "type": "text", "text": "I should start by looking at the project structure..." } +} +``` + + + +Echo of the user's prompt being processed. + +```json +{ + "sessionUpdate": "user_message_chunk", + "content": { "type": "text", "text": "Summarize the repository structure." } +} +``` + + + +The agent invoked a tool (file edit, terminal command, etc.). + +```json +{ + "sessionUpdate": "tool_call", + "toolCallId": "tc_abc123", + "title": "Read file", + "status": "in_progress", + "rawInput": { "path": "/src/index.ts" } +} +``` + + + +Progress or result update for an in-progress tool call. + +```json +{ + "sessionUpdate": "tool_call_update", + "toolCallId": "tc_abc123", + "status": "completed", + "content": [{ "type": "text", "text": "import express from 'express';\n..." }] +} +``` + + + +The agent's execution plan for the current task. + +```json +{ + "sessionUpdate": "plan", + "entries": [ + { "content": "Read the project structure", "status": "completed" }, + { "content": "Identify main entrypoints", "status": "in_progress" }, + { "content": "Write summary", "status": "pending" } + ] +} +``` + + + +Token usage metrics for the current turn. + +```json +{ + "sessionUpdate": "usage_update" +} +``` + + + +Session metadata changed (e.g. agent-generated title). + +```json +{ + "sessionUpdate": "session_info_update", + "title": "Repository structure analysis" +} +``` + + + ## Fetch persisted event history ```ts @@ -82,9 +181,88 @@ if (sessions.items.length > 0) { } ``` +## Configure model, mode, and thought level + +Set the model, mode, or thought level on a session at creation time or after: + +```ts +// At creation time +const session = await sdk.createSession({ + agent: "codex", + model: "gpt-5.3-codex", + mode: "auto", + thoughtLevel: "high", +}); +``` + +```ts +// After creation +await session.setModel("gpt-5.2-codex"); +await session.setMode("full-access"); +await session.setThoughtLevel("medium"); +``` + +Query available modes: + +```ts +const modes = await session.getModes(); +console.log(modes?.currentModeId, modes?.availableModes); +``` + +### Advanced config options + +For config options beyond model, mode, and thought level, use `getConfigOptions` to discover what the agent supports and `setConfigOption` to set any option by ID: + +```ts +const options = await session.getConfigOptions(); +for (const opt of options) { + console.log(opt.id, opt.category, opt.type); +} +``` + +```ts +await session.setConfigOption("some-agent-option", "value"); +``` + +## Handle permission requests + +For agents that request tool-use permissions, register a permission listener and reply with `once`, `always`, or `reject`: + +```ts +const session = await sdk.createSession({ + agent: "claude", + mode: "default", +}); + +session.onPermissionRequest((request) => { + console.log(request.toolCall.title, request.availableReplies); + void session.respondPermission(request.id, "once"); +}); + +await session.prompt([ + { type: "text", text: "Create ./permission-example.txt with the text hello." }, +]); +``` + + +### Auto-approving permissions + +To auto-approve all permission requests, respond with `"once"` or `"always"` in your listener: + +```ts +session.onPermissionRequest((request) => { + void session.respondPermission(request.id, "always"); +}); +``` + +See `examples/permissions/src/index.ts` for a complete permissions example that works with Claude and Codex. + + +Some agents like Claude allow configuring permission behavior through modes (e.g. `bypassPermissions`, `acceptEdits`). We recommend leaving the mode as `default` and handling permission decisions explicitly in `onPermissionRequest` instead. + + ## Destroy a session ```ts await sdk.destroySession(session.id); ``` - diff --git a/docs/agents/amp.mdx b/docs/agents/amp.mdx new file mode 100644 index 0000000..f94e97d --- /dev/null +++ b/docs/agents/amp.mdx @@ -0,0 +1,20 @@ +--- +title: "Amp" +description: "Use Amp as a sandbox agent." +--- + +## Usage + +```typescript +const session = await client.createSession({ + agent: "amp", +}); +``` + +## Capabilities + +| Category | Values | +|----------|--------| +| **Models** | `amp-default` | +| **Modes** | `default`, `bypass` | +| **Thought levels** | Unsupported | diff --git a/docs/agents/claude.mdx b/docs/agents/claude.mdx new file mode 100644 index 0000000..2e4fd43 --- /dev/null +++ b/docs/agents/claude.mdx @@ -0,0 +1,49 @@ +--- +title: "Claude" +description: "Use Claude Code as a sandbox agent." +--- + +## Usage + +```typescript +const session = await client.createSession({ + agent: "claude", +}); +``` + +## Capabilities + +| Category | Values | +|----------|--------| +| **Models** | `default`, `sonnet`, `opus`, `haiku` | +| **Modes** | `default`, `acceptEdits`, `plan`, `dontAsk`, `bypassPermissions` | +| **Thought levels** | Unsupported | + +## Configuring effort level + +Claude does not support changing effort level after a session starts. Configure it in the filesystem before creating the session. + +```ts +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const cwd = "/path/to/workspace"; +await mkdir(path.join(cwd, ".claude"), { recursive: true }); +await writeFile( + path.join(cwd, ".claude", "settings.json"), + JSON.stringify({ effortLevel: "high" }, null, 2), +); + +const session = await client.createSession({ + agent: "claude", + cwd, +}); +``` + + + +1. `~/.claude/settings.json` +2. `/.claude/settings.json` +3. `/.claude/settings.local.json` + + diff --git a/docs/agents/codex.mdx b/docs/agents/codex.mdx new file mode 100644 index 0000000..d359beb --- /dev/null +++ b/docs/agents/codex.mdx @@ -0,0 +1,20 @@ +--- +title: "Codex" +description: "Use OpenAI Codex as a sandbox agent." +--- + +## Usage + +```typescript +const session = await client.createSession({ + agent: "codex", +}); +``` + +## Capabilities + +| Category | Values | +|----------|--------| +| **Models** | `gpt-5.3-codex` (default), `gpt-5.3-codex-spark`, `gpt-5.2-codex`, `gpt-5.1-codex-max`, `gpt-5.2`, `gpt-5.1-codex-mini` | +| **Modes** | `read-only` (default), `auto`, `full-access` | +| **Thought levels** | `low`, `medium`, `high` (default), `xhigh` | diff --git a/docs/agents/cursor.mdx b/docs/agents/cursor.mdx new file mode 100644 index 0000000..0905baa --- /dev/null +++ b/docs/agents/cursor.mdx @@ -0,0 +1,34 @@ +--- +title: "Cursor" +description: "Use Cursor as a sandbox agent." +--- + +## Usage + +```typescript +const session = await client.createSession({ + agent: "cursor", +}); +``` + +## Capabilities + +| Category | Values | +|----------|--------| +| **Models** | See below | +| **Modes** | Unsupported | +| **Thought levels** | Unsupported | + + + +| Group | Models | +|-------|--------| +| **Auto** | `auto` | +| **Composer** | `composer-1.5`, `composer-1` | +| **GPT-5.3 Codex** | `gpt-5.3-codex`, `gpt-5.3-codex-low`, `gpt-5.3-codex-high`, `gpt-5.3-codex-xhigh`, `gpt-5.3-codex-fast`, `gpt-5.3-codex-low-fast`, `gpt-5.3-codex-high-fast`, `gpt-5.3-codex-xhigh-fast` | +| **GPT-5.2** | `gpt-5.2`, `gpt-5.2-high`, `gpt-5.2-codex`, `gpt-5.2-codex-low`, `gpt-5.2-codex-high`, `gpt-5.2-codex-xhigh`, `gpt-5.2-codex-fast`, `gpt-5.2-codex-low-fast`, `gpt-5.2-codex-high-fast`, `gpt-5.2-codex-xhigh-fast` | +| **GPT-5.1** | `gpt-5.1-high`, `gpt-5.1-codex-max`, `gpt-5.1-codex-max-high` | +| **Claude** | `opus-4.6-thinking` (default), `opus-4.6`, `opus-4.5`, `opus-4.5-thinking`, `sonnet-4.5`, `sonnet-4.5-thinking` | +| **Other** | `gemini-3-pro`, `gemini-3-flash`, `grok` | + + diff --git a/docs/agents/opencode.mdx b/docs/agents/opencode.mdx new file mode 100644 index 0000000..db7b640 --- /dev/null +++ b/docs/agents/opencode.mdx @@ -0,0 +1,31 @@ +--- +title: "OpenCode" +description: "Use OpenCode as a sandbox agent." +--- + +## Usage + +```typescript +const session = await client.createSession({ + agent: "opencode", +}); +``` + +## Capabilities + +| Category | Values | +|----------|--------| +| **Models** | See below | +| **Modes** | `build` (default), `plan` | +| **Thought levels** | Unsupported | + + + +| Provider | Models | +|----------|--------| +| **Anthropic** | `anthropic/claude-3-5-haiku-20241022`, `anthropic/claude-3-5-haiku-latest`, `anthropic/claude-3-5-sonnet-20240620`, `anthropic/claude-3-5-sonnet-20241022`, `anthropic/claude-3-7-sonnet-20250219`, `anthropic/claude-3-7-sonnet-latest`, `anthropic/claude-3-haiku-20240307`, `anthropic/claude-3-opus-20240229`, `anthropic/claude-3-sonnet-20240229`, `anthropic/claude-haiku-4-5`, `anthropic/claude-haiku-4-5-20251001`, `anthropic/claude-opus-4-0`, `anthropic/claude-opus-4-1`, `anthropic/claude-opus-4-1-20250805`, `anthropic/claude-opus-4-20250514`, `anthropic/claude-opus-4-5`, `anthropic/claude-opus-4-5-20251101`, `anthropic/claude-opus-4-6`, `anthropic/claude-sonnet-4-0`, `anthropic/claude-sonnet-4-20250514`, `anthropic/claude-sonnet-4-5`, `anthropic/claude-sonnet-4-5-20250929` | +| **OpenAI** | `openai/gpt-5.1-codex`, `openai/gpt-5.1-codex-max`, `openai/gpt-5.1-codex-mini`, `openai/gpt-5.2`, `openai/gpt-5.2-codex`, `openai/gpt-5.3-codex` | +| **Cerebras** | `cerebras/gpt-oss-120b`, `cerebras/qwen-3-235b-a22b-instruct-2507`, `cerebras/zai-glm-4.7` | +| **OpenCode Zen** | `opencode/big-pickle`, `opencode/claude-3-5-haiku`, `opencode/claude-haiku-4-5`, `opencode/claude-opus-4-1`, `opencode/claude-opus-4-5`, `opencode/claude-opus-4-6`, `opencode/claude-sonnet-4`, `opencode/claude-sonnet-4-5`, `opencode/gemini-3-flash`, `opencode/gemini-3-pro` (default), `opencode/glm-4.6`, `opencode/glm-4.7`, `opencode/gpt-5`, `opencode/gpt-5-codex`, `opencode/gpt-5-nano`, `opencode/gpt-5.1`, `opencode/gpt-5.1-codex`, `opencode/gpt-5.1-codex-max`, `opencode/gpt-5.1-codex-mini`, `opencode/gpt-5.2`, `opencode/gpt-5.2-codex`, `opencode/kimi-k2`, `opencode/kimi-k2-thinking`, `opencode/kimi-k2.5`, `opencode/kimi-k2.5-free`, `opencode/minimax-m2.1`, `opencode/minimax-m2.1-free`, `opencode/trinity-large-preview-free` | + + diff --git a/docs/agents/pi.mdx b/docs/agents/pi.mdx new file mode 100644 index 0000000..1d56370 --- /dev/null +++ b/docs/agents/pi.mdx @@ -0,0 +1,20 @@ +--- +title: "Pi" +description: "Use Pi as a sandbox agent." +--- + +## Usage + +```typescript +const session = await client.createSession({ + agent: "pi", +}); +``` + +## Capabilities + +| Category | Values | +|----------|--------| +| **Models** | `default` | +| **Modes** | Unsupported | +| **Thought levels** | Unsupported | diff --git a/docs/ai/llms-txt.mdx b/docs/ai/llms-txt.mdx index ad6fa8b..e5aaf8d 100644 --- a/docs/ai/llms-txt.mdx +++ b/docs/ai/llms-txt.mdx @@ -8,8 +8,8 @@ Mintlify publishes `llms.txt` and `llms-full.txt` for this documentation site. Access them at: ``` -https://rivet.dev/docs/llms.txt -https://rivet.dev/docs/llms-full.txt +https://sandboxagent.dev/docs/llms.txt +https://sandboxagent.dev/docs/llms-full.txt ``` If you run a reverse proxy in front of the docs, forward `/llms.txt` and `/llms-full.txt` to Mintlify. diff --git a/docs/ai/skill.mdx b/docs/ai/skill.mdx index b06e657..105026c 100644 --- a/docs/ai/skill.mdx +++ b/docs/ai/skill.mdx @@ -8,7 +8,7 @@ Mintlify hosts a `skill.md` file for this documentation site. Access it at: ``` -https://rivet.dev/docs/skill.md +https://sandboxagent.dev/docs/skill.md ``` To add it to an agent using the Skills CLI: diff --git a/docs/architecture.mdx b/docs/architecture.mdx index 78585a2..61b4689 100644 --- a/docs/architecture.mdx +++ b/docs/architecture.mdx @@ -1,64 +1,63 @@ --- title: "Architecture" -description: "How the client, sandbox, server, and agent fit together." -icon: "microchip" +description: "How the Sandbox Agent server, SDK, and agent processes fit together." --- -Sandbox Agent runs as an HTTP server inside your sandbox. Your app talks to it remotely. +Sandbox Agent is a lightweight HTTP server that runs **inside** a sandbox. It: + +- **Agent management**: Installs, spawns, and stops coding agent processes +- **Sessions**: Routes prompts to agents and streams events back in real time +- **Sandbox APIs**: Filesystem, process, and terminal access for the sandbox environment ## Components -- `Your client`: your app code using the `sandbox-agent` SDK. -- `Sandbox`: isolated runtime (E2B, Daytona, Docker, etc.). -- `Sandbox Agent server`: process inside the sandbox exposing HTTP transport. -- `Agent`: Claude/Codex/OpenCode/Amp process managed by Sandbox Agent. - -```mermaid placement="top-right" - flowchart LR - CLIENT["Sandbox Agent SDK"] - SERVER["Sandbox Agent server"] - AGENT["Agent process"] +```mermaid +flowchart LR + CLIENT["Your App"] subgraph SANDBOX["Sandbox"] - direction TB - SERVER --> AGENT + direction TB + SERVER["Sandbox Agent Server"] + AGENT["Agent Process
(Claude, Codex, etc.)"] + SERVER --> AGENT end - CLIENT -->|HTTP| SERVER + CLIENT -->|"SDK (HTTP)"| SERVER ``` -## Suggested Topology +- **Your app**: Uses the `sandbox-agent` TypeScript SDK to talk to the server over HTTP. +- **Sandbox**: An isolated runtime (local process, Docker, E2B, Daytona, Vercel, Cloudflare). +- **Sandbox Agent server**: A single binary inside the sandbox that manages agent lifecycles, routes prompts, streams events, and exposes filesystem/process/terminal APIs. +- **Agent process**: A coding agent (Claude Code, Codex, etc.) spawned by the server. Each session maps to one agent process. -Run the SDK on your backend, then call it from your frontend. +## What `SandboxAgent.start()` does -This extra hop is recommended because it keeps auth/token logic on the backend and makes persistence simpler. +1. **Provision**: The provider creates a sandbox (starts a container, creates a VM, etc.) +2. **Install**: The Sandbox Agent binary is installed inside the sandbox +3. **Boot**: The server starts listening on an HTTP port +4. **Health check**: The SDK waits for `/v1/health` to respond +5. **Ready**: The SDK returns a connected client -```mermaid placement="top-right" - flowchart LR - BROWSER["Browser"] - subgraph BACKEND["Your backend"] - direction TB - SDK["Sandbox Agent SDK"] - end - subgraph SANDBOX_SIMPLE["Sandbox"] - SERVER_SIMPLE["Sandbox Agent server"] - end +For the `local` provider, provisioning is a no-op and the server runs as a local subprocess. - BROWSER --> BACKEND - BACKEND --> SDK --> SERVER_SIMPLE +### Server recovery + +If the server process stops, the SDK automatically calls the provider's `ensureServer()` after 3 consecutive health-check failures. Most built-in providers implement this. Custom providers can add `ensureServer(sandboxId)` to their `SandboxProvider` object. + +## Server HTTP API + +See the [HTTP API reference](/api-reference) for the full list of server endpoints. + +## Agent installation + +Agents are installed lazily on first use. To avoid the cold-start delay, pre-install them: + +```bash +sandbox-agent install-agent --all ``` -### Backend requirements +The `rivetdev/sandbox-agent:0.4.2-full` Docker image ships with all agents pre-installed. -Your backend layer needs to handle: +## Production-ready agent orchestration -- **Long-running connections**: prompts can take minutes. -- **Session affinity**: follow-up messages must reach the same session. -- **State between requests**: session metadata and event history must persist across requests. -- **Graceful recovery**: sessions should resume after backend restarts. - -We recommend [Rivet](https://rivet.dev) over serverless because actors natively support the long-lived connections, session routing, and state persistence that agent workloads require. - -## Session persistence - -For storage driver options and replay behavior, see [Persisting Sessions](/session-persistence). +For production deployments, see [Orchestration Architecture](/orchestration-architecture) for recommended topology, backend requirements, and session persistence patterns. diff --git a/docs/attachments.mdx b/docs/attachments.mdx index dcfb412..8deac05 100644 --- a/docs/attachments.mdx +++ b/docs/attachments.mdx @@ -58,4 +58,4 @@ Use the filesystem API to upload files, then include file references in prompt c - Use absolute file URIs in `resource_link` blocks. - If `mimeType` is omitted, the agent/runtime may infer a default. -- Support for non-text resources depends on each agent's ACP prompt capabilities. +- Support for non-text resources depends on each agent's prompt capabilities. diff --git a/docs/building-chat-ui.mdx b/docs/building-chat-ui.mdx deleted file mode 100644 index da706ff..0000000 --- a/docs/building-chat-ui.mdx +++ /dev/null @@ -1,370 +0,0 @@ ---- -title: "Building a Chat UI" -description: "Build a chat interface using the universal event stream." -icon: "comments" ---- - -## Setup - -### List agents - -```ts -const { agents } = await client.listAgents(); - -// Each agent exposes feature coverage via `capabilities` to determine what UI to show -const claude = agents.find((a) => a.id === "claude"); -if (claude?.capabilities.permissions) { - // Show permission approval UI -} -if (claude?.capabilities.questions) { - // Show question response UI -} -``` - -### Create a session - -```ts -const sessionId = `session-${crypto.randomUUID()}`; - -await client.createSession(sessionId, { - agent: "claude", - agentMode: "code", // Optional: agent-specific mode - permissionMode: "default", // Optional: "default" | "plan" | "bypass" | "acceptEdits" (Claude: accept edits; Codex: auto-approve file changes; others: default) - model: "claude-sonnet-4", // Optional: model override -}); -``` - -### Send a message - -```ts -await client.postMessage(sessionId, { message: "Hello, world!" }); -``` - -### Stream events - -Three options for receiving events: - -```ts -// Option 1: SSE (recommended for real-time UI) -const stream = client.streamEvents(sessionId, { offset: 0 }); -for await (const event of stream) { - handleEvent(event); -} - -// Option 2: Polling -const { events, hasMore } = await client.getEvents(sessionId, { offset: 0 }); -events.forEach(handleEvent); - -// Option 3: Turn streaming (send + stream in one call) -const stream = client.streamTurn(sessionId, { message: "Hello" }); -for await (const event of stream) { - handleEvent(event); -} -``` - -Use `offset` to track the last seen `sequence` number and resume from where you left off. - ---- - -## Handling Events - -### Bare minimum - -Handle item lifecycle plus turn lifecycle to render a basic chat: - -```ts -type ItemState = { - item: UniversalItem; - deltas: string[]; -}; - -const items = new Map(); -let turnInProgress = false; - -function handleEvent(event: UniversalEvent) { - switch (event.type) { - case "turn.started": { - turnInProgress = true; - break; - } - - case "turn.ended": { - turnInProgress = false; - break; - } - - case "item.started": { - const { item } = event.data as ItemEventData; - items.set(item.item_id, { item, deltas: [] }); - break; - } - - case "item.delta": { - const { item_id, delta } = event.data as ItemDeltaData; - const state = items.get(item_id); - if (state) { - state.deltas.push(delta); - } - break; - } - - case "item.completed": { - const { item } = event.data as ItemEventData; - const state = items.get(item.item_id); - if (state) { - state.item = item; - state.deltas = []; // Clear deltas, use final content - } - break; - } - } -} -``` - -When rendering: -- Use `turnInProgress` for turn-level UI state (disable send button, show global "Agent is responding", etc.). -- Use `item.status === "in_progress"` for per-item streaming state. - -```ts -function renderItem(state: ItemState) { - const { item, deltas } = state; - const isItemLoading = item.status === "in_progress"; - - // For streaming text, combine item content with accumulated deltas - const text = item.content - .filter((p) => p.type === "text") - .map((p) => p.text) - .join(""); - const streamedText = text + deltas.join(""); - - return { - content: streamedText, - isItemLoading, - isTurnLoading: turnInProgress, - role: item.role, - kind: item.kind, - }; -} -``` - -### Extra events - -Handle these for a complete implementation: - -```ts -function handleEvent(event: UniversalEvent) { - switch (event.type) { - // ... bare minimum events above ... - - case "session.started": { - // Session is ready - break; - } - - case "session.ended": { - const { reason, terminated_by } = event.data as SessionEndedData; - // Disable input, show end reason - // reason: "completed" | "error" | "terminated" - // terminated_by: "agent" | "daemon" - break; - } - - case "error": { - const { message, code } = event.data as ErrorData; - // Display error to user - break; - } - - case "agent.unparsed": { - const { error, location } = event.data as AgentUnparsedData; - // Parsing failure - treat as bug in development - console.error(`Parse error at ${location}: ${error}`); - break; - } - } -} -``` - -### Content parts - -Each item has `content` parts. Render based on `type`: - -```ts -function renderContentPart(part: ContentPart) { - switch (part.type) { - case "text": - return {part.text}; - - case "tool_call": - return ; - - case "tool_result": - return ; - - case "file_ref": - return ; - - case "reasoning": - return {part.text}; - - case "status": - return ; - - case "image": - return ; - } -} -``` - ---- - -## Handling Permissions - -When `permission.requested` arrives, show an approval UI: - -```ts -const pendingPermissions = new Map(); - -function handleEvent(event: UniversalEvent) { - if (event.type === "permission.requested") { - const data = event.data as PermissionEventData; - pendingPermissions.set(data.permission_id, data); - } - - if (event.type === "permission.resolved") { - const data = event.data as PermissionEventData; - pendingPermissions.delete(data.permission_id); - } -} - -// User clicks approve/deny -async function replyPermission(id: string, reply: "once" | "always" | "reject") { - await client.replyPermission(sessionId, id, { reply }); - pendingPermissions.delete(id); -} -``` - -Render permission requests: - -```ts -function PermissionRequest({ data }: { data: PermissionEventData }) { - return ( -

-

Allow: {data.action}

- - - -
- ); -} -``` - ---- - -## Handling Questions - -When `question.requested` arrives, show a selection UI: - -```ts -const pendingQuestions = new Map(); - -function handleEvent(event: UniversalEvent) { - if (event.type === "question.requested") { - const data = event.data as QuestionEventData; - pendingQuestions.set(data.question_id, data); - } - - if (event.type === "question.resolved") { - const data = event.data as QuestionEventData; - pendingQuestions.delete(data.question_id); - } -} - -// User selects answer(s) -async function answerQuestion(id: string, answers: string[][]) { - await client.replyQuestion(sessionId, id, { answers }); - pendingQuestions.delete(id); -} - -async function rejectQuestion(id: string) { - await client.rejectQuestion(sessionId, id); - pendingQuestions.delete(id); -} -``` - -Render question requests: - -```ts -function QuestionRequest({ data }: { data: QuestionEventData }) { - const [selected, setSelected] = useState([]); - - return ( -
-

{data.prompt}

- {data.options.map((option) => ( - - ))} - - -
- ); -} -``` - ---- - -## Testing with Mock Agent - -The `mock` agent lets you test UI behaviors without external credentials: - -```ts -await client.createSession("test-session", { agent: "mock" }); -``` - -Send `help` to see available commands: - -| Command | Tests | -|---------|-------| -| `help` | Lists all commands | -| `demo` | Full UI coverage sequence with markers | -| `markdown` | Streaming markdown rendering | -| `tool` | Tool call + result with file refs | -| `status` | Status item updates | -| `image` | Image content part | -| `permission` | Permission request flow | -| `question` | Question request flow | -| `error` | Error + unparsed events | -| `end` | Session ended event | -| `echo ` | Echo text as assistant message | - -Any unrecognized text is echoed back as an assistant message. - ---- - -## Reference Implementation - -The [Inspector UI](https://github.com/rivet-dev/sandbox-agent/blob/main/frontend/packages/inspector/src/App.tsx) -is a complete reference showing session management, event rendering, and HITL flows. diff --git a/docs/cli.mdx b/docs/cli.mdx index 9472a5e..362de49 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -37,24 +37,91 @@ Notes: - Set `SANDBOX_AGENT_LOG_STDOUT=1` to force stdout/stderr logging. - Use `SANDBOX_AGENT_LOG_DIR` to override log directory. -## install-agent +## install -Install or reinstall a single agent. +Install first-party runtime dependencies. + +### install desktop + +Install the Linux desktop runtime packages required by `/v1/desktop/*`. ```bash -sandbox-agent install-agent [OPTIONS] +sandbox-agent install desktop [OPTIONS] ``` | Option | Description | |--------|-------------| +| `--yes` | Skip the confirmation prompt | +| `--print-only` | Print the package-manager command without executing it | +| `--package-manager ` | Override package-manager detection | +| `--no-fonts` | Skip the default DejaVu font package | + +```bash +sandbox-agent install desktop --yes +sandbox-agent install desktop --print-only +``` + +Notes: + +- Supported on Linux only. +- The command detects `apt`, `dnf`, or `apk`. +- If the host is not already running as root, the command requires `sudo`. + +## install-agent + +Install or reinstall a single agent, or every supported agent with `--all`. + +```bash +sandbox-agent install-agent [] [OPTIONS] +``` + +| Option | Description | +|--------|-------------| +| `--all` | Install every supported agent | | `-r, --reinstall` | Force reinstall | -| `--agent-version ` | Override agent package version | -| `--agent-process-version ` | Override agent process version | +| `--agent-version ` | Override agent package version (conflicts with `--all`) | +| `--agent-process-version ` | Override agent process version (conflicts with `--all`) | + +Examples: ```bash sandbox-agent install-agent claude --reinstall +sandbox-agent install-agent --all ``` +### Custom Pi implementation path + +If you use a forked/custom `pi` binary with `pi-acp`, you can override what executable gets launched. + +#### Option 1: explicit command override (recommended) + +Set `PI_ACP_PI_COMMAND` in the environment where `sandbox-agent` runs: + +```bash +PI_ACP_PI_COMMAND=/absolute/path/to/your/pi-fork sandbox-agent server +``` + +This is forwarded to `pi-acp`, which uses it instead of looking up `pi` on `PATH`. + +#### Option 2: PATH override + +Put your custom `pi` first on `PATH` before starting `sandbox-agent`: + +```bash +export PATH="/path/to/custom-pi-dir:$PATH" +sandbox-agent server +``` + +#### Option 3: symlink override + +Point `pi` to your custom binary via symlink in a directory that is early on `PATH`: + +```bash +ln -sf /absolute/path/to/your/pi-fork /usr/local/bin/pi +``` + +Then start `sandbox-agent` normally. + ## opencode (experimental) Start/reuse daemon and run `opencode attach` against `/opencode`. @@ -167,6 +234,65 @@ Shared option: ```bash sandbox-agent api agents list [--endpoint ] +sandbox-agent api agents report [--endpoint ] sandbox-agent api agents install [--reinstall] [--endpoint ] ``` +#### api agents list + +List all agents and their install status. + +```bash +sandbox-agent api agents list +``` + +#### api agents report + +Emit a JSON report of available models, modes, and thought levels for every agent, grouped by category. + +```bash +sandbox-agent api agents report --endpoint http://127.0.0.1:2468 | jq . +``` + +Example output: + +```json +{ + "generatedAtMs": 1740000000000, + "endpoint": "http://127.0.0.1:2468", + "agents": [ + { + "id": "claude", + "installed": true, + "models": { + "currentValue": "default", + "values": [ + { "value": "default", "name": "Default" }, + { "value": "sonnet", "name": "Sonnet" }, + { "value": "opus", "name": "Opus" }, + { "value": "haiku", "name": "Haiku" } + ] + }, + "modes": { + "currentValue": "default", + "values": [ + { "value": "default", "name": "Default" }, + { "value": "acceptEdits", "name": "Accept Edits" }, + { "value": "plan", "name": "Plan" }, + { "value": "dontAsk", "name": "Don't Ask" }, + { "value": "bypassPermissions", "name": "Bypass Permissions" } + ] + }, + "thoughtLevels": { "values": [] } + } + ] +} +``` + +See individual agent pages (e.g. [Claude](/agents/claude), [Codex](/agents/codex)) for supported models, modes, and thought levels. + +#### api agents install + +```bash +sandbox-agent api agents install codex --reinstall +``` diff --git a/docs/common-software.mdx b/docs/common-software.mdx new file mode 100644 index 0000000..7997a92 --- /dev/null +++ b/docs/common-software.mdx @@ -0,0 +1,560 @@ +--- +title: "Common Software" +description: "Install browsers, languages, databases, and other tools inside the sandbox." +sidebarTitle: "Common Software" +icon: "box-open" +--- + +The sandbox runs a Debian/Ubuntu base image. You can install software with `apt-get` via the [Process API](/processes) or by customizing your Docker image. This page covers commonly needed packages and how to install them. + +## Browsers + +### Chromium + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "chromium", "chromium-sandbox"], +}); + +// Launch headless +await sdk.runProcess({ + command: "chromium", + args: ["--headless", "--no-sandbox", "--disable-gpu", "https://example.com"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","chromium","chromium-sandbox"]}' +``` + + + +Use `--no-sandbox` when running Chromium inside a container. The container itself provides isolation. + + +### Firefox + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "firefox-esr"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","firefox-esr"]}' +``` + + +### Playwright browsers + +Playwright bundles its own browser binaries. Install the Playwright CLI and let it download browsers for you. + + +```ts TypeScript +await sdk.runProcess({ + command: "npx", + args: ["playwright", "install", "--with-deps", "chromium"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"npx","args":["playwright","install","--with-deps","chromium"]}' +``` + + +--- + +## Languages and runtimes + +### Node.js + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "nodejs", "npm"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","nodejs","npm"]}' +``` + + +For a specific version, use [nvm](https://github.com/nvm-sh/nvm): + +```ts TypeScript +await sdk.runProcess({ + command: "bash", + args: ["-c", "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && . ~/.nvm/nvm.sh && nvm install 22"], +}); +``` + +### Python + +Python 3 is typically pre-installed. To add pip and common packages: + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "python3", "python3-pip", "python3-venv"], +}); + +await sdk.runProcess({ + command: "pip3", + args: ["install", "numpy", "pandas", "matplotlib"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","python3","python3-pip","python3-venv"]}' + +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"pip3","args":["install","numpy","pandas","matplotlib"]}' +``` + + +### Go + + +```ts TypeScript +await sdk.runProcess({ + command: "bash", + args: ["-c", "curl -fsSL https://go.dev/dl/go1.23.6.linux-amd64.tar.gz | tar -C /usr/local -xz"], +}); + +// Add to PATH for subsequent commands +await sdk.runProcess({ + command: "bash", + args: ["-c", "export PATH=$PATH:/usr/local/go/bin && go version"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"bash","args":["-c","curl -fsSL https://go.dev/dl/go1.23.6.linux-amd64.tar.gz | tar -C /usr/local -xz"]}' +``` + + +### Rust + + +```ts TypeScript +await sdk.runProcess({ + command: "bash", + args: ["-c", "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"bash","args":["-c","curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"]}' +``` + + +### Java (OpenJDK) + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "default-jdk"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","default-jdk"]}' +``` + + +### Ruby + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "ruby-full"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","ruby-full"]}' +``` + + +--- + +## Databases + +### PostgreSQL + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "postgresql", "postgresql-client"], +}); + +// Start the service +const proc = await sdk.createProcess({ + command: "bash", + args: ["-c", "su - postgres -c 'pg_ctlcluster 15 main start'"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","postgresql","postgresql-client"]}' +``` + + +### SQLite + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "sqlite3"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","sqlite3"]}' +``` + + +### Redis + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "redis-server"], +}); + +const proc = await sdk.createProcess({ + command: "redis-server", + args: ["--daemonize", "no"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","redis-server"]}' + +curl -X POST "http://127.0.0.1:2468/v1/processes" \ + -H "Content-Type: application/json" \ + -d '{"command":"redis-server","args":["--daemonize","no"]}' +``` + + +### MySQL / MariaDB + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "mariadb-server", "mariadb-client"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","mariadb-server","mariadb-client"]}' +``` + + +--- + +## Build tools + +### Essential build toolchain + +Most compiled software needs the standard build toolchain: + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "build-essential", "cmake", "pkg-config"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","build-essential","cmake","pkg-config"]}' +``` + + +This installs `gcc`, `g++`, `make`, `cmake`, and related tools. + +--- + +## Desktop applications + +These require the [Computer Use](/computer-use) desktop to be started first. + +### LibreOffice + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "libreoffice"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","libreoffice"]}' +``` + + +### GIMP + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "gimp"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","gimp"]}' +``` + + +### VLC + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "vlc"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","vlc"]}' +``` + + +### VS Code (code-server) + + +```ts TypeScript +await sdk.runProcess({ + command: "bash", + args: ["-c", "curl -fsSL https://code-server.dev/install.sh | sh"], +}); + +const proc = await sdk.createProcess({ + command: "code-server", + args: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"bash","args":["-c","curl -fsSL https://code-server.dev/install.sh | sh"]}' + +curl -X POST "http://127.0.0.1:2468/v1/processes" \ + -H "Content-Type: application/json" \ + -d '{"command":"code-server","args":["--bind-addr","0.0.0.0:8080","--auth","none"]}' +``` + + +--- + +## CLI tools + +### Git + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "git"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","git"]}' +``` + + +### Docker + + +```ts TypeScript +await sdk.runProcess({ + command: "bash", + args: ["-c", "curl -fsSL https://get.docker.com | sh"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"bash","args":["-c","curl -fsSL https://get.docker.com | sh"]}' +``` + + +### jq + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "jq"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","jq"]}' +``` + + +### tmux + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "tmux"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","tmux"]}' +``` + + +--- + +## Media and graphics + +### FFmpeg + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "ffmpeg"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","ffmpeg"]}' +``` + + +### ImageMagick + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "imagemagick"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","imagemagick"]}' +``` + + +### Poppler (PDF utilities) + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "poppler-utils"], +}); + +// Convert PDF to images +await sdk.runProcess({ + command: "pdftoppm", + args: ["-png", "document.pdf", "output"], +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","poppler-utils"]}' +``` + + +--- + +## Pre-installing in a Docker image + +For production use, install software in your Dockerfile instead of at runtime. This avoids repeated downloads and makes startup faster. + +```dockerfile +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y \ + chromium \ + firefox-esr \ + nodejs npm \ + python3 python3-pip \ + git curl wget \ + build-essential \ + sqlite3 \ + ffmpeg \ + imagemagick \ + jq \ + && rm -rf /var/lib/apt/lists/* + +RUN pip3 install numpy pandas matplotlib +``` + +See [Docker deployment](/deploy/docker) for how to use custom images with Sandbox Agent. diff --git a/docs/computer-use.mdx b/docs/computer-use.mdx new file mode 100644 index 0000000..fc6b7d0 --- /dev/null +++ b/docs/computer-use.mdx @@ -0,0 +1,859 @@ +--- +title: "Computer Use" +description: "Control a virtual desktop inside the sandbox with mouse, keyboard, screenshots, recordings, and live streaming." +sidebarTitle: "Computer Use" +icon: "desktop" +--- + +Sandbox Agent provides a managed virtual desktop (Xvfb + openbox) that you can control programmatically. This is useful for browser automation, GUI testing, and AI computer-use workflows. + +## Start and stop + + +```ts TypeScript +import { SandboxAgent } from "sandbox-agent"; + +const sdk = await SandboxAgent.connect({ + baseUrl: "http://127.0.0.1:2468", +}); + +const status = await sdk.startDesktop({ + width: 1920, + height: 1080, + dpi: 96, +}); + +console.log(status.state); // "active" +console.log(status.display); // ":99" + +// When done +await sdk.stopDesktop(); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/desktop/start" \ + -H "Content-Type: application/json" \ + -d '{"width":1920,"height":1080,"dpi":96}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/stop" +``` + + +All fields in the start request are optional. Defaults are 1440x900 at 96 DPI. + +### Start request options + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `width` | number | 1440 | Desktop width in pixels | +| `height` | number | 900 | Desktop height in pixels | +| `dpi` | number | 96 | Display DPI | +| `displayNum` | number | 99 | Starting X display number. The runtime probes from this number upward to find an available display. | +| `stateDir` | string | (auto) | Desktop state directory for home, logs, recordings | +| `streamVideoCodec` | string | `"vp8"` | WebRTC video codec (`vp8`, `vp9`, `h264`) | +| `streamAudioCodec` | string | `"opus"` | WebRTC audio codec (`opus`, `g722`) | +| `streamFrameRate` | number | 30 | Streaming frame rate (1-60) | +| `webrtcPortRange` | string | `"59050-59070"` | UDP port range for WebRTC media | +| `recordingFps` | number | 30 | Default recording FPS when not specified in `startDesktopRecording` (1-60) | + +The streaming and recording options configure defaults for the desktop session. They take effect when streaming or recording is started later. + + +```ts TypeScript +const status = await sdk.startDesktop({ + width: 1920, + height: 1080, + streamVideoCodec: "h264", + streamFrameRate: 60, + webrtcPortRange: "59100-59120", + recordingFps: 15, +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/desktop/start" \ + -H "Content-Type: application/json" \ + -d '{ + "width": 1920, + "height": 1080, + "streamVideoCodec": "h264", + "streamFrameRate": 60, + "webrtcPortRange": "59100-59120", + "recordingFps": 15 + }' +``` + + +## Status + + +```ts TypeScript +const status = await sdk.getDesktopStatus(); +console.log(status.state); // "inactive" | "active" | "failed" | ... +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/status" +``` + + +## Screenshots + +Capture the full desktop or a specific region. Optionally include the cursor position. + + +```ts TypeScript +// Full screenshot (PNG by default) +const png = await sdk.takeDesktopScreenshot(); + +// JPEG at 70% quality, half scale +const jpeg = await sdk.takeDesktopScreenshot({ + format: "jpeg", + quality: 70, + scale: 0.5, +}); + +// Include cursor overlay +const withCursor = await sdk.takeDesktopScreenshot({ + showCursor: true, +}); + +// Region screenshot +const region = await sdk.takeDesktopRegionScreenshot({ + x: 100, + y: 100, + width: 400, + height: 300, +}); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/screenshot" --output screenshot.png + +curl "http://127.0.0.1:2468/v1/desktop/screenshot?format=jpeg&quality=70&scale=0.5" \ + --output screenshot.jpg + +# Include cursor overlay +curl "http://127.0.0.1:2468/v1/desktop/screenshot?show_cursor=true" \ + --output with_cursor.png + +curl "http://127.0.0.1:2468/v1/desktop/screenshot/region?x=100&y=100&width=400&height=300" \ + --output region.png +``` + + +### Screenshot options + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `format` | string | `"png"` | Output format: `png`, `jpeg`, or `webp` | +| `quality` | number | 85 | Compression quality (1-100, JPEG/WebP only) | +| `scale` | number | 1.0 | Scale factor (0.1-1.0) | +| `showCursor` | boolean | `false` | Composite a crosshair at the cursor position | + +When `showCursor` is enabled, the cursor position is captured at the moment of the screenshot and a red crosshair is drawn at that location. This is useful for AI agents that need to see where the cursor is in the screenshot. + +## Mouse + + +```ts TypeScript +// Get current position +const pos = await sdk.getDesktopMousePosition(); +console.log(pos.x, pos.y); + +// Move +await sdk.moveDesktopMouse({ x: 500, y: 300 }); + +// Click (left by default) +await sdk.clickDesktop({ x: 500, y: 300 }); + +// Right click +await sdk.clickDesktop({ x: 500, y: 300, button: "right" }); + +// Double click +await sdk.clickDesktop({ x: 500, y: 300, clickCount: 2 }); + +// Drag +await sdk.dragDesktopMouse({ + startX: 100, startY: 100, + endX: 400, endY: 400, +}); + +// Scroll +await sdk.scrollDesktop({ x: 500, y: 300, deltaY: -3 }); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/mouse/position" + +curl -X POST "http://127.0.0.1:2468/v1/desktop/mouse/click" \ + -H "Content-Type: application/json" \ + -d '{"x":500,"y":300}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/mouse/drag" \ + -H "Content-Type: application/json" \ + -d '{"startX":100,"startY":100,"endX":400,"endY":400}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/mouse/scroll" \ + -H "Content-Type: application/json" \ + -d '{"x":500,"y":300,"deltaY":-3}' +``` + + +## Keyboard + + +```ts TypeScript +// Type text +await sdk.typeDesktopText({ text: "Hello, world!" }); + +// Press a key with modifiers +await sdk.pressDesktopKey({ + key: "c", + modifiers: { ctrl: true }, +}); + +// Low-level key down/up +await sdk.keyDownDesktop({ key: "Shift_L" }); +await sdk.keyUpDesktop({ key: "Shift_L" }); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/desktop/keyboard/type" \ + -H "Content-Type: application/json" \ + -d '{"text":"Hello, world!"}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/keyboard/press" \ + -H "Content-Type: application/json" \ + -d '{"key":"c","modifiers":{"ctrl":true}}' +``` + + +## Clipboard + +Read and write the X11 clipboard programmatically. + + +```ts TypeScript +// Read clipboard +const clipboard = await sdk.getDesktopClipboard(); +console.log(clipboard.text); + +// Read primary selection (mouse-selected text) +const primary = await sdk.getDesktopClipboard({ selection: "primary" }); + +// Write to clipboard +await sdk.setDesktopClipboard({ text: "Pasted via API" }); + +// Write to both clipboard and primary selection +await sdk.setDesktopClipboard({ + text: "Synced text", + selection: "both", +}); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/clipboard" + +curl "http://127.0.0.1:2468/v1/desktop/clipboard?selection=primary" + +curl -X POST "http://127.0.0.1:2468/v1/desktop/clipboard" \ + -H "Content-Type: application/json" \ + -d '{"text":"Pasted via API"}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/clipboard" \ + -H "Content-Type: application/json" \ + -d '{"text":"Synced text","selection":"both"}' +``` + + +The `selection` parameter controls which X11 selection to read or write: + +| Value | Description | +|-------|-------------| +| `clipboard` (default) | The standard clipboard (Ctrl+C / Ctrl+V) | +| `primary` | The primary selection (text selected with the mouse) | +| `both` | Write to both clipboard and primary selection (write only) | + +## Display and windows + + +```ts TypeScript +const display = await sdk.getDesktopDisplayInfo(); +console.log(display.resolution); // { width: 1920, height: 1080, dpi: 96 } + +const { windows } = await sdk.listDesktopWindows(); +for (const win of windows) { + console.log(win.title, win.x, win.y, win.width, win.height); +} +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/display/info" + +curl "http://127.0.0.1:2468/v1/desktop/windows" +``` + + +The windows endpoint filters out noise automatically: window manager internals (Openbox), windows with empty titles, and tiny helper windows (under 120x80) are excluded. The currently active/focused window is always included regardless of filters. + +### Focused window + +Get the currently focused window without listing all windows. + + +```ts TypeScript +const focused = await sdk.getDesktopFocusedWindow(); +console.log(focused.title, focused.id); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/windows/focused" +``` + + +Returns 404 if no window currently has focus. + +### Window management + +Focus, move, and resize windows by their X11 window ID. + + +```ts TypeScript +const { windows } = await sdk.listDesktopWindows(); +const win = windows[0]; + +// Bring window to foreground +await sdk.focusDesktopWindow(win.id); + +// Move window +await sdk.moveDesktopWindow(win.id, { x: 100, y: 50 }); + +// Resize window +await sdk.resizeDesktopWindow(win.id, { width: 1280, height: 720 }); +``` + +```bash cURL +# Focus a window +curl -X POST "http://127.0.0.1:2468/v1/desktop/windows/12345/focus" + +# Move a window +curl -X POST "http://127.0.0.1:2468/v1/desktop/windows/12345/move" \ + -H "Content-Type: application/json" \ + -d '{"x":100,"y":50}' + +# Resize a window +curl -X POST "http://127.0.0.1:2468/v1/desktop/windows/12345/resize" \ + -H "Content-Type: application/json" \ + -d '{"width":1280,"height":720}' +``` + + +All three endpoints return the updated window info so you can verify the operation took effect. The window manager may adjust the requested position or size. + +## App launching + +Launch applications or open files/URLs on the desktop without needing to shell out. + + +```ts TypeScript +// Launch an app by name +const result = await sdk.launchDesktopApp({ + app: "firefox", + args: ["--private"], +}); +console.log(result.processId); // "proc_7" + +// Launch and wait for the window to appear +const withWindow = await sdk.launchDesktopApp({ + app: "xterm", + wait: true, +}); +console.log(withWindow.windowId); // "12345" or null if timed out + +// Open a URL with the default handler +const opened = await sdk.openDesktopTarget({ + target: "https://example.com", +}); +console.log(opened.processId); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/desktop/launch" \ + -H "Content-Type: application/json" \ + -d '{"app":"firefox","args":["--private"]}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/launch" \ + -H "Content-Type: application/json" \ + -d '{"app":"xterm","wait":true}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/open" \ + -H "Content-Type: application/json" \ + -d '{"target":"https://example.com"}' +``` + + +The returned `processId` can be used with the [Process API](/processes) to read logs (`GET /v1/processes/{id}/logs`) or stop the application (`POST /v1/processes/{id}/stop`). + +When `wait` is `true`, the API polls for up to 5 seconds for a window to appear. If the window appears, its ID is returned in `windowId`. If it times out, `windowId` is `null` but the process is still running. + + +**Launch/Open vs the Process API:** Both `launch` and `open` are convenience wrappers around the [Process API](/processes). They create managed processes (with `owner: "desktop"`) that you can inspect, log, and stop through the same Process endpoints. The difference is that `launch` validates the binary exists in PATH first and can optionally wait for a window to appear, while `open` delegates to the system default handler (`xdg-open`). Use the Process API directly when you need full control over command, environment, working directory, or restart policies. + + +## Recording + +Record the desktop to MP4. + + +```ts TypeScript +const recording = await sdk.startDesktopRecording({ fps: 30 }); +console.log(recording.id); + +// ... do things ... + +const stopped = await sdk.stopDesktopRecording(); + +// List all recordings +const { recordings } = await sdk.listDesktopRecordings(); + +// Download +const mp4 = await sdk.downloadDesktopRecording(recording.id); + +// Clean up +await sdk.deleteDesktopRecording(recording.id); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/desktop/recording/start" \ + -H "Content-Type: application/json" \ + -d '{"fps":30}' + +curl -X POST "http://127.0.0.1:2468/v1/desktop/recording/stop" + +curl "http://127.0.0.1:2468/v1/desktop/recordings" + +curl "http://127.0.0.1:2468/v1/desktop/recordings/rec_1/download" --output recording.mp4 + +curl -X DELETE "http://127.0.0.1:2468/v1/desktop/recordings/rec_1" +``` + + +## Desktop processes + +The desktop runtime manages several background processes (Xvfb, openbox, neko, ffmpeg). These are all registered with the general [Process API](/processes) under the `desktop` owner, so you can inspect logs, check status, and troubleshoot using the same tools you use for any other managed process. + + +```ts TypeScript +// List all processes, including desktop-owned ones +const { processes } = await sdk.listProcesses(); + +const desktopProcs = processes.filter((p) => p.owner === "desktop"); +for (const p of desktopProcs) { + console.log(p.id, p.command, p.status); +} + +// Read logs from a specific desktop process +const logs = await sdk.getProcessLogs(desktopProcs[0].id, { tail: 50 }); +for (const entry of logs.entries) { + console.log(entry.stream, atob(entry.data)); +} +``` + +```bash cURL +# List all processes (desktop processes have owner: "desktop") +curl "http://127.0.0.1:2468/v1/processes" + +# Get logs from a specific desktop process +curl "http://127.0.0.1:2468/v1/processes/proc_1/logs?tail=50" +``` + + +The desktop status endpoint also includes a summary of running processes: + + +```ts TypeScript +const status = await sdk.getDesktopStatus(); +for (const proc of status.processes) { + console.log(proc.name, proc.pid, proc.running); +} +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/desktop/status" +# Response includes: processes: [{ name: "Xvfb", pid: 123, running: true }, ...] +``` + + +| Process | Role | Restart policy | +|---------|------|---------------| +| Xvfb | Virtual X11 framebuffer | Auto-restart while desktop is active | +| openbox | Window manager | Auto-restart while desktop is active | +| neko | WebRTC streaming server (started by `startDesktopStream`) | No auto-restart | +| ffmpeg | Screen recorder (started by `startDesktopRecording`) | No auto-restart | + +## Live streaming + +Start a WebRTC stream for real-time desktop viewing in a browser. + + +```ts TypeScript +await sdk.startDesktopStream(); + +// Check stream status +const status = await sdk.getDesktopStreamStatus(); +console.log(status.active); // true +console.log(status.processId); // "proc_5" + +// Connect via the React DesktopViewer component or +// use the WebSocket signaling endpoint directly +// at ws://127.0.0.1:2468/v1/desktop/stream/signaling + +await sdk.stopDesktopStream(); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/desktop/stream/start" + +# Check stream status +curl "http://127.0.0.1:2468/v1/desktop/stream/status" + +# Connect to ws://127.0.0.1:2468/v1/desktop/stream/signaling for WebRTC signaling + +curl -X POST "http://127.0.0.1:2468/v1/desktop/stream/stop" +``` + + +For a drop-in React component, see [React Components](/react-components). + +## API reference + +### Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/v1/desktop/start` | Start the desktop runtime | +| `POST` | `/v1/desktop/stop` | Stop the desktop runtime | +| `GET` | `/v1/desktop/status` | Get desktop runtime status | +| `GET` | `/v1/desktop/screenshot` | Capture full desktop screenshot | +| `GET` | `/v1/desktop/screenshot/region` | Capture a region screenshot | +| `GET` | `/v1/desktop/mouse/position` | Get current mouse position | +| `POST` | `/v1/desktop/mouse/move` | Move the mouse | +| `POST` | `/v1/desktop/mouse/click` | Click the mouse | +| `POST` | `/v1/desktop/mouse/down` | Press mouse button down | +| `POST` | `/v1/desktop/mouse/up` | Release mouse button | +| `POST` | `/v1/desktop/mouse/drag` | Drag from one point to another | +| `POST` | `/v1/desktop/mouse/scroll` | Scroll at a position | +| `POST` | `/v1/desktop/keyboard/type` | Type text | +| `POST` | `/v1/desktop/keyboard/press` | Press a key with optional modifiers | +| `POST` | `/v1/desktop/keyboard/down` | Press a key down (hold) | +| `POST` | `/v1/desktop/keyboard/up` | Release a key | +| `GET` | `/v1/desktop/display/info` | Get display info | +| `GET` | `/v1/desktop/windows` | List visible windows | +| `GET` | `/v1/desktop/windows/focused` | Get focused window info | +| `POST` | `/v1/desktop/windows/{id}/focus` | Focus a window | +| `POST` | `/v1/desktop/windows/{id}/move` | Move a window | +| `POST` | `/v1/desktop/windows/{id}/resize` | Resize a window | +| `GET` | `/v1/desktop/clipboard` | Read clipboard contents | +| `POST` | `/v1/desktop/clipboard` | Write to clipboard | +| `POST` | `/v1/desktop/launch` | Launch an application | +| `POST` | `/v1/desktop/open` | Open a file or URL | +| `POST` | `/v1/desktop/recording/start` | Start recording | +| `POST` | `/v1/desktop/recording/stop` | Stop recording | +| `GET` | `/v1/desktop/recordings` | List recordings | +| `GET` | `/v1/desktop/recordings/{id}` | Get recording metadata | +| `GET` | `/v1/desktop/recordings/{id}/download` | Download recording | +| `DELETE` | `/v1/desktop/recordings/{id}` | Delete recording | +| `POST` | `/v1/desktop/stream/start` | Start WebRTC streaming | +| `POST` | `/v1/desktop/stream/stop` | Stop WebRTC streaming | +| `GET` | `/v1/desktop/stream/status` | Get stream status | +| `GET` | `/v1/desktop/stream/signaling` | WebSocket for WebRTC signaling | + +### TypeScript SDK methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `startDesktop(request?)` | `DesktopStatusResponse` | Start the desktop | +| `stopDesktop()` | `DesktopStatusResponse` | Stop the desktop | +| `getDesktopStatus()` | `DesktopStatusResponse` | Get desktop status | +| `takeDesktopScreenshot(query?)` | `Uint8Array` | Capture screenshot | +| `takeDesktopRegionScreenshot(query)` | `Uint8Array` | Capture region screenshot | +| `getDesktopMousePosition()` | `DesktopMousePositionResponse` | Get mouse position | +| `moveDesktopMouse(request)` | `DesktopMousePositionResponse` | Move mouse | +| `clickDesktop(request)` | `DesktopMousePositionResponse` | Click mouse | +| `mouseDownDesktop(request)` | `DesktopMousePositionResponse` | Mouse button down | +| `mouseUpDesktop(request)` | `DesktopMousePositionResponse` | Mouse button up | +| `dragDesktopMouse(request)` | `DesktopMousePositionResponse` | Drag mouse | +| `scrollDesktop(request)` | `DesktopMousePositionResponse` | Scroll | +| `typeDesktopText(request)` | `DesktopActionResponse` | Type text | +| `pressDesktopKey(request)` | `DesktopActionResponse` | Press key | +| `keyDownDesktop(request)` | `DesktopActionResponse` | Key down | +| `keyUpDesktop(request)` | `DesktopActionResponse` | Key up | +| `getDesktopDisplayInfo()` | `DesktopDisplayInfoResponse` | Get display info | +| `listDesktopWindows()` | `DesktopWindowListResponse` | List windows | +| `getDesktopFocusedWindow()` | `DesktopWindowInfo` | Get focused window | +| `focusDesktopWindow(id)` | `DesktopWindowInfo` | Focus a window | +| `moveDesktopWindow(id, request)` | `DesktopWindowInfo` | Move a window | +| `resizeDesktopWindow(id, request)` | `DesktopWindowInfo` | Resize a window | +| `getDesktopClipboard(query?)` | `DesktopClipboardResponse` | Read clipboard | +| `setDesktopClipboard(request)` | `DesktopActionResponse` | Write clipboard | +| `launchDesktopApp(request)` | `DesktopLaunchResponse` | Launch an app | +| `openDesktopTarget(request)` | `DesktopOpenResponse` | Open file/URL | +| `startDesktopRecording(request?)` | `DesktopRecordingInfo` | Start recording | +| `stopDesktopRecording()` | `DesktopRecordingInfo` | Stop recording | +| `listDesktopRecordings()` | `DesktopRecordingListResponse` | List recordings | +| `getDesktopRecording(id)` | `DesktopRecordingInfo` | Get recording | +| `downloadDesktopRecording(id)` | `Uint8Array` | Download recording | +| `deleteDesktopRecording(id)` | `void` | Delete recording | +| `startDesktopStream()` | `DesktopStreamStatusResponse` | Start streaming | +| `stopDesktopStream()` | `DesktopStreamStatusResponse` | Stop streaming | +| `getDesktopStreamStatus()` | `DesktopStreamStatusResponse` | Stream status | + +## Customizing the desktop environment + +The desktop runs inside the sandbox filesystem, so you can customize it using the [File System](/file-system) API before or after starting the desktop. The desktop HOME directory is located at `~/.local/state/sandbox-agent/desktop/home` (or `$XDG_STATE_HOME/sandbox-agent/desktop/home` if `XDG_STATE_HOME` is set). + +All configuration files below are written to paths relative to this HOME directory. + +### Window manager (openbox) + +The desktop uses [openbox](http://openbox.org/) as its window manager. You can customize its behavior, theme, and keyboard shortcuts by writing an `rc.xml` config file. + + +```ts TypeScript +const openboxConfig = ` + + + Clearlooks + NLIMC + DejaVu Sans10 + + 1 + + + + +`; + +await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox" }); +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox/rc.xml" }, + openboxConfig, +); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox" + +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox/rc.xml" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @rc.xml +``` + + +### Autostart programs + +Openbox runs scripts in `~/.config/openbox/autostart` on startup. Use this to launch applications, set the background, or configure the environment. + + +```ts TypeScript +const autostart = `#!/bin/sh +# Set a solid background color +xsetroot -solid "#1e1e2e" & + +# Launch a terminal +xterm -geometry 120x40+50+50 & + +# Launch a browser +firefox --no-remote & +`; + +await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox" }); +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" }, + autostart, +); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox" + +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @autostart.sh +``` + + + +The autostart script runs when openbox starts, which happens during `startDesktop()`. Write the autostart file before calling `startDesktop()` for it to take effect. + + +### Background + +There is no wallpaper set by default (the background is the X root window default). You can set it using `xsetroot` in the autostart script (as shown above), or use `feh` if you need an image: + + +```ts TypeScript +// Upload a wallpaper image +import fs from "node:fs"; + +const wallpaper = await fs.promises.readFile("./wallpaper.png"); +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/wallpaper.png" }, + wallpaper, +); + +// Set the autostart to apply it +const autostart = `#!/bin/sh +feh --bg-fill ~/wallpaper.png & +`; + +await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox" }); +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" }, + autostart, +); +``` + +```bash cURL +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/wallpaper.png" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @wallpaper.png + +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/openbox/autostart" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @autostart.sh +``` + + + +`feh` is not installed by default. Install it via the [Process API](/processes) before starting the desktop: `await sdk.runProcess({ command: "apt-get", args: ["install", "-y", "feh"] })`. + + +### Fonts + +Only `fonts-dejavu-core` is installed by default. To add more fonts, install them with your system package manager or copy font files into the sandbox: + + +```ts TypeScript +// Install a font package +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "fonts-noto", "fonts-liberation"], +}); + +// Or copy a custom font file +import fs from "node:fs"; + +const font = await fs.promises.readFile("./CustomFont.ttf"); +await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.local/share/fonts" }); +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/.local/share/fonts/CustomFont.ttf" }, + font, +); + +// Rebuild the font cache +await sdk.runProcess({ command: "fc-cache", args: ["-fv"] }); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","fonts-noto","fonts-liberation"]}' + +curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.local/share/fonts" + +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.local/share/fonts/CustomFont.ttf" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @CustomFont.ttf + +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"fc-cache","args":["-fv"]}' +``` + + +### Cursor theme + + +```ts TypeScript +await sdk.runProcess({ + command: "apt-get", + args: ["install", "-y", "dmz-cursor-theme"], +}); + +const xresources = `Xcursor.theme: DMZ-White\nXcursor.size: 24\n`; +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/.Xresources" }, + xresources, +); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"apt-get","args":["install","-y","dmz-cursor-theme"]}' + +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.Xresources" \ + -H "Content-Type: application/octet-stream" \ + --data-binary 'Xcursor.theme: DMZ-White\nXcursor.size: 24' +``` + + + +Run `xrdb -merge ~/.Xresources` (via the autostart or process API) after writing the file for changes to take effect. + + +### Shell and terminal + +No terminal emulator or shell is launched by default. Add one to the openbox autostart: + +```sh +# In ~/.config/openbox/autostart +xterm -geometry 120x40+50+50 & +``` + +To use a different shell, set the `SHELL` environment variable in your Dockerfile or install your preferred shell and configure the terminal to use it. + +### GTK theme + +Applications using GTK will pick up settings from `~/.config/gtk-3.0/settings.ini`: + + +```ts TypeScript +const gtkSettings = `[Settings] +gtk-theme-name=Adwaita +gtk-icon-theme-name=Adwaita +gtk-font-name=DejaVu Sans 10 +gtk-cursor-theme-name=DMZ-White +gtk-cursor-theme-size=24 +`; + +await sdk.mkdirFs({ path: "~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0" }); +await sdk.writeFsFile( + { path: "~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0/settings.ini" }, + gtkSettings, +); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0" + +curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=~/.local/state/sandbox-agent/desktop/home/.config/gtk-3.0/settings.ini" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @settings.ini +``` + + +### Summary of configuration paths + +All paths are relative to the desktop HOME directory (`~/.local/state/sandbox-agent/desktop/home`). + +| What | Path | Notes | +|------|------|-------| +| Openbox config | `.config/openbox/rc.xml` | Window manager theme, keybindings, behavior | +| Autostart | `.config/openbox/autostart` | Shell script run on desktop start | +| Custom fonts | `.local/share/fonts/` | TTF/OTF files, run `fc-cache -fv` after | +| Cursor theme | `.Xresources` | Requires `xrdb -merge` to apply | +| GTK 3 settings | `.config/gtk-3.0/settings.ini` | Theme, icons, fonts for GTK apps | +| Wallpaper | Any path, referenced from autostart | Requires `feh` or similar tool | diff --git a/docs/credentials.mdx b/docs/credentials.mdx deleted file mode 100644 index d014921..0000000 --- a/docs/credentials.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: "Credentials" -description: "How Sandbox Agent discovers and uses provider credentials." ---- - -Sandbox Agent discovers API credentials from environment variables and local agent config files. -These credentials are passed through to underlying agent runtimes. - -## Credential sources - -Credentials are discovered in priority order. - -### Environment variables (highest priority) - -API keys first: - -| Variable | Provider | -|----------|----------| -| `ANTHROPIC_API_KEY` | Anthropic | -| `CLAUDE_API_KEY` | Anthropic fallback | -| `OPENAI_API_KEY` | OpenAI | -| `CODEX_API_KEY` | OpenAI fallback | - -OAuth tokens (used when OAuth extraction is enabled): - -| Variable | Provider | -|----------|----------| -| `CLAUDE_CODE_OAUTH_TOKEN` | Anthropic | -| `ANTHROPIC_AUTH_TOKEN` | Anthropic fallback | - -### Agent config files - -| Agent | Config path | Provider | -|-------|-------------|----------| -| Amp | `~/.amp/config.json` | Anthropic | -| Claude Code | `~/.claude.json`, `~/.claude/.credentials.json` | Anthropic | -| Codex | `~/.codex/auth.json` | OpenAI | -| OpenCode | `~/.local/share/opencode/auth.json` | Anthropic/OpenAI | - -## Provider requirements by agent - -| Agent | Required provider | -|-------|-------------------| -| Claude Code | Anthropic | -| Amp | Anthropic | -| Codex | OpenAI | -| OpenCode | Anthropic or OpenAI | -| Mock | None | - -## Error handling behavior - -Credential extraction is best-effort: - -- Missing or malformed files are skipped. -- Discovery continues to later sources. -- Missing credentials mark providers unavailable instead of failing server startup. - -When prompting, Sandbox Agent does not pre-validate provider credentials. Agent-native authentication errors surface through session events/output. - -## Checking credential status - -### API - -`GET /v1/agents` includes `credentialsAvailable` per agent. - -```json -{ - "agents": [ - { - "id": "claude", - "installed": true, - "credentialsAvailable": true - }, - { - "id": "codex", - "installed": true, - "credentialsAvailable": false - } - ] -} -``` - -### TypeScript SDK - -```typescript -const result = await sdk.listAgents(); - -for (const agent of result.agents) { - console.log(`${agent.id}: ${agent.credentialsAvailable ? "authenticated" : "no credentials"}`); -} -``` - -## Passing credentials explicitly - -Set environment variables before starting Sandbox Agent: - -```bash -export ANTHROPIC_API_KEY=sk-ant-... -export OPENAI_API_KEY=sk-... -sandbox-agent daemon start -``` - -Or with SDK-managed local spawn: - -```typescript -import { SandboxAgent } from "sandbox-agent"; - -const sdk = await SandboxAgent.start({ - spawn: { - env: { - ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY, - }, - }, -}); -``` diff --git a/docs/custom-tools.mdx b/docs/custom-tools.mdx index 727fb02..2fb3e15 100644 --- a/docs/custom-tools.mdx +++ b/docs/custom-tools.mdx @@ -80,9 +80,7 @@ await sdk.setMcpConfig( const session = await sdk.createSession({ agent: "claude", - sessionInit: { - cwd: "/workspace", - }, + cwd: "/workspace", }); await session.prompt([ @@ -145,9 +143,7 @@ await sdk.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skill); ```ts const session = await sdk.createSession({ agent: "claude", - sessionInit: { - cwd: "/workspace", - }, + cwd: "/workspace", }); await session.prompt([ diff --git a/docs/deploy/boxlite.mdx b/docs/deploy/boxlite.mdx new file mode 100644 index 0000000..8c02bb4 --- /dev/null +++ b/docs/deploy/boxlite.mdx @@ -0,0 +1,67 @@ +--- +title: "BoxLite" +description: "Run Sandbox Agent inside a BoxLite micro-VM." +--- + +BoxLite is a local-first micro-VM sandbox — no cloud account needed. +See [BoxLite docs](https://docs.boxlite.ai) for platform requirements (KVM on Linux, Apple Silicon on macOS). + +## Prerequisites + +- `@boxlite-ai/boxlite` installed (requires KVM or Apple Hypervisor) +- Docker (to build the base image) +- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` + +## Base image + +Build a Docker image with Sandbox Agent pre-installed, then export it as an OCI layout +that BoxLite can load directly (BoxLite has its own image store separate from Docker): + +```dockerfile +FROM node:22-bookworm-slim +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* +RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh +RUN sandbox-agent install-agent claude +RUN sandbox-agent install-agent codex +``` + +```bash +docker build -t sandbox-agent-boxlite . +mkdir -p oci-image +docker save sandbox-agent-boxlite | tar -xf - -C oci-image +``` + +## TypeScript example + +```typescript +import { SimpleBox } from "@boxlite-ai/boxlite"; +import { SandboxAgent } from "sandbox-agent"; + +const env: Record = {}; +if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY; + +const box = new SimpleBox({ + rootfsPath: "./oci-image", + env, + ports: [{ hostPort: 3000, guestPort: 3000 }], + diskSizeGb: 4, +}); + +await box.exec("sh", "-c", + "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &" +); + +const baseUrl = "http://localhost:3000"; +const sdk = await SandboxAgent.connect({ baseUrl }); + +const session = await sdk.createSession({ agent: "claude" }); +const off = session.onEvent((event) => { + console.log(event.sender, event.payload); +}); + +await session.prompt([{ type: "text", text: "Summarize this repository" }]); +off(); + +await box.stop(); +``` diff --git a/docs/deploy/cloudflare.mdx b/docs/deploy/cloudflare.mdx index 5328653..c0370e4 100644 --- a/docs/deploy/cloudflare.mdx +++ b/docs/deploy/cloudflare.mdx @@ -25,24 +25,60 @@ cd my-sandbox ```dockerfile FROM cloudflare/sandbox:0.7.0 -RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh +RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex EXPOSE 8000 ``` -## TypeScript proxy example +## TypeScript example (with provider) + +For standalone scripts, use the `cloudflare` provider: + +```bash +npm install sandbox-agent@0.4.x @cloudflare/sandbox +``` + +```typescript +import { SandboxAgent } from "sandbox-agent"; +import { cloudflare } from "sandbox-agent/cloudflare"; + +const sdk = await SandboxAgent.start({ + sandbox: cloudflare(), +}); + +try { + const session = await sdk.createSession({ agent: "codex" }); + const response = await session.prompt([ + { type: "text", text: "Summarize this repository" }, + ]); + console.log(response.stopReason); +} finally { + await sdk.destroySandbox(); +} +``` + +The `cloudflare` provider uses `containerFetch` under the hood, automatically stripping `AbortSignal` to avoid dropped streaming updates. + +## TypeScript example (Durable Objects) + +For Workers with Durable Objects, use `SandboxAgent.connect(...)` with a custom `fetch` backed by `sandbox.containerFetch(...)`: ```typescript import { getSandbox, type Sandbox } from "@cloudflare/sandbox"; +import { Hono } from "hono"; +import { SandboxAgent } from "sandbox-agent"; + export { Sandbox } from "@cloudflare/sandbox"; -type Env = { +type Bindings = { Sandbox: DurableObjectNamespace; + ASSETS: Fetcher; ANTHROPIC_API_KEY?: string; OPENAI_API_KEY?: string; }; +const app = new Hono<{ Bindings: Bindings }>(); const PORT = 8000; async function isServerRunning(sandbox: Sandbox): Promise { @@ -54,64 +90,85 @@ async function isServerRunning(sandbox: Sandbox): Promise { } } -async function ensureRunning(sandbox: Sandbox, env: Env): Promise { - if (await isServerRunning(sandbox)) return; - - const envVars: Record = {}; - if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY; - if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY; - await sandbox.setEnvVars(envVars); - - await sandbox.startProcess(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`); - - for (let i = 0; i < 30; i++) { - if (await isServerRunning(sandbox)) return; - await new Promise((r) => setTimeout(r, 200)); +async function getReadySandbox(name: string, env: Bindings): Promise { + const sandbox = getSandbox(env.Sandbox, name); + if (!(await isServerRunning(sandbox))) { + const envVars: Record = {}; + if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY; + if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY; + await sandbox.setEnvVars(envVars); + await sandbox.startProcess(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`); } - - throw new Error("sandbox-agent failed to start"); + return sandbox; } -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/); +app.post("/sandbox/:name/prompt", async (c) => { + const sandbox = await getReadySandbox(c.req.param("name"), c.env); - if (!match) { - return new Response("Not found", { status: 404 }); - } + const sdk = await SandboxAgent.connect({ + fetch: (input, init) => + sandbox.containerFetch( + input as Request | string | URL, + { + ...(init ?? {}), + // Avoid passing AbortSignal through containerFetch; it can drop streamed session updates. + signal: undefined, + }, + PORT, + ), + }); - const [, name, path = "/"] = match; - const sandbox = getSandbox(env.Sandbox, name); - await ensureRunning(sandbox, env); + const session = await sdk.createSession({ agent: "codex" }); + const response = await session.prompt([{ type: "text", text: "Summarize this repository" }]); + await sdk.destroySession(session.id); + await sdk.dispose(); - return sandbox.containerFetch( - new Request(`http://localhost${path}${url.search}`, request), - PORT, - ); - }, -}; + return c.json(response); +}); + +app.all("/sandbox/:name/proxy/*", async (c) => { + const sandbox = await getReadySandbox(c.req.param("name"), c.env); + const wildcard = c.req.param("*"); + const path = wildcard ? `/${wildcard}` : "/"; + const query = new URL(c.req.raw.url).search; + + return sandbox.containerFetch(new Request(`http://localhost${path}${query}`, c.req.raw), PORT); +}); + +app.all("*", (c) => c.env.ASSETS.fetch(c.req.raw)); + +export default app; ``` -## Connect from a client +This keeps all Sandbox Agent calls inside the Cloudflare sandbox routing path and does not require a `baseUrl`. -```typescript -import { SandboxAgent } from "sandbox-agent"; +## Troubleshooting streaming updates +If you only receive: +- the outbound prompt request +- the final `{ stopReason: "end_turn" }` response + +then the streamed update channel dropped. In Cloudflare sandbox paths, this is typically caused by forwarding `AbortSignal` from SDK fetch init into `containerFetch(...)`. + +Fix: + +```ts const sdk = await SandboxAgent.connect({ - baseUrl: "http://localhost:8787/sandbox/my-sandbox", + fetch: (input, init) => + sandbox.containerFetch( + input as Request | string | URL, + { + ...(init ?? {}), + // Avoid passing AbortSignal through containerFetch; it can drop streamed session updates. + signal: undefined, + }, + PORT, + ), }); - -const session = await sdk.createSession({ agent: "claude" }); - -const off = session.onEvent((event) => { - console.log(event.sender, event.payload); -}); - -await session.prompt([{ type: "text", text: "Summarize this repository" }]); -off(); ``` +This keeps prompt completion behavior the same, but restores streamed text/tool updates. + ## Local development ```bash @@ -121,7 +178,7 @@ npm run dev Test health: ```bash -curl http://localhost:8787/sandbox/demo/v1/health +curl http://localhost:8787/sandbox/demo/proxy/v1/health ``` ## Production deployment diff --git a/docs/deploy/computesdk.mdx b/docs/deploy/computesdk.mdx index 5e07da0..601d9c7 100644 --- a/docs/deploy/computesdk.mdx +++ b/docs/deploy/computesdk.mdx @@ -1,160 +1,66 @@ --- title: "ComputeSDK" -description: "Deploy the daemon using ComputeSDK's provider-agnostic sandbox API." +description: "Deploy Sandbox Agent using ComputeSDK's provider-agnostic sandbox API." --- -[ComputeSDK](https://computesdk.com) provides a unified interface for managing sandboxes across multiple providers. Write once, deploy anywhere—switch providers by changing environment variables. +[ComputeSDK](https://computesdk.com) provides a unified interface for managing sandboxes across multiple providers. Write once, deploy anywhere by changing environment variables. ## Prerequisites - `COMPUTESDK_API_KEY` from [console.computesdk.com](https://console.computesdk.com) - Provider API key (one of: `E2B_API_KEY`, `DAYTONA_API_KEY`, `VERCEL_TOKEN`, `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET`, `BLAXEL_API_KEY`, `CSB_API_KEY`) -- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents +- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` -## TypeScript Example +## TypeScript example + +```bash +npm install sandbox-agent@0.4.x computesdk +``` ```typescript -import { - compute, - detectProvider, - getMissingEnvVars, - getProviderConfigFromEnv, - isProviderAuthComplete, - isValidProvider, - PROVIDER_NAMES, - type ExplicitComputeConfig, - type ProviderName, -} from "computesdk"; import { SandboxAgent } from "sandbox-agent"; +import { computesdk } from "sandbox-agent/computesdk"; -const PORT = 3000; -const REQUEST_TIMEOUT_MS = - Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000; - -/** - * Detects and validates the provider to use. - * Priority: COMPUTESDK_PROVIDER env var > auto-detection from API keys - */ -function resolveProvider(): ProviderName { - const providerOverride = process.env.COMPUTESDK_PROVIDER; - - if (providerOverride) { - if (!isValidProvider(providerOverride)) { - throw new Error( - `Unsupported provider "${providerOverride}". Supported: ${PROVIDER_NAMES.join(", ")}` - ); - } - if (!isProviderAuthComplete(providerOverride)) { - const missing = getMissingEnvVars(providerOverride); - throw new Error( - `Missing credentials for "${providerOverride}". Set: ${missing.join(", ")}` - ); - } - return providerOverride as ProviderName; - } - - const detected = detectProvider(); - if (!detected) { - throw new Error( - `No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}` - ); - } - return detected as ProviderName; -} - -function configureComputeSDK(): void { - const provider = resolveProvider(); - - const config: ExplicitComputeConfig = { - provider, - computesdkApiKey: process.env.COMPUTESDK_API_KEY, - requestTimeoutMs: REQUEST_TIMEOUT_MS, - }; - - // Add provider-specific config from environment - const providerConfig = getProviderConfigFromEnv(provider); - if (Object.keys(providerConfig).length > 0) { - (config as any)[provider] = providerConfig; - } - - compute.setConfig(config); -} - -configureComputeSDK(); - -// Build environment variables to pass to sandbox const envs: Record = {}; if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; -// Create sandbox -const sandbox = await compute.sandbox.create({ - envs: Object.keys(envs).length > 0 ? envs : undefined, +const sdk = await SandboxAgent.start({ + sandbox: computesdk({ + create: { + envs, + image: process.env.COMPUTESDK_IMAGE, + templateId: process.env.COMPUTESDK_TEMPLATE_ID, + }, + }), }); -// Helper to run commands with error handling -const run = async (cmd: string, options?: { background?: boolean }) => { - const result = await sandbox.runCommand(cmd, options); - if (typeof result?.exitCode === "number" && result.exitCode !== 0) { - throw new Error(`Command failed: ${cmd} (exit ${result.exitCode})\n${result.stderr || ""}`); - } - return result; -}; - -// Install sandbox-agent -await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); - -// Install agents conditionally based on available API keys -if (envs.ANTHROPIC_API_KEY) { - await run("sandbox-agent install-agent claude"); +try { + const session = await sdk.createSession({ agent: "claude" }); + const response = await session.prompt([ + { type: "text", text: "Summarize this repository" }, + ]); + console.log(response.stopReason); +} finally { + await sdk.destroySandbox(); } -if (envs.OPENAI_API_KEY) { - await run("sandbox-agent install-agent codex"); -} - -// Start the server in the background -await run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, { background: true }); - -// Get the public URL for the sandbox -const baseUrl = await sandbox.getUrl({ port: PORT }); - -// Wait for server to be ready -const deadline = Date.now() + REQUEST_TIMEOUT_MS; -while (Date.now() < deadline) { - try { - const response = await fetch(`${baseUrl}/v1/health`); - if (response.ok) { - const data = await response.json(); - if (data?.status === "ok") break; - } - } catch { - // Server not ready yet - } - await new Promise((r) => setTimeout(r, 500)); -} - -// Connect to the server -const client = await SandboxAgent.connect({ baseUrl }); - -// Detect which agent to use based on available API keys -const agent = envs.ANTHROPIC_API_KEY ? "claude" : "codex"; - -// Create a session and start coding -await client.createSession("my-session", { agent }); - -await client.postMessage("my-session", { - message: "Summarize this repository", -}); - -for await (const event of client.streamEvents("my-session")) { - console.log(event.type, event.data); -} - -// Cleanup -await sandbox.destroy(); ``` -## Supported Providers +The `computesdk` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. ComputeSDK routes to your configured provider behind the scenes. +The `create` option now forwards the full ComputeSDK sandbox-create payload, including provider-specific fields such as `image` and `templateId` when the selected provider supports them. + +Before calling `SandboxAgent.start()`, configure ComputeSDK with your provider: + +```typescript +import { compute } from "computesdk"; + +compute.setConfig({ + provider: "e2b", // or auto-detect via detectProvider() + computesdkApiKey: process.env.COMPUTESDK_API_KEY, +}); +``` + +## Supported providers ComputeSDK auto-detects your provider from environment variables: @@ -169,46 +75,7 @@ ComputeSDK auto-detects your provider from environment variables: ## Notes -- **Provider resolution order**: `COMPUTESDK_PROVIDER` env var takes priority, otherwise auto-detection from API keys. -- **Conditional agent installation**: Only agents with available API keys are installed, reducing setup time. -- **Command error handling**: The example validates exit codes and throws on failures for easier debugging. +- **Provider resolution**: Set `COMPUTESDK_PROVIDER` to force a specific provider, or let ComputeSDK auto-detect from API keys. - `sandbox.runCommand(..., { background: true })` keeps the server running while your app continues. - `sandbox.getUrl({ port })` returns a public URL for the sandbox port. -- Always destroy the sandbox when you are done to avoid leaking resources. -- If sandbox creation times out, set `COMPUTESDK_TIMEOUT_MS` to a higher value (default: 120000ms). - -## Explicit Provider Selection - -To force a specific provider instead of auto-detection, set the `COMPUTESDK_PROVIDER` environment variable: - -```bash -export COMPUTESDK_PROVIDER=e2b -``` - -Or configure programmatically using `getProviderConfigFromEnv()`: - -```typescript -import { compute, getProviderConfigFromEnv, type ExplicitComputeConfig } from "computesdk"; - -const config: ExplicitComputeConfig = { - provider: "e2b", - computesdkApiKey: process.env.COMPUTESDK_API_KEY, - requestTimeoutMs: 120_000, -}; - -// Automatically populate provider-specific config from environment -const providerConfig = getProviderConfigFromEnv("e2b"); -if (Object.keys(providerConfig).length > 0) { - (config as any).e2b = providerConfig; -} - -compute.setConfig(config); -``` - -## Direct Mode (No ComputeSDK API Key) - -To bypass the ComputeSDK gateway and use provider SDKs directly, see the provider-specific examples: - -- [E2B](/deploy/e2b) -- [Daytona](/deploy/daytona) -- [Vercel](/deploy/vercel) +- Always destroy the sandbox when done to avoid leaking resources. diff --git a/docs/deploy/daytona.mdx b/docs/deploy/daytona.mdx index bf81c8f..e546bef 100644 --- a/docs/deploy/daytona.mdx +++ b/docs/deploy/daytona.mdx @@ -15,37 +15,37 @@ See [Daytona network limits](https://www.daytona.io/docs/en/network-limits/). ## TypeScript example -```typescript -import { Daytona } from "@daytonaio/sdk"; -import { SandboxAgent } from "sandbox-agent"; +```bash +npm install sandbox-agent@0.4.x @daytonaio/sdk +``` -const daytona = new Daytona(); +```typescript +import { SandboxAgent } from "sandbox-agent"; +import { daytona } from "sandbox-agent/daytona"; const envVars: Record = {}; if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY; -const sandbox = await daytona.create({ envVars }); +const sdk = await SandboxAgent.start({ + sandbox: daytona({ + create: { envVars }, + }), +}); -await sandbox.process.executeCommand( - "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh" -); - -await sandbox.process.executeCommand( - "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &" -); - -await new Promise((r) => setTimeout(r, 2000)); - -const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url; -const sdk = await SandboxAgent.connect({ baseUrl }); - -const session = await sdk.createSession({ agent: "claude" }); -await session.prompt([{ type: "text", text: "Summarize this repository" }]); - -await sandbox.delete(); +try { + const session = await sdk.createSession({ agent: "claude" }); + const response = await session.prompt([ + { type: "text", text: "Summarize this repository" }, + ]); + console.log(response.stopReason); +} finally { + await sdk.destroySandbox(); +} ``` +The `daytona` provider uses the `rivetdev/sandbox-agent:0.4.2-full` image by default and starts the server automatically. + ## Using snapshots for faster startup ```typescript @@ -61,7 +61,7 @@ if (!hasSnapshot) { name: SNAPSHOT, image: Image.base("ubuntu:22.04").runCommands( "apt-get update && apt-get install -y curl ca-certificates", - "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh", + "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh", "sandbox-agent install-agent claude", "sandbox-agent install-agent codex", ), diff --git a/docs/deploy/docker.mdx b/docs/deploy/docker.mdx index 28c9f77..c5a3432 100644 --- a/docs/deploy/docker.mdx +++ b/docs/deploy/docker.mdx @@ -9,23 +9,38 @@ Docker is not recommended for production isolation of untrusted workloads. Use d ## Quick start -Run Sandbox Agent with agents pre-installed: +Run the published full image with all supported agents pre-installed: ```bash docker run --rm -p 3000:3000 \ -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ -e OPENAI_API_KEY="$OPENAI_API_KEY" \ - alpine:latest sh -c "\ - apk add --no-cache curl ca-certificates libstdc++ libgcc bash && \ - curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh && \ - sandbox-agent install-agent claude && \ - sandbox-agent install-agent codex && \ + rivetdev/sandbox-agent:0.4.2-full \ + server --no-token --host 0.0.0.0 --port 3000 +``` + +The `0.4.2-full` tag pins the exact version. The moving `full` tag is also published for contributors who want the latest full image. + +If you also want the desktop API inside the container, install desktop dependencies before starting the server: + +```bash +docker run --rm -p 3000:3000 \ + -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ + -e OPENAI_API_KEY="$OPENAI_API_KEY" \ + node:22-bookworm-slim sh -c "\ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6 && \ + rm -rf /var/lib/apt/lists/* && \ + curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && \ + sandbox-agent install desktop --yes && \ sandbox-agent server --no-token --host 0.0.0.0 --port 3000" ``` - -Alpine is required for some agent binaries that target musl libc. - +In a Dockerfile: + +```dockerfile +RUN sandbox-agent install desktop --yes +``` ## TypeScript with dockerode @@ -37,17 +52,12 @@ const docker = new Docker(); const PORT = 3000; const container = await docker.createContainer({ - Image: "alpine:latest", - Cmd: ["sh", "-c", [ - "apk add --no-cache curl ca-certificates libstdc++ libgcc bash", - "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh", - "sandbox-agent install-agent claude", - "sandbox-agent install-agent codex", - `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, - ].join(" && ")], + Image: "rivetdev/sandbox-agent:0.4.2-full", + Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`], Env: [ `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`, `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`, + `CODEX_API_KEY=${process.env.CODEX_API_KEY}`, ].filter(Boolean), ExposedPorts: { [`${PORT}/tcp`]: {} }, HostConfig: { @@ -61,10 +71,33 @@ await container.start(); const baseUrl = `http://127.0.0.1:${PORT}`; const sdk = await SandboxAgent.connect({ baseUrl }); -const session = await sdk.createSession({ agent: "claude" }); +const session = await sdk.createSession({ agent: "codex" }); await session.prompt([{ type: "text", text: "Summarize this repository." }]); ``` +## Building a custom image with everything preinstalled + +If you need to extend your own base image, install Sandbox Agent and preinstall every supported agent in one step: + +```dockerfile +FROM node:22-bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash ca-certificates curl git && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh && \ + sandbox-agent install-agent --all + +RUN useradd -m -s /bin/bash sandbox +USER sandbox +WORKDIR /home/sandbox + +EXPOSE 2468 +ENTRYPOINT ["sandbox-agent"] +CMD ["server", "--host", "0.0.0.0", "--port", "2468"] +``` + ## Building from source ```bash diff --git a/docs/deploy/e2b.mdx b/docs/deploy/e2b.mdx index e2d5922..225cfdc 100644 --- a/docs/deploy/e2b.mdx +++ b/docs/deploy/e2b.mdx @@ -10,43 +10,43 @@ description: "Deploy Sandbox Agent inside an E2B sandbox." ## TypeScript example +```bash +npm install sandbox-agent@0.4.x @e2b/code-interpreter +``` + ```typescript -import { Sandbox } from "@e2b/code-interpreter"; import { SandboxAgent } from "sandbox-agent"; +import { e2b } from "sandbox-agent/e2b"; const envs: Record = {}; if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const template = process.env.E2B_TEMPLATE; -const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); - -await sandbox.commands.run( - "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh" -); - -await sandbox.commands.run("sandbox-agent install-agent claude"); -await sandbox.commands.run("sandbox-agent install-agent codex"); - -await sandbox.commands.run( - "sandbox-agent server --no-token --host 0.0.0.0 --port 3000", - { background: true } -); - -const baseUrl = `https://${sandbox.getHost(3000)}`; -const sdk = await SandboxAgent.connect({ baseUrl }); - -const session = await sdk.createSession({ agent: "claude" }); -const off = session.onEvent((event) => { - console.log(event.sender, event.payload); +const sdk = await SandboxAgent.start({ + sandbox: e2b({ + template, + create: { envs }, + }), }); -await session.prompt([{ type: "text", text: "Summarize this repository" }]); -off(); - -await sandbox.kill(); +try { + const session = await sdk.createSession({ agent: "claude" }); + const response = await session.prompt([ + { type: "text", text: "Summarize this repository" }, + ]); + console.log(response.stopReason); +} finally { + await sdk.destroySandbox(); +} ``` +The `e2b` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. Sandboxes pause by default instead of being deleted, and reconnecting with the same `sandboxId` resumes them automatically. + +Pass `template` when you want to start from a custom E2B template alias or template ID. E2B base-image selection happens when you build the template, then `sandbox-agent/e2b` uses that template at sandbox creation time. + ## Faster cold starts For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed. -See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template). +Build System 2.0 also lets you choose the template's base image in code. +See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template) and [E2B Base Images](https://e2b.dev/docs/template/base-image). diff --git a/docs/deploy/local.mdx b/docs/deploy/local.mdx index 8af9f51..6ecdb09 100644 --- a/docs/deploy/local.mdx +++ b/docs/deploy/local.mdx @@ -9,7 +9,7 @@ For local development, run Sandbox Agent directly on your machine. ```bash # Install -curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh +curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh # Run sandbox-agent server --no-token --host 127.0.0.1 --port 2468 @@ -20,24 +20,27 @@ Or with npm/Bun: ```bash - npx @sandbox-agent/cli@0.2.x server --no-token --host 127.0.0.1 --port 2468 + npx @sandbox-agent/cli@0.4.x server --no-token --host 127.0.0.1 --port 2468 ``` ```bash - bunx @sandbox-agent/cli@0.2.x server --no-token --host 127.0.0.1 --port 2468 + bunx @sandbox-agent/cli@0.4.x server --no-token --host 127.0.0.1 --port 2468 ``` ## With the TypeScript SDK -The SDK can spawn and manage the server as a subprocess: +The SDK can spawn and manage the server as a subprocess using the `local` provider: ```typescript import { SandboxAgent } from "sandbox-agent"; +import { local } from "sandbox-agent/local"; -const sdk = await SandboxAgent.start(); +const sdk = await SandboxAgent.start({ + sandbox: local(), +}); const session = await sdk.createSession({ agent: "claude", @@ -47,7 +50,21 @@ await session.prompt([ { type: "text", text: "Summarize this repository." }, ]); -await sdk.dispose(); +await sdk.destroySandbox(); ``` This starts the server on an available local port and connects automatically. + +Pass options to customize the local provider: + +```typescript +const sdk = await SandboxAgent.start({ + sandbox: local({ + port: 3000, + log: "inherit", + env: { + ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY, + }, + }), +}); +``` diff --git a/docs/deploy/modal.mdx b/docs/deploy/modal.mdx new file mode 100644 index 0000000..5850fd8 --- /dev/null +++ b/docs/deploy/modal.mdx @@ -0,0 +1,55 @@ +--- +title: "Modal" +description: "Deploy Sandbox Agent inside a Modal sandbox." +--- + +## Prerequisites + +- `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` from [modal.com/settings](https://modal.com/settings) +- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` + +## TypeScript example + +```bash +npm install sandbox-agent@0.4.x modal +``` + +```typescript +import { SandboxAgent } from "sandbox-agent"; +import { modal } from "sandbox-agent/modal"; + +const secrets: Record = {}; +if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const baseImage = process.env.MODAL_BASE_IMAGE ?? "node:22-slim"; + +const sdk = await SandboxAgent.start({ + sandbox: modal({ + image: baseImage, + create: { secrets }, + }), +}); + +try { + const session = await sdk.createSession({ agent: "claude" }); + const response = await session.prompt([ + { type: "text", text: "Summarize this repository" }, + ]); + console.log(response.stopReason); +} finally { + await sdk.destroySandbox(); +} +``` + +The `modal` provider handles app creation, image building, sandbox provisioning, agent installation, server startup, and tunnel networking automatically. +Set `image` to change the base Docker image before Sandbox Agent and its agent binaries are layered on top. You can also pass a prebuilt Modal `Image` object. + +## Faster cold starts + +Modal caches image layers, so the Dockerfile commands that install `curl` and `sandbox-agent` only run on the first build. Subsequent sandbox creates reuse the cached image. + +## Notes + +- Modal sandboxes use [gVisor](https://gvisor.dev/) for strong isolation. +- Ports are exposed via encrypted tunnels (`encryptedPorts`). The provider uses `sb.tunnels()` to get the public HTTPS URL. +- Environment variables (API keys) are passed as Modal [Secrets](https://modal.com/docs/guide/secrets) for security. diff --git a/docs/deploy/vercel.mdx b/docs/deploy/vercel.mdx index 4a840ee..ec931d8 100644 --- a/docs/deploy/vercel.mdx +++ b/docs/deploy/vercel.mdx @@ -10,52 +10,40 @@ description: "Deploy Sandbox Agent inside a Vercel Sandbox." ## TypeScript example -```typescript -import { Sandbox } from "@vercel/sandbox"; -import { SandboxAgent } from "sandbox-agent"; - -const envs: Record = {}; -if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; -if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; - -const sandbox = await Sandbox.create({ - runtime: "node24", - ports: [3000], -}); - -const run = async (cmd: string, args: string[] = []) => { - const result = await sandbox.runCommand({ cmd, args, env: envs }); - if (result.exitCode !== 0) { - throw new Error(`Command failed: ${cmd} ${args.join(" ")}`); - } -}; - -await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh"]); -await run("sandbox-agent", ["install-agent", "claude"]); -await run("sandbox-agent", ["install-agent", "codex"]); - -await sandbox.runCommand({ - cmd: "sandbox-agent", - args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"], - env: envs, - detached: true, -}); - -const baseUrl = sandbox.domain(3000); -const sdk = await SandboxAgent.connect({ baseUrl }); - -const session = await sdk.createSession({ agent: "claude" }); - -const off = session.onEvent((event) => { - console.log(event.sender, event.payload); -}); - -await session.prompt([{ type: "text", text: "Summarize this repository" }]); -off(); - -await sandbox.stop(); +```bash +npm install sandbox-agent@0.4.x @vercel/sandbox ``` +```typescript +import { SandboxAgent } from "sandbox-agent"; +import { vercel } from "sandbox-agent/vercel"; + +const env: Record = {}; +if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY; + +const sdk = await SandboxAgent.start({ + sandbox: vercel({ + create: { + runtime: "node24", + env, + }, + }), +}); + +try { + const session = await sdk.createSession({ agent: "claude" }); + const response = await session.prompt([ + { type: "text", text: "Summarize this repository" }, + ]); + console.log(response.stopReason); +} finally { + await sdk.destroySandbox(); +} +``` + +The `vercel` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. + ## Authentication Vercel Sandboxes support OIDC token auth (recommended) and access-token auth. diff --git a/docs/docs.json b/docs/docs.json index e7c2fdf..dbcc407 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,128 +1,130 @@ { - "$schema": "https://mintlify.com/docs.json", - "theme": "willow", - "name": "Sandbox Agent SDK", - "appearance": { - "default": "dark", - "strict": true - }, - "colors": { - "primary": "#ff4f00", - "light": "#ff4f00", - "dark": "#ff4f00" - }, - "favicon": "/favicon.svg", - "logo": { - "light": "/logo/light.svg", - "dark": "/logo/dark.svg" - }, - "integrations": { - "posthog": { - "apiKey": "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo", - "apiHost": "https://ph.rivet.gg", - "sessionRecording": true - } - }, - "navbar": { - "links": [ - { - "label": "Gigacode", - "icon": "terminal", - "href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode" - }, - { - "label": "Discord", - "icon": "discord", - "href": "https://discord.gg/auCecybynK" - }, - { - "type": "github", - "href": "https://github.com/rivet-dev/sandbox-agent" - } - ] - }, - "navigation": { - "tabs": [ - { - "tab": "Documentation", - "pages": [ - { - "group": "Getting started", - "pages": [ - "quickstart", - "sdk-overview", - { - "group": "Deploy", - "icon": "server", - "pages": [ - "deploy/local", - "deploy/computesdk", - "deploy/e2b", - "deploy/daytona", - "deploy/vercel", - "deploy/cloudflare", - "deploy/docker" - ] - } - ] - }, - { - "group": "Agent", - "pages": [ - "agent-sessions", - "attachments", - "skills-config", - "mcp-config", - "custom-tools" - ] - }, - { - "group": "System", - "pages": ["file-system"] - }, - { - "group": "Orchestration", - "pages": [ - "architecture", - "session-persistence", - "observability", - "multiplayer", - "security" - ] - }, - { - "group": "Reference", - "pages": [ - "cli", - "inspector", - "opencode-compatibility", - { - "group": "More", - "pages": [ - "credentials", - "daemon", - "cors", - "session-restoration", - "telemetry", - { - "group": "AI", - "pages": ["ai/skill", "ai/llms-txt"] - } - ] - } - ] - } - ] - }, - { - "tab": "HTTP API", - "pages": [ - { - "group": "HTTP Reference", - "openapi": "openapi.json" - } - ] - } - ] - } + "$schema": "https://mintlify.com/docs.json", + "theme": "mint", + "name": "Sandbox Agent SDK", + "appearance": { + "default": "dark", + "strict": true + }, + "colors": { + "primary": "#ff4f00", + "light": "#ff6a2a", + "dark": "#cc3f00" + }, + "favicon": "/favicon.svg", + "logo": { + "light": "/logo/light.svg", + "dark": "/logo/dark.svg" + }, + "integrations": { + "posthog": { + "apiKey": "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo", + "apiHost": "https://ph.rivet.gg", + "sessionRecording": true + } + }, + "navbar": { + "links": [ + { + "label": "Discord", + "icon": "discord", + "href": "https://discord.gg/auCecybynK" + }, + { + "label": "GitHub", + "type": "github", + "href": "https://github.com/rivet-dev/sandbox-agent" + } + ] + }, + "navigation": { + "tabs": [ + { + "tab": "Documentation", + "pages": [ + { + "group": "Getting started", + "pages": [ + "quickstart", + "sdk-overview", + "llm-credentials", + "react-components", + { + "group": "Deploy", + "icon": "server", + "pages": [ + "deploy/local", + "deploy/e2b", + "deploy/daytona", + "deploy/vercel", + "deploy/cloudflare", + "deploy/docker", + "deploy/modal", + "deploy/boxlite", + "deploy/computesdk" + ] + } + ] + }, + { + "group": "Agent", + "pages": [ + "agent-sessions", + { + "group": "Agents", + "icon": "robot", + "pages": ["agents/claude", "agents/codex", "agents/opencode", "agents/cursor", "agents/amp", "agents/pi"] + }, + "attachments", + "skills-config", + "mcp-config", + "custom-tools" + ] + }, + { + "group": "System", + "pages": ["file-system", "processes", "computer-use", "common-software"] + }, + { + "group": "Reference", + "pages": [ + "troubleshooting", + "architecture", + "cli", + "inspector", + "opencode-compatibility", + { + "group": "More", + "pages": [ + "daemon", + "cors", + "session-restoration", + "telemetry", + { + "group": "AI", + "pages": ["ai/skill", "ai/llms-txt"] + } + ] + } + ] + } + ] + }, + { + "tab": "HTTP API", + "pages": [ + { + "group": "HTTP Reference", + "openapi": "openapi.json" + } + ] + } + ] + }, + "__removed": [ + { + "group": "Orchestration", + "pages": ["orchestration-architecture", "session-persistence", "observability", "multiplayer", "security"] + } + ] } diff --git a/docs/gigacode.mdx b/docs/gigacode.mdx deleted file mode 100644 index ccc9e39..0000000 --- a/docs/gigacode.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Gigacode -url: "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode" ---- - - diff --git a/docs/images/inspector.png b/docs/images/inspector.png index 1c16ed2..02e588e 100644 Binary files a/docs/images/inspector.png and b/docs/images/inspector.png differ diff --git a/docs/inspector.mdx b/docs/inspector.mdx index f3b3dc6..1412c21 100644 --- a/docs/inspector.mdx +++ b/docs/inspector.mdx @@ -34,9 +34,33 @@ console.log(url); - Event JSON inspector - Prompt testing - Request/response debugging +- Interactive permission prompts (approve, always-allow, or reject tool-use requests) +- Desktop panel for status, remediation, start/stop, and screenshot refresh +- Process management (create, stop, kill, delete, view logs) +- Interactive PTY terminal for tty processes +- One-shot command execution ## When to use - Development: validate session behavior quickly - Debugging: inspect raw event payloads - Integration work: compare UI behavior with SDK/API calls + +## Process terminal + +The Inspector includes an embedded Ghostty-based terminal for interactive tty +processes. The UI uses the SDK's high-level `connectProcessTerminal(...)` +wrapper via the shared `@sandbox-agent/react` `ProcessTerminal` component. + +## Desktop panel + +The `Desktop` panel shows the current desktop runtime state, missing dependencies, +the suggested install command, last error details, process/log paths, and the +latest captured screenshot. + +Use it to: + +- Check whether desktop dependencies are installed +- Start or stop the managed desktop runtime +- Refresh desktop status +- Capture a fresh screenshot on demand diff --git a/docs/llm-credentials.mdx b/docs/llm-credentials.mdx new file mode 100644 index 0000000..e771740 --- /dev/null +++ b/docs/llm-credentials.mdx @@ -0,0 +1,250 @@ +--- +title: "LLM Credentials" +description: "Strategies for providing LLM provider credentials to agents." +icon: "key" +--- + +Sandbox Agent needs LLM provider credentials (Anthropic, OpenAI, etc.) to run agent sessions. + +## Configuration + +Pass credentials via `spawn.env` when starting a sandbox. Each call to `SandboxAgent.start()` can use different credentials: + +```typescript +import { SandboxAgent } from "sandbox-agent"; + +const sdk = await SandboxAgent.start({ + spawn: { + env: { + ANTHROPIC_API_KEY: "sk-ant-...", + OPENAI_API_KEY: "sk-...", + }, + }, +}); +``` + +Each agent requires credentials from a specific provider. Sandbox Agent checks environment variables (including those passed via `spawn.env`) and host config files: + +| Agent | Provider | Environment variables | Config files | +|-------|----------|----------------------|--------------| +| Claude Code | Anthropic | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` | `~/.claude.json`, `~/.claude/.credentials.json` | +| Amp | Anthropic | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` | `~/.amp/config.json` | +| Codex | OpenAI | `OPENAI_API_KEY`, `CODEX_API_KEY` | `~/.codex/auth.json` | +| OpenCode | Anthropic or OpenAI | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | `~/.local/share/opencode/auth.json` | +| Mock | None | - | - | + +## Credential strategies + +LLM credentials are passed into the sandbox as environment variables. The agent and everything inside the sandbox has access to the token, so it's important to choose the right strategy for how you provision and scope these credentials. + +| Strategy | Who pays | Cost attribution | Best for | +|----------|----------|-----------------|----------| +| **Per-tenant gateway** (recommended) | Your organization, billed back per tenant | Per-tenant keys with budgets | Multi-tenant SaaS, usage-based billing | +| **Bring your own key** | Each user (usage-based) | Per-user by default | Dev environments, internal tools | +| **Shared API key** | Your organization | None (single bill) | Single-tenant apps, internal platforms | +| **Personal subscription** | Each user (existing subscription) | Per-user by default | Local dev, internal tools where users have Claude or Codex subscriptions | + +### Per-tenant gateway (recommended) + +Route LLM traffic through a gateway that mints per-tenant API keys, each with its own spend tracking and budget limits. + +```mermaid +graph LR + B[Your Backend] -->|tenant key| S[Sandbox] + S -->|LLM requests| G[Gateway] + G -->|scoped key| P[LLM Provider] +``` + +Your backend issues a scoped key per tenant, then passes it to the sandbox. This is the typical pattern when using sandbox providers (E2B, Daytona, Docker). + +```typescript expandable +import { SandboxAgent } from "sandbox-agent"; + +async function createTenantSandbox(tenantId: string) { + // Issue a scoped key for this tenant via OpenRouter + const res = await fetch("https://openrouter.ai/api/v1/keys", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.OPENROUTER_PROVISIONING_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: `tenant-${tenantId}`, + limit: 50, + limitResetType: "monthly", + }), + }); + const { key } = await res.json(); + + // Start a sandbox with the tenant's scoped key + const sdk = await SandboxAgent.start({ + spawn: { + env: { + OPENAI_API_KEY: key, // OpenRouter uses OpenAI-compatible endpoints + }, + }, + }); + + const session = await sdk.createSession({ + agent: "claude", + sessionInit: { cwd: "/workspace" }, + }); + + return { sdk, session }; +} +``` + +#### Security + +Recommended for multi-tenant applications. Each tenant gets a scoped key with its own budget, so exfiltration only exposes that tenant's allowance. + +#### Use cases + +- **Multi-tenant SaaS**: per-tenant spend tracking and budget limits +- **Production apps**: exposed to end users who need isolated credentials +- **Usage-based billing**: each tenant pays for their own consumption + +#### Choosing a gateway + + + + + +Managed service, zero infrastructure. [OpenRouter](https://openrouter.ai/docs/features/provisioning-api-keys) provides per-tenant API keys with spend tracking and budget limits via their Provisioning API. Pass the tenant key to Sandbox Agent as `OPENAI_API_KEY` (OpenRouter uses OpenAI-compatible endpoints). + +```bash +# Create a key for a tenant with a $50/month budget +curl https://openrouter.ai/api/v1/keys \ + -H "Authorization: Bearer $PROVISIONING_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "tenant-acme", + "limit": 50, + "limitResetType": "monthly" + }' +``` + +Easiest to set up but not open-source. See [OpenRouter pricing](https://openrouter.ai/docs/framework/pricing) for details. + + + + + +Self-hosted, open-source (MIT). [LiteLLM](https://github.com/BerriAI/litellm) is an OpenAI-compatible proxy with hierarchical budgets (org, team, user, key), virtual keys, and spend tracking. Requires Python + PostgreSQL. + +```bash +# Create a team (tenant) with a $500 budget +curl http://litellm:4000/team/new \ + -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "team_alias": "tenant-acme", + "max_budget": 500 + }' + +# Generate a key for that team +curl http://litellm:4000/key/generate \ + -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "team_id": "team-abc123", + "max_budget": 100 + }' +``` + +Full control with no vendor lock-in. Organization-level features require an enterprise license. + + + + + +Self-hosted, open-source (Apache 2.0). [Portkey](https://github.com/Portkey-AI/gateway) is a lightweight OpenAI-compatible gateway supporting 200+ providers. Single binary, no database required. Create virtual keys with per-tenant budget limits and pass them to Sandbox Agent. + +Lightest operational footprint of the self-hosted options. Observability and analytics require the managed platform or your own tooling. + + + + + +To bill tenants for LLM usage, use [Stripe token billing](https://docs.stripe.com/billing/token-billing) (integrates natively with OpenRouter) or query your gateway's spend API and feed usage into your billing system. + +### Bring your own key + +Each user provides their own API key. Users are billed directly by the LLM provider with no additional infrastructure needed. + +Pass the user's key via `spawn.env`: + +```typescript +const sdk = await SandboxAgent.start({ + spawn: { + env: { + ANTHROPIC_API_KEY: userProvidedKey, + }, + }, +}); +``` + +#### Security + +API keys are typically long-lived. The key is visible to the agent and anything running inside the sandbox, so exfiltration is possible. This is usually acceptable for developer-facing tools where the user owns the key. + +#### Use cases + +- **Developer tools**: each user manages their own API key +- **Internal platforms**: users already have LLM provider accounts +- **Per-user billing**: no extra infrastructure needed + +### Shared credentials + +A single organization-wide API key is used for all sessions. All token usage appears on one bill with no per-user or per-tenant cost attribution. + +```typescript +const sdk = await SandboxAgent.start({ + spawn: { + env: { + ANTHROPIC_API_KEY: process.env.ORG_ANTHROPIC_KEY!, + OPENAI_API_KEY: process.env.ORG_OPENAI_KEY!, + }, + }, +}); +``` + +If you need to track or limit spend per tenant, use a per-tenant gateway instead. + +#### Security + +Not recommended for anything other than internal tooling. A single exfiltrated key exposes your organization's entire LLM budget. If you need org-paid credentials for external users, use a per-tenant gateway with scoped keys instead. + +#### Use cases + +- **Single-tenant apps**: small number of users, one bill +- **Prototyping**: cost attribution not needed yet +- **Simplicity over security**: acceptable when exfiltration risk is low + +### Personal subscription + +If the user is signed into Claude Code or Codex on the host machine, Sandbox Agent automatically picks up their OAuth tokens. No configuration is needed. + +#### Remote sandboxes + +Extract credentials locally and pass them to a remote sandbox via `spawn.env`: + +```bash +$ sandbox-agent credentials extract-env +ANTHROPIC_API_KEY=sk-ant-... +CLAUDE_API_KEY=sk-ant-... +OPENAI_API_KEY=sk-... +CODEX_API_KEY=sk-... +``` + +Use `-e` to prefix with `export` for shell sourcing. + +#### Security + +Personal subscriptions use OAuth tokens with a limited lifespan. These are the same credentials used when running an agent normally on the host. If a token is exfiltrated from the sandbox, the exposure window is short. + +#### Use cases + +- **Local development**: users are already signed into Claude Code or Codex +- **Internal tools**: every user has their own subscription +- **Prototyping**: no key management needed \ No newline at end of file diff --git a/docs/manage-sessions.mdx b/docs/manage-sessions.mdx index 7d51538..c39dd04 100644 --- a/docs/manage-sessions.mdx +++ b/docs/manage-sessions.mdx @@ -6,8 +6,6 @@ icon: "database" Sandbox Agent stores sessions in memory only. When the server restarts or the sandbox is destroyed, all session data is lost. It's your responsibility to persist events to your own database. -See the [Building a Chat UI](/building-chat-ui) guide for understanding session lifecycle events like `session.started` and `session.ended`. - ## Recommended approach 1. Store events to your database as they arrive @@ -18,11 +16,11 @@ This prevents duplicate writes and lets you recover from disconnects. ## Receiving Events -Two ways to receive events: SSE streaming (recommended) or polling. +Two ways to receive events: streaming (recommended) or polling. ### Streaming -Use SSE for real-time events with automatic reconnection support. +Use streaming for real-time events with automatic reconnection support. ```typescript import { SandboxAgentClient } from "sandbox-agent"; @@ -44,7 +42,7 @@ for await (const event of client.streamEvents("my-session", { offset })) { ### Polling -If you can't use SSE streaming, poll the events endpoint: +If you can't use streaming, poll the events endpoint: ```typescript const lastEvent = await db.getLastEvent("my-session"); @@ -244,7 +242,7 @@ const events = await redis.lrange(`session:${sessionId}`, offset, -1); ## Handling disconnects -The SSE stream may disconnect due to network issues. Handle reconnection gracefully: +The event stream may disconnect due to network issues. Handle reconnection gracefully: ```typescript async function streamWithRetry(sessionId: string) { diff --git a/docs/mcp-config.mdx b/docs/mcp-config.mdx index 71e8105..cc1c976 100644 --- a/docs/mcp-config.mdx +++ b/docs/mcp-config.mdx @@ -27,9 +27,7 @@ await sdk.setMcpConfig( // Create a session using the configured MCP servers const session = await sdk.createSession({ agent: "claude", - sessionInit: { - cwd: "/workspace", - }, + cwd: "/workspace", }); await session.prompt([ diff --git a/docs/multiplayer.mdx b/docs/multiplayer.mdx index 4f405ea..215bb1c 100644 --- a/docs/multiplayer.mdx +++ b/docs/multiplayer.mdx @@ -20,8 +20,40 @@ Use [actor keys](https://rivet.dev/docs/actors/keys) to map each workspace to on ```ts Actor (server) import { actor, setup } from "rivetkit"; -import { SandboxAgent } from "sandbox-agent"; -import { RivetSessionPersistDriver, type RivetPersistState } from "@sandbox-agent/persist-rivet"; +import { SandboxAgent, type SessionPersistDriver, type SessionRecord, type SessionEvent, type ListPageRequest, type ListPage, type ListEventsRequest } from "sandbox-agent"; + +interface RivetPersistData { sessions: Record; events: Record; } +type RivetPersistState = { _sandboxAgentPersist: RivetPersistData }; + +class RivetSessionPersistDriver implements SessionPersistDriver { + private readonly stateKey: string; + private readonly ctx: { state: Record }; + constructor(ctx: { state: Record }, options: { stateKey?: string } = {}) { + this.ctx = ctx; + this.stateKey = options.stateKey ?? "_sandboxAgentPersist"; + if (!this.ctx.state[this.stateKey]) { + this.ctx.state[this.stateKey] = { sessions: {}, events: {} }; + } + } + private get data(): RivetPersistData { return this.ctx.state[this.stateKey] as RivetPersistData; } + async getSession(id: string) { const s = this.data.sessions[id]; return s ? { ...s } : undefined; } + async listSessions(request: ListPageRequest = {}): Promise> { + const sorted = Object.values(this.data.sessions).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)); + const offset = Number(request.cursor ?? 0); + const limit = request.limit ?? 100; + const slice = sorted.slice(offset, offset + limit); + return { items: slice, nextCursor: offset + slice.length < sorted.length ? String(offset + slice.length) : undefined }; + } + async updateSession(session: SessionRecord) { this.data.sessions[session.id] = { ...session }; if (!this.data.events[session.id]) this.data.events[session.id] = []; } + async listEvents(request: ListEventsRequest): Promise> { + const all = [...(this.data.events[request.sessionId] ?? [])].sort((a, b) => a.eventIndex - b.eventIndex || a.id.localeCompare(b.id)); + const offset = Number(request.cursor ?? 0); + const limit = request.limit ?? 100; + const slice = all.slice(offset, offset + limit); + return { items: slice, nextCursor: offset + slice.length < all.length ? String(offset + slice.length) : undefined }; + } + async insertEvent(sessionId: string, event: SessionEvent) { const events = this.data.events[sessionId] ?? []; events.push({ ...event, payload: JSON.parse(JSON.stringify(event.payload)) }); this.data.events[sessionId] = events; } +} type WorkspaceState = RivetPersistState & { sandboxId: string; @@ -111,5 +143,5 @@ await conn.prompt({ ## Notes - Keep sandbox calls actor-only. Browser clients should not call Sandbox Agent directly. -- Use `@sandbox-agent/persist-rivet` so session history persists in actor state. +- Copy the Rivet persist driver from the example above into your project so session history persists in actor state. - For client connection patterns, see [Rivet JavaScript client](https://rivet.dev/docs/clients/javascript). diff --git a/docs/observability.mdx b/docs/observability.mdx index 770fe8b..5b5751b 100644 --- a/docs/observability.mdx +++ b/docs/observability.mdx @@ -1,7 +1,7 @@ --- title: "Observability" description: "Track session activity with OpenTelemetry." -icon: "terminal" +icon: "chart-line" --- Use OpenTelemetry to instrument session traffic, then ship telemetry to your collector/backend. diff --git a/docs/openapi.json b/docs/openapi.json index 01fd073..3624707 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "0.2.0" + "version": "0.4.2" }, "servers": [ { @@ -20,9 +20,7 @@ "paths": { "/v1/acp": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_acp_servers", "responses": { "200": { @@ -40,9 +38,7 @@ }, "/v1/acp/{server_id}": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_acp", "parameters": [ { @@ -92,9 +88,7 @@ } }, "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_acp", "parameters": [ { @@ -204,9 +198,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_acp", "parameters": [ { @@ -228,9 +220,7 @@ }, "/v1/agents": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_agents", "parameters": [ { @@ -280,9 +270,7 @@ }, "/v1/agents/{agent}": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_agent", "parameters": [ { @@ -351,9 +339,7 @@ }, "/v1/agents/{agent}/install": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_agent_install", "parameters": [ { @@ -412,9 +398,7 @@ }, "/v1/config/mcp": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_config_mcp", "parameters": [ { @@ -460,9 +444,7 @@ } }, "put": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "put_v1_config_mcp", "parameters": [ { @@ -501,9 +483,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_config_mcp", "parameters": [ { @@ -534,9 +514,7 @@ }, "/v1/config/skills": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_config_skills", "parameters": [ { @@ -582,9 +560,7 @@ } }, "put": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "put_v1_config_skills", "parameters": [ { @@ -623,9 +599,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_config_skills", "parameters": [ { @@ -654,11 +628,1817 @@ } } }, + "/v1/desktop/clipboard": { + "get": { + "tags": ["v1"], + "summary": "Read the desktop clipboard.", + "description": "Returns the current text content of the X11 clipboard.", + "operationId": "get_v1_desktop_clipboard", + "parameters": [ + { + "name": "selection", + "in": "query", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Clipboard contents", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopClipboardResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "500": { + "description": "Clipboard read failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "post": { + "tags": ["v1"], + "summary": "Write to the desktop clipboard.", + "description": "Sets the text content of the X11 clipboard.", + "operationId": "post_v1_desktop_clipboard", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopClipboardWriteRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Clipboard updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopActionResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "500": { + "description": "Clipboard write failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/display/info": { + "get": { + "tags": ["v1"], + "summary": "Get desktop display information.", + "description": "Performs a health-gated display query against the managed desktop and\nreturns the current display identifier and resolution.", + "operationId": "get_v1_desktop_display_info", + "responses": { + "200": { + "description": "Desktop display information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopDisplayInfoResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "Desktop runtime health or display query failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/keyboard/down": { + "post": { + "tags": ["v1"], + "summary": "Press and hold a desktop keyboard key.", + "description": "Performs a health-gated `xdotool keydown` operation against the managed\ndesktop.", + "operationId": "post_v1_desktop_keyboard_down", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopKeyboardDownRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop keyboard action result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopActionResponse" + } + } + } + }, + "400": { + "description": "Invalid keyboard down request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/keyboard/press": { + "post": { + "tags": ["v1"], + "summary": "Press a desktop keyboard shortcut.", + "description": "Performs a health-gated `xdotool key` operation against the managed\ndesktop.", + "operationId": "post_v1_desktop_keyboard_press", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopKeyboardPressRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop keyboard action result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopActionResponse" + } + } + } + }, + "400": { + "description": "Invalid keyboard press request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/keyboard/type": { + "post": { + "tags": ["v1"], + "summary": "Type desktop keyboard text.", + "description": "Performs a health-gated `xdotool type` operation against the managed\ndesktop.", + "operationId": "post_v1_desktop_keyboard_type", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopKeyboardTypeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop keyboard action result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopActionResponse" + } + } + } + }, + "400": { + "description": "Invalid keyboard type request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/keyboard/up": { + "post": { + "tags": ["v1"], + "summary": "Release a desktop keyboard key.", + "description": "Performs a health-gated `xdotool keyup` operation against the managed\ndesktop.", + "operationId": "post_v1_desktop_keyboard_up", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopKeyboardUpRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop keyboard action result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopActionResponse" + } + } + } + }, + "400": { + "description": "Invalid keyboard up request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/launch": { + "post": { + "tags": ["v1"], + "summary": "Launch a desktop application.", + "description": "Launches an application by name on the managed desktop, optionally waiting\nfor its window to appear.", + "operationId": "post_v1_desktop_launch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopLaunchRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Application launched", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopLaunchResponse" + } + } + } + }, + "404": { + "description": "Application not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/click": { + "post": { + "tags": ["v1"], + "summary": "Click on the desktop.", + "description": "Performs a health-gated pointer move and click against the managed desktop\nand returns the resulting mouse position.", + "operationId": "post_v1_desktop_mouse_click", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMouseClickRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop mouse position after click", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "400": { + "description": "Invalid mouse click request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/down": { + "post": { + "tags": ["v1"], + "summary": "Press and hold a desktop mouse button.", + "description": "Performs a health-gated optional pointer move followed by `xdotool mousedown`\nand returns the resulting mouse position.", + "operationId": "post_v1_desktop_mouse_down", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMouseDownRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop mouse position after button press", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "400": { + "description": "Invalid mouse down request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/drag": { + "post": { + "tags": ["v1"], + "summary": "Drag the desktop mouse.", + "description": "Performs a health-gated drag gesture against the managed desktop and\nreturns the resulting mouse position.", + "operationId": "post_v1_desktop_mouse_drag", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMouseDragRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop mouse position after drag", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "400": { + "description": "Invalid mouse drag request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/move": { + "post": { + "tags": ["v1"], + "summary": "Move the desktop mouse.", + "description": "Performs a health-gated absolute pointer move on the managed desktop and\nreturns the resulting mouse position.", + "operationId": "post_v1_desktop_mouse_move", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMouseMoveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop mouse position after move", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "400": { + "description": "Invalid mouse move request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/position": { + "get": { + "tags": ["v1"], + "summary": "Get the current desktop mouse position.", + "description": "Performs a health-gated mouse position query against the managed desktop.", + "operationId": "get_v1_desktop_mouse_position", + "responses": { + "200": { + "description": "Desktop mouse position", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input check failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/scroll": { + "post": { + "tags": ["v1"], + "summary": "Scroll the desktop mouse wheel.", + "description": "Performs a health-gated scroll gesture at the requested coordinates and\nreturns the resulting mouse position.", + "operationId": "post_v1_desktop_mouse_scroll", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMouseScrollRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop mouse position after scroll", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "400": { + "description": "Invalid mouse scroll request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/mouse/up": { + "post": { + "tags": ["v1"], + "summary": "Release a desktop mouse button.", + "description": "Performs a health-gated optional pointer move followed by `xdotool mouseup`\nand returns the resulting mouse position.", + "operationId": "post_v1_desktop_mouse_up", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMouseUpRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop mouse position after button release", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopMousePositionResponse" + } + } + } + }, + "400": { + "description": "Invalid mouse up request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or input failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/open": { + "post": { + "tags": ["v1"], + "summary": "Open a file or URL with the default handler.", + "description": "Opens a file path or URL using xdg-open on the managed desktop.", + "operationId": "post_v1_desktop_open", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopOpenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Target opened", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopOpenResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/recording/start": { + "post": { + "tags": ["v1"], + "summary": "Start desktop recording.", + "description": "Starts an ffmpeg x11grab recording against the managed desktop and returns\nthe created recording metadata.", + "operationId": "post_v1_desktop_recording_start", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopRecordingStartRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop recording started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopRecordingInfo" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready or a recording is already active", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop recording failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/recording/stop": { + "post": { + "tags": ["v1"], + "summary": "Stop desktop recording.", + "description": "Stops the active desktop recording and returns the finalized recording\nmetadata.", + "operationId": "post_v1_desktop_recording_stop", + "responses": { + "200": { + "description": "Desktop recording stopped", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopRecordingInfo" + } + } + } + }, + "409": { + "description": "No active desktop recording", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop recording stop failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/recordings": { + "get": { + "tags": ["v1"], + "summary": "List desktop recordings.", + "description": "Returns the current desktop recording catalog.", + "operationId": "get_v1_desktop_recordings", + "responses": { + "200": { + "description": "Desktop recordings", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopRecordingListResponse" + } + } + } + }, + "502": { + "description": "Desktop recordings query failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/recordings/{id}": { + "get": { + "tags": ["v1"], + "summary": "Get desktop recording metadata.", + "description": "Returns metadata for a single desktop recording.", + "operationId": "get_v1_desktop_recording", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Desktop recording ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Desktop recording metadata", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopRecordingInfo" + } + } + } + }, + "404": { + "description": "Unknown desktop recording", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "delete": { + "tags": ["v1"], + "summary": "Delete a desktop recording.", + "description": "Removes a completed desktop recording and its file from disk.", + "operationId": "delete_v1_desktop_recording", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Desktop recording ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Desktop recording deleted" + }, + "404": { + "description": "Unknown desktop recording", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop recording is still active", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/recordings/{id}/download": { + "get": { + "tags": ["v1"], + "summary": "Download a desktop recording.", + "description": "Serves the recorded MP4 bytes for a completed desktop recording.", + "operationId": "get_v1_desktop_recording_download", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Desktop recording ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Desktop recording as MP4 bytes" + }, + "404": { + "description": "Unknown desktop recording", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/screenshot": { + "get": { + "tags": ["v1"], + "summary": "Capture a full desktop screenshot.", + "description": "Performs a health-gated full-frame screenshot of the managed desktop and\nreturns the requested image bytes.", + "operationId": "get_v1_desktop_screenshot", + "parameters": [ + { + "name": "format", + "in": "query", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopScreenshotFormat" + } + ], + "nullable": true + } + }, + { + "name": "quality", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + }, + { + "name": "scale", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "float", + "nullable": true + } + }, + { + "name": "showCursor", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Desktop screenshot as image bytes" + }, + "400": { + "description": "Invalid screenshot query", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or screenshot capture failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/screenshot/region": { + "get": { + "tags": ["v1"], + "summary": "Capture a desktop screenshot region.", + "description": "Performs a health-gated screenshot crop against the managed desktop and\nreturns the requested region image bytes.", + "operationId": "get_v1_desktop_screenshot_region", + "parameters": [ + { + "name": "x", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "y", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "width", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + { + "name": "height", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + { + "name": "format", + "in": "query", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopScreenshotFormat" + } + ], + "nullable": true + } + }, + { + "name": "quality", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + }, + { + "name": "scale", + "in": "query", + "required": false, + "schema": { + "type": "number", + "format": "float", + "nullable": true + } + }, + { + "name": "showCursor", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Desktop screenshot region as image bytes" + }, + "400": { + "description": "Invalid screenshot region", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop runtime health or screenshot capture failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/start": { + "post": { + "tags": ["v1"], + "summary": "Start the private desktop runtime.", + "description": "Lazily launches the managed Xvfb/openbox stack, validates display health,\nand returns the resulting desktop status snapshot.", + "operationId": "post_v1_desktop_start", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStartRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Desktop runtime status after start", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStatusResponse" + } + } + } + }, + "400": { + "description": "Invalid desktop start request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is already transitioning", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Desktop API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "Desktop runtime could not be started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/status": { + "get": { + "tags": ["v1"], + "summary": "Get desktop runtime status.", + "description": "Returns the current desktop runtime state, dependency status, active\ndisplay metadata, and supervised process information.", + "operationId": "get_v1_desktop_status", + "responses": { + "200": { + "description": "Desktop runtime status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStatusResponse" + } + } + } + }, + "401": { + "description": "Authentication required", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/stop": { + "post": { + "tags": ["v1"], + "summary": "Stop the private desktop runtime.", + "description": "Terminates the managed openbox/Xvfb/dbus processes owned by the desktop\nruntime and returns the resulting status snapshot.", + "operationId": "post_v1_desktop_stop", + "responses": { + "200": { + "description": "Desktop runtime status after stop", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStatusResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is already transitioning", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/stream/signaling": { + "get": { + "tags": ["v1"], + "summary": "Open a desktop WebRTC signaling session.", + "description": "Upgrades the connection to a WebSocket used for WebRTC signaling between\nthe browser client and the desktop streaming process. Also accepts mouse\nand keyboard input frames as a fallback transport.", + "operationId": "get_v1_desktop_stream_ws", + "parameters": [ + { + "name": "access_token", + "in": "query", + "description": "Bearer token alternative for WS auth", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "101": { + "description": "WebSocket upgraded" + }, + "409": { + "description": "Desktop runtime or streaming session is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "502": { + "description": "Desktop stream failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/stream/start": { + "post": { + "tags": ["v1"], + "summary": "Start desktop streaming.", + "description": "Enables desktop websocket streaming for the managed desktop.", + "operationId": "post_v1_desktop_stream_start", + "responses": { + "200": { + "description": "Desktop streaming started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStreamStatusResponse" + } + } + } + } + } + } + }, + "/v1/desktop/stream/status": { + "get": { + "tags": ["v1"], + "summary": "Get desktop stream status.", + "description": "Returns the current state of the desktop WebRTC streaming session.", + "operationId": "get_v1_desktop_stream_status", + "responses": { + "200": { + "description": "Desktop stream status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStreamStatusResponse" + } + } + } + } + } + } + }, + "/v1/desktop/stream/stop": { + "post": { + "tags": ["v1"], + "summary": "Stop desktop streaming.", + "description": "Disables desktop websocket streaming for the managed desktop.", + "operationId": "post_v1_desktop_stream_stop", + "responses": { + "200": { + "description": "Desktop streaming stopped", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopStreamStatusResponse" + } + } + } + } + } + } + }, + "/v1/desktop/windows": { + "get": { + "tags": ["v1"], + "summary": "List visible desktop windows.", + "description": "Performs a health-gated visible-window enumeration against the managed\ndesktop and returns the current window metadata.", + "operationId": "get_v1_desktop_windows", + "responses": { + "200": { + "description": "Visible desktop windows", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowListResponse" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "503": { + "description": "Desktop runtime health or window query failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/windows/focused": { + "get": { + "tags": ["v1"], + "summary": "Get the currently focused desktop window.", + "description": "Returns information about the window that currently has input focus.", + "operationId": "get_v1_desktop_windows_focused", + "responses": { + "200": { + "description": "Focused window info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowInfo" + } + } + } + }, + "404": { + "description": "No window is focused", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/windows/{id}/focus": { + "post": { + "tags": ["v1"], + "summary": "Focus a desktop window.", + "description": "Brings the specified window to the foreground and gives it input focus.", + "operationId": "post_v1_desktop_window_focus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "X11 window ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Window info after focus", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowInfo" + } + } + } + }, + "404": { + "description": "Window not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/windows/{id}/move": { + "post": { + "tags": ["v1"], + "summary": "Move a desktop window.", + "description": "Moves the specified window to the given position.", + "operationId": "post_v1_desktop_window_move", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "X11 window ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowMoveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Window info after move", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowInfo" + } + } + } + }, + "404": { + "description": "Window not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/desktop/windows/{id}/resize": { + "post": { + "tags": ["v1"], + "summary": "Resize a desktop window.", + "description": "Resizes the specified window to the given dimensions.", + "operationId": "post_v1_desktop_window_resize", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "X11 window ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowResizeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Window info after resize", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopWindowInfo" + } + } + } + }, + "404": { + "description": "Window not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Desktop runtime is not ready", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/v1/fs/entries": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_fs_entries", "parameters": [ { @@ -691,9 +2471,7 @@ }, "/v1/fs/entry": { "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_fs_entry", "parameters": [ { @@ -732,9 +2510,7 @@ }, "/v1/fs/file": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_fs_file", "parameters": [ { @@ -754,9 +2530,7 @@ } }, "put": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "put_v1_fs_file", "parameters": [ { @@ -796,9 +2570,7 @@ }, "/v1/fs/mkdir": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_fs_mkdir", "parameters": [ { @@ -827,9 +2599,7 @@ }, "/v1/fs/move": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_fs_move", "requestBody": { "content": { @@ -857,9 +2627,7 @@ }, "/v1/fs/stat": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_fs_stat", "parameters": [ { @@ -888,9 +2656,7 @@ }, "/v1/fs/upload-batch": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_fs_upload_batch", "parameters": [ { @@ -931,9 +2697,7 @@ }, "/v1/health": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_health", "responses": { "200": { @@ -948,15 +2712,807 @@ } } } + }, + "/v1/processes": { + "get": { + "tags": ["v1"], + "summary": "List all managed processes.", + "description": "Returns a list of all processes (running and exited) currently tracked\nby the runtime, sorted by process ID.", + "operationId": "get_v1_processes", + "parameters": [ + { + "name": "owner", + "in": "query", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ProcessOwner" + } + ], + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "List processes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessListResponse" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "post": { + "tags": ["v1"], + "summary": "Create a long-lived managed process.", + "description": "Spawns a new process with the given command and arguments. Supports both\npipe-based and PTY (tty) modes. Returns the process descriptor on success.", + "operationId": "post_v1_processes", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Started process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessInfo" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Process limit or state conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/config": { + "get": { + "tags": ["v1"], + "summary": "Get process runtime configuration.", + "description": "Returns the current runtime configuration for the process management API,\nincluding limits for concurrency, timeouts, and buffer sizes.", + "operationId": "get_v1_processes_config", + "responses": { + "200": { + "description": "Current runtime process config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessConfig" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "post": { + "tags": ["v1"], + "summary": "Update process runtime configuration.", + "description": "Replaces the runtime configuration for the process management API.\nValidates that all values are non-zero and clamps default timeout to max.", + "operationId": "post_v1_processes_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated runtime process config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessConfig" + } + } + } + }, + "400": { + "description": "Invalid config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/run": { + "post": { + "tags": ["v1"], + "summary": "Run a one-shot command.", + "description": "Executes a command to completion and returns its stdout, stderr, exit code,\nand duration. Supports configurable timeout and output size limits.", + "operationId": "post_v1_processes_run", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessRunRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "One-off command result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessRunResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}": { + "get": { + "tags": ["v1"], + "summary": "Get a single process by ID.", + "description": "Returns the current state of a managed process including its status,\nPID, exit code, and creation/exit timestamps.", + "operationId": "get_v1_process", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Process details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessInfo" + } + } + } + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "delete": { + "tags": ["v1"], + "summary": "Delete a process record.", + "description": "Removes a stopped process from the runtime. Returns 409 if the process\nis still running; stop or kill it first.", + "operationId": "delete_v1_process", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Process deleted" + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Process is still running", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}/input": { + "post": { + "tags": ["v1"], + "summary": "Write input to a process.", + "description": "Sends data to a process's stdin (pipe mode) or PTY writer (tty mode).\nData can be encoded as base64, utf8, or text. Returns 413 if the decoded\npayload exceeds the configured `maxInputBytesPerRequest` limit.", + "operationId": "post_v1_process_input", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessInputRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Input accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessInputResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Process not writable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "413": { + "description": "Input exceeds configured limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}/kill": { + "post": { + "tags": ["v1"], + "summary": "Send SIGKILL to a process.", + "description": "Sends SIGKILL to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.", + "operationId": "post_v1_process_kill", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "waitMs", + "in": "query", + "description": "Wait up to N ms for process to exit", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "Kill signal sent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessInfo" + } + } + } + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}/logs": { + "get": { + "tags": ["v1"], + "summary": "Fetch process logs.", + "description": "Returns buffered log entries for a process. Supports filtering by stream\ntype, tail count, and sequence-based resumption. When `follow=true`,\nreturns an SSE stream that replays buffered entries then streams live output.", + "operationId": "get_v1_process_logs", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "stream", + "in": "query", + "description": "stdout|stderr|combined|pty", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ProcessLogsStream" + } + ], + "nullable": true + } + }, + { + "name": "tail", + "in": "query", + "description": "Tail N entries", + "required": false, + "schema": { + "type": "integer", + "nullable": true, + "minimum": 0 + } + }, + { + "name": "follow", + "in": "query", + "description": "Follow via SSE", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + }, + { + "name": "since", + "in": "query", + "description": "Only entries with sequence greater than this", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "Process logs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessLogsResponse" + } + } + } + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}/stop": { + "post": { + "tags": ["v1"], + "summary": "Send SIGTERM to a process.", + "description": "Sends SIGTERM to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.", + "operationId": "post_v1_process_stop", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "waitMs", + "in": "query", + "description": "Wait up to N ms for process to exit", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "Stop signal sent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessInfo" + } + } + } + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}/terminal/resize": { + "post": { + "tags": ["v1"], + "summary": "Resize a process terminal.", + "description": "Sets the PTY window size (columns and rows) for a tty-mode process and\nsends SIGWINCH so the child process can adapt.", + "operationId": "post_v1_process_terminal_resize", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessTerminalResizeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Resize accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessTerminalResizeResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Not a terminal process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/v1/processes/{id}/terminal/ws": { + "get": { + "tags": ["v1"], + "summary": "Open an interactive WebSocket terminal session.", + "description": "Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts\n`access_token` query param for browser-based auth (WebSocket API cannot\nsend custom headers). Streams raw PTY output as binary frames and accepts\nJSON control frames for input, resize, and close.", + "operationId": "get_v1_process_terminal_ws", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Process ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "access_token", + "in": "query", + "description": "Bearer token alternative for WS auth", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "101": { + "description": "WebSocket upgraded" + }, + "400": { + "description": "Invalid websocket frame or upgrade request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Unknown process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "409": { + "description": "Not a terminal process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "501": { + "description": "Process API unsupported on this platform", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } } }, "components": { "schemas": { "AcpEnvelope": { "type": "object", - "required": [ - "jsonrpc" - ], + "required": ["jsonrpc"], "properties": { "error": { "nullable": true @@ -990,11 +3546,7 @@ }, "AcpServerInfo": { "type": "object", - "required": [ - "serverId", - "agent", - "createdAtMs" - ], + "required": ["serverId", "agent", "createdAtMs"], "properties": { "agent": { "type": "string" @@ -1010,9 +3562,7 @@ }, "AcpServerListResponse": { "type": "object", - "required": [ - "servers" - ], + "required": ["servers"], "properties": { "servers": { "type": "array", @@ -1103,12 +3653,7 @@ }, "AgentInfo": { "type": "object", - "required": [ - "id", - "installed", - "credentialsAvailable", - "capabilities" - ], + "required": ["id", "installed", "credentialsAvailable", "capabilities"], "properties": { "capabilities": { "$ref": "#/components/schemas/AgentCapabilities" @@ -1151,11 +3696,7 @@ }, "AgentInstallArtifact": { "type": "object", - "required": [ - "kind", - "path", - "source" - ], + "required": ["kind", "path", "source"], "properties": { "kind": { "type": "string" @@ -1191,10 +3732,7 @@ }, "AgentInstallResponse": { "type": "object", - "required": [ - "already_installed", - "artifacts" - ], + "required": ["already_installed", "artifacts"], "properties": { "already_installed": { "type": "boolean" @@ -1209,9 +3747,7 @@ }, "AgentListResponse": { "type": "object", - "required": [ - "agents" - ], + "required": ["agents"], "properties": { "agents": { "type": "array", @@ -1221,6 +3757,769 @@ } } }, + "DesktopActionResponse": { + "type": "object", + "required": ["ok"], + "properties": { + "ok": { + "type": "boolean" + } + } + }, + "DesktopClipboardQuery": { + "type": "object", + "properties": { + "selection": { + "type": "string", + "nullable": true + } + } + }, + "DesktopClipboardResponse": { + "type": "object", + "required": ["text", "selection"], + "properties": { + "selection": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "DesktopClipboardWriteRequest": { + "type": "object", + "required": ["text"], + "properties": { + "selection": { + "type": "string", + "nullable": true + }, + "text": { + "type": "string" + } + } + }, + "DesktopDisplayInfoResponse": { + "type": "object", + "required": ["display", "resolution"], + "properties": { + "display": { + "type": "string" + }, + "resolution": { + "$ref": "#/components/schemas/DesktopResolution" + } + } + }, + "DesktopErrorInfo": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "DesktopKeyModifiers": { + "type": "object", + "properties": { + "alt": { + "type": "boolean", + "nullable": true + }, + "cmd": { + "type": "boolean", + "nullable": true + }, + "ctrl": { + "type": "boolean", + "nullable": true + }, + "shift": { + "type": "boolean", + "nullable": true + } + } + }, + "DesktopKeyboardDownRequest": { + "type": "object", + "required": ["key"], + "properties": { + "key": { + "type": "string" + } + } + }, + "DesktopKeyboardPressRequest": { + "type": "object", + "required": ["key"], + "properties": { + "key": { + "type": "string" + }, + "modifiers": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopKeyModifiers" + } + ], + "nullable": true + } + } + }, + "DesktopKeyboardTypeRequest": { + "type": "object", + "required": ["text"], + "properties": { + "delayMs": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "text": { + "type": "string" + } + } + }, + "DesktopKeyboardUpRequest": { + "type": "object", + "required": ["key"], + "properties": { + "key": { + "type": "string" + } + } + }, + "DesktopLaunchRequest": { + "type": "object", + "required": ["app"], + "properties": { + "app": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "wait": { + "type": "boolean", + "nullable": true + } + } + }, + "DesktopLaunchResponse": { + "type": "object", + "required": ["processId"], + "properties": { + "pid": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "processId": { + "type": "string" + }, + "windowId": { + "type": "string", + "nullable": true + } + } + }, + "DesktopMouseButton": { + "type": "string", + "enum": ["left", "middle", "right"] + }, + "DesktopMouseClickRequest": { + "type": "object", + "required": ["x", "y"], + "properties": { + "button": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopMouseButton" + } + ], + "nullable": true + }, + "clickCount": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopMouseDownRequest": { + "type": "object", + "properties": { + "button": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopMouseButton" + } + ], + "nullable": true + }, + "x": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "y": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, + "DesktopMouseDragRequest": { + "type": "object", + "required": ["startX", "startY", "endX", "endY"], + "properties": { + "button": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopMouseButton" + } + ], + "nullable": true + }, + "endX": { + "type": "integer", + "format": "int32" + }, + "endY": { + "type": "integer", + "format": "int32" + }, + "startX": { + "type": "integer", + "format": "int32" + }, + "startY": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopMouseMoveRequest": { + "type": "object", + "required": ["x", "y"], + "properties": { + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopMousePositionResponse": { + "type": "object", + "required": ["x", "y"], + "properties": { + "screen": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "window": { + "type": "string", + "nullable": true + }, + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopMouseScrollRequest": { + "type": "object", + "required": ["x", "y"], + "properties": { + "deltaX": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "deltaY": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopMouseUpRequest": { + "type": "object", + "properties": { + "button": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopMouseButton" + } + ], + "nullable": true + }, + "x": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "y": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, + "DesktopOpenRequest": { + "type": "object", + "required": ["target"], + "properties": { + "target": { + "type": "string" + } + } + }, + "DesktopOpenResponse": { + "type": "object", + "required": ["processId"], + "properties": { + "pid": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "processId": { + "type": "string" + } + } + }, + "DesktopProcessInfo": { + "type": "object", + "required": ["name", "running"], + "properties": { + "logPath": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "pid": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "running": { + "type": "boolean" + } + } + }, + "DesktopRecordingInfo": { + "type": "object", + "required": ["id", "status", "fileName", "bytes", "startedAt"], + "properties": { + "bytes": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "endedAt": { + "type": "string", + "nullable": true + }, + "fileName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "processId": { + "type": "string", + "nullable": true + }, + "startedAt": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/DesktopRecordingStatus" + } + } + }, + "DesktopRecordingListResponse": { + "type": "object", + "required": ["recordings"], + "properties": { + "recordings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DesktopRecordingInfo" + } + } + } + }, + "DesktopRecordingStartRequest": { + "type": "object", + "properties": { + "fps": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + } + }, + "DesktopRecordingStatus": { + "type": "string", + "enum": ["recording", "completed", "failed"] + }, + "DesktopRegionScreenshotQuery": { + "type": "object", + "required": ["x", "y", "width", "height"], + "properties": { + "format": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopScreenshotFormat" + } + ], + "nullable": true + }, + "height": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "quality": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "scale": { + "type": "number", + "format": "float", + "nullable": true + }, + "showCursor": { + "type": "boolean", + "nullable": true + }, + "width": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopResolution": { + "type": "object", + "required": ["width", "height"], + "properties": { + "dpi": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "height": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "width": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, + "DesktopScreenshotFormat": { + "type": "string", + "enum": ["png", "jpeg", "webp"] + }, + "DesktopScreenshotQuery": { + "type": "object", + "properties": { + "format": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopScreenshotFormat" + } + ], + "nullable": true + }, + "quality": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "scale": { + "type": "number", + "format": "float", + "nullable": true + }, + "showCursor": { + "type": "boolean", + "nullable": true + } + } + }, + "DesktopStartRequest": { + "type": "object", + "properties": { + "displayNum": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "dpi": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "height": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "recordingFps": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "stateDir": { + "type": "string", + "nullable": true + }, + "streamAudioCodec": { + "type": "string", + "nullable": true + }, + "streamFrameRate": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "streamVideoCodec": { + "type": "string", + "nullable": true + }, + "webrtcPortRange": { + "type": "string", + "nullable": true + }, + "width": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + } + }, + "DesktopState": { + "type": "string", + "enum": ["inactive", "install_required", "starting", "active", "stopping", "failed"] + }, + "DesktopStatusResponse": { + "type": "object", + "required": ["state"], + "properties": { + "display": { + "type": "string", + "nullable": true + }, + "installCommand": { + "type": "string", + "nullable": true + }, + "lastError": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopErrorInfo" + } + ], + "nullable": true + }, + "missingDependencies": { + "type": "array", + "items": { + "type": "string" + } + }, + "processes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DesktopProcessInfo" + } + }, + "resolution": { + "allOf": [ + { + "$ref": "#/components/schemas/DesktopResolution" + } + ], + "nullable": true + }, + "runtimeLogPath": { + "type": "string", + "nullable": true + }, + "startedAt": { + "type": "string", + "nullable": true + }, + "state": { + "$ref": "#/components/schemas/DesktopState" + }, + "windows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DesktopWindowInfo" + }, + "description": "Current visible windows (included when the desktop is active)." + } + } + }, + "DesktopStreamStatusResponse": { + "type": "object", + "required": ["active"], + "properties": { + "active": { + "type": "boolean" + }, + "processId": { + "type": "string", + "nullable": true + }, + "windowId": { + "type": "string", + "nullable": true + } + } + }, + "DesktopWindowInfo": { + "type": "object", + "required": ["id", "title", "x", "y", "width", "height", "isActive"], + "properties": { + "height": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "id": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "width": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopWindowListResponse": { + "type": "object", + "required": ["windows"], + "properties": { + "windows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DesktopWindowInfo" + } + } + } + }, + "DesktopWindowMoveRequest": { + "type": "object", + "required": ["x", "y"], + "properties": { + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + } + }, + "DesktopWindowResizeRequest": { + "type": "object", + "required": ["width", "height"], + "properties": { + "height": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "width": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, "ErrorType": { "type": "string", "enum": [ @@ -1234,6 +4533,7 @@ "permission_denied", "not_acceptable", "unsupported_media_type", + "not_found", "session_not_found", "session_already_exists", "mode_not_supported", @@ -1243,9 +4543,7 @@ }, "FsActionResponse": { "type": "object", - "required": [ - "path" - ], + "required": ["path"], "properties": { "path": { "type": "string" @@ -1254,9 +4552,7 @@ }, "FsDeleteQuery": { "type": "object", - "required": [ - "path" - ], + "required": ["path"], "properties": { "path": { "type": "string" @@ -1278,12 +4574,7 @@ }, "FsEntry": { "type": "object", - "required": [ - "name", - "path", - "entryType", - "size" - ], + "required": ["name", "path", "entryType", "size"], "properties": { "entryType": { "$ref": "#/components/schemas/FsEntryType" @@ -1307,17 +4598,11 @@ }, "FsEntryType": { "type": "string", - "enum": [ - "file", - "directory" - ] + "enum": ["file", "directory"] }, "FsMoveRequest": { "type": "object", - "required": [ - "from", - "to" - ], + "required": ["from", "to"], "properties": { "from": { "type": "string" @@ -1333,10 +4618,7 @@ }, "FsMoveResponse": { "type": "object", - "required": [ - "from", - "to" - ], + "required": ["from", "to"], "properties": { "from": { "type": "string" @@ -1348,9 +4630,7 @@ }, "FsPathQuery": { "type": "object", - "required": [ - "path" - ], + "required": ["path"], "properties": { "path": { "type": "string" @@ -1359,11 +4639,7 @@ }, "FsStat": { "type": "object", - "required": [ - "path", - "entryType", - "size" - ], + "required": ["path", "entryType", "size"], "properties": { "entryType": { "$ref": "#/components/schemas/FsEntryType" @@ -1393,10 +4669,7 @@ }, "FsUploadBatchResponse": { "type": "object", - "required": [ - "paths", - "truncated" - ], + "required": ["paths", "truncated"], "properties": { "paths": { "type": "array", @@ -1411,10 +4684,7 @@ }, "FsWriteResponse": { "type": "object", - "required": [ - "path", - "bytesWritten" - ], + "required": ["path", "bytesWritten"], "properties": { "bytesWritten": { "type": "integer", @@ -1428,9 +4698,7 @@ }, "HealthResponse": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "status": { "type": "string" @@ -1439,10 +4707,7 @@ }, "McpConfigQuery": { "type": "object", - "required": [ - "directory", - "mcpName" - ], + "required": ["directory", "mcpName"], "properties": { "directory": { "type": "string" @@ -1456,10 +4721,7 @@ "oneOf": [ { "type": "object", - "required": [ - "command", - "type" - ], + "required": ["command", "type"], "properties": { "args": { "type": "array", @@ -1493,18 +4755,13 @@ }, "type": { "type": "string", - "enum": [ - "local" - ] + "enum": ["local"] } } }, { "type": "object", - "required": [ - "url", - "type" - ], + "required": ["url", "type"], "properties": { "bearerTokenEnvVar": { "type": "string", @@ -1552,9 +4809,7 @@ }, "type": { "type": "string", - "enum": [ - "remote" - ] + "enum": ["remote"] }, "url": { "type": "string" @@ -1568,11 +4823,7 @@ }, "ProblemDetails": { "type": "object", - "required": [ - "type", - "title", - "status" - ], + "required": ["type", "title", "status"], "properties": { "detail": { "type": "string", @@ -1596,18 +4847,370 @@ }, "additionalProperties": {} }, + "ProcessConfig": { + "type": "object", + "required": ["maxConcurrentProcesses", "defaultRunTimeoutMs", "maxRunTimeoutMs", "maxOutputBytes", "maxLogBytesPerProcess", "maxInputBytesPerRequest"], + "properties": { + "defaultRunTimeoutMs": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "maxConcurrentProcesses": { + "type": "integer", + "minimum": 0 + }, + "maxInputBytesPerRequest": { + "type": "integer", + "minimum": 0 + }, + "maxLogBytesPerProcess": { + "type": "integer", + "minimum": 0 + }, + "maxOutputBytes": { + "type": "integer", + "minimum": 0 + }, + "maxRunTimeoutMs": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + }, + "ProcessCreateRequest": { + "type": "object", + "required": ["command"], + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string", + "nullable": true + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "interactive": { + "type": "boolean" + }, + "tty": { + "type": "boolean" + } + } + }, + "ProcessInfo": { + "type": "object", + "required": ["id", "command", "args", "tty", "interactive", "owner", "status", "createdAtMs"], + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "type": "string" + }, + "createdAtMs": { + "type": "integer", + "format": "int64" + }, + "cwd": { + "type": "string", + "nullable": true + }, + "exitCode": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "exitedAtMs": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "id": { + "type": "string" + }, + "interactive": { + "type": "boolean" + }, + "owner": { + "$ref": "#/components/schemas/ProcessOwner" + }, + "pid": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "status": { + "$ref": "#/components/schemas/ProcessState" + }, + "tty": { + "type": "boolean" + } + } + }, + "ProcessInputRequest": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "string" + }, + "encoding": { + "type": "string", + "nullable": true + } + } + }, + "ProcessInputResponse": { + "type": "object", + "required": ["bytesWritten"], + "properties": { + "bytesWritten": { + "type": "integer", + "minimum": 0 + } + } + }, + "ProcessListQuery": { + "type": "object", + "properties": { + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/ProcessOwner" + } + ], + "nullable": true + } + } + }, + "ProcessListResponse": { + "type": "object", + "required": ["processes"], + "properties": { + "processes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessInfo" + } + } + } + }, + "ProcessLogEntry": { + "type": "object", + "required": ["sequence", "stream", "timestampMs", "data", "encoding"], + "properties": { + "data": { + "type": "string" + }, + "encoding": { + "type": "string" + }, + "sequence": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "stream": { + "$ref": "#/components/schemas/ProcessLogsStream" + }, + "timestampMs": { + "type": "integer", + "format": "int64" + } + } + }, + "ProcessLogsQuery": { + "type": "object", + "properties": { + "follow": { + "type": "boolean", + "nullable": true + }, + "since": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + }, + "stream": { + "allOf": [ + { + "$ref": "#/components/schemas/ProcessLogsStream" + } + ], + "nullable": true + }, + "tail": { + "type": "integer", + "nullable": true, + "minimum": 0 + } + } + }, + "ProcessLogsResponse": { + "type": "object", + "required": ["processId", "stream", "entries"], + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessLogEntry" + } + }, + "processId": { + "type": "string" + }, + "stream": { + "$ref": "#/components/schemas/ProcessLogsStream" + } + } + }, + "ProcessLogsStream": { + "type": "string", + "enum": ["stdout", "stderr", "combined", "pty"] + }, + "ProcessOwner": { + "type": "string", + "enum": ["user", "desktop", "system"] + }, + "ProcessRunRequest": { + "type": "object", + "required": ["command"], + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string", + "nullable": true + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "maxOutputBytes": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "timeoutMs": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + } + } + }, + "ProcessRunResponse": { + "type": "object", + "required": ["timedOut", "stdout", "stderr", "stdoutTruncated", "stderrTruncated", "durationMs"], + "properties": { + "durationMs": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "exitCode": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "stderr": { + "type": "string" + }, + "stderrTruncated": { + "type": "boolean" + }, + "stdout": { + "type": "string" + }, + "stdoutTruncated": { + "type": "boolean" + }, + "timedOut": { + "type": "boolean" + } + } + }, + "ProcessSignalQuery": { + "type": "object", + "properties": { + "waitMs": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + } + } + }, + "ProcessState": { + "type": "string", + "enum": ["running", "exited"] + }, + "ProcessTerminalResizeRequest": { + "type": "object", + "required": ["cols", "rows"], + "properties": { + "cols": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "rows": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, + "ProcessTerminalResizeResponse": { + "type": "object", + "required": ["cols", "rows"], + "properties": { + "cols": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "rows": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, "ServerStatus": { "type": "string", - "enum": [ - "running", - "stopped" - ] + "enum": ["running", "stopped"] }, "ServerStatusInfo": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "status": { "$ref": "#/components/schemas/ServerStatus" @@ -1622,10 +5225,7 @@ }, "SkillSource": { "type": "object", - "required": [ - "type", - "source" - ], + "required": ["type", "source"], "properties": { "ref": { "type": "string", @@ -1652,9 +5252,7 @@ }, "SkillsConfig": { "type": "object", - "required": [ - "sources" - ], + "required": ["sources"], "properties": { "sources": { "type": "array", @@ -1666,10 +5264,7 @@ }, "SkillsConfigQuery": { "type": "object", - "required": [ - "directory", - "skillName" - ], + "required": ["directory", "skillName"], "properties": { "directory": { "type": "string" @@ -1687,4 +5282,4 @@ "description": "ACP proxy v1 API" } ] -} \ No newline at end of file +} diff --git a/docs/orchestration-architecture.mdx b/docs/orchestration-architecture.mdx new file mode 100644 index 0000000..08c776c --- /dev/null +++ b/docs/orchestration-architecture.mdx @@ -0,0 +1,43 @@ +--- +title: "Orchestration Architecture" +description: "Production topology, backend requirements, and session persistence." +icon: "sitemap" +--- + +This page covers production topology and backend requirements. Read [Architecture](/architecture) first for an overview of how the server, SDK, and agent processes fit together. + +## Suggested Topology + +Run the SDK on your backend, then call it from your frontend. + +This extra hop is recommended because it keeps auth/token logic on the backend and makes persistence simpler. + +```mermaid placement="top-right" + flowchart LR + BROWSER["Browser"] + subgraph BACKEND["Your backend"] + direction TB + SDK["Sandbox Agent SDK"] + end + subgraph SANDBOX_SIMPLE["Sandbox"] + SERVER_SIMPLE["Sandbox Agent server"] + end + + BROWSER --> BACKEND + BACKEND --> SDK --> SERVER_SIMPLE +``` + +### Backend requirements + +Your backend layer needs to handle: + +- **Long-running connections**: prompts can take minutes. +- **Session affinity**: follow-up messages must reach the same session. +- **State between requests**: session metadata and event history must persist across requests. +- **Graceful recovery**: sessions should resume after backend restarts. + +We recommend [Rivet](https://rivet.dev) over serverless because actors natively support the long-lived connections, session routing, and state persistence that agent workloads require. + +## Session persistence + +For storage driver options and replay behavior, see [Persisting Sessions](/session-persistence). diff --git a/docs/pi-support-plan.md b/docs/pi-support-plan.md deleted file mode 100644 index 5e207a5..0000000 --- a/docs/pi-support-plan.md +++ /dev/null @@ -1,210 +0,0 @@ -# Pi Agent Support Plan (pi-mono) - -## Implementation Status Update - -- Runtime selection now supports two internal modes: - - `PerSession` (default for unknown/non-allowlisted Pi capabilities) - - `Shared` (allowlist-only compatibility path) -- Pi sessions now use per-session process isolation by default, enabling true concurrent Pi sessions in Inspector and API clients. -- Shared Pi server code remains available and is used only when capability checks allow multiplexing. -- Session termination for per-session Pi mode hard-kills the underlying Pi process and clears queued prompts/pending waiters. -- In-session concurrent sends are serialized with an unbounded daemon-side FIFO queue per session. - -## Investigation Summary - -### Pi CLI modes and RPC protocol -- Pi supports multiple modes including interactive, print/JSON output, RPC, and SDK usage. JSON mode outputs a stream of JSON events suitable for parsing, and RPC mode is intended for programmatic control over stdin/stdout. -- RPC mode is started with `pi --mode rpc` and supports options like `--provider`, `--model`, `--no-session`, and `--session-dir`. -- The RPC protocol is newline-delimited JSON over stdin/stdout: - - Commands are JSON objects written to stdin. - - Responses are JSON objects with `type: "response"` and optional `id`. - - Events are JSON objects without `id`. -- `prompt` can include images using `ImageContent` (base64 or URL) alongside text. -- JSON/print mode (`pi -p` or `pi --print --mode json`) produces JSONL for non-interactive parsing and can resume sessions with a token. - -### RPC commands -RPC commands listed in `rpc.md` include: -- `new_session`, `get_state`, `list_sessions`, `delete_session`, `rename_session`, `clear_session` -- `prompt`, `queue_message`, `abort`, `get_queued_messages` - -### RPC event types -RPC events listed in `rpc.md` include: -- `agent_start`, `agent_end` -- `turn_start`, `turn_end` -- `message_start`, `message_update`, `message_end` -- `tool_execution_start`, `tool_execution_update`, `tool_execution_end` -- `auto_compaction`, `auto_retry`, `hook_error` - -`message_update` uses `assistantMessageEvent` deltas such as: -- `start`, `text_start`, `text_delta`, `text_end` -- `thinking_start`, `thinking_delta`, `thinking_end` -- `toolcall_start`, `toolcall_delta`, `toolcall_end` -- `toolcall_args_start`, `toolcall_args_delta`, `toolcall_args_end` -- `done`, `error` - -`tool_execution_update` includes `partialResult`, which is described as accumulated output so far. - -### Schema source locations (pi-mono) -RPC types are documented as living in: -- `packages/ai/src/types.ts` (Model types) -- `packages/agent/src/types.ts` (AgentResponse types) -- `packages/coding-agent/src/core/messages.ts` (message types) -- `packages/coding-agent/src/modes/rpc/rpc-types.ts` (RPC protocol types) - -### Distribution assets -Pi releases provide platform-specific binaries such as: -- `pi-darwin-arm64`, `pi-darwin-x64` -- `pi-linux-arm64`, `pi-linux-x64` -- `pi-win-x64.zip` - -## Integration Decisions -- Follow the OpenCode pattern: a shared long-running process (stdio RPC) with session multiplexing. -- Primary integration path is RPC streaming (`pi --mode rpc`). -- JSON/print mode is a fallback only (diagnostics or non-interactive runs). -- Create sessions via `new_session`; store the returned `sessionId` as `native_session_id`. -- Use `get_state` as a re-sync path after server restarts. -- Use `prompt` for send-message, with optional image content. -- Convert Pi events into universal events; emit daemon synthetic `session.started` on session creation and `session.ended` only on errors/termination. - -## Implementation Plan - -### 1) Agent Identity + Capabilities -Files: -- `server/packages/agent-management/src/agents.rs` -- `server/packages/sandbox-agent/src/router.rs` -- `docs/cli.mdx`, `docs/conversion.mdx`, `docs/session-transcript-schema.mdx` -- `README.md`, `frontend/packages/website/src/components/FAQ.tsx` - -Tasks: -- Add `AgentId::Pi` with string/binary name `"pi"` and parsing rules. -- Add Pi to `all_agents()` and agent lists. -- Define `AgentCapabilities` for Pi: - - `tool_calls=true`, `tool_results=true` - - `text_messages=true`, `streaming_deltas=true`, `item_started=true` - - `reasoning=true` (from `thinking_*` deltas) - - `images=true` (ImageContent in `prompt`) - - `permissions=false`, `questions=false`, `mcp_tools=false` - - `shared_process=true`, `session_lifecycle=false` (no native session events) - - `error_events=true` (hook_error) - - `command_execution=false`, `file_changes=false`, `file_attachments=false` - -### 2) Installer and Binary Resolution -Files: -- `server/packages/agent-management/src/agents.rs` - -Tasks: -- Add `install_pi()` that: - - Downloads the correct release asset per platform (`pi-`). - - Handles `.zip` on Windows and raw binaries elsewhere. - - Marks binary executable. -- Add Pi to `AgentManager::install`, `is_installed`, `version`. -- Version detection: try `--version`, `version`, `-V`. - -### 3) Schema Extraction for Pi -Files: -- `resources/agent-schemas/src/pi.ts` (new) -- `resources/agent-schemas/src/index.ts` -- `resources/agent-schemas/artifacts/json-schema/pi.json` -- `server/packages/extracted-agent-schemas/build.rs` -- `server/packages/extracted-agent-schemas/src/lib.rs` - -Tasks: -- Implement `extractPiSchema()`: - - Download pi-mono sources (zip/tarball) into a temp dir. - - Use `ts-json-schema-generator` against `packages/coding-agent/src/modes/rpc/rpc-types.ts`. - - Include dependent files per `rpc.md` (ai/types, agent/types, core/messages). - - Extract `RpcEvent`, `RpcResponse`, `RpcCommand` unions (exact type names from source). -- Add fallback schema if remote fetch fails (minimal union with event/response fields). -- Wire pi into extractor index and artifact generation. - -### 4) Universal Schema Conversion (Pi -> Universal) -Files: -- `server/packages/universal-agent-schema/src/agents/pi.rs` (new) -- `server/packages/universal-agent-schema/src/agents/mod.rs` -- `server/packages/universal-agent-schema/src/lib.rs` -- `server/packages/sandbox-agent/src/router.rs` - -Mapping rules: -- `message_start` -> `item.started` (kind=message, role=assistant, native_item_id=messageId) -- `message_update`: - - `text_*` -> `item.delta` (assistant text delta) - - `thinking_*` -> `item.delta` with `ContentPart::Reasoning` (visibility=Private) - - `toolcall_*` and `toolcall_args_*` -> ignore for now (tool_execution_* is authoritative) - - `error` -> `item.completed` with `ItemStatus::Failed` (if no later message_end) -- `message_end` -> `item.completed` (finalize assistant message) -- `tool_execution_start` -> `item.started` (kind=tool_call, ContentPart::ToolCall) -- `tool_execution_update` -> `item.delta` for a synthetic tool_result item: - - Maintain a per-toolCallId buffer to compute delta from accumulated `partialResult`. -- `tool_execution_end` -> `item.completed` (kind=tool_result, output from `result.content`) - - If `isError=true`, set item status to failed. -- `agent_start`, `turn_start`, `turn_end`, `agent_end`, `auto_compaction`, `auto_retry`, `hook_error`: - - Map to `ItemKind::Status` with a label like `pi.agent_start`, `pi.auto_retry`, etc. - - Do not emit `session.ended` for these events. -- If event parsing fails, emit `agent.unparsed` (source=daemon, synthetic=true) and fail tests. - -### 5) Shared RPC Server Integration -Files: -- `server/packages/sandbox-agent/src/router.rs` - -Tasks: -- Add a new managed stdio server type for Pi, similar to Codex: - - Create `PiServer` struct with: - - stdin sender - - pending request map keyed by request id - - per-session native session id mapping - - Extend `ManagedServerKind` to include Pi. - - Add `ensure_pi_server()` and `spawn_pi_server()` using `pi --mode rpc`. - - Add a `handle_pi_server_output()` loop to parse stdout lines into events/responses. -- Session creation: - - On `create_session`, ensure Pi server is running, send `new_session`, store sessionId. - - Register session with `server_manager.register_session` for native mapping. -- Sending messages: - - Use `prompt` command; include sessionId and optional images. - - Emit synthetic `item.started` only if Pi does not emit `message_start`. - -### 6) Router + Streaming Path Changes -Files: -- `server/packages/sandbox-agent/src/router.rs` - -Tasks: -- Add Pi handling to: - - `create_session` (new_session) - - `send_message` (prompt) - - `parse_agent_line` (Pi event conversion) - - `agent_modes` (default to `default` unless Pi exposes a mode list) - - `agent_supports_resume` (true if Pi supports session resume) - -### 7) Tests -Files: -- `server/packages/sandbox-agent/tests/...` -- `server/packages/universal-agent-schema/tests/...` (if present) - -Tasks: -- Unit tests for conversion: - - `message_start/update/end` -> item.started/delta/completed - - `tool_execution_*` -> tool call/result mapping with partialResult delta - - failure -> agent.unparsed -- Integration tests: - - Start Pi RPC server, create session, send prompt, stream events. - - Validate `native_session_id` mapping and event ordering. -- Update HTTP/SSE test coverage to include Pi agent if relevant. - -## Risk Areas / Edge Cases -- `tool_execution_update.partialResult` is cumulative; must compute deltas. -- `message_update` may emit `done`/`error` without `message_end`; handle both paths. -- No native session lifecycle events; rely on daemon synthetic events. -- Session recovery after RPC server restart requires `get_state` + re-register sessions. - -## Acceptance Criteria -- Pi appears in `/v1/agents`, CLI list, and docs. -- `create_session` returns `native_session_id` from Pi `new_session`. -- Streaming prompt yields universal events with proper ordering: - - message -> item.started/delta/completed - - tool execution -> tool call + tool result -- Tests pass and no synthetic data is used in test fixtures. - -## Sources -- https://upd.dev/badlogic/pi-mono/src/commit/d36e0ea07303d8a76d51b4a7bd5f0d6d3c490860/packages/coding-agent/docs/rpc.md -- https://buildwithpi.ai/pi-cli -- https://takopi.dev/docs/pi-cli/ -- https://upd.dev/badlogic/pi-mono/releases diff --git a/docs/processes.mdx b/docs/processes.mdx new file mode 100644 index 0000000..282c0f1 --- /dev/null +++ b/docs/processes.mdx @@ -0,0 +1,258 @@ +--- +title: "Processes" +description: "Run commands and manage long-lived processes inside the sandbox." +sidebarTitle: "Processes" +icon: "terminal" +--- + +The process API supports: + +- **One-shot execution** — run a command to completion and capture stdout, stderr, and exit code +- **Managed processes** — spawn, list, stop, kill, and delete long-lived processes +- **Log streaming** — fetch buffered logs or follow live output +- **Terminals** — full PTY support with bidirectional WebSocket I/O +- **Configurable limits** — control concurrency, timeouts, and buffer sizes per runtime + +## Run a command + +Execute a command to completion and get its output. + + +```ts TypeScript +import { SandboxAgent } from "sandbox-agent"; + +const sdk = await SandboxAgent.connect({ + baseUrl: "http://127.0.0.1:2468", +}); + +const result = await sdk.runProcess({ + command: "ls", + args: ["-la", "/workspace"], +}); + +console.log(result.exitCode); // 0 +console.log(result.stdout); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"ls","args":["-la","/workspace"]}' +``` + + +You can set a timeout and cap output size: + + +```ts TypeScript +const result = await sdk.runProcess({ + command: "make", + args: ["build"], + timeoutMs: 60000, + maxOutputBytes: 1048576, +}); + +if (result.timedOut) { + console.log("Build timed out"); +} +if (result.stdoutTruncated) { + console.log("Output was truncated"); +} +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"make","args":["build"],"timeoutMs":60000,"maxOutputBytes":1048576}' +``` + + +## Managed processes + +Create a long-lived process that you can interact with, monitor, and stop later. + +### Create + + +```ts TypeScript +const proc = await sdk.createProcess({ + command: "node", + args: ["server.js"], + cwd: "/workspace", +}); + +console.log(proc.id, proc.pid); // proc_1, 12345 +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes" \ + -H "Content-Type: application/json" \ + -d '{"command":"node","args":["server.js"],"cwd":"/workspace"}' +``` + + +### List and get + + +```ts TypeScript +const { processes } = await sdk.listProcesses(); + +for (const p of processes) { + console.log(p.id, p.command, p.status); +} + +const proc = await sdk.getProcess("proc_1"); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/processes" + +curl "http://127.0.0.1:2468/v1/processes/proc_1" +``` + + +### Stop, kill, and delete + + +```ts TypeScript +// SIGTERM with optional wait +await sdk.stopProcess("proc_1", { waitMs: 5000 }); + +// SIGKILL +await sdk.killProcess("proc_1", { waitMs: 1000 }); + +// Remove exited process record +await sdk.deleteProcess("proc_1"); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/stop?waitMs=5000" + +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/kill?waitMs=1000" + +curl -X DELETE "http://127.0.0.1:2468/v1/processes/proc_1" +``` + + +## Logs + +### Fetch buffered logs + + +```ts TypeScript +const logs = await sdk.getProcessLogs("proc_1", { + tail: 50, + stream: "combined", +}); + +for (const entry of logs.entries) { + console.log(entry.stream, atob(entry.data)); +} +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/processes/proc_1/logs?tail=50&stream=combined" +``` + + +### Follow logs + +Stream log entries in real time. The subscription replays buffered entries first, then streams new output as it arrives. + +```ts TypeScript +const sub = await sdk.followProcessLogs("proc_1", (entry) => { + console.log(entry.stream, atob(entry.data)); +}); + +// Later, stop following +sub.close(); +await sub.closed; +``` + +## Terminals + +Create a process with `tty: true` to allocate a pseudo-terminal, then connect via WebSocket for full bidirectional I/O. + +```ts TypeScript +const proc = await sdk.createProcess({ + command: "bash", + tty: true, +}); +``` + +### Write input + + +```ts TypeScript +await sdk.sendProcessInput("proc_1", { + data: "echo hello\n", + encoding: "utf8", +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/input" \ + -H "Content-Type: application/json" \ + -d '{"data":"echo hello\n","encoding":"utf8"}' +``` + + +### Connect to a terminal + +Use `ProcessTerminalSession` unless you need direct frame access. + +```ts TypeScript +const terminal = sdk.connectProcessTerminal("proc_1"); + +terminal.onReady(() => { + terminal.resize({ cols: 120, rows: 40 }); + terminal.sendInput("ls\n"); +}); + +terminal.onData((bytes) => { + process.stdout.write(new TextDecoder().decode(bytes)); +}); + +terminal.onExit((status) => { + console.log("exit:", status.exitCode); +}); + +terminal.onError((error) => { + console.error(error instanceof Error ? error.message : error.message); +}); + +terminal.onClose(() => { + console.log("terminal closed"); +}); +``` + +Since the browser WebSocket API cannot send custom headers, the endpoint accepts an `access_token` query parameter for authentication. The SDK handles this automatically. + +### Browser terminal emulators + +The terminal session works with any browser terminal emulator like ghostty-web or xterm.js. For a drop-in React terminal, see [React Components](/react-components). + +## Configuration + +Adjust runtime limits like max concurrent processes, timeouts, and buffer sizes. + + +```ts TypeScript +const config = await sdk.getProcessConfig(); +console.log(config); + +await sdk.setProcessConfig({ + ...config, + maxConcurrentProcesses: 32, + defaultRunTimeoutMs: 60000, +}); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/processes/config" + +curl -X POST "http://127.0.0.1:2468/v1/processes/config" \ + -H "Content-Type: application/json" \ + -d '{"maxConcurrentProcesses":32,"defaultRunTimeoutMs":60000,"maxRunTimeoutMs":300000,"maxOutputBytes":1048576,"maxLogBytesPerProcess":10485760,"maxInputBytesPerRequest":65536}' +``` + diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 37eacb9..223a54d 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -61,9 +61,11 @@ icon: "rocket" ```bash - docker run -e ANTHROPIC_API_KEY="sk-ant-..." \ + docker run -p 2468:2468 \ + -e ANTHROPIC_API_KEY="sk-ant-..." \ -e OPENAI_API_KEY="sk-..." \ - your-image + rivetdev/sandbox-agent:0.4.2-full \ + server --no-token --host 0.0.0.0 --port 2468 ``` @@ -75,6 +77,9 @@ icon: "rocket" Use the `mock` agent for SDK and integration testing without provider credentials. + + For per-tenant token tracking, budget enforcement, or usage-based billing, see [LLM Credentials](/llm-credentials) for gateway options like OpenRouter, LiteLLM, and Portkey. + @@ -84,7 +89,7 @@ icon: "rocket" Install and run the binary directly. ```bash - curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh + curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh sandbox-agent server --no-token --host 0.0.0.0 --port 2468 ``` @@ -93,7 +98,7 @@ icon: "rocket" Run without installing globally. ```bash - npx @sandbox-agent/cli@0.2.x server --no-token --host 0.0.0.0 --port 2468 + npx @sandbox-agent/cli@0.4.x server --no-token --host 0.0.0.0 --port 2468 ``` @@ -101,7 +106,7 @@ icon: "rocket" Run without installing globally. ```bash - bunx @sandbox-agent/cli@0.2.x server --no-token --host 0.0.0.0 --port 2468 + bunx @sandbox-agent/cli@0.4.x server --no-token --host 0.0.0.0 --port 2468 ``` @@ -109,7 +114,7 @@ icon: "rocket" Install globally, then run. ```bash - npm install -g @sandbox-agent/cli@0.2.x + npm install -g @sandbox-agent/cli@0.4.x sandbox-agent server --no-token --host 0.0.0.0 --port 2468 ``` @@ -118,9 +123,9 @@ icon: "rocket" Install globally, then run. ```bash - bun add -g @sandbox-agent/cli@0.2.x + bun add -g @sandbox-agent/cli@0.4.x # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()). - bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 + bun pm -g trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 sandbox-agent server --no-token --host 0.0.0.0 --port 2468 ``` @@ -129,7 +134,7 @@ icon: "rocket" For local development, use `SandboxAgent.start()` to spawn and manage the server as a subprocess. ```bash - npm install sandbox-agent@0.2.x + npm install sandbox-agent@0.4.x ``` ```typescript @@ -143,9 +148,9 @@ icon: "rocket" For local development, use `SandboxAgent.start()` to spawn and manage the server as a subprocess. ```bash - bun add sandbox-agent@0.2.x + bun add sandbox-agent@0.4.x # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()). - bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 + bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 ``` ```typescript @@ -215,15 +220,22 @@ icon: "rocket" To preinstall agents: ```bash - sandbox-agent install-agent claude - sandbox-agent install-agent codex - sandbox-agent install-agent opencode - sandbox-agent install-agent amp + sandbox-agent install-agent --all ``` If agents are not installed up front, they are lazily installed when creating a session. + + If you want to use `/v1/desktop/*`, install the desktop runtime packages first: + + ```bash + sandbox-agent install desktop --yes + ``` + + Then use `GET /v1/desktop/status` or `sdk.getDesktopStatus()` to verify the runtime is ready before calling desktop screenshot or input APIs. + + ```typescript import { SandboxAgent } from "sandbox-agent"; diff --git a/docs/react-components.mdx b/docs/react-components.mdx new file mode 100644 index 0000000..71a76d2 --- /dev/null +++ b/docs/react-components.mdx @@ -0,0 +1,245 @@ +--- +title: "React Components" +description: "Drop-in React components for Sandbox Agent frontends." +icon: "react" +--- + +`@sandbox-agent/react` exposes small React components built on top of the `sandbox-agent` SDK. + +Current exports: + +- `AgentConversation` for a combined transcript + composer surface +- `ProcessTerminal` for attaching to a running tty process +- `AgentTranscript` for rendering session/message timelines without bundling any styles +- `ChatComposer` for a reusable prompt input/send surface +- `useTranscriptVirtualizer` for wiring large transcript lists to a scroll container + +## Install + +```bash +npm install @sandbox-agent/react@0.4.x +``` + +## Full example + +This example connects to a running Sandbox Agent server, starts a tty shell, renders `ProcessTerminal`, and cleans up the process when the component unmounts. + +```tsx TerminalPane.tsx expandable highlight={5,32-36,71} +"use client"; + +import { useEffect, useState } from "react"; +import { SandboxAgent } from "sandbox-agent"; +import { ProcessTerminal } from "@sandbox-agent/react"; + +export default function TerminalPane() { + const [client, setClient] = useState(null); + const [processId, setProcessId] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + let sdk: SandboxAgent | null = null; + let createdProcessId: string | null = null; + + const cleanup = async () => { + if (!sdk || !createdProcessId) { + return; + } + + await sdk.killProcess(createdProcessId, { waitMs: 1_000 }).catch(() => {}); + await sdk.deleteProcess(createdProcessId).catch(() => {}); + }; + + const start = async () => { + try { + sdk = await SandboxAgent.connect({ + baseUrl: "http://127.0.0.1:2468", + }); + + const process = await sdk.createProcess({ + command: "sh", + interactive: true, + tty: true, + }); + + if (cancelled) { + createdProcessId = process.id; + await cleanup(); + await sdk.dispose(); + return; + } + + createdProcessId = process.id; + setClient(sdk); + setProcessId(process.id); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to start terminal."; + setError(message); + } + }; + + void start(); + + return () => { + cancelled = true; + void cleanup(); + void sdk?.dispose(); + }; + }, []); + + if (error) { + return
{error}
; + } + + if (!client || !processId) { + return
Starting terminal...
; + } + + return ; +} +``` + +## Component + +`ProcessTerminal` attaches to a running tty process. + +- `client`: a `SandboxAgent` client +- `processId`: the process to attach to +- `height`, `style`, `terminalStyle`: optional layout overrides +- `onExit`, `onError`: optional lifecycle callbacks + +See [Processes](/processes) for the lower-level terminal APIs. + +## Headless transcript + +`AgentTranscript` is intentionally unstyled. It follows the common headless React pattern used by libraries like Radix, Headless UI, and React Aria: behavior lives in the component, while styling stays in your app through `className`, slot-level `classNames`, and `data-*` state attributes on the rendered DOM. + +```tsx TranscriptPane.tsx +import { + AgentTranscript, + type AgentTranscriptClassNames, + type TranscriptEntry, +} from "@sandbox-agent/react"; + +const transcriptClasses: Partial = { + root: "transcript", + message: "transcript-message", + messageContent: "transcript-message-content", + toolGroupContainer: "transcript-tools", + toolGroupHeader: "transcript-tools-header", + toolItem: "transcript-tool-item", + toolItemHeader: "transcript-tool-item-header", + toolItemBody: "transcript-tool-item-body", + divider: "transcript-divider", + dividerText: "transcript-divider-text", + error: "transcript-error", +}; + +export function TranscriptPane({ entries }: { entries: TranscriptEntry[] }) { + return ( +
{entry.text}
} + renderInlinePendingIndicator={() => ...} + renderToolGroupIcon={() => Events} + renderChevron={(expanded) => {expanded ? "Hide" : "Show"}} + /> + ); +} +``` + +```css +.transcript { + display: grid; + gap: 12px; +} + +.transcript [data-slot="message"][data-variant="user"] .transcript-message-content { + background: #161616; + color: white; +} + +.transcript [data-slot="message"][data-variant="assistant"] .transcript-message-content { + background: #f4f4f0; + color: #161616; +} + +.transcript [data-slot="tool-item"][data-failed="true"] { + border-color: #d33; +} + +.transcript [data-slot="tool-item-header"][data-expanded="true"] { + background: rgba(0, 0, 0, 0.06); +} +``` + +`AgentTranscript` accepts `TranscriptEntry[]`, which matches the Inspector timeline shape: + +- `message` entries render user/assistant text +- `tool` entries render expandable tool input/output sections +- `reasoning` entries render expandable reasoning blocks +- `meta` entries render status rows or expandable metadata details + +Useful props: + +- `className`: root class hook +- `classNames`: slot-level class hooks for styling from outside the package +- `scrollRef` + `virtualize`: opt into TanStack Virtual against an external scroll container +- `renderMessageText`: custom text or markdown renderer +- `renderToolItemIcon`, `renderToolGroupIcon`, `renderChevron`, `renderEventLinkContent`: presentation overrides +- `renderInlinePendingIndicator`, `renderThinkingState`: loading/thinking UI overrides +- `isDividerEntry`, `canOpenEvent`, `getToolGroupSummary`: behavior overrides for grouping and labels + +## Transcript virtualization hook + +`useTranscriptVirtualizer` exposes the same TanStack Virtual behavior used by `AgentTranscript` when `virtualize` is enabled. + +- Pass the grouped transcript rows you want to virtualize +- Pass a `scrollRef` that points at the actual scrollable element +- Use it when you need transcript-aware virtualization outside the stock `AgentTranscript` renderer + +## Composer and conversation + +`ChatComposer` is the headless message input. `AgentConversation` composes `AgentTranscript` and `ChatComposer` so apps can reuse the transcript/composer pairing without pulling in Inspector session chrome. + +```tsx ConversationPane.tsx +import { AgentConversation, type TranscriptEntry } from "@sandbox-agent/react"; + +export function ConversationPane({ + entries, + message, + onMessageChange, + onSubmit, +}: { + entries: TranscriptEntry[]; + message: string; + onMessageChange: (value: string) => void; + onSubmit: () => void; +}) { + return ( + Start the conversation.} + transcriptProps={{ + renderMessageText: (entry) =>
{entry.text}
, + }} + composerProps={{ + message, + onMessageChange, + onSubmit, + placeholder: "Send a message...", + }} + /> + ); +} +``` + +Useful `ChatComposer` props: + +- `className` and `classNames` for external styling +- `inputRef` to manage focus or autoresize from the consumer +- `textareaProps` for lower-level textarea behavior +- `allowEmptySubmit` when the submit action is valid without draft text, such as a stop button + +Use `transcriptProps` and `composerProps` when you want the shared composition but still need custom rendering or behavior. Use `transcriptClassNames` and `composerClassNames` when you want styling hooks for each subcomponent. diff --git a/docs/sdk-overview.mdx b/docs/sdk-overview.mdx index 8a973a2..73e0d35 100644 --- a/docs/sdk-overview.mdx +++ b/docs/sdk-overview.mdx @@ -11,22 +11,22 @@ The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgent` class. ```bash - npm install sandbox-agent@0.2.x + npm install sandbox-agent@0.4.x ``` ```bash - bun add sandbox-agent@0.2.x + bun add sandbox-agent@0.4.x # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()). - bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 + bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 ``` -## Optional persistence drivers +## Optional React components ```bash -npm install @sandbox-agent/persist-indexeddb@0.2.x @sandbox-agent/persist-sqlite@0.2.x @sandbox-agent/persist-postgres@0.2.x +npm install @sandbox-agent/react@0.4.x ``` ## Create a client @@ -39,15 +39,35 @@ const sdk = await SandboxAgent.connect({ }); ``` -With persistence: +`SandboxAgent.connect(...)` now waits for `/v1/health` by default before other SDK requests proceed. To disable that gate, pass `waitForHealth: false`. To keep the default gate but fail after a bounded wait, pass `waitForHealth: { timeoutMs: 120_000 }`. To cancel the startup wait early, pass `signal: abortController.signal`. + +With a custom fetch handler (for example, proxying requests inside Workers): ```ts -import { SandboxAgent } from "sandbox-agent"; -import { SQLiteSessionPersistDriver } from "@sandbox-agent/persist-sqlite"; - -const persist = new SQLiteSessionPersistDriver({ - filename: "./sessions.db", +const sdk = await SandboxAgent.connect({ + fetch: (input, init) => customFetch(input, init), }); +``` + +With an abort signal for the startup health gate: + +```ts +const controller = new AbortController(); + +const sdk = await SandboxAgent.connect({ + baseUrl: "http://127.0.0.1:2468", + signal: controller.signal, +}); + +controller.abort(); +``` + +With persistence (see [Persisting Sessions](/session-persistence) for driver options): + +```ts +import { SandboxAgent, InMemorySessionPersistDriver } from "sandbox-agent"; + +const persist = new InMemorySessionPersistDriver(); const sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", @@ -55,25 +75,40 @@ const sdk = await SandboxAgent.connect({ }); ``` -Local autospawn (Node.js only): +Local spawn with a sandbox provider: ```ts import { SandboxAgent } from "sandbox-agent"; +import { local } from "sandbox-agent/local"; -const localSdk = await SandboxAgent.start(); +const sdk = await SandboxAgent.start({ + sandbox: local(), +}); -await localSdk.dispose(); +// sdk.sandboxId — prefixed provider ID (e.g. "local/127.0.0.1:2468") + +await sdk.destroySandbox(); // provider-defined cleanup + disposes client ``` +`SandboxAgent.start(...)` requires a `sandbox` provider. Built-in providers: + +| Import | Provider | +|--------|----------| +| `sandbox-agent/local` | Local subprocess | +| `sandbox-agent/docker` | Docker container | +| `sandbox-agent/e2b` | E2B sandbox | +| `sandbox-agent/daytona` | Daytona workspace | +| `sandbox-agent/vercel` | Vercel Sandbox | +| `sandbox-agent/cloudflare` | Cloudflare Sandbox | + +Use `sdk.dispose()` to disconnect without changing sandbox state, `sdk.pauseSandbox()` for graceful suspension when supported, or `sdk.killSandbox()` for permanent deletion. + ## Session flow ```ts const session = await sdk.createSession({ agent: "mock", - sessionInit: { - cwd: "/", - mcpServers: [], - }, + cwd: "/", }); const prompt = await session.prompt([ @@ -92,6 +127,38 @@ await restored.prompt([{ type: "text", text: "Continue from previous context." } await sdk.destroySession(restored.id); ``` +## Session configuration + +Set model, mode, or thought level at creation or on an existing session: + +```ts +const session = await sdk.createSession({ + agent: "codex", + model: "gpt-5.3-codex", +}); + +await session.setModel("gpt-5.2-codex"); +await session.setMode("auto"); + +const options = await session.getConfigOptions(); +const modes = await session.getModes(); +``` + +Handle permission requests from agents that ask before executing tools: + +```ts +const claude = await sdk.createSession({ + agent: "claude", + mode: "default", +}); + +claude.onPermissionRequest((request) => { + void claude.respondPermission(request.id, "once"); +}); +``` + +See [Agent Sessions](/agent-sessions) for full details on config options and error handling. + ## Events Subscribe to live events: @@ -129,6 +196,44 @@ const writeResult = await sdk.writeFsFile({ path: "./hello.txt" }, "hello"); console.log(health.status, agents.agents.length, entries.length, writeResult.path); ``` +## Desktop API + +The SDK also wraps the desktop host/runtime HTTP API. + +Install desktop dependencies first on Linux hosts: + +```bash +sandbox-agent install desktop --yes +``` + +Then query status, surface remediation if needed, and start the runtime: + +```ts +const status = await sdk.getDesktopStatus(); + +if (status.state === "install_required") { + console.log(status.installCommand); +} + +const started = await sdk.startDesktop({ + width: 1440, + height: 900, + dpi: 96, +}); + +const screenshot = await sdk.takeDesktopScreenshot(); +const displayInfo = await sdk.getDesktopDisplayInfo(); + +await sdk.moveDesktopMouse({ x: 400, y: 300 }); +await sdk.clickDesktop({ x: 400, y: 300, button: "left", clickCount: 1 }); +await sdk.typeDesktopText({ text: "hello world", delayMs: 10 }); +await sdk.pressDesktopKey({ key: "ctrl+l" }); + +await sdk.stopDesktop(); +``` + +Screenshot helpers return `Uint8Array` PNG bytes. The SDK does not attempt to install OS packages remotely; callers should surface `missingDependencies` and `installCommand` from `getDesktopStatus()`. + ## Error handling ```ts @@ -158,17 +263,14 @@ console.log(url); Parameters: -- `baseUrl` (required): Sandbox Agent server URL +- `baseUrl` (required unless `fetch` is provided): Sandbox Agent server URL - `token` (optional): Bearer token for authenticated servers - `headers` (optional): Additional request headers +- `fetch` (optional): Custom fetch implementation used by SDK HTTP and session calls +- `skipHealthCheck` (optional): set `true` to skip the startup `/v1/health` wait +- `waitForHealth` (optional, defaults to enabled): waits for `/v1/health` before HTTP helpers and session setup proceed; pass `false` to disable or `{ timeoutMs }` to bound the wait +- `signal` (optional): aborts the startup `/v1/health` wait used by `connect()` -## Types +## LLM credentials -```ts -import type { - AgentInfo, - HealthResponse, - SessionEvent, - SessionRecord, -} from "sandbox-agent"; -``` +Sandbox Agent supports personal API keys, shared organization keys, and per-tenant gateway keys with budget enforcement. See [LLM Credentials](/llm-credentials) for setup details. diff --git a/docs/sdks/python.mdx b/docs/sdks/python.mdx deleted file mode 100644 index 80f667a..0000000 --- a/docs/sdks/python.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "Python" -description: "Python client for managing sessions and streaming events." -icon: "python" -tag: "Coming Soon" ---- - -The Python SDK is on our roadmap. It will provide a typed client for managing sessions and streaming events, similar to the TypeScript SDK. - -In the meantime, you can use the [HTTP API](/http-api) directly with any HTTP client like `requests` or `httpx`. - -```python -import httpx - -base_url = "http://127.0.0.1:2468" -headers = {"Authorization": f"Bearer {token}"} - -# Create a session -httpx.post( - f"{base_url}/v1/sessions/my-session", - headers=headers, - json={"agent": "claude", "permissionMode": "default"} -) - -# Send a message -httpx.post( - f"{base_url}/v1/sessions/my-session/messages", - headers=headers, - json={"message": "Hello from Python"} -) - -# Get events -response = httpx.get( - f"{base_url}/v1/sessions/my-session/events", - headers=headers, - params={"offset": 0, "limit": 50} -) -events = response.json()["events"] -``` - -Want the Python SDK sooner? [Open an issue](https://github.com/rivet-dev/sandbox-agent/issues) to let us know. diff --git a/docs/sdks/typescript.mdx b/docs/sdks/typescript.mdx deleted file mode 100644 index 9c74214..0000000 --- a/docs/sdks/typescript.mdx +++ /dev/null @@ -1,267 +0,0 @@ ---- -title: "TypeScript" -description: "Use the TypeScript SDK to manage ACP sessions and Sandbox Agent HTTP APIs." -icon: "js" ---- - -The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgentClient`, which provides a Sandbox-facing API for session flows, ACP extensions, and binary HTTP filesystem helpers. - -## Install - - - - ```bash - npm install sandbox-agent - ``` - - - ```bash - bun add sandbox-agent - # Allow Bun to run postinstall scripts for native binaries (required for SandboxAgent.start()). - bun pm trust @sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64 - ``` - - - -## Create a client - -```ts -import { SandboxAgentClient } from "sandbox-agent"; - -const client = new SandboxAgentClient({ - baseUrl: "http://127.0.0.1:2468", - token: process.env.SANDBOX_TOKEN, - agent: "mock", -}); -``` - -`SandboxAgentClient` is the canonical API. By default it auto-connects (`autoConnect: true`), so provide `agent` in the constructor. Use the instance method `client.connect()` only when you explicitly set `autoConnect: false`. - -## Autospawn (Node only) - -If you run locally, the SDK can launch the server for you. - -```ts -import { SandboxAgent } from "sandbox-agent"; - -const client = await SandboxAgent.start({ - agent: "mock", -}); - -await client.dispose(); -``` - -Autospawn uses the local `sandbox-agent` binary. Install `@sandbox-agent/cli` (recommended) or set -`SANDBOX_AGENT_BIN` to a custom path. - -## Connect lifecycle - -Use manual mode when you want explicit ACP session lifecycle control. - -```ts -import { - AlreadyConnectedError, - NotConnectedError, - SandboxAgentClient, -} from "sandbox-agent"; - -const client = new SandboxAgentClient({ - baseUrl: "http://127.0.0.1:2468", - token: process.env.SANDBOX_TOKEN, - agent: "mock", - autoConnect: false, -}); - -await client.connect(); - -try { - await client.connect(); -} catch (error) { - if (error instanceof AlreadyConnectedError) { - console.error("already connected"); - } -} - -await client.disconnect(); - -try { - await client.prompt({ sessionId: "s", prompt: [{ type: "text", text: "hi" }] }); -} catch (error) { - if (error instanceof NotConnectedError) { - console.error("connect first"); - } -} -``` - -## Session flow - -```ts -const session = await client.newSession({ - cwd: "/", - mcpServers: [], - metadata: { - agent: "mock", - title: "Demo Session", - variant: "high", - permissionMode: "ask", - }, -}); - -const result = await client.prompt({ - sessionId: session.sessionId, - prompt: [{ type: "text", text: "Summarize this repository." }], -}); - -console.log(result.stopReason); -``` - -Load, cancel, and runtime settings use ACP-aligned method names: - -```ts -await client.loadSession({ sessionId: session.sessionId, cwd: "/", mcpServers: [] }); -await client.cancel({ sessionId: session.sessionId }); -await client.setSessionMode({ sessionId: session.sessionId, modeId: "default" }); -await client.setSessionConfigOption({ - sessionId: session.sessionId, - configId: "config-id-from-session", - value: "config-value-id", -}); -``` - -## Extension helpers - -Sandbox extensions are exposed as first-class methods: - -```ts -const models = await client.listModels({ sessionId: session.sessionId }); -console.log(models.currentModelId, models.availableModels.length); - -await client.setMetadata(session.sessionId, { - title: "Renamed Session", - model: "mock", - permissionMode: "ask", -}); - -await client.detachSession(session.sessionId); -await client.terminateSession(session.sessionId); -``` - -## Event handling - -Use `onEvent` to consume converted SDK events. - -```ts -import { SandboxAgentClient, type AgentEvent } from "sandbox-agent"; - -const events: AgentEvent[] = []; - -const client = new SandboxAgentClient({ - baseUrl: "http://127.0.0.1:2468", - token: process.env.SANDBOX_TOKEN, - agent: "mock", - onEvent: (event) => { - events.push(event); - - if (event.type === "sessionEnded") { - console.log("ended", event.notification.params.sessionId ?? event.notification.params.session_id); - } - - if (event.type === "agentUnparsed") { - console.warn("unparsed", event.notification.params); - } - }, -}); -``` - -You can also handle raw session update notifications directly: - -```ts -const client = new SandboxAgentClient({ - baseUrl: "http://127.0.0.1:2468", - token: process.env.SANDBOX_TOKEN, - agent: "mock", - onSessionUpdate: (notification) => { - console.log(notification.update.sessionUpdate); - }, -}); -``` - -## Control + HTTP helpers - -Agent/session and non-binary filesystem control helpers use ACP extension methods over `/v2/rpc`: - -```ts -const health = await client.getHealth(); -const agents = await client.listAgents(); -await client.installAgent("codex", { reinstall: true }); - -const sessions = await client.listSessions(); -const sessionInfo = await client.getSession(sessions.sessions[0].session_id); -``` - -These methods require an active ACP connection and throw `NotConnectedError` when disconnected. - -Binary filesystem transfer intentionally remains HTTP: - -- `readFsFile` -> `GET /v2/fs/file` -- `writeFsFile` -> `PUT /v2/fs/file` -- `uploadFsBatch` -> `POST /v2/fs/upload-batch` - -Reason: these are Sandbox Agent host/runtime filesystem operations (not agent-specific ACP behavior), intentionally separate from ACP native `fs/read_text_file` / `fs/write_text_file`, and they may require streaming very large binary payloads that ACP JSON-RPC is not suited to transport efficiently. - -ACP extension variants can exist in parallel for compatibility, but `SandboxAgentClient` should prefer the HTTP endpoints above by default. - -## Error handling - -All HTTP errors throw `SandboxAgentError`: - -```ts -import { SandboxAgentError } from "sandbox-agent"; - -try { - await client.listAgents(); -} catch (error) { - if (error instanceof SandboxAgentError) { - console.error(error.status, error.problem); - } -} -``` - -## Inspector URL - -Build a URL to open the sandbox-agent Inspector UI with pre-filled connection settings: - -```ts -import { buildInspectorUrl } from "sandbox-agent"; - -const url = buildInspectorUrl({ - baseUrl: "https://your-sandbox-agent.example.com", - token: "optional-bearer-token", - headers: { "X-Custom-Header": "value" }, -}); - -console.log(url); -// https://your-sandbox-agent.example.com/ui/?token=...&headers=... -``` - -Parameters: -- `baseUrl` (required): The sandbox-agent server URL -- `token` (optional): Bearer token for authentication -- `headers` (optional): Extra headers to pass to the server (JSON-encoded in the URL) - -## Types - -The SDK exports typed events and responses for the Sandbox layer: - -```ts -import type { - AgentEvent, - AgentInfo, - HealthResponse, - SessionInfo, - SessionListResponse, - SessionTerminateResponse, -} from "sandbox-agent"; -``` - -For low-level protocol transport details, see [ACP HTTP Client](/advanced/acp-http-client). diff --git a/docs/security.mdx b/docs/security.mdx index ec00f49..c8b02ad 100644 --- a/docs/security.mdx +++ b/docs/security.mdx @@ -4,7 +4,7 @@ description: "Backend-first auth and access control patterns." icon: "shield" --- -As covered in [Architecture](/architecture), run the Sandbox Agent client on your backend, not in the browser. +As covered in [Orchestration Architecture](/orchestration-architecture), run the Sandbox Agent client on your backend, not in the browser. This keeps sandbox credentials private and gives you one place for authz, rate limiting, and audit logging. @@ -92,7 +92,7 @@ export const workspace = actor({ const session = await sdk.createSession({ agent: "claude", - sessionInit: { cwd: "/workspace" }, + cwd: "/workspace", }); session.onEvent((event) => { diff --git a/docs/session-persistence.mdx b/docs/session-persistence.mdx index 43bbcef..5505864 100644 --- a/docs/session-persistence.mdx +++ b/docs/session-persistence.mdx @@ -10,14 +10,22 @@ With persistence enabled, sessions can be restored after runtime/session loss. S Each driver stores: -- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sessionInit`) +- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sandboxId`, optional `sessionInit`, optional `configOptions`, optional `modes`) - `SessionEvent` (`id`, `eventIndex`, `sessionId`, `connectionId`, `sender`, `payload`, `createdAt`) ## Persistence drivers -### In-memory +### Rivet -Best for local dev and ephemeral workloads. +Recommended for sandbox orchestration with actor state. See [Multiplayer](/multiplayer) for a full Rivet actor example with persistence in actor state. + +### IndexedDB (browser) + +Best for browser apps that should survive reloads. See the [Inspector source](https://github.com/rivet-dev/sandbox-agent/tree/main/frontend/packages/inspector/src/persist-indexeddb.ts) for a complete IndexedDB driver you can copy into your project. + +### In-memory (built-in) + +Best for local dev and ephemeral workloads. No extra dependencies required. ```ts import { InMemorySessionPersistDriver, SandboxAgent } from "sandbox-agent"; @@ -33,91 +41,17 @@ const sdk = await SandboxAgent.connect({ }); ``` -### Rivet - -Recommended for sandbox orchestration with actor state. - -```bash -npm install @sandbox-agent/persist-rivet@0.1.x -``` - -```ts -import { actor } from "rivetkit"; -import { SandboxAgent } from "sandbox-agent"; -import { RivetSessionPersistDriver, type RivetPersistState } from "@sandbox-agent/persist-rivet"; - -type PersistedState = RivetPersistState & { - sandboxId: string; - baseUrl: string; -}; - -export default actor({ - createState: async () => { - return { - sandboxId: "sbx_123", - baseUrl: "http://127.0.0.1:2468", - } satisfies Partial; - }, - createVars: async (c) => { - const persist = new RivetSessionPersistDriver(c); - const sdk = await SandboxAgent.connect({ - baseUrl: c.state.baseUrl, - persist, - }); - - const session = await sdk.resumeOrCreateSession({ id: "default", agent: "codex" }); - - const unsubscribe = session.onEvent((event) => { - c.broadcast("session.event", event); - }); - - return { sdk, session, unsubscribe }; - }, - actions: { - sendMessage: async (c, message: string) => { - await c.vars.session.prompt([{ type: "text", text: message }]); - }, - }, - onSleep: async (c) => { - c.vars.unsubscribe?.(); - await c.vars.sdk.dispose(); - }, -}); -``` - -### IndexedDB - -Best for browser apps that should survive reloads. - -```bash -npm install @sandbox-agent/persist-indexeddb@0.2.x -``` - -```ts -import { SandboxAgent } from "sandbox-agent"; -import { IndexedDbSessionPersistDriver } from "@sandbox-agent/persist-indexeddb"; - -const persist = new IndexedDbSessionPersistDriver({ - databaseName: "sandbox-agent-session-store", -}); - -const sdk = await SandboxAgent.connect({ - baseUrl: "http://127.0.0.1:2468", - persist, -}); -``` - ### SQLite Best for local/server Node apps that need durable storage without a DB server. ```bash -npm install @sandbox-agent/persist-sqlite@0.2.x +npm install better-sqlite3 ``` ```ts import { SandboxAgent } from "sandbox-agent"; -import { SQLiteSessionPersistDriver } from "@sandbox-agent/persist-sqlite"; +import { SQLiteSessionPersistDriver } from "./persist.ts"; const persist = new SQLiteSessionPersistDriver({ filename: "./sandbox-agent.db", @@ -129,17 +63,19 @@ const sdk = await SandboxAgent.connect({ }); ``` +See the [full SQLite example](https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-sqlite) for the complete driver implementation you can copy into your project. + ### Postgres Use when you already run Postgres and want shared relational storage. ```bash -npm install @sandbox-agent/persist-postgres@0.2.x +npm install pg ``` ```ts import { SandboxAgent } from "sandbox-agent"; -import { PostgresSessionPersistDriver } from "@sandbox-agent/persist-postgres"; +import { PostgresSessionPersistDriver } from "./persist.ts"; const persist = new PostgresSessionPersistDriver({ connectionString: process.env.DATABASE_URL, @@ -152,6 +88,8 @@ const sdk = await SandboxAgent.connect({ }); ``` +See the [full Postgres example](https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-postgres) for the complete driver implementation you can copy into your project. + ### Custom driver Implement `SessionPersistDriver` for custom backends. @@ -160,11 +98,11 @@ Implement `SessionPersistDriver` for custom backends. import type { SessionPersistDriver } from "sandbox-agent"; class MyDriver implements SessionPersistDriver { - async getSession(id) { return null; } + async getSession(id) { return undefined; } async listSessions(request) { return { items: [] }; } async updateSession(session) {} async listEvents(request) { return { items: [] }; } - async insertEvent(event) {} + async insertEvent(sessionId, event) {} } ``` diff --git a/docs/session-transcript-schema.mdx b/docs/session-transcript-schema.mdx deleted file mode 100644 index c9c004a..0000000 --- a/docs/session-transcript-schema.mdx +++ /dev/null @@ -1,388 +0,0 @@ ---- -title: "Session Transcript Schema" -description: "Universal event schema for session transcripts across all agents." ---- - -Each coding agent outputs events in its own native format. The sandbox-agent converts these into a universal event schema, giving you a consistent session transcript regardless of which agent you use. - -The schema is defined in [OpenAPI format](https://github.com/rivet-dev/sandbox-agent/blob/main/docs/openapi.json). See the [HTTP API Reference](/api-reference) for endpoint documentation. - -## Coverage Matrix - -This table shows which agent feature coverage appears in the universal event stream. All agents retain their full native feature coverage—this only reflects what's normalized into the schema. - -| Feature | Claude | Codex | OpenCode | Amp | Pi (RPC) | -|--------------------|:------:|:-----:|:------------:|:------------:|:------------:| -| Stability | Stable | Stable| Experimental | 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 | ✓ | ✓ | ✓ | - | ✓ | -| Variants | | ✓ | ✓ | ✓ | ✓ | - -Agents: [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) · [Pi](https://buildwithpi.ai/pi-cli) - -- ✓ = Appears in session events -- \- = Agent supports natively, schema conversion coming soon -- (blank) = Not supported by agent -- Pi runtime model is router-managed per-session RPC (`pi --mode rpc`); it does not use generic subprocess streaming. - - - - Basic message exchange between user and assistant. - - - Visibility into tool invocations (file reads, command execution, etc.) and their results. When not natively supported, tool activity is embedded in message content. - - - Interactive questions the agent asks the user. Emits `question.requested` and `question.resolved` events. - - - Permission requests for sensitive operations. Emits `permission.requested` and `permission.resolved` events. - - - Support for image attachments in messages. - - - Support for file attachments in messages. - - - Native `session.started` and `session.ended` events. When not supported, the daemon emits synthetic lifecycle events. - - - Structured error events for runtime failures. - - - Extended thinking or reasoning content with visibility controls. - - - Detailed command execution events with stdout/stderr. - - - Structured file modification events with diffs. - - - Model Context Protocol tool support. - - - Native streaming of content deltas. When not supported, the daemon emits a single synthetic delta before `item.completed`. - - - Model variants such as reasoning effort or depth. Agents may expose different variant sets per model. - - - -Want support for another agent? [Open an issue](https://github.com/rivet-dev/sandbox-agent/issues/new) to request it. - -## UniversalEvent - -Every event from the API is wrapped in a `UniversalEvent` envelope. - -| Field | Type | Description | -|-------|------|-------------| -| `event_id` | string | Unique identifier for this event | -| `sequence` | integer | Monotonic sequence number within the session (starts at 1) | -| `time` | string | RFC3339 timestamp | -| `session_id` | string | Daemon-generated session identifier | -| `native_session_id` | string? | Provider-native session/thread identifier (e.g., Codex `threadId`, OpenCode `sessionID`) | -| `source` | string | Event origin: `agent` (native) or `daemon` (synthetic) | -| `synthetic` | boolean | Whether this event was generated by the daemon to fill gaps | -| `type` | string | Event type (see [Event Types](#event-types)) | -| `data` | object | Event-specific payload | -| `raw` | any? | Original provider payload (only when `include_raw=true`) | - -```json -{ - "event_id": "evt_abc123", - "sequence": 1, - "time": "2025-01-28T12:00:00Z", - "session_id": "my-session", - "native_session_id": "thread_xyz", - "source": "agent", - "synthetic": false, - "type": "item.completed", - "data": { ... } -} -``` - -## Event Types - -### Session Lifecycle - -| Type | Description | Data | -|------|-------------|------| -| `session.started` | Session has started | `{ metadata?: any }` | -| `session.ended` | Session has ended | `{ reason, terminated_by, message?, exit_code? }` | - -### Turn Lifecycle - -| Type | Description | Data | -|------|-------------|------| -| `turn.started` | Turn has started | `{ phase: "started", turn_id?, metadata? }` | -| `turn.ended` | Turn has ended | `{ phase: "ended", turn_id?, metadata? }` | - -**SessionEndedData** - -| Field | Type | Values | -|-------|------|--------| -| `reason` | string | `completed`, `error`, `terminated` | -| `terminated_by` | string | `agent`, `daemon` | -| `message` | string? | Error message (only present when reason is `error`) | -| `exit_code` | int? | Process exit code (only present when reason is `error`) | -| `stderr` | StderrOutput? | Structured stderr output (only present when reason is `error`) | - -**StderrOutput** - -| Field | Type | Description | -|-------|------|-------------| -| `head` | string? | First 20 lines of stderr (if truncated) or full stderr (if not truncated) | -| `tail` | string? | Last 50 lines of stderr (only present if truncated) | -| `truncated` | boolean | Whether the output was truncated | -| `total_lines` | int? | Total number of lines in stderr | - -### Item Lifecycle - -| Type | Description | Data | -|------|-------------|------| -| `item.started` | Item creation | `{ item }` | -| `item.delta` | Streaming content delta | `{ item_id, native_item_id?, delta }` | -| `item.completed` | Item finalized | `{ item }` | - -Items follow a consistent lifecycle: `item.started` → `item.delta` (0 or more) → `item.completed`. - -### HITL (Human-in-the-Loop) - -| Type | Description | Data | -|------|-------------|------| -| `permission.requested` | Permission request pending | `{ permission_id, action, status, metadata? }` | -| `permission.resolved` | Permission decision recorded | `{ permission_id, action, status, metadata? }` | -| `question.requested` | Question pending user input | `{ question_id, prompt, options, status }` | -| `question.resolved` | Question answered or rejected | `{ question_id, prompt, options, status, response? }` | - -**PermissionEventData** - -| Field | Type | Description | -|-------|------|-------------| -| `permission_id` | string | Identifier for the permission request | -| `action` | string | What the agent wants to do | -| `status` | string | `requested`, `accept`, `accept_for_session`, `reject` | -| `metadata` | any? | Additional context | - -**QuestionEventData** - -| Field | Type | Description | -|-------|------|-------------| -| `question_id` | string | Identifier for the question | -| `prompt` | string | Question text | -| `options` | string[] | Available answer options | -| `status` | string | `requested`, `answered`, `rejected` | -| `response` | string? | Selected answer (when resolved) | - -### Errors - -| Type | Description | Data | -|------|-------------|------| -| `error` | Runtime error | `{ message, code?, details? }` | -| `agent.unparsed` | Parse failure | `{ error, location, raw_hash? }` | - -The `agent.unparsed` event indicates the daemon failed to parse an agent payload. This should be treated as a bug. - -## UniversalItem - -Items represent discrete units of content within a session. - -| Field | Type | Description | -|-------|------|-------------| -| `item_id` | string | Daemon-generated identifier | -| `native_item_id` | string? | Provider-native item/message identifier | -| `parent_id` | string? | Parent item ID (e.g., tool call/result parented to a message) | -| `kind` | string | Item category (see below) | -| `role` | string? | Actor role for message items | -| `status` | string | Lifecycle status | -| `content` | ContentPart[] | Ordered list of content parts | - -### ItemKind - -| Value | Description | -|-------|-------------| -| `message` | User or assistant message | -| `tool_call` | Tool invocation | -| `tool_result` | Tool execution result | -| `system` | System message | -| `status` | Status update | -| `unknown` | Unrecognized item type | - -### ItemRole - -| Value | Description | -|-------|-------------| -| `user` | User message | -| `assistant` | Assistant response | -| `system` | System prompt | -| `tool` | Tool-related message | - -### ItemStatus - -| Value | Description | -|-------|-------------| -| `in_progress` | Item is streaming or pending | -| `completed` | Item is finalized | -| `failed` | Item execution failed | - -## Content Parts - -The `content` array contains typed parts that make up an item's payload. - -### text - -Plain text content. - -```json -{ "type": "text", "text": "Hello, world!" } -``` - -### json - -Structured JSON content. - -```json -{ "type": "json", "json": { "key": "value" } } -``` - -### tool_call - -Tool invocation. - -| Field | Type | Description | -|-------|------|-------------| -| `name` | string | Tool name | -| `arguments` | string | JSON-encoded arguments | -| `call_id` | string | Unique call identifier | - -```json -{ - "type": "tool_call", - "name": "read_file", - "arguments": "{\"path\": \"/src/main.ts\"}", - "call_id": "call_abc123" -} -``` - -### tool_result - -Tool execution result. - -| Field | Type | Description | -|-------|------|-------------| -| `call_id` | string | Matching call identifier | -| `output` | string | Tool output | - -```json -{ - "type": "tool_result", - "call_id": "call_abc123", - "output": "File contents here..." -} -``` - -### file_ref - -File reference with optional diff. - -| Field | Type | Description | -|-------|------|-------------| -| `path` | string | File path | -| `action` | string | `read`, `write`, `patch` | -| `diff` | string? | Unified diff (for patches) | - -```json -{ - "type": "file_ref", - "path": "/src/main.ts", - "action": "write", - "diff": "@@ -1,3 +1,4 @@\n+import { foo } from 'bar';" -} -``` - -### image - -Image reference. - -| Field | Type | Description | -|-------|------|-------------| -| `path` | string | Image file path | -| `mime` | string? | MIME type | - -```json -{ "type": "image", "path": "/tmp/screenshot.png", "mime": "image/png" } -``` - -### reasoning - -Model reasoning/thinking content. - -| Field | Type | Description | -|-------|------|-------------| -| `text` | string | Reasoning text | -| `visibility` | string | `public` or `private` | - -```json -{ "type": "reasoning", "text": "Let me think about this...", "visibility": "public" } -``` - -### status - -Status indicator. - -| Field | Type | Description | -|-------|------|-------------| -| `label` | string | Status label | -| `detail` | string? | Additional detail | - -```json -{ "type": "status", "label": "Running tests", "detail": "3 of 10 passed" } -``` - -## Source & Synthetics - -### EventSource - -The `source` field indicates who emitted the event: - -| Value | Description | -|-------|-------------| -| `agent` | Native event from the agent | -| `daemon` | Synthetic event generated by the daemon | - -### Synthetic Events - -The daemon emits synthetic events (`synthetic: true`, `source: "daemon"`) to provide a consistent event stream across all agents. Common synthetics: - -| Synthetic | When | -|-----------|------| -| `session.started` | Agent doesn't emit explicit session start | -| `session.ended` | Agent doesn't emit explicit session end | -| `turn.started` | Agent doesn't emit explicit turn start | -| `turn.ended` | Agent doesn't emit explicit turn end | -| `item.started` | Agent doesn't emit item start events | -| `item.delta` | Agent doesn't stream deltas natively | -| `question.*` | Claude Code plan mode (from ExitPlanMode tool) | - -### Raw Payloads - -Pass `include_raw=true` to event endpoints to receive the original agent payload in the `raw` field. Useful for debugging or accessing agent-specific data not in the universal schema. - -```typescript -const events = await client.getEvents("my-session", { includeRaw: true }); -// events[0].raw contains the original agent payload -``` diff --git a/docs/skills-config.mdx b/docs/skills-config.mdx index c85bc2c..c3145c2 100644 --- a/docs/skills-config.mdx +++ b/docs/skills-config.mdx @@ -35,9 +35,7 @@ await sdk.setSkillsConfig( // Create a session using the configured skills const session = await sdk.createSession({ agent: "claude", - sessionInit: { - cwd: "/workspace", - }, + cwd: "/workspace", }); await session.prompt([ diff --git a/docs/theme.css b/docs/theme.css index daeb719..4286d2c 100644 --- a/docs/theme.css +++ b/docs/theme.css @@ -20,7 +20,6 @@ body { color: var(--sa-text); } -/* a { color: var(--sa-primary); } @@ -41,6 +40,13 @@ select { color: var(--sa-text); } +code, +pre { + background-color: var(--sa-card); + border: 1px solid var(--sa-border); + color: var(--sa-text); +} + .card, .mintlify-card, .docs-card { @@ -64,4 +70,3 @@ select { .alert-danger { border-color: var(--sa-danger); } -*/ diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx index 838cc28..18186d6 100644 --- a/docs/troubleshooting.mdx +++ b/docs/troubleshooting.mdx @@ -29,25 +29,6 @@ Verify the agent is installed: ls -la ~/.local/share/sandbox-agent/bin/ ``` -### 4. Binary libc mismatch (musl vs glibc) - -Claude Code binaries are available in both musl and glibc variants. If you see errors like: - -``` -cannot execute: required file not found -Error loading shared library libstdc++.so.6: No such file or directory -``` - -This means the wrong binary variant was downloaded. - -**For sandbox-agent 0.2.0+**: Platform detection is automatic. The correct binary (musl or glibc) is downloaded based on the runtime environment. - -**For sandbox-agent 0.1.x**: Use Alpine Linux which has native musl support: - -```dockerfile -FROM alpine:latest -RUN apk add --no-cache curl ca-certificates libstdc++ libgcc bash -``` ## Daytona Network Restrictions diff --git a/examples/boxlite-python/.gitignore b/examples/boxlite-python/.gitignore new file mode 100644 index 0000000..f878106 --- /dev/null +++ b/examples/boxlite-python/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.venv/ +oci-image/ diff --git a/examples/boxlite-python/Dockerfile b/examples/boxlite-python/Dockerfile new file mode 100644 index 0000000..8aba774 --- /dev/null +++ b/examples/boxlite-python/Dockerfile @@ -0,0 +1,5 @@ +FROM node:22-bookworm-slim +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* +RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh +RUN sandbox-agent install-agent claude +RUN sandbox-agent install-agent codex diff --git a/examples/boxlite-python/client.py b/examples/boxlite-python/client.py new file mode 100644 index 0000000..29e4609 --- /dev/null +++ b/examples/boxlite-python/client.py @@ -0,0 +1,145 @@ +"""Minimal JSON-RPC client for sandbox-agent's streamable HTTP transport.""" + +import json +import threading +import time +import uuid + +import httpx + + +class SandboxConnection: + """Connects to a sandbox-agent server via JSON-RPC over streamable HTTP. + + Endpoints used: + POST /v1/acp/{server_id}?agent=... (bootstrap + requests) + GET /v1/acp/{server_id} (SSE event stream) + DELETE /v1/acp/{server_id} (close) + """ + + def __init__(self, base_url: str, agent: str): + self.base_url = base_url.rstrip("/") + self.agent = agent + self.server_id = f"py-{uuid.uuid4().hex[:8]}" + self.url = f"{self.base_url}/v1/acp/{self.server_id}" + self._next_id = 0 + self._events: list[dict] = [] + self._stop = threading.Event() + self._sse_thread: threading.Thread | None = None + + def _alloc_id(self) -> int: + self._next_id += 1 + return self._next_id + + def _post(self, method: str, params: dict | None = None, *, bootstrap: bool = False) -> dict: + payload: dict = { + "jsonrpc": "2.0", + "id": self._alloc_id(), + "method": method, + } + if params is not None: + payload["params"] = params + + url = f"{self.url}?agent={self.agent}" if bootstrap else self.url + r = httpx.post(url, json=payload, timeout=120) + r.raise_for_status() + body = r.text.strip() + return json.loads(body) if body else {} + + # -- Lifecycle ----------------------------------------------------------- + + def initialize(self) -> dict: + result = self._post( + "initialize", + { + "protocolVersion": 1, + "clientInfo": {"name": "python-example", "version": "0.1.0"}, + }, + bootstrap=True, + ) + self._start_sse() + + # Auto-authenticate if the agent advertises env-var-based auth methods. + auth_methods = result.get("result", {}).get("authMethods", []) + env_ids = ("anthropic-api-key", "codex-api-key", "openai-api-key") + for method in auth_methods: + if method.get("id") not in env_ids: + continue + try: + resp = self._post("authenticate", {"methodId": method["id"]}) + if "error" not in resp: + break + except Exception: + continue + + return result + + def new_session(self, cwd: str = "/root") -> str: + result = self._post("session/new", {"cwd": cwd, "mcpServers": []}) + if "error" in result: + raise RuntimeError(f"session/new failed: {result['error'].get('message', result['error'])}") + return result["result"]["sessionId"] + + def prompt(self, session_id: str, text: str) -> dict: + result = self._post( + "session/prompt", + { + "sessionId": session_id, + "prompt": [{"type": "text", "text": text}], + }, + ) + return result + + def close(self) -> None: + self._stop.set() + try: + httpx.delete(self.url, timeout=2) + except Exception: + pass + + # -- SSE event stream (background thread) -------------------------------- + + @property + def events(self) -> list[dict]: + return list(self._events) + + def _start_sse(self) -> None: + self._sse_thread = threading.Thread(target=self._sse_loop, daemon=True) + self._sse_thread.start() + + def _sse_loop(self) -> None: + while not self._stop.is_set(): + try: + with httpx.stream( + "GET", + self.url, + headers={"Accept": "text/event-stream"}, + timeout=httpx.Timeout(connect=5, read=None, write=5, pool=5), + ) as resp: + buffer = "" + for chunk in resp.iter_text(): + if self._stop.is_set(): + break + buffer += chunk.replace("\r\n", "\n") + while "\n\n" in buffer: + event_chunk, buffer = buffer.split("\n\n", 1) + self._process_sse_event(event_chunk) + except Exception: + if self._stop.is_set(): + return + time.sleep(0.15) + + def _process_sse_event(self, chunk: str) -> None: + data_lines: list[str] = [] + for line in chunk.split("\n"): + if line.startswith("data:"): + data_lines.append(line[5:].lstrip()) + if not data_lines: + return + payload = "\n".join(data_lines).strip() + if not payload: + return + try: + self._events.append(json.loads(payload)) + except json.JSONDecodeError: + pass diff --git a/examples/boxlite-python/credentials.py b/examples/boxlite-python/credentials.py new file mode 100644 index 0000000..46114dc --- /dev/null +++ b/examples/boxlite-python/credentials.py @@ -0,0 +1,32 @@ +"""Agent detection and credential helpers for sandbox-agent examples.""" + +import os +import sys + + +def detect_agent() -> str: + """Pick an agent based on env vars. Exits if no credentials are found.""" + if os.environ.get("SANDBOX_AGENT"): + return os.environ["SANDBOX_AGENT"] + has_claude = bool( + os.environ.get("ANTHROPIC_API_KEY") + or os.environ.get("CLAUDE_API_KEY") + or os.environ.get("CLAUDE_CODE_OAUTH_TOKEN") + ) + has_codex = (os.environ.get("OPENAI_API_KEY") or "").startswith("sk-") + if has_codex: + return "codex" + if has_claude: + return "claude" + print("No API keys found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.") + sys.exit(1) + + +def build_box_env() -> list[tuple[str, str]]: + """Collect credential env vars to forward into the BoxLite sandbox.""" + env: list[tuple[str, str]] = [] + for key in ("ANTHROPIC_API_KEY", "CLAUDE_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"): + val = os.environ.get(key) + if val: + env.append((key, val)) + return env diff --git a/examples/boxlite-python/main.py b/examples/boxlite-python/main.py new file mode 100644 index 0000000..244985f --- /dev/null +++ b/examples/boxlite-python/main.py @@ -0,0 +1,110 @@ +""" +Sandbox Agent – Python + BoxLite example. + +Builds a Docker image, exports it to OCI layout, runs it inside a BoxLite +sandbox, connects to the sandbox-agent server, creates a session, and sends a prompt. + +Usage: + pip install -r requirements.txt + python main.py +""" + +import asyncio +import json +import signal +import time + +import boxlite +import httpx + +from client import SandboxConnection +from credentials import build_box_env, detect_agent +from setup_image import OCI_DIR, setup_image + +PORT = 3000 + + +def wait_for_health(base_url: str, timeout_s: float = 120) -> None: + deadline = time.monotonic() + timeout_s + last_err: str | None = None + while time.monotonic() < deadline: + try: + r = httpx.get(f"{base_url}/v1/health", timeout=5) + if r.status_code == 200 and r.json().get("status") == "ok": + return + last_err = f"health returned {r.status_code}" + except Exception as exc: + last_err = str(exc) + time.sleep(0.5) + raise RuntimeError(f"Timed out waiting for /v1/health: {last_err}") + + +async def main() -> None: + agent = detect_agent() + print(f"Agent: {agent}") + + setup_image() + + env = build_box_env() + + print("Creating BoxLite sandbox...") + box = boxlite.SimpleBox( + rootfs_path=OCI_DIR, + env=env, + ports=[(PORT, PORT, "tcp")], + ) + + async with box: + print("Starting server...") + result = await box.exec( + "sh", "-c", + f"nohup sandbox-agent server --no-token --host 0.0.0.0 --port {PORT} " + ">/tmp/sandbox-agent.log 2>&1 &", + ) + if result.exit_code != 0: + raise RuntimeError(f"Failed to start server: {result.stderr}") + + base_url = f"http://localhost:{PORT}" + print("Waiting for server...") + wait_for_health(base_url) + print("Server ready.") + print(f"Inspector: {base_url}/ui/") + + # -- Session flow ---------------------------------------------------- + conn = SandboxConnection(base_url, agent) + + print("Connecting...") + init_result = conn.initialize() + agent_info = init_result.get("result", {}).get("agentInfo", {}) + print(f"Connected to: {agent_info.get('title', agent)} {agent_info.get('version', '')}") + + session_id = conn.new_session() + print(f"Session: {session_id}") + + prompt_text = "Say hello and tell me what you are. Be brief (one sentence)." + print(f"\n> {prompt_text}") + response = conn.prompt(session_id, prompt_text) + + if "error" in response: + err = response["error"] + print(f"Error: {err.get('message', err)}") + else: + print(f"Stop reason: {response.get('result', {}).get('stopReason', 'unknown')}") + + # Give SSE events a moment to arrive. + time.sleep(1) + + if conn.events: + for ev in conn.events: + if ev.get("method") == "session/update": + content = ev.get("params", {}).get("update", {}).get("content", {}) + if content.get("text"): + print(content["text"], end="") + print() + + conn.close() + print("\nDone.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/boxlite-python/requirements.txt b/examples/boxlite-python/requirements.txt new file mode 100644 index 0000000..2977142 --- /dev/null +++ b/examples/boxlite-python/requirements.txt @@ -0,0 +1,2 @@ +boxlite>=0.5.0 +httpx>=0.27.0 diff --git a/examples/boxlite-python/setup_image.py b/examples/boxlite-python/setup_image.py new file mode 100644 index 0000000..f56b76f --- /dev/null +++ b/examples/boxlite-python/setup_image.py @@ -0,0 +1,29 @@ +"""Build the sandbox-agent Docker image and export it to OCI layout.""" + +import os +import subprocess + +DOCKER_IMAGE = "sandbox-agent-boxlite" +OCI_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "oci-image") + + +def setup_image() -> None: + dockerfile_dir = os.path.dirname(os.path.abspath(__file__)) + + print(f'Building image "{DOCKER_IMAGE}" (cached after first run)...') + subprocess.run( + ["docker", "build", "-t", DOCKER_IMAGE, dockerfile_dir], + check=True, + ) + + if not os.path.exists(os.path.join(OCI_DIR, "oci-layout")): + print("Exporting to OCI layout...") + os.makedirs(OCI_DIR, exist_ok=True) + subprocess.run( + [ + "skopeo", "copy", + f"docker-daemon:{DOCKER_IMAGE}:latest", + f"oci:{OCI_DIR}:latest", + ], + check=True, + ) diff --git a/examples/boxlite/.gitignore b/examples/boxlite/.gitignore new file mode 100644 index 0000000..329f592 --- /dev/null +++ b/examples/boxlite/.gitignore @@ -0,0 +1 @@ +oci-image/ diff --git a/examples/boxlite/Dockerfile b/examples/boxlite/Dockerfile new file mode 100644 index 0000000..8aba774 --- /dev/null +++ b/examples/boxlite/Dockerfile @@ -0,0 +1,5 @@ +FROM node:22-bookworm-slim +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* +RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh +RUN sandbox-agent install-agent claude +RUN sandbox-agent install-agent codex diff --git a/examples/boxlite/package.json b/examples/boxlite/package.json new file mode 100644 index 0000000..8e7a5d8 --- /dev/null +++ b/examples/boxlite/package.json @@ -0,0 +1,19 @@ +{ + "name": "@sandbox-agent/example-boxlite", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@boxlite-ai/boxlite": "latest", + "@sandbox-agent/example-shared": "workspace:*", + "sandbox-agent": "workspace:*" + }, + "devDependencies": { + "@types/node": "latest", + "tsx": "latest", + "typescript": "latest" + } +} diff --git a/examples/boxlite/src/index.ts b/examples/boxlite/src/index.ts new file mode 100644 index 0000000..171166b --- /dev/null +++ b/examples/boxlite/src/index.ts @@ -0,0 +1,41 @@ +import { SimpleBox } from "@boxlite-ai/boxlite"; +import { SandboxAgent } from "sandbox-agent"; +import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared"; +import { setupImage, OCI_DIR } from "./setup-image.ts"; + +const env: Record = {}; +if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY; + +setupImage(); + +console.log("Creating BoxLite sandbox..."); +const box = new SimpleBox({ + rootfsPath: OCI_DIR, + env, + ports: [{ hostPort: 3000, guestPort: 3000 }], + diskSizeGb: 4, +}); + +console.log("Starting server..."); +const result = await box.exec("sh", "-c", "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"); +if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.stderr}`); + +const baseUrl = "http://localhost:3000"; + +console.log("Connecting to server..."); +const client = await SandboxAgent.connect({ baseUrl }); +const session = await client.createSession({ agent: detectAgent(), cwd: "/root" }); +const sessionId = session.id; + +console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`); +console.log(" Press Ctrl+C to stop."); + +const keepAlive = setInterval(() => {}, 60_000); +const cleanup = async () => { + clearInterval(keepAlive); + await box.stop(); + process.exit(0); +}; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); diff --git a/examples/boxlite/src/setup-image.ts b/examples/boxlite/src/setup-image.ts new file mode 100644 index 0000000..9c15c99 --- /dev/null +++ b/examples/boxlite/src/setup-image.ts @@ -0,0 +1,16 @@ +import { execSync } from "node:child_process"; +import { existsSync, mkdirSync } from "node:fs"; + +export const DOCKER_IMAGE = "sandbox-agent-boxlite"; +export const OCI_DIR = new URL("../oci-image", import.meta.url).pathname; + +export function setupImage() { + console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`); + execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" }); + + if (!existsSync(`${OCI_DIR}/oci-layout`)) { + console.log("Exporting to OCI layout..."); + mkdirSync(OCI_DIR, { recursive: true }); + execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" }); + } +} diff --git a/examples/boxlite/tsconfig.json b/examples/boxlite/tsconfig.json new file mode 100644 index 0000000..ad591c3 --- /dev/null +++ b/examples/boxlite/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/examples/cloudflare/Dockerfile b/examples/cloudflare/Dockerfile index 17ddb78..738f8a2 100644 --- a/examples/cloudflare/Dockerfile +++ b/examples/cloudflare/Dockerfile @@ -1,7 +1,7 @@ FROM cloudflare/sandbox:0.7.0 # Install sandbox-agent -RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh +RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh # Pre-install agents RUN sandbox-agent install-agent claude && \ diff --git a/examples/cloudflare/README.md b/examples/cloudflare/README.md index 783d34e..b8461f2 100644 --- a/examples/cloudflare/README.md +++ b/examples/cloudflare/README.md @@ -36,6 +36,48 @@ Test the endpoint: curl http://localhost:8787 ``` +Test prompt routing through the SDK with a custom sandbox fetch handler: + +```bash +curl -N -X POST "http://localhost:8787/sandbox/demo/prompt" \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{"agent":"codex","prompt":"Reply with one short sentence."}' +``` + +The response is an SSE stream with events: +- `session.created` +- `session.event` +- `prompt.completed` +- `done` + +### Troubleshooting: only two events + +If you only see: +- outbound `session/prompt` +- inbound prompt result with `stopReason: "end_turn"` + +then ACP `session/update` notifications are not flowing. In Cloudflare sandbox paths this can happen if you forward `AbortSignal` from SDK fetch init into `containerFetch(...)` for long-lived ACP SSE requests. + +Use: + +```ts +const sdk = await SandboxAgent.connect({ + fetch: (input, init) => + sandbox.containerFetch( + input as Request | string | URL, + { + ...(init ?? {}), + // Avoid passing AbortSignal through containerFetch; it can drop ACP SSE updates. + signal: undefined, + }, + PORT, + ), +}); +``` + +Without `session/update` events, assistant text/tool deltas will not appear in UI streams. + ## Deploy ```bash diff --git a/examples/cloudflare/frontend/App.tsx b/examples/cloudflare/frontend/App.tsx index 2f7f0c5..499fc63 100644 --- a/examples/cloudflare/frontend/App.tsx +++ b/examples/cloudflare/frontend/App.tsx @@ -25,7 +25,7 @@ export function App() { try { // Connect via proxy endpoint (need full URL for SDK) - const baseUrl = `${window.location.origin}/sandbox/${encodeURIComponent(sandboxName)}`; + const baseUrl = `${window.location.origin}/sandbox/${encodeURIComponent(sandboxName)}/proxy`; log(`Connecting to sandbox: ${sandboxName}`); const client = await SandboxAgent.connect({ baseUrl }); @@ -71,7 +71,7 @@ export function App() { if (event.type === "permission.requested") { const data = event.data as PermissionEventData; log(`[Auto-approved] ${data.action}`); - await client.replyPermission(sessionIdRef.current, data.permission_id, { reply: "once" }); + await client.respondPermission(sessionIdRef.current, data.permission_id, { reply: "once" }); } // Reject questions (don't support interactive input) @@ -128,7 +128,7 @@ export function App() { console.error("Event stream error:", err); } }, - [log] + [log], ); const send = useCallback(async () => { @@ -162,12 +162,7 @@ export function App() {