diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..96880e9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Build outputs +target/ +dist/ +build/ + +# Dependencies +node_modules/ + +# Cache +.cache/ +.turbo/ +*.tsbuildinfo + +# Environment +.env +.env.* + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store + +# Git +.git/ diff --git a/Cargo.toml b/Cargo.toml index 65200b4..a55bdf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["server/packages/*"] [workspace.package] -version = "0.1.0" +version = "0.1.4-rc.7" edition = "2021" authors = [ "Rivet Gaming, LLC " ] license = "Apache-2.0" @@ -12,12 +12,12 @@ description = "Universal API for automatic coding agents in sandboxes. Supprots [workspace.dependencies] # Internal crates -sandbox-agent = { version = "0.1.0", path = "server/packages/sandbox-agent" } -sandbox-agent-error = { version = "0.1.0", path = "server/packages/error" } -sandbox-agent-agent-management = { version = "0.1.0", path = "server/packages/agent-management" } -sandbox-agent-agent-credentials = { version = "0.1.0", path = "server/packages/agent-credentials" } -sandbox-agent-universal-agent-schema = { version = "0.1.0", path = "server/packages/universal-agent-schema" } -sandbox-agent-extracted-agent-schemas = { version = "0.1.0", path = "server/packages/extracted-agent-schemas" } +sandbox-agent = { version = "0.1.4-rc.7", path = "server/packages/sandbox-agent" } +sandbox-agent-error = { version = "0.1.4-rc.7", path = "server/packages/error" } +sandbox-agent-agent-management = { version = "0.1.4-rc.7", path = "server/packages/agent-management" } +sandbox-agent-agent-credentials = { version = "0.1.4-rc.7", path = "server/packages/agent-credentials" } +sandbox-agent-universal-agent-schema = { version = "0.1.4-rc.7", path = "server/packages/universal-agent-schema" } +sandbox-agent-extracted-agent-schemas = { version = "0.1.4-rc.7", path = "server/packages/extracted-agent-schemas" } # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index 9224bf1..1f90841 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The Sandbox Agent acts as a universal adapter between your client application an |-----------|-------------| | **Server** | Rust daemon (`sandbox-agent server`) exposing the HTTP + SSE API | | **SDK** | TypeScript client with embedded and server modes | -| **Inspector** | [inspect.sandboxagent.dev](https://inspect.sandboxagent.dev) for browsing sessions and events | +| **Inspector** | Built-in UI at inspecting sessions and events | | **CLI** | `sandbox-agent` (same binary, plus npm wrapper) mirrors the HTTP endpoints | ## Get Started @@ -172,7 +172,7 @@ npx sandbox-agent --help ### Inspector -Debug sessions and events with the [Inspector UI](https://inspect.sandboxagent.dev). +Debug sessions and events with the built-in Inspector UI (e.g., `http://localhost:2468/ui/`). ![Sandbox Agent Inspector](./.github/media/inspector.png) diff --git a/docker/release/linux-x86_64.Dockerfile b/docker/release/linux-x86_64.Dockerfile index f838b93..3831cb0 100644 --- a/docker/release/linux-x86_64.Dockerfile +++ b/docker/release/linux-x86_64.Dockerfile @@ -11,7 +11,7 @@ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies -RUN pnpm install --filter @anthropic-ai/sdk-inspector... +RUN pnpm install --filter @sandbox-agent/inspector... # Copy SDK source (with pre-generated types from docs/openapi.json) COPY docs/openapi.json ./docs/ diff --git a/docker/release/macos-aarch64.Dockerfile b/docker/release/macos-aarch64.Dockerfile index 29e4b32..ed5e79c 100644 --- a/docker/release/macos-aarch64.Dockerfile +++ b/docker/release/macos-aarch64.Dockerfile @@ -11,7 +11,7 @@ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies -RUN pnpm install --filter @anthropic-ai/sdk-inspector... +RUN pnpm install --filter @sandbox-agent/inspector... # Copy SDK source (with pre-generated types from docs/openapi.json) COPY docs/openapi.json ./docs/ diff --git a/docker/release/macos-x86_64.Dockerfile b/docker/release/macos-x86_64.Dockerfile index 5a33b15..34fce08 100644 --- a/docker/release/macos-x86_64.Dockerfile +++ b/docker/release/macos-x86_64.Dockerfile @@ -11,7 +11,7 @@ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies -RUN pnpm install --filter @anthropic-ai/sdk-inspector... +RUN pnpm install --filter @sandbox-agent/inspector... # Copy SDK source (with pre-generated types from docs/openapi.json) COPY docs/openapi.json ./docs/ diff --git a/docker/release/windows.Dockerfile b/docker/release/windows.Dockerfile index 2acabf2..914c955 100644 --- a/docker/release/windows.Dockerfile +++ b/docker/release/windows.Dockerfile @@ -11,7 +11,7 @@ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies -RUN pnpm install --filter @anthropic-ai/sdk-inspector... +RUN pnpm install --filter @sandbox-agent/inspector... # Copy SDK source (with pre-generated types from docs/openapi.json) COPY docs/openapi.json ./docs/ diff --git a/docker/runtime/Dockerfile b/docker/runtime/Dockerfile index fed9840..520aa79 100644 --- a/docker/runtime/Dockerfile +++ b/docker/runtime/Dockerfile @@ -13,7 +13,7 @@ COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/ COPY sdks/typescript/package.json ./sdks/typescript/ # Install dependencies -RUN pnpm install --filter @anthropic-ai/sdk-inspector... +RUN pnpm install --filter @sandbox-agent/inspector... # Copy SDK source (with pre-generated types from docs/openapi.json) COPY docs/openapi.json ./docs/ diff --git a/docs/cli.mdx b/docs/cli.mdx index 1d48812..855bd44 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -19,11 +19,10 @@ sandbox-agent server [OPTIONS] | `-n, --no-token` | - | Disable authentication (local dev only) | | `-H, --host ` | `127.0.0.1` | Host to bind to | | `-p, --port ` | `2468` | Port to bind to | -| `-O, --cors-allow-origin ` | - | Additional CORS origin (repeatable, cumulative with Inspector) | +| `-O, --cors-allow-origin ` | - | CORS origin to allow (repeatable) | | `-M, --cors-allow-method ` | all | CORS allowed method (repeatable) | | `-A, --cors-allow-header
` | all | CORS allowed header (repeatable) | | `-C, --cors-allow-credentials` | - | Enable CORS credentials | -| `--no-inspector-cors` | - | Disable default Inspector CORS | | `--no-telemetry` | - | Disable anonymous telemetry | ```bash diff --git a/docs/cors.mdx b/docs/cors.mdx index c9fe888..5e50888 100644 --- a/docs/cors.mdx +++ b/docs/cors.mdx @@ -9,47 +9,26 @@ When calling the Sandbox Agent server from a browser, CORS (Cross-Origin Resourc ## Default Behavior -By default, the server allows CORS requests from the [Inspector](https://inspect.sandboxagent.dev): +By default, no CORS origins are allowed. You must explicitly specify origins for browser-based applications: ```bash -# Inspector CORS is enabled by default -sandbox-agent server --token "$SANDBOX_TOKEN" -``` - -This allows you to use the hosted Inspector to connect to any running Sandbox Agent server without additional configuration. - -## Adding Origins - -Use `--cors-allow-origin` to allow additional origins. These are **cumulative** with the default Inspector origin: - -```bash -# Allows both Inspector AND localhost:5173 sandbox-agent server \ --token "$SANDBOX_TOKEN" \ --cors-allow-origin "http://localhost:5173" ``` + +The built-in Inspector UI at `/ui/` is served from the same origin as the server, so it does not require CORS configuration. + + ## Options | Flag | Description | |------|-------------| -| `--cors-allow-origin` | Additional origins to allow (cumulative with Inspector) | +| `--cors-allow-origin` | Origins to allow | | `--cors-allow-method` | HTTP methods to allow (defaults to all if not specified) | | `--cors-allow-header` | Headers to allow (defaults to all if not specified) | | `--cors-allow-credentials` | Allow credentials (cookies, authorization headers) | -| `--no-inspector-cors` | Disable the default Inspector origin | - -## Disabling Inspector CORS - -To disable the default Inspector origin and only allow explicitly specified origins: - -```bash -# Only allows localhost:5173, not Inspector -sandbox-agent server \ - --token "$SANDBOX_TOKEN" \ - --no-inspector-cors \ - --cors-allow-origin "http://localhost:5173" -``` ## Multiple Origins diff --git a/docs/inspector.mdx b/docs/inspector.mdx index b095ff2..8e80c22 100644 --- a/docs/inspector.mdx +++ b/docs/inspector.mdx @@ -12,9 +12,9 @@ The Inspector is a web-based GUI for debugging and inspecting Sandbox Agent sess ## Open the Inspector -Visit [inspect.sandboxagent.dev](https://inspect.sandboxagent.dev) and enter your server URL and token to connect. +The Inspector UI is served at `/ui/` on your sandbox-agent server. For example, if your server is running at `http://localhost:2468`, open `http://localhost:2468/ui/` in your browser. -You can also generate a pre-filled Inspector URL from the TypeScript SDK: +You can also generate a pre-filled Inspector URL with authentication from the TypeScript SDK: ```typescript import { buildInspectorUrl } from "sandbox-agent"; @@ -24,7 +24,7 @@ const url = buildInspectorUrl({ token: process.env.SANDBOX_TOKEN, }); console.log(url); -// https://inspect.sandboxagent.dev?url=http%3A%2F%2F127.0.0.1%3A2468&token=... +// http://127.0.0.1:2468/ui/?token=... ``` ## Features diff --git a/docs/openapi.json b/docs/openapi.json index 5fa7897..a003943 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "0.1.0" + "version": "0.1.4-rc.7" }, "servers": [ { diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index b8bce19..4902344 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -290,7 +290,7 @@ icon: "rocket" - Open the [Inspector UI](https://inspect.sandboxagent.dev) to inspect session state using a GUI. + Open the Inspector UI at `/ui/` on your server (e.g., `http://localhost:2468/ui/`) to inspect session state using a GUI. Sandbox Agent Inspector diff --git a/docs/sdks/typescript.mdx b/docs/sdks/typescript.mdx index cbaf598..7ebc165 100644 --- a/docs/sdks/typescript.mdx +++ b/docs/sdks/typescript.mdx @@ -132,7 +132,7 @@ const url = buildInspectorUrl({ headers: { "X-Custom-Header": "value" }, }); console.log(url); -// https://inspect.sandboxagent.dev?url=https%3A%2F%2Fyour-sandbox-agent.example.com&token=...&headers=... +// https://your-sandbox-agent.example.com/ui/?token=...&headers=... ``` Parameters: diff --git a/examples/shared/src/sandbox-agent-client.ts b/examples/shared/src/sandbox-agent-client.ts index 2ccd8f6..ff80f3b 100644 --- a/examples/shared/src/sandbox-agent-client.ts +++ b/examples/shared/src/sandbox-agent-client.ts @@ -18,8 +18,6 @@ export function ensureUrl(rawUrl: string): string { return `https://${rawUrl}`; } -const INSPECTOR_URL = "https://inspect.sandboxagent.dev"; - export function buildInspectorUrl({ baseUrl, token, @@ -30,14 +28,15 @@ export function buildInspectorUrl({ headers?: Record; }): string { const normalized = normalizeBaseUrl(ensureUrl(baseUrl)); - const params = new URLSearchParams({ url: normalized }); + const params = new URLSearchParams(); if (token) { params.set("token", token); } if (headers && Object.keys(headers).length > 0) { params.set("headers", JSON.stringify(headers)); } - return `${INSPECTOR_URL}?${params.toString()}`; + const queryString = params.toString(); + return `${normalized}/ui/${queryString ? `?${queryString}` : ""}`; } export function logInspectorUrl({ @@ -432,11 +431,11 @@ export async function runPrompt(options: RunPromptOptions): Promise { } } - // Print text deltas - if (event.type === "item.delta") { + // Print text deltas (only during assistant turn) + if (event.type === "item.delta" && isThinking) { const delta = (event.data as any)?.delta; if (delta) { - if (isThinking && !hasStartedOutput) { + if (!hasStartedOutput) { process.stdout.write("\r\x1b[K"); // Clear line hasStartedOutput = true; } diff --git a/frontend/packages/website/src/components/Inspector.tsx b/frontend/packages/website/src/components/Inspector.tsx index c0c7b71..7e29185 100644 --- a/frontend/packages/website/src/components/Inspector.tsx +++ b/frontend/packages/website/src/components/Inspector.tsx @@ -1,7 +1,5 @@ 'use client'; -import { ArrowRight } from 'lucide-react'; - export function Inspector() { return (
@@ -13,23 +11,13 @@ export function Inspector() { Inspect sessions, view event payloads, and troubleshoot without writing code.

-
); diff --git a/sdks/cli/package.json b/sdks/cli/package.json index e0ddb18..6001f31 100644 --- a/sdks/cli/package.json +++ b/sdks/cli/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli", - "version": "0.1.0", + "version": "0.1.4-rc.7", "description": "CLI for sandbox-agent - run AI coding agents in sandboxes", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/darwin-arm64/package.json b/sdks/cli/platforms/darwin-arm64/package.json index 51ccb8f..07d5628 100644 --- a/sdks/cli/platforms/darwin-arm64/package.json +++ b/sdks/cli/platforms/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-darwin-arm64", - "version": "0.1.0", + "version": "0.1.4-rc.7", "description": "sandbox-agent CLI binary for macOS ARM64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/darwin-x64/package.json b/sdks/cli/platforms/darwin-x64/package.json index 34b6a81..414cf04 100644 --- a/sdks/cli/platforms/darwin-x64/package.json +++ b/sdks/cli/platforms/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-darwin-x64", - "version": "0.1.0", + "version": "0.1.4-rc.7", "description": "sandbox-agent CLI binary for macOS x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/linux-x64/package.json b/sdks/cli/platforms/linux-x64/package.json index 9d330e5..ef8ac77 100644 --- a/sdks/cli/platforms/linux-x64/package.json +++ b/sdks/cli/platforms/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-linux-x64", - "version": "0.1.0", + "version": "0.1.4-rc.7", "description": "sandbox-agent CLI binary for Linux x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/win32-x64/package.json b/sdks/cli/platforms/win32-x64/package.json index d40f4c3..eb2b47d 100644 --- a/sdks/cli/platforms/win32-x64/package.json +++ b/sdks/cli/platforms/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-win32-x64", - "version": "0.1.0", + "version": "0.1.4-rc.7", "description": "sandbox-agent CLI binary for Windows x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index cc08c50..b6cde13 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "sandbox-agent", - "version": "0.1.0", + "version": "0.1.4-rc.7", "description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.", "license": "Apache-2.0", "repository": { diff --git a/sdks/typescript/src/inspector.ts b/sdks/typescript/src/inspector.ts index 9c448e2..dfc830d 100644 --- a/sdks/typescript/src/inspector.ts +++ b/sdks/typescript/src/inspector.ts @@ -1,5 +1,3 @@ -const INSPECTOR_URL = "https://inspect.sandboxagent.dev"; - export interface InspectorUrlOptions { /** * Base URL of the sandbox-agent server. @@ -18,15 +16,17 @@ export interface InspectorUrlOptions { /** * Builds a URL to the sandbox-agent inspector UI with the given connection parameters. + * The inspector UI is served at /ui/ on the sandbox-agent server. */ export function buildInspectorUrl(options: InspectorUrlOptions): string { const normalized = options.baseUrl.replace(/\/+$/, ""); - const params = new URLSearchParams({ url: normalized }); + const params = new URLSearchParams(); if (options.token) { params.set("token", options.token); } if (options.headers && Object.keys(options.headers).length > 0) { params.set("headers", JSON.stringify(options.headers)); } - return `${INSPECTOR_URL}?${params.toString()}`; + const queryString = params.toString(); + return `${normalized}/ui/${queryString ? `?${queryString}` : ""}`; } diff --git a/server/packages/agent-credentials/src/lib.rs b/server/packages/agent-credentials/src/lib.rs index 64d3d4b..1192241 100644 --- a/server/packages/agent-credentials/src/lib.rs +++ b/server/packages/agent-credentials/src/lib.rs @@ -43,7 +43,9 @@ impl CredentialExtractionOptions { } } -pub fn extract_claude_credentials(options: &CredentialExtractionOptions) -> Option { +pub fn extract_claude_credentials( + options: &CredentialExtractionOptions, +) -> Option { let home_dir = options.home_dir.clone().unwrap_or_else(default_home_dir); let include_oauth = options.include_oauth; @@ -88,8 +90,7 @@ pub fn extract_claude_credentials(options: &CredentialExtractionOptions) -> Opti }; let access = read_string_field(&data, &["claudeAiOauth", "accessToken"]); if let Some(token) = access { - if let Some(expires_at) = - read_string_field(&data, &["claudeAiOauth", "expiresAt"]) + if let Some(expires_at) = read_string_field(&data, &["claudeAiOauth", "expiresAt"]) { if is_expired_rfc3339(&expires_at) { continue; @@ -108,7 +109,9 @@ pub fn extract_claude_credentials(options: &CredentialExtractionOptions) -> Opti None } -pub fn extract_codex_credentials(options: &CredentialExtractionOptions) -> Option { +pub fn extract_codex_credentials( + options: &CredentialExtractionOptions, +) -> Option { let home_dir = options.home_dir.clone().unwrap_or_else(default_home_dir); let include_oauth = options.include_oauth; let path = home_dir.join(".codex").join("auth.json"); @@ -165,18 +168,18 @@ pub fn extract_opencode_credentials(options: &CredentialExtractionOptions) -> Ex None => continue, }; - let auth_type = config - .get("type") - .and_then(Value::as_str) - .unwrap_or(""); + let auth_type = config.get("type").and_then(Value::as_str).unwrap_or(""); let credentials = if auth_type == "api" { - config.get("key").and_then(Value::as_str).map(|key| ProviderCredentials { - api_key: key.to_string(), - source: "opencode".to_string(), - auth_type: AuthType::ApiKey, - provider: provider_name.to_string(), - }) + config + .get("key") + .and_then(Value::as_str) + .map(|key| ProviderCredentials { + api_key: key.to_string(), + source: "opencode".to_string(), + auth_type: AuthType::ApiKey, + provider: provider_name.to_string(), + }) } else if auth_type == "oauth" && include_oauth { let expires = config.get("expires").and_then(Value::as_i64); if let Some(expires) = expires { @@ -214,7 +217,9 @@ pub fn extract_opencode_credentials(options: &CredentialExtractionOptions) -> Ex } else if provider_name == "openai" { result.openai = Some(credentials.clone()); } else { - result.other.insert(provider_name.to_string(), credentials.clone()); + result + .other + .insert(provider_name.to_string(), credentials.clone()); } } } @@ -222,7 +227,9 @@ pub fn extract_opencode_credentials(options: &CredentialExtractionOptions) -> Ex result } -pub fn extract_amp_credentials(options: &CredentialExtractionOptions) -> Option { +pub fn extract_amp_credentials( + options: &CredentialExtractionOptions, +) -> Option { let home_dir = options.home_dir.clone().unwrap_or_else(default_home_dir); let path = home_dir.join(".amp").join("config.json"); let data = read_json_file(&path)?; diff --git a/server/packages/agent-management/src/agents.rs b/server/packages/agent-management/src/agents.rs index 65c7051..e110c96 100644 --- a/server/packages/agent-management/src/agents.rs +++ b/server/packages/agent-management/src/agents.rs @@ -3,15 +3,7 @@ use std::fmt; use std::fs; use std::io::{self, BufRead, BufReader, Read, Write}; use std::path::{Path, PathBuf}; -use std::process::{ - Child, - ChildStderr, - ChildStdin, - ChildStdout, - Command, - ExitStatus, - Stdio, -}; +use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus, Stdio}; use std::time::{Duration, Instant}; use flate2::read::GzDecoder; @@ -124,17 +116,18 @@ impl AgentManager { }) } - pub fn with_platform( - install_dir: impl Into, - platform: Platform, - ) -> Self { + pub fn with_platform(install_dir: impl Into, platform: Platform) -> Self { Self { install_dir: install_dir.into(), platform, } } - pub fn install(&self, agent: AgentId, options: InstallOptions) -> Result { + pub fn install( + &self, + agent: AgentId, + options: InstallOptions, + ) -> Result { let install_path = self.binary_path(agent); if !options.reinstall { if let Ok(existing_path) = self.resolve_binary(agent) { @@ -148,9 +141,15 @@ impl AgentManager { fs::create_dir_all(&self.install_dir)?; match agent { - AgentId::Claude => install_claude(&install_path, self.platform, options.version.as_deref())?, - AgentId::Codex => install_codex(&install_path, self.platform, options.version.as_deref())?, - AgentId::Opencode => install_opencode(&install_path, self.platform, options.version.as_deref())?, + AgentId::Claude => { + install_claude(&install_path, self.platform, options.version.as_deref())? + } + AgentId::Codex => { + install_codex(&install_path, self.platform, options.version.as_deref())? + } + AgentId::Opencode => { + install_opencode(&install_path, self.platform, options.version.as_deref())? + } AgentId::Amp => install_amp(&install_path, self.platform, options.version.as_deref())?, AgentId::Mock => { if !install_path.exists() { @@ -256,10 +255,7 @@ impl AgentManager { command.arg("app-server"); } AgentId::Opencode => { - command - .arg("run") - .arg("--format") - .arg("json"); + command.arg("run").arg("--format").arg("json"); if let Some(model) = options.model.as_deref() { command.arg("-m").arg(model); } @@ -346,10 +342,15 @@ impl AgentManager { fn spawn_codex_app_server(&self, options: SpawnOptions) -> Result { if options.session_id.is_some() { - return Err(AgentError::ResumeUnsupported { agent: AgentId::Codex }); + return Err(AgentError::ResumeUnsupported { + agent: AgentId::Codex, + }); } let mut command = self.build_command(AgentId::Codex, &options)?; - command.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()); + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); for (key, value) in options.env { command.env(key, value); } @@ -417,11 +418,11 @@ impl AgentManager { Ok(value) => value, Err(_) => continue, }; - let message: codex_schema::JsonrpcMessage = - match serde_json::from_value(value.clone()) { - Ok(message) => message, - Err(_) => continue, - }; + let message: codex_schema::JsonrpcMessage = match serde_json::from_value(value.clone()) + { + Ok(message) => message, + Err(_) => continue, + }; match message { codex_schema::JsonrpcMessage::Response(response) => { let response_id = response.id.to_string(); @@ -443,11 +444,16 @@ impl AgentManager { params.cwd = cwd.clone(); send_json_line( &mut stdin, - &codex_schema::ClientRequest::ThreadStart { id: request_id, params }, + &codex_schema::ClientRequest::ThreadStart { + id: request_id, + params, + }, )?; thread_start_id = Some(request_id_str); thread_start_sent = true; - } else if thread_start_id.as_deref() == Some(&response_id) && thread_id.is_none() { + } else if thread_start_id.as_deref() == Some(&response_id) + && thread_id.is_none() + { thread_id = codex_thread_id_from_response(&response.result); } events.push(value); @@ -466,8 +472,11 @@ impl AgentManager { ) { completed = true; } - if let codex_schema::ServerNotification::ItemCompleted(params) = ¬ification { - if matches!(params.item, codex_schema::ThreadItem::AgentMessage { .. }) { + if let codex_schema::ServerNotification::ItemCompleted(params) = + ¬ification + { + if matches!(params.item, codex_schema::ThreadItem::AgentMessage { .. }) + { completed = true; } } @@ -809,17 +818,35 @@ fn codex_thread_id_from_notification( codex_schema::ServerNotification::TurnCompleted(params) => Some(params.thread_id.clone()), codex_schema::ServerNotification::ItemStarted(params) => Some(params.thread_id.clone()), codex_schema::ServerNotification::ItemCompleted(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemAgentMessageDelta(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemReasoningTextDelta(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemFileChangeOutputDelta(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemMcpToolCallProgress(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ThreadTokenUsageUpdated(params) => Some(params.thread_id.clone()), + codex_schema::ServerNotification::ItemAgentMessageDelta(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ItemReasoningTextDelta(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ItemFileChangeOutputDelta(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ItemMcpToolCallProgress(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ThreadTokenUsageUpdated(params) => { + Some(params.thread_id.clone()) + } codex_schema::ServerNotification::TurnDiffUpdated(params) => Some(params.thread_id.clone()), codex_schema::ServerNotification::TurnPlanUpdated(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => Some(params.thread_id.clone()), - codex_schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => Some(params.thread_id.clone()), + codex_schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => { + Some(params.thread_id.clone()) + } + codex_schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => { + Some(params.thread_id.clone()) + } codex_schema::ServerNotification::ThreadCompacted(params) => Some(params.thread_id.clone()), _ => None, } @@ -859,36 +886,36 @@ fn extract_session_id(agent: AgentId, events: &[Value]) -> Option { return Some(id.to_string()); } } - AgentId::Codex => { - if let Ok(notification) = - serde_json::from_value::(event.clone()) - { - match notification { - codex_schema::ServerNotification::ThreadStarted(params) => { - return Some(params.thread.id); + AgentId::Codex => { + if let Ok(notification) = + serde_json::from_value::(event.clone()) + { + match notification { + codex_schema::ServerNotification::ThreadStarted(params) => { + return Some(params.thread.id); + } + codex_schema::ServerNotification::TurnStarted(params) => { + return Some(params.thread_id); + } + codex_schema::ServerNotification::TurnCompleted(params) => { + return Some(params.thread_id); + } + codex_schema::ServerNotification::ItemStarted(params) => { + return Some(params.thread_id); + } + codex_schema::ServerNotification::ItemCompleted(params) => { + return Some(params.thread_id); + } + _ => {} } - codex_schema::ServerNotification::TurnStarted(params) => { - return Some(params.thread_id); - } - codex_schema::ServerNotification::TurnCompleted(params) => { - return Some(params.thread_id); - } - codex_schema::ServerNotification::ItemStarted(params) => { - return Some(params.thread_id); - } - codex_schema::ServerNotification::ItemCompleted(params) => { - return Some(params.thread_id); - } - _ => {} + } + if let Some(id) = event.get("thread_id").and_then(Value::as_str) { + return Some(id.to_string()); + } + if let Some(id) = event.get("threadId").and_then(Value::as_str) { + return Some(id.to_string()); } } - if let Some(id) = event.get("thread_id").and_then(Value::as_str) { - return Some(id.to_string()); - } - if let Some(id) = event.get("threadId").and_then(Value::as_str) { - return Some(id.to_string()); - } - } AgentId::Opencode => { if let Some(id) = event.get("session_id").and_then(Value::as_str) { return Some(id.to_string()); @@ -902,7 +929,8 @@ fn extract_session_id(agent: AgentId, events: &[Value]) -> Option { if let Some(id) = extract_nested_string(event, &["properties", "sessionID"]) { return Some(id); } - if let Some(id) = extract_nested_string(event, &["properties", "part", "sessionID"]) { + if let Some(id) = extract_nested_string(event, &["properties", "part", "sessionID"]) + { return Some(id); } if let Some(id) = extract_nested_string(event, &["session", "id"]) { @@ -925,7 +953,9 @@ fn extract_result_text(agent: AgentId, events: &[Value]) -> Option { if let Some(result) = event.get("result").and_then(Value::as_str) { return Some(result.to_string()); } - if let Some(text) = extract_nested_string(event, &["message", "content", "0", "text"]) { + if let Some(text) = + extract_nested_string(event, &["message", "content", "0", "text"]) + { return Some(text); } } @@ -974,7 +1004,9 @@ fn extract_result_text(agent: AgentId, events: &[Value]) -> Option { if let Some(delta) = extract_nested_string(event, &["properties", "delta"]) { buffer.push_str(&delta); } - if let Some(content) = extract_nested_string(event, &["properties", "part", "content"]) { + if let Some(content) = + extract_nested_string(event, &["properties", "part", "content"]) + { buffer.push_str(&content); } } @@ -1178,14 +1210,19 @@ fn download_bytes(url: &Url) -> Result, AgentError> { Ok(bytes) } -fn install_claude(path: &Path, platform: Platform, version: Option<&str>) -> Result<(), AgentError> { +fn install_claude( + path: &Path, + platform: Platform, + version: Option<&str>, +) -> Result<(), AgentError> { let version = match version { Some(version) => version.to_string(), None => { let url = Url::parse( "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest", )?; - let text = String::from_utf8(download_bytes(&url)?).map_err(|err| AgentError::ExtractFailed(err.to_string()))?; + let text = String::from_utf8(download_bytes(&url)?) + .map_err(|err| AgentError::ExtractFailed(err.to_string()))?; text.trim().to_string() } }; @@ -1210,8 +1247,11 @@ fn install_amp(path: &Path, platform: Platform, version: Option<&str>) -> Result let version = match version { Some(version) => version.to_string(), None => { - let url = Url::parse("https://storage.googleapis.com/amp-public-assets-prod-0/cli/cli-version.txt")?; - let text = String::from_utf8(download_bytes(&url)?).map_err(|err| AgentError::ExtractFailed(err.to_string()))?; + let url = Url::parse( + "https://storage.googleapis.com/amp-public-assets-prod-0/cli/cli-version.txt", + )?; + let text = String::from_utf8(download_bytes(&url)?) + .map_err(|err| AgentError::ExtractFailed(err.to_string()))?; text.trim().to_string() } }; @@ -1261,7 +1301,11 @@ fn install_codex(path: &Path, platform: Platform, version: Option<&str>) -> Resu Ok(()) } -fn install_opencode(path: &Path, platform: Platform, version: Option<&str>) -> Result<(), AgentError> { +fn install_opencode( + path: &Path, + platform: Platform, + version: Option<&str>, +) -> Result<(), AgentError> { match platform { Platform::MacosArm64 => { let url = match version { @@ -1317,7 +1361,8 @@ fn install_opencode(path: &Path, platform: Platform, version: Option<&str>) -> R fn install_zip_binary(path: &Path, url: &Url, binary_name: &str) -> Result<(), AgentError> { let bytes = download_bytes(url)?; let reader = io::Cursor::new(bytes); - let mut archive = zip::ZipArchive::new(reader).map_err(|err| AgentError::ExtractFailed(err.to_string()))?; + let mut archive = + zip::ZipArchive::new(reader).map_err(|err| AgentError::ExtractFailed(err.to_string()))?; let temp_dir = tempfile::tempdir()?; for i in 0..archive.len() { let mut file = archive diff --git a/server/packages/agent-management/src/testing.rs b/server/packages/agent-management/src/testing.rs index 3543f56..6ba411d 100644 --- a/server/packages/agent-management/src/testing.rs +++ b/server/packages/agent-management/src/testing.rs @@ -285,10 +285,7 @@ fn handle_health_response( }) } -fn run_blocking_check( - provider: &str, - check: F, -) -> Result<(), TestAgentConfigError> +fn run_blocking_check(provider: &str, check: F) -> Result<(), TestAgentConfigError> where F: FnOnce() -> Result<(), TestAgentConfigError> + Send + 'static, { @@ -301,7 +298,12 @@ where } fn detect_system_agents() -> Vec { - let candidates = [AgentId::Claude, AgentId::Codex, AgentId::Opencode, AgentId::Amp]; + let candidates = [ + AgentId::Claude, + AgentId::Codex, + AgentId::Opencode, + AgentId::Amp, + ]; let install_dir = default_install_dir(); candidates .into_iter() diff --git a/server/packages/error/src/lib.rs b/server/packages/error/src/lib.rs index 60609c2..c24a6e1 100644 --- a/server/packages/error/src/lib.rs +++ b/server/packages/error/src/lib.rs @@ -123,7 +123,10 @@ pub enum SandboxError { #[error("agent not installed: {agent}")] AgentNotInstalled { agent: String }, #[error("install failed: {agent}")] - InstallFailed { agent: String, stderr: Option }, + InstallFailed { + agent: String, + stderr: Option, + }, #[error("agent process exited: {agent}")] AgentProcessExited { agent: String, @@ -167,9 +170,7 @@ impl SandboxError { pub fn to_agent_error(&self) -> AgentError { let (agent, session_id, details) = match self { Self::InvalidRequest { .. } => (None, None, None), - Self::UnsupportedAgent { agent } => { - (Some(agent.clone()), None, None) - } + Self::UnsupportedAgent { agent } => (Some(agent.clone()), None, None), Self::AgentNotInstalled { agent } => (Some(agent.clone()), None, None), Self::InstallFailed { agent, stderr } => { let mut map = Map::new(); @@ -179,7 +180,11 @@ impl SandboxError { ( Some(agent.clone()), None, - if map.is_empty() { None } else { Some(Value::Object(map)) }, + if map.is_empty() { + None + } else { + Some(Value::Object(map)) + }, ) } Self::AgentProcessExited { @@ -200,7 +205,11 @@ impl SandboxError { ( Some(agent.clone()), None, - if map.is_empty() { None } else { Some(Value::Object(map)) }, + if map.is_empty() { + None + } else { + Some(Value::Object(map)) + }, ) } Self::TokenInvalid { message } => { @@ -219,20 +228,12 @@ impl SandboxError { }); (None, None, details) } - Self::SessionNotFound { session_id } => { - (None, Some(session_id.clone()), None) - } - Self::SessionAlreadyExists { session_id } => { - (None, Some(session_id.clone()), None) - } + Self::SessionNotFound { session_id } => (None, Some(session_id.clone()), None), + Self::SessionAlreadyExists { session_id } => (None, Some(session_id.clone()), None), Self::ModeNotSupported { agent, mode } => { let mut map = Map::new(); map.insert("mode".to_string(), Value::String(mode.clone())); - ( - Some(agent.clone()), - None, - Some(Value::Object(map)), - ) + (Some(agent.clone()), None, Some(Value::Object(map))) } Self::StreamError { message } => { let mut map = Map::new(); diff --git a/server/packages/extracted-agent-schemas/build.rs b/server/packages/extracted-agent-schemas/build.rs index cdf328b..5807bfc 100644 --- a/server/packages/extracted-agent-schemas/build.rs +++ b/server/packages/extracted-agent-schemas/build.rs @@ -17,10 +17,7 @@ fn main() { let schema_path = schema_dir.join(file); // Tell cargo to rerun if schema changes - emit_stdout(&format!( - "cargo:rerun-if-changed={}", - schema_path.display() - )); + emit_stdout(&format!("cargo:rerun-if-changed={}", schema_path.display())); if !schema_path.exists() { emit_stdout(&format!( @@ -48,9 +45,10 @@ fn main() { let contents = type_space.to_stream(); // Format the generated code - let formatted = prettyplease::unparse(&syn::parse2(contents.clone()).unwrap_or_else(|e| { - panic!("Failed to parse generated code for {}: {}", name, e) - })); + let formatted = prettyplease::unparse( + &syn::parse2(contents.clone()) + .unwrap_or_else(|e| panic!("Failed to parse generated code for {}: {}", name, e)), + ); let out_path = Path::new(&out_dir).join(format!("{}.rs", name)); fs::write(&out_path, formatted) diff --git a/server/packages/extracted-agent-schemas/tests/schema_roundtrip.rs b/server/packages/extracted-agent-schemas/tests/schema_roundtrip.rs index 7c992b0..db7003b 100644 --- a/server/packages/extracted-agent-schemas/tests/schema_roundtrip.rs +++ b/server/packages/extracted-agent-schemas/tests/schema_roundtrip.rs @@ -17,16 +17,14 @@ fn test_claude_bash_input() { #[test] fn test_codex_server_notification() { - let notification = codex::ServerNotification::ItemCompleted( - codex::ItemCompletedNotification { - item: codex::ThreadItem::AgentMessage { - id: "msg-123".to_string(), - text: "Hello from Codex".to_string(), - }, - thread_id: "thread-123".to_string(), - turn_id: "turn-456".to_string(), + let notification = codex::ServerNotification::ItemCompleted(codex::ItemCompletedNotification { + item: codex::ThreadItem::AgentMessage { + id: "msg-123".to_string(), + text: "Hello from Codex".to_string(), }, - ); + thread_id: "thread-123".to_string(), + turn_id: "turn-456".to_string(), + }); let json = serde_json::to_string(¬ification).unwrap(); assert!(json.contains("item/completed")); diff --git a/server/packages/openapi-gen/build.rs b/server/packages/openapi-gen/build.rs index 2ac2ce3..b3c0c90 100644 --- a/server/packages/openapi-gen/build.rs +++ b/server/packages/openapi-gen/build.rs @@ -13,8 +13,7 @@ fn main() { let out_path = Path::new(&out_dir).join("openapi.json"); let openapi = ApiDoc::openapi(); - let json = serde_json::to_string_pretty(&openapi) - .expect("Failed to serialize OpenAPI spec"); + let json = serde_json::to_string_pretty(&openapi).expect("Failed to serialize OpenAPI spec"); fs::write(&out_path, json).expect("Failed to write OpenAPI spec"); emit_stdout(&format!( diff --git a/server/packages/openapi-gen/src/main.rs b/server/packages/openapi-gen/src/main.rs index 7b74a6f..439a4be 100644 --- a/server/packages/openapi-gen/src/main.rs +++ b/server/packages/openapi-gen/src/main.rs @@ -47,7 +47,11 @@ fn init_logging() { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::registry() .with(filter) - .with(tracing_logfmt::builder().layer().with_writer(std::io::stderr)) + .with( + tracing_logfmt::builder() + .layer() + .with_writer(std::io::stderr), + ) .init(); } diff --git a/server/packages/sandbox-agent/src/agent_server_logs/unix.rs b/server/packages/sandbox-agent/src/agent_server_logs/unix.rs index 326af42..2c875d7 100644 --- a/server/packages/sandbox-agent/src/agent_server_logs/unix.rs +++ b/server/packages/sandbox-agent/src/agent_server_logs/unix.rs @@ -3,8 +3,8 @@ use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use sandbox_agent_error::SandboxError; -use time::{Duration, OffsetDateTime}; use sandbox_agent_universal_agent_schema::StderrOutput; +use time::{Duration, OffsetDateTime}; const LOG_RETENTION_DAYS: i64 = 7; const LOG_HEAD_LINES: usize = 20; diff --git a/server/packages/sandbox-agent/src/lib.rs b/server/packages/sandbox-agent/src/lib.rs index 1767f73..098d0b3 100644 --- a/server/packages/sandbox-agent/src/lib.rs +++ b/server/packages/sandbox-agent/src/lib.rs @@ -1,7 +1,7 @@ //! Sandbox agent core utilities. -pub mod credentials; mod agent_server_logs; +pub mod credentials; pub mod router; pub mod telemetry; pub mod ui; diff --git a/server/packages/sandbox-agent/src/main.rs b/server/packages/sandbox-agent/src/main.rs index c0a57ff..8904ddd 100644 --- a/server/packages/sandbox-agent/src/main.rs +++ b/server/packages/sandbox-agent/src/main.rs @@ -79,10 +79,6 @@ struct ServerArgs { #[arg(long = "cors-allow-credentials", short = 'C')] cors_allow_credentials: bool, - /// Disable default CORS for the inspector (https://inspect.sandboxagent.dev) - #[arg(long = "no-inspector-cors")] - no_inspector_cors: bool, - #[arg(long = "no-telemetry")] no_telemetry: bool, } @@ -848,19 +844,11 @@ fn available_providers(credentials: &ExtractedCredentials) -> Vec { providers } -const INSPECTOR_ORIGIN: &str = "https://inspect.sandboxagent.dev"; - fn build_cors_layer(server: &ServerArgs) -> Result { let mut cors = CorsLayer::new(); - // Build origins list: inspector by default + any additional origins + // Build origins list from provided origins let mut origins = Vec::new(); - if !server.no_inspector_cors { - let inspector_origin = INSPECTOR_ORIGIN - .parse() - .map_err(|_| CliError::InvalidCorsOrigin(INSPECTOR_ORIGIN.to_string()))?; - origins.push(inspector_origin); - } for origin in &server.cors_allow_origin { let value = origin .parse() diff --git a/server/packages/sandbox-agent/src/router.rs b/server/packages/sandbox-agent/src/router.rs index fa587c0..5f16582 100644 --- a/server/packages/sandbox-agent/src/router.rs +++ b/server/packages/sandbox-agent/src/router.rs @@ -25,8 +25,7 @@ use sandbox_agent_universal_agent_schema::{ ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus, PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, ReasoningVisibility, SessionEndReason, SessionEndedData, SessionStartedData, StderrOutput, TerminatedBy, UniversalEvent, - UniversalEventData, - UniversalEventType, UniversalItem, + UniversalEventData, UniversalEventType, UniversalItem, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -37,6 +36,7 @@ use tokio_stream::wrappers::BroadcastStream; use tower_http::trace::TraceLayer; use utoipa::{Modify, OpenApi, ToSchema}; +use crate::agent_server_logs::AgentServerLogs; use crate::ui; use sandbox_agent_agent_management::agents::{ AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn, @@ -44,7 +44,6 @@ use sandbox_agent_agent_management::agents::{ use sandbox_agent_agent_management::credentials::{ extract_all_credentials, CredentialExtractionOptions, ExtractedCredentials, }; -use crate::agent_server_logs::AgentServerLogs; const MOCK_EVENT_DELAY_MS: u64 = 200; static USER_MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1); @@ -99,7 +98,10 @@ pub fn build_router_with_state(shared: Arc) -> (Router, Arc) .route("/sessions", get(list_sessions)) .route("/sessions/:session_id", post(create_session)) .route("/sessions/:session_id/messages", post(post_message)) - .route("/sessions/:session_id/messages/stream", post(post_message_stream)) + .route( + "/sessions/:session_id/messages/stream", + post(post_message_stream), + ) .route("/sessions/:session_id/terminate", post(terminate_session)) .route("/sessions/:session_id/events", get(get_events)) .route("/sessions/:session_id/events/sse", get(get_events_sse)) @@ -126,7 +128,8 @@ pub fn build_router_with_state(shared: Arc) -> (Router, Arc) let mut router = Router::new() .route("/", get(get_root)) - .nest("/v1", v1_router); + .nest("/v1", v1_router) + .fallback(not_found); if ui::is_enabled() { router = router.merge(ui::router()); @@ -1141,8 +1144,8 @@ impl AgentServerManager { ) -> Result<(String, Arc>>), SandboxError> { let manager = self.agent_manager.clone(); let log_dir = self.log_base_dir.clone(); - let (base_url, child) = - tokio::task::spawn_blocking(move || -> Result<(String, std::process::Child), SandboxError> { + let (base_url, child) = tokio::task::spawn_blocking( + move || -> Result<(String, std::process::Child), SandboxError> { let path = manager .resolve_binary(agent) .map_err(|err| map_spawn_error(agent, err))?; @@ -1159,16 +1162,14 @@ impl AgentServerManager { message: err.to_string(), })?; Ok((format!("http://127.0.0.1:{port}"), child)) - }) - .await - .map_err(|err| SandboxError::StreamError { - message: err.to_string(), - })??; + }, + ) + .await + .map_err(|err| SandboxError::StreamError { + message: err.to_string(), + })??; - Ok(( - base_url, - Arc::new(std::sync::Mutex::new(Some(child))), - )) + Ok((base_url, Arc::new(std::sync::Mutex::new(Some(child))))) } async fn spawn_stdio_server( @@ -1187,59 +1188,66 @@ impl AgentServerManager { let (stdin_tx, stdin_rx) = mpsc::unbounded_channel::(); let (stdout_tx, stdout_rx) = mpsc::unbounded_channel::(); - let child = tokio::task::spawn_blocking(move || -> Result { - let path = manager - .resolve_binary(agent) - .map_err(|err| map_spawn_error(agent, err))?; - let mut command = std::process::Command::new(path); - let stderr = AgentServerLogs::new(log_dir, agent.as_str()).open()?; - command - .arg("app-server") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(stderr); + let child = + tokio::task::spawn_blocking(move || -> Result { + let path = manager + .resolve_binary(agent) + .map_err(|err| map_spawn_error(agent, err))?; + let mut command = std::process::Command::new(path); + let stderr = AgentServerLogs::new(log_dir, agent.as_str()).open()?; + command + .arg("app-server") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(stderr); - let mut child = command.spawn().map_err(|err| SandboxError::StreamError { + let mut child = command.spawn().map_err(|err| SandboxError::StreamError { + message: err.to_string(), + })?; + + let stdin = child + .stdin + .take() + .ok_or_else(|| SandboxError::StreamError { + message: "codex stdin unavailable".to_string(), + })?; + let stdout = child + .stdout + .take() + .ok_or_else(|| SandboxError::StreamError { + message: "codex stdout unavailable".to_string(), + })?; + + let stdin_rx_mut = std::sync::Mutex::new(stdin_rx); + std::thread::spawn(move || { + let mut stdin = stdin; + let mut rx = stdin_rx_mut.lock().unwrap(); + while let Some(line) = rx.blocking_recv() { + if writeln!(stdin, "{line}").is_err() { + break; + } + if stdin.flush().is_err() { + break; + } + } + }); + + std::thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines() { + let Ok(line) = line else { break }; + if stdout_tx.send(line).is_err() { + break; + } + } + }); + + Ok(child) + }) + .await + .map_err(|err| SandboxError::StreamError { message: err.to_string(), - })?; - - let stdin = child.stdin.take().ok_or_else(|| SandboxError::StreamError { - message: "codex stdin unavailable".to_string(), - })?; - let stdout = child.stdout.take().ok_or_else(|| SandboxError::StreamError { - message: "codex stdout unavailable".to_string(), - })?; - - let stdin_rx_mut = std::sync::Mutex::new(stdin_rx); - std::thread::spawn(move || { - let mut stdin = stdin; - let mut rx = stdin_rx_mut.lock().unwrap(); - while let Some(line) = rx.blocking_recv() { - if writeln!(stdin, "{line}").is_err() { - break; - } - if stdin.flush().is_err() { - break; - } - } - }); - - std::thread::spawn(move || { - let reader = BufReader::new(stdout); - for line in reader.lines() { - let Ok(line) = line else { break }; - if stdout_tx.send(line).is_err() { - break; - } - } - }); - - Ok(child) - }) - .await - .map_err(|err| SandboxError::StreamError { - message: err.to_string(), - })??; + })??; let server = Arc::new(CodexServer::new(stdin_tx)); @@ -1347,7 +1355,10 @@ impl AgentServerManager { } } - async fn ensure_server_for_restart(self: Arc, agent: AgentId) -> Result<(), SandboxError> { + async fn ensure_server_for_restart( + self: Arc, + agent: AgentId, + ) -> Result<(), SandboxError> { sleep(Duration::from_millis(500)).await; match agent { AgentId::Opencode => { @@ -1445,26 +1456,24 @@ impl SessionManager { } } - fn session_ref<'a>( - sessions: &'a [SessionState], - session_id: &str, - ) -> Option<&'a SessionState> { - sessions.iter().find(|session| session.session_id == session_id) + fn session_ref<'a>(sessions: &'a [SessionState], session_id: &str) -> Option<&'a SessionState> { + sessions + .iter() + .find(|session| session.session_id == session_id) } fn session_mut<'a>( sessions: &'a mut [SessionState], session_id: &str, ) -> Option<&'a mut SessionState> { - sessions.iter_mut().find(|session| session.session_id == session_id) + sessions + .iter_mut() + .find(|session| session.session_id == session_id) } /// Read agent stderr for error diagnostics fn read_agent_stderr(&self, agent: AgentId) -> Option { - let logs = AgentServerLogs::new( - self.server_manager.log_base_dir.clone(), - agent.as_str(), - ); + let logs = AgentServerLogs::new(self.server_manager.log_base_dir.clone(), agent.as_str()); logs.read_stderr() } @@ -1476,7 +1485,10 @@ impl SessionManager { let agent_id = parse_agent_id(&request.agent)?; { let sessions = self.sessions.lock().await; - if sessions.iter().any(|session| session.session_id == session_id) { + if sessions + .iter() + .any(|session| session.session_id == session_id) + { return Err(SandboxError::SessionAlreadyExists { session_id }); } } @@ -1675,10 +1687,7 @@ impl SessionManager { Ok(()) } - async fn emit_synthetic_assistant_start( - &self, - session_id: &str, - ) -> Result<(), SandboxError> { + async fn emit_synthetic_assistant_start(&self, session_id: &str) -> Result<(), SandboxError> { let conversion = { let mut sessions = self.sessions.lock().await; let session = Self::session_mut(&mut sessions, session_id).ok_or_else(|| { @@ -1688,7 +1697,9 @@ impl SessionManager { })?; session.enqueue_pending_assistant_start() }; - let _ = self.record_conversions(session_id, vec![conversion]).await?; + let _ = self + .record_conversions(session_id, vec![conversion]) + .await?; Ok(()) } @@ -1905,12 +1916,16 @@ impl SessionManager { let sender = claude_sender.ok_or_else(|| SandboxError::InvalidRequest { message: "Claude session is not active".to_string(), })?; - let session_id = native_session_id.clone().unwrap_or_else(|| session_id.to_string()); + let session_id = native_session_id + .clone() + .unwrap_or_else(|| session_id.to_string()); let response_text = response.clone().unwrap_or_default(); let line = claude_tool_result_line(&session_id, question_id, &response_text, false); - sender.send(line).map_err(|_| SandboxError::InvalidRequest { - message: "Claude session is not active".to_string(), - })?; + sender + .send(line) + .map_err(|_| SandboxError::InvalidRequest { + message: "Claude session is not active".to_string(), + })?; } else { // TODO: Forward question replies to subprocess agents. } @@ -1976,16 +1991,20 @@ impl SessionManager { let sender = claude_sender.ok_or_else(|| SandboxError::InvalidRequest { message: "Claude session is not active".to_string(), })?; - let session_id = native_session_id.clone().unwrap_or_else(|| session_id.to_string()); + let session_id = native_session_id + .clone() + .unwrap_or_else(|| session_id.to_string()); let line = claude_tool_result_line( &session_id, question_id, "User rejected the question.", true, ); - sender.send(line).map_err(|_| SandboxError::InvalidRequest { - message: "Claude session is not active".to_string(), - })?; + sender + .send(line) + .map_err(|_| SandboxError::InvalidRequest { + message: "Claude session is not active".to_string(), + })?; } else { // TODO: Forward question rejections to subprocess agents. } @@ -2139,9 +2158,11 @@ impl SessionManager { }; let line = claude_control_response_line(permission_id, behavior, response_value); - sender.send(line).map_err(|_| SandboxError::InvalidRequest { - message: "Claude session is not active".to_string(), - })?; + sender + .send(line) + .map_err(|_| SandboxError::InvalidRequest { + message: "Claude session is not active".to_string(), + })?; } else { // TODO: Forward permission replies to subprocess agents. } @@ -2811,7 +2832,8 @@ impl SessionManager { Err(_) => continue, }; - let message: codex_schema::JsonrpcMessage = match serde_json::from_value(value.clone()) { + let message: codex_schema::JsonrpcMessage = match serde_json::from_value(value.clone()) + { Ok(m) => m, Err(_) => continue, }; @@ -2828,12 +2850,17 @@ impl SessionManager { if let Ok(notification) = serde_json::from_value::(value.clone()) { - if let Some(thread_id) = codex_thread_id_from_server_notification(¬ification) { + if let Some(thread_id) = + codex_thread_id_from_server_notification(¬ification) + { if let Some(session_id) = server.session_for_thread(&thread_id) { - let conversions = match convert_codex::notification_to_universal(¬ification) { - Ok(c) => c, - Err(err) => vec![agent_unparsed("codex", &err, value.clone())], - }; + let conversions = + match convert_codex::notification_to_universal(¬ification) { + Ok(c) => c, + Err(err) => { + vec![agent_unparsed("codex", &err, value.clone())] + } + }; let _ = self.record_conversions(&session_id, conversions).await; } } @@ -2851,7 +2878,8 @@ impl SessionManager { for conversion in &mut conversions { conversion.raw = Some(value.clone()); } - let _ = self.record_conversions(&session_id, conversions).await; + let _ = + self.record_conversions(&session_id, conversions).await; } Err(err) => { let _ = self @@ -2982,12 +3010,13 @@ impl SessionManager { ) -> Result<(), SandboxError> { let server = self.ensure_codex_server().await?; - let thread_id = session - .native_session_id - .as_ref() - .ok_or_else(|| SandboxError::InvalidRequest { - message: "missing Codex thread id".to_string(), - })?; + let thread_id = + session + .native_session_id + .as_ref() + .ok_or_else(|| SandboxError::InvalidRequest { + message: "missing Codex thread id".to_string(), + })?; let id = server.next_request_id(); let prompt_text = codex_prompt_for_mode(prompt, Some(&session.agent_mode)); @@ -3430,14 +3459,22 @@ pub struct EventsQuery { pub offset: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub limit: Option, - #[serde(default, skip_serializing_if = "Option::is_none", alias = "include_raw")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + alias = "include_raw" + )] pub include_raw: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct TurnStreamQuery { - #[serde(default, skip_serializing_if = "Option::is_none", alias = "include_raw")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + alias = "include_raw" + )] pub include_raw: Option, } @@ -3541,10 +3578,22 @@ async fn get_agent_modes( Ok(Json(AgentModesResponse { modes })) } +const SERVER_INFO: &str = "\ +This is a Sandbox Agent server. Available endpoints:\n\ + - GET / - Server info\n\ + - GET /v1/health - Health check\n\ + - GET /ui/ - Inspector UI\n\n\ +See https://sandboxagent.dev for API documentation."; + async fn get_root() -> &'static str { - "This is a Sandbox Agent server for orchestrating coding agents.\n\ - See https://sandboxagent.dev for more information.\n\ - Visit /ui/ for the inspector UI." + SERVER_INFO +} + +async fn not_found() -> (StatusCode, String) { + ( + StatusCode::NOT_FOUND, + format!("404 Not Found\n\n{SERVER_INFO}"), + ) } #[utoipa::path( @@ -3571,48 +3620,47 @@ async fn list_agents( let manager = state.agent_manager.clone(); let server_statuses = state.session_manager.server_manager.status_snapshot().await; - let agents = tokio::task::spawn_blocking(move || { - all_agents() - .into_iter() - .map(|agent_id| { - let installed = manager.is_installed(agent_id); - let version = manager.version(agent_id).ok().flatten(); - let path = manager.resolve_binary(agent_id).ok(); - let capabilities = agent_capabilities_for(agent_id); + let agents = + tokio::task::spawn_blocking(move || { + all_agents() + .into_iter() + .map(|agent_id| { + let installed = manager.is_installed(agent_id); + let version = manager.version(agent_id).ok().flatten(); + let path = manager.resolve_binary(agent_id).ok(); + let capabilities = agent_capabilities_for(agent_id); - // Add server_status for agents with shared processes - let server_status = if capabilities.shared_process { - Some( - server_statuses - .get(&agent_id) - .cloned() - .unwrap_or(ServerStatusInfo { - status: ServerStatus::Stopped, - base_url: None, - uptime_ms: None, - restart_count: 0, - last_error: None, - }), - ) - } else { - None - }; + // Add server_status for agents with shared processes + let server_status = + if capabilities.shared_process { + Some(server_statuses.get(&agent_id).cloned().unwrap_or( + ServerStatusInfo { + status: ServerStatus::Stopped, + base_url: None, + uptime_ms: None, + restart_count: 0, + last_error: None, + }, + )) + } else { + None + }; - AgentInfo { - id: agent_id.as_str().to_string(), - installed, - version, - path: path.map(|path| path.to_string_lossy().to_string()), - capabilities, - server_status, - } - }) - .collect::>() - }) - .await - .map_err(|err| SandboxError::StreamError { - message: err.to_string(), - })?; + AgentInfo { + id: agent_id.as_str().to_string(), + installed, + version, + path: path.map(|path| path.to_string_lossy().to_string()), + capabilities, + server_status, + } + }) + .collect::>() + }) + .await + .map_err(|err| SandboxError::StreamError { + message: err.to_string(), + })?; Ok(Json(AgentListResponse { agents })) } @@ -3898,7 +3946,10 @@ fn all_agents() -> [AgentId; 5] { /// Returns true if the agent supports resuming a session after its process exits. /// These agents can use --resume/--continue to continue a conversation. fn agent_supports_resume(agent: AgentId) -> bool { - matches!(agent, AgentId::Claude | AgentId::Amp | AgentId::Opencode | AgentId::Codex) + matches!( + agent, + AgentId::Claude | AgentId::Amp | AgentId::Opencode | AgentId::Codex + ) } fn agent_supports_item_started(agent: AgentId) -> bool { @@ -4066,16 +4117,18 @@ fn agent_modes_for(agent: AgentId) -> Vec { name: "Build".to_string(), description: "Default build mode".to_string(), }], - AgentId::Mock => vec![AgentModeInfo { - id: "build".to_string(), - name: "Build".to_string(), - description: "Mock agent for UI testing".to_string(), - }, - AgentModeInfo { - id: "plan".to_string(), - name: "Plan".to_string(), - description: "Plan-only mock mode".to_string(), - }], + AgentId::Mock => vec![ + AgentModeInfo { + id: "build".to_string(), + name: "Build".to_string(), + description: "Mock agent for UI testing".to_string(), + }, + AgentModeInfo { + id: "plan".to_string(), + name: "Plan".to_string(), + description: "Plan-only mock mode".to_string(), + }, + ], } } @@ -4307,16 +4360,9 @@ fn claude_tool_result_line( .to_string() } -fn claude_control_response_line( - request_id: &str, - behavior: &str, - response: Value, -) -> String { +fn claude_control_response_line(request_id: &str, behavior: &str, response: Value) -> String { let mut response_obj = serde_json::Map::new(); - response_obj.insert( - "behavior".to_string(), - Value::String(behavior.to_string()), - ); + response_obj.insert("behavior".to_string(), Value::String(behavior.to_string())); if let Some(message) = response.get("message") { response_obj.insert("message".to_string(), message.clone()); } @@ -5022,7 +5068,8 @@ pub mod test_utils { impl TestHarness { pub async fn new() -> Self { let temp_dir = TempDir::new().expect("temp dir"); - let agent_manager = Arc::new(AgentManager::new(temp_dir.path()).expect("agent manager")); + let agent_manager = + Arc::new(AgentManager::new(temp_dir.path()).expect("agent manager")); let session_manager = Arc::new(SessionManager::new(agent_manager)); session_manager .server_manager @@ -5058,11 +5105,7 @@ pub mod test_utils { .await; } - pub async fn has_session_mapping( - &self, - agent: AgentId, - session_id: &str, - ) -> bool { + pub async fn has_session_mapping(&self, agent: AgentId, session_id: &str) -> bool { let sessions = self.session_manager.server_manager.sessions.lock().await; sessions .get(&agent) @@ -5101,8 +5144,8 @@ pub mod test_utils { variant: None, agent_version: None, }; - let mut session = SessionState::new(session_id.to_string(), agent, &request) - .expect("session"); + let mut session = + SessionState::new(session_id.to_string(), agent, &request).expect("session"); session.native_session_id = native_session_id.map(|id| id.to_string()); self.session_manager.sessions.lock().await.push(session); } @@ -5116,38 +5159,48 @@ pub mod test_utils { let (stdin_tx, _stdin_rx) = mpsc::unbounded_channel::(); let server = Arc::new(CodexServer::new(stdin_tx)); let child = Arc::new(std::sync::Mutex::new(child)); - self.session_manager.server_manager.servers.lock().await.insert( - agent, - ManagedServer { - kind: ManagedServerKind::Stdio { server }, - child: child.clone(), - status: ServerStatus::Running, - start_time: Some(Instant::now()), - restart_count: 0, - last_error: None, - shutdown_requested: false, - instance_id, - }, - ); + self.session_manager + .server_manager + .servers + .lock() + .await + .insert( + agent, + ManagedServer { + kind: ManagedServerKind::Stdio { server }, + child: child.clone(), + status: ServerStatus::Running, + start_time: Some(Instant::now()), + restart_count: 0, + last_error: None, + shutdown_requested: false, + instance_id, + }, + ); child } pub async fn insert_http_server(&self, agent: AgentId, instance_id: u64) { - self.session_manager.server_manager.servers.lock().await.insert( - agent, - ManagedServer { - kind: ManagedServerKind::Http { - base_url: "http://127.0.0.1:1".to_string(), + self.session_manager + .server_manager + .servers + .lock() + .await + .insert( + agent, + ManagedServer { + kind: ManagedServerKind::Http { + base_url: "http://127.0.0.1:1".to_string(), + }, + child: Arc::new(std::sync::Mutex::new(None)), + status: ServerStatus::Running, + start_time: Some(Instant::now()), + restart_count: 0, + last_error: None, + shutdown_requested: false, + instance_id, }, - child: Arc::new(std::sync::Mutex::new(None)), - status: ServerStatus::Running, - start_time: Some(Instant::now()), - restart_count: 0, - last_error: None, - shutdown_requested: false, - instance_id, - }, - ); + ); } pub async fn handle_process_exit( @@ -5236,7 +5289,12 @@ pub mod test_utils { fn default_log_dir() -> PathBuf { dirs::data_dir() .map(|dir| dir.join("sandbox-agent").join("logs").join("servers")) - .unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("logs").join("servers")) + .unwrap_or_else(|| { + PathBuf::from(".") + .join(".sandbox-agent") + .join("logs") + .join("servers") + }) } fn find_available_port() -> Result { @@ -5287,7 +5345,6 @@ impl SseAccumulator { } } - fn parse_opencode_modes(value: &Value) -> Vec { let mut modes = Vec::new(); let mut seen = HashSet::new(); diff --git a/server/packages/sandbox-agent/src/telemetry.rs b/server/packages/sandbox-agent/src/telemetry.rs index 74ba268..6ff221b 100644 --- a/server/packages/sandbox-agent/src/telemetry.rs +++ b/server/packages/sandbox-agent/src/telemetry.rs @@ -90,7 +90,8 @@ pub fn spawn_telemetry_task() { attempt_send(&client).await; let start = Instant::now() + Duration::from_secs(TELEMETRY_INTERVAL_SECS); - let mut interval = tokio::time::interval_at(start, Duration::from_secs(TELEMETRY_INTERVAL_SECS)); + let mut interval = + tokio::time::interval_at(start, Duration::from_secs(TELEMETRY_INTERVAL_SECS)); loop { interval.tick().await; attempt_send(&client).await; @@ -150,7 +151,12 @@ fn load_or_create_id() -> String { } } - if let Ok(mut file) = fs::OpenOptions::new().create(true).write(true).truncate(true).open(&path) { + if let Ok(mut file) = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + { let _ = file.write_all(id.as_bytes()); } id @@ -194,7 +200,12 @@ fn write_last_sent(timestamp: i64) { return; } } - if let Ok(mut file) = fs::OpenOptions::new().create(true).write(true).truncate(true).open(&path) { + if let Ok(mut file) = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + { let _ = file.write_all(timestamp.to_string().as_bytes()); } } @@ -266,7 +277,8 @@ fn detect_provider() -> ProviderInfo { }; } - if env::var("MODAL_IS_REMOTE").as_deref() == Ok("1") || env::var("MODAL_CLOUD_PROVIDER").is_ok() { + if env::var("MODAL_IS_REMOTE").as_deref() == Ok("1") || env::var("MODAL_CLOUD_PROVIDER").is_ok() + { let metadata = metadata_or_none([ ("cloudProvider", env::var("MODAL_CLOUD_PROVIDER").ok()), ("region", env::var("MODAL_REGION").ok()), @@ -395,7 +407,9 @@ fn detect_docker() -> bool { false } -fn filter_metadata(pairs: impl IntoIterator)>) -> HashMap { +fn filter_metadata( + pairs: impl IntoIterator)>, +) -> HashMap { let mut map = HashMap::new(); for (key, value) in pairs { if let Some(value) = value { @@ -407,7 +421,9 @@ fn filter_metadata(pairs: impl IntoIterator map } -fn metadata_or_none(pairs: impl IntoIterator)>) -> Option> { +fn metadata_or_none( + pairs: impl IntoIterator)>, +) -> Option> { let map = filter_metadata(pairs); if map.is_empty() { None diff --git a/server/packages/sandbox-agent/src/ui.rs b/server/packages/sandbox-agent/src/ui.rs index c1757d5..3bb475f 100644 --- a/server/packages/sandbox-agent/src/ui.rs +++ b/server/packages/sandbox-agent/src/ui.rs @@ -37,7 +37,11 @@ fn serve_path(path: &str) -> Response { }; let trimmed = path.trim_start_matches('/'); - let target = if trimmed.is_empty() { "index.html" } else { trimmed }; + let target = if trimmed.is_empty() { + "index.html" + } else { + trimmed + }; if let Some(file) = dir.get_file(target) { return file_response(file); diff --git a/server/packages/sandbox-agent/tests/agent-flows/agent_file_edit_flow.rs b/server/packages/sandbox-agent/tests/agent-flows/agent_file_edit_flow.rs index de6bb67..4f1d349 100644 --- a/server/packages/sandbox-agent/tests/agent-flows/agent_file_edit_flow.rs +++ b/server/packages/sandbox-agent/tests/agent-flows/agent_file_edit_flow.rs @@ -1,13 +1,13 @@ #[path = "../common/mod.rs"] mod common; +use axum::http::Method; use common::*; -use sandbox_agent_agent_management::testing::test_agents_from_env; use sandbox_agent_agent_management::agents::AgentId; +use sandbox_agent_agent_management::testing::test_agents_from_env; use serde_json::Value; use std::fs; use std::time::{Duration, Instant}; -use axum::http::Method; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agent_file_edit_flow() { @@ -77,9 +77,7 @@ Do not change any other files. Reply only with DONE after editing.", let _ = send_status( &app.app, Method::POST, - &format!( - "/v1/sessions/{session_id}/permissions/{permission_id}/reply" - ), + &format!("/v1/sessions/{session_id}/permissions/{permission_id}/reply"), Some(serde_json::json!({ "reply": "once" })), ) .await; diff --git a/server/packages/sandbox-agent/tests/agent-flows/agent_permission_flow.rs b/server/packages/sandbox-agent/tests/agent-flows/agent_permission_flow.rs index 4698d0c..d5711ef 100644 --- a/server/packages/sandbox-agent/tests/agent-flows/agent_permission_flow.rs +++ b/server/packages/sandbox-agent/tests/agent-flows/agent_permission_flow.rs @@ -1,11 +1,11 @@ #[path = "../common/mod.rs"] mod common; +use axum::http::Method; use common::*; use sandbox_agent_agent_management::testing::test_agents_from_env; -use std::time::Duration; -use axum::http::Method; use serde_json::json; +use std::time::Duration; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agent_permission_flow() { @@ -41,14 +41,20 @@ async fn agent_permission_flow() { Some(json!({ "reply": "once" })), ) .await; - assert_eq!(status, axum::http::StatusCode::NO_CONTENT, "permission reply"); + assert_eq!( + status, + axum::http::StatusCode::NO_CONTENT, + "permission reply" + ); - let resolved = poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| { - events.iter().any(|event| { - event.get("type").and_then(serde_json::Value::as_str) == Some("permission.resolved") + let resolved = + poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| { + events.iter().any(|event| { + event.get("type").and_then(serde_json::Value::as_str) + == Some("permission.resolved") + }) }) - }) - .await; + .await; assert!( resolved.iter().any(|event| { diff --git a/server/packages/sandbox-agent/tests/agent-flows/agent_question_flow.rs b/server/packages/sandbox-agent/tests/agent-flows/agent_question_flow.rs index e5a85b7..b3064ad 100644 --- a/server/packages/sandbox-agent/tests/agent-flows/agent_question_flow.rs +++ b/server/packages/sandbox-agent/tests/agent-flows/agent_question_flow.rs @@ -1,11 +1,11 @@ #[path = "../common/mod.rs"] mod common; +use axum::http::Method; use common::*; use sandbox_agent_agent_management::testing::test_agents_from_env; -use std::time::Duration; -use axum::http::Method; use serde_json::json; +use std::time::Duration; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agent_question_flow() { @@ -44,12 +44,14 @@ async fn agent_question_flow() { .await; assert_eq!(status, axum::http::StatusCode::NO_CONTENT, "question reply"); - let resolved = poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| { - events.iter().any(|event| { - event.get("type").and_then(serde_json::Value::as_str) == Some("question.resolved") + let resolved = + poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| { + events.iter().any(|event| { + event.get("type").and_then(serde_json::Value::as_str) + == Some("question.resolved") + }) }) - }) - .await; + .await; assert!( resolved.iter().any(|event| { diff --git a/server/packages/sandbox-agent/tests/agent-flows/agent_termination.rs b/server/packages/sandbox-agent/tests/agent-flows/agent_termination.rs index 961197a..0bff5f3 100644 --- a/server/packages/sandbox-agent/tests/agent-flows/agent_termination.rs +++ b/server/packages/sandbox-agent/tests/agent-flows/agent_termination.rs @@ -1,11 +1,11 @@ #[path = "../common/mod.rs"] mod common; +use axum::http::Method; use common::*; use sandbox_agent_agent_management::testing::test_agents_from_env; -use std::time::Duration; -use axum::http::Method; use serde_json::json; +use std::time::Duration; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agent_termination() { @@ -26,13 +26,20 @@ async fn agent_termination() { None, ) .await; - assert_eq!(status, axum::http::StatusCode::NO_CONTENT, "terminate session"); + assert_eq!( + status, + axum::http::StatusCode::NO_CONTENT, + "terminate session" + ); let events = poll_events_until(&app.app, &session_id, Duration::from_secs(30), |events| { has_event_type(events, "session.ended") }) .await; - assert!(has_event_type(&events, "session.ended"), "missing session.ended"); + assert!( + has_event_type(&events, "session.ended"), + "missing session.ended" + ); let status = send_status( &app.app, @@ -41,6 +48,9 @@ async fn agent_termination() { Some(json!({ "message": PROMPT })), ) .await; - assert!(!status.is_success(), "terminated session should reject messages"); + assert!( + !status.is_success(), + "terminated session should reject messages" + ); } } diff --git a/server/packages/sandbox-agent/tests/agent-flows/agent_tool_flow.rs b/server/packages/sandbox-agent/tests/agent-flows/agent_tool_flow.rs index 3674c34..66e38e0 100644 --- a/server/packages/sandbox-agent/tests/agent-flows/agent_tool_flow.rs +++ b/server/packages/sandbox-agent/tests/agent-flows/agent_tool_flow.rs @@ -1,11 +1,11 @@ #[path = "../common/mod.rs"] mod common; +use axum::http::Method; use common::*; use sandbox_agent_agent_management::testing::test_agents_from_env; use serde_json::Value; use std::time::{Duration, Instant}; -use axum::http::Method; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agent_tool_flow() { @@ -61,9 +61,7 @@ async fn agent_tool_flow() { let _ = send_status( &app.app, Method::POST, - &format!( - "/v1/sessions/{session_id}/permissions/{permission_id}/reply" - ), + &format!("/v1/sessions/{session_id}/permissions/{permission_id}/reply"), Some(serde_json::json!({ "reply": "once" })), ) .await; diff --git a/server/packages/sandbox-agent/tests/agent-management/agents.rs b/server/packages/sandbox-agent/tests/agent-management/agents.rs index 79aad4c..ab1e7ae 100644 --- a/server/packages/sandbox-agent/tests/agent-management/agents.rs +++ b/server/packages/sandbox-agent/tests/agent-management/agents.rs @@ -36,11 +36,19 @@ fn test_agents_install_version_spawn() -> Result<(), Box> let env = build_env(); assert!(!env.is_empty(), "expected credentials to be available"); - let agents = [AgentId::Claude, AgentId::Codex, AgentId::Opencode, AgentId::Amp]; + let agents = [ + AgentId::Claude, + AgentId::Codex, + AgentId::Opencode, + AgentId::Amp, + ]; for agent in agents { let install = manager.install(agent, InstallOptions::default())?; assert!(install.path.exists(), "expected install for {agent}"); - assert!(manager.is_installed(agent), "expected is_installed for {agent}"); + assert!( + manager.is_installed(agent), + "expected is_installed for {agent}" + ); manager.install( agent, InstallOptions { @@ -70,9 +78,15 @@ fn test_agents_install_version_spawn() -> Result<(), Box> ); let combined = format!("{}{}", result.stdout, result.stderr); let output = result.result.clone().unwrap_or(combined); - assert!(output.contains("OK"), "expected OK for {agent}, got: {output}"); + assert!( + output.contains("OK"), + "expected OK for {agent}, got: {output}" + ); - if agent == AgentId::Claude || agent == AgentId::Opencode || (agent == AgentId::Amp && amp_configured()) { + if agent == AgentId::Claude + || agent == AgentId::Opencode + || (agent == AgentId::Amp && amp_configured()) + { let mut resume = SpawnOptions::new(prompt_ok("OK2")); resume.env = env.clone(); resume.session_id = result.session_id.clone(); @@ -84,12 +98,17 @@ fn test_agents_install_version_spawn() -> Result<(), Box> ); let combined = format!("{}{}", resumed.stdout, resumed.stderr); let output = resumed.result.clone().unwrap_or(combined); - assert!(output.contains("OK2"), "expected OK2 for {agent}, got: {output}"); + assert!( + output.contains("OK2"), + "expected OK2 for {agent}, got: {output}" + ); } else if agent == AgentId::Codex { let mut resume = SpawnOptions::new(prompt_ok("OK2")); resume.env = env.clone(); resume.session_id = result.session_id.clone(); - let err = manager.spawn(agent, resume).expect_err("expected resume error for codex"); + let err = manager + .spawn(agent, resume) + .expect_err("expected resume error for codex"); assert!(matches!(err, AgentError::ResumeUnsupported { .. })); } @@ -105,7 +124,10 @@ fn test_agents_install_version_spawn() -> Result<(), Box> ); let combined = format!("{}{}", planned.stdout, planned.stderr); let output = planned.result.clone().unwrap_or(combined); - assert!(output.contains("OK3"), "expected OK3 for {agent}, got: {output}"); + assert!( + output.contains("OK3"), + "expected OK3 for {agent}, got: {output}" + ); } } } diff --git a/server/packages/sandbox-agent/tests/common/mod.rs b/server/packages/sandbox-agent/tests/common/mod.rs index 1d2be09..ea98b47 100644 --- a/server/packages/sandbox-agent/tests/common/mod.rs +++ b/server/packages/sandbox-agent/tests/common/mod.rs @@ -9,12 +9,7 @@ use serde_json::{json, Value}; use tempfile::TempDir; use tower::util::ServiceExt; -use sandbox_agent::router::{ - build_router, - AgentCapabilities, - AgentListResponse, - AuthConfig, -}; +use sandbox_agent::router::{build_router, AgentCapabilities, AgentListResponse, AuthConfig}; use sandbox_agent_agent_credentials::ExtractedCredentials; use sandbox_agent_agent_management::agents::{AgentId, AgentManager}; @@ -32,8 +27,7 @@ pub struct TestApp { impl TestApp { pub fn new() -> Self { let install_dir = tempfile::tempdir().expect("create temp install dir"); - let manager = AgentManager::new(install_dir.path()) - .expect("create agent manager"); + let manager = AgentManager::new(install_dir.path()).expect("create agent manager"); let state = sandbox_agent::router::AppState::new(AuthConfig::disabled(), manager); let app = build_router(state); Self { @@ -59,7 +53,12 @@ impl Drop for EnvGuard { } pub fn apply_credentials(creds: &ExtractedCredentials) -> EnvGuard { - let keys = ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"]; + let keys = [ + "ANTHROPIC_API_KEY", + "CLAUDE_API_KEY", + "OPENAI_API_KEY", + "CODEX_API_KEY", + ]; let mut saved = HashMap::new(); for key in keys { saved.insert(key.to_string(), std::env::var(key).ok()); @@ -100,13 +99,11 @@ pub async fn send_json( .method(method) .uri(path) .header("content-type", "application/json") - .body(Body::from(body.map(|value| value.to_string()).unwrap_or_default())) + .body(Body::from( + body.map(|value| value.to_string()).unwrap_or_default(), + )) .expect("request"); - let response = app - .clone() - .oneshot(request) - .await - .expect("response"); + let response = app.clone().oneshot(request).await.expect("response"); let status = response.status(); let bytes = response .into_body() @@ -140,15 +137,15 @@ pub async fn install_agent(app: &Router, agent: AgentId) { Some(json!({})), ) .await; - assert_eq!(status, StatusCode::NO_CONTENT, "install agent {}", agent.as_str()); + assert_eq!( + status, + StatusCode::NO_CONTENT, + "install agent {}", + agent.as_str() + ); } -pub async fn create_session( - app: &Router, - agent: AgentId, - session_id: &str, - permission_mode: &str, -) { +pub async fn create_session(app: &Router, agent: AgentId, session_id: &str, permission_mode: &str) { let status = send_status( app, Method::POST, diff --git a/server/packages/sandbox-agent/tests/server-manager/agent_server_manager.rs b/server/packages/sandbox-agent/tests/server-manager/agent_server_manager.rs index 7ef46a8..9b98e4a 100644 --- a/server/packages/sandbox-agent/tests/server-manager/agent_server_manager.rs +++ b/server/packages/sandbox-agent/tests/server-manager/agent_server_manager.rs @@ -28,11 +28,7 @@ async fn register_and_unregister_sessions() { .register_session(AgentId::Codex, "sess-1", Some("thread-1")) .await; - assert!( - harness - .has_session_mapping(AgentId::Codex, "sess-1") - .await - ); + assert!(harness.has_session_mapping(AgentId::Codex, "sess-1").await); assert_eq!( harness .native_mapping(AgentId::Codex, "thread-1") @@ -45,17 +41,11 @@ async fn register_and_unregister_sessions() { .unregister_session(AgentId::Codex, "sess-1", Some("thread-1")) .await; - assert!( - !harness - .has_session_mapping(AgentId::Codex, "sess-1") - .await - ); - assert!( - harness - .native_mapping(AgentId::Codex, "thread-1") - .await - .is_none() - ); + assert!(!harness.has_session_mapping(AgentId::Codex, "sess-1").await); + assert!(harness + .native_mapping(AgentId::Codex, "thread-1") + .await + .is_none()); } #[tokio::test] @@ -92,9 +82,7 @@ async fn handle_process_exit_marks_error_and_ends_sessions() { harness .register_session(AgentId::Codex, "sess-1", Some("thread-1")) .await; - harness - .insert_stdio_server(AgentId::Codex, None, 1) - .await; + harness.insert_stdio_server(AgentId::Codex, None, 1).await; harness .handle_process_exit(AgentId::Codex, 1, exit_status(7)) @@ -104,13 +92,11 @@ async fn handle_process_exit_marks_error_and_ends_sessions() { harness.server_status(AgentId::Codex).await, Some(sandbox_agent::router::ServerStatus::Error) )); - assert!( - harness - .server_last_error(AgentId::Codex) - .await - .unwrap_or_default() - .contains("exited") - ); + assert!(harness + .server_last_error(AgentId::Codex) + .await + .unwrap_or_default() + .contains("exited")); assert!(harness.session_ended("sess-1").await); assert!(matches!( harness.session_end_reason("sess-1").await, diff --git a/server/packages/sandbox-agent/tests/sessions/mod.rs b/server/packages/sandbox-agent/tests/sessions/mod.rs index a740914..c4ade0b 100644 --- a/server/packages/sandbox-agent/tests/sessions/mod.rs +++ b/server/packages/sandbox-agent/tests/sessions/mod.rs @@ -1,6 +1,6 @@ -mod session_lifecycle; mod multi_turn; mod permissions; mod questions; mod reasoning; +mod session_lifecycle; mod status; diff --git a/server/packages/sandbox-agent/tests/sessions/multi_turn.rs b/server/packages/sandbox-agent/tests/sessions/multi_turn.rs index 7cba8ba..2f6bf2d 100644 --- a/server/packages/sandbox-agent/tests/sessions/multi_turn.rs +++ b/server/packages/sandbox-agent/tests/sessions/multi_turn.rs @@ -81,8 +81,13 @@ async fn multi_turn_snapshots() { install_agent(&app.app, config.agent).await; let session_id = format!("multi-turn-{}", config.agent.as_str()); - create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent)) - .await; + create_session( + &app.app, + config.agent, + &session_id, + test_permission_mode(config.agent), + ) + .await; send_message_with_text(&app.app, &session_id, FIRST_PROMPT).await; let (first_events, offset) = @@ -100,13 +105,8 @@ async fn multi_turn_snapshots() { ); send_message_with_text(&app.app, &session_id, SECOND_PROMPT).await; - let (second_events, _offset) = poll_events_until_from( - &app.app, - &session_id, - offset, - Duration::from_secs(120), - ) - .await; + let (second_events, _offset) = + poll_events_until_from(&app.app, &session_id, offset, Duration::from_secs(120)).await; let second_events = truncate_after_first_stop(&second_events); assert!( !second_events.is_empty(), diff --git a/server/packages/sandbox-agent/tests/sessions/permissions.rs b/server/packages/sandbox-agent/tests/sessions/permissions.rs index 996522d..a114236 100644 --- a/server/packages/sandbox-agent/tests/sessions/permissions.rs +++ b/server/packages/sandbox-agent/tests/sessions/permissions.rs @@ -55,9 +55,7 @@ async fn permission_flow_snapshots() { let status = send_status( &app.app, Method::POST, - &format!( - "/v1/sessions/{permission_session}/permissions/{permission_id}/reply" - ), + &format!("/v1/sessions/{permission_session}/permissions/{permission_id}/reply"), Some(json!({ "reply": "once" })), ) .await; @@ -67,9 +65,7 @@ async fn permission_flow_snapshots() { let (status, payload) = send_json( &app.app, Method::POST, - &format!( - "/v1/sessions/{permission_session}/permissions/missing-permission/reply" - ), + &format!("/v1/sessions/{permission_session}/permissions/missing-permission/reply"), Some(json!({ "reply": "once" })), ) .await; diff --git a/server/packages/sandbox-agent/tests/sessions/questions.rs b/server/packages/sandbox-agent/tests/sessions/questions.rs index 889f0d4..db14fb1 100644 --- a/server/packages/sandbox-agent/tests/sessions/questions.rs +++ b/server/packages/sandbox-agent/tests/sessions/questions.rs @@ -55,9 +55,7 @@ async fn question_flow_snapshots() { let status = send_status( &app.app, Method::POST, - &format!( - "/v1/sessions/{question_reply_session}/questions/{question_id}/reply" - ), + &format!("/v1/sessions/{question_reply_session}/questions/{question_id}/reply"), Some(json!({ "answers": answers })), ) .await; @@ -67,9 +65,7 @@ async fn question_flow_snapshots() { let (status, payload) = send_json( &app.app, Method::POST, - &format!( - "/v1/sessions/{question_reply_session}/questions/missing-question/reply" - ), + &format!("/v1/sessions/{question_reply_session}/questions/missing-question/reply"), Some(json!({ "answers": [] })), ) .await; @@ -92,7 +88,11 @@ async fn question_flow_snapshots() { Some(json!({ "message": QUESTION_PROMPT })), ) .await; - assert_eq!(status, StatusCode::NO_CONTENT, "send question prompt reject"); + assert_eq!( + status, + StatusCode::NO_CONTENT, + "send question prompt reject" + ); let reject_events = poll_events_until_match( &app.app, @@ -108,9 +108,7 @@ async fn question_flow_snapshots() { let status = send_status( &app.app, Method::POST, - &format!( - "/v1/sessions/{question_reject_session}/questions/{question_id}/reject" - ), + &format!("/v1/sessions/{question_reject_session}/questions/{question_id}/reject"), None, ) .await; @@ -126,7 +124,10 @@ async fn question_flow_snapshots() { None, ) .await; - assert!(!status.is_success(), "missing question id reject should error"); + assert!( + !status.is_success(), + "missing question id reject should error" + ); assert_session_snapshot( "question_reject_missing", json!({ diff --git a/server/packages/sandbox-agent/tests/sessions/reasoning.rs b/server/packages/sandbox-agent/tests/sessions/reasoning.rs index 9be1919..a4f5acc 100644 --- a/server/packages/sandbox-agent/tests/sessions/reasoning.rs +++ b/server/packages/sandbox-agent/tests/sessions/reasoning.rs @@ -23,8 +23,13 @@ async fn reasoning_events_present() { install_agent(&app.app, config.agent).await; let session_id = format!("reasoning-{}", config.agent.as_str()); - create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent)) - .await; + create_session( + &app.app, + config.agent, + &session_id, + test_permission_mode(config.agent), + ) + .await; let status = send_status( &app.app, Method::POST, @@ -34,13 +39,11 @@ async fn reasoning_events_present() { .await; assert_eq!(status, StatusCode::NO_CONTENT, "send reasoning prompt"); - let events = poll_events_until_match( - &app.app, - &session_id, - Duration::from_secs(120), - |events| events_have_content_type(events, "reasoning") || events.iter().any(is_error_event), - ) - .await; + let events = + poll_events_until_match(&app.app, &session_id, Duration::from_secs(120), |events| { + events_have_content_type(events, "reasoning") || events.iter().any(is_error_event) + }) + .await; assert!( events_have_content_type(&events, "reasoning"), "expected reasoning content for {}", diff --git a/server/packages/sandbox-agent/tests/sessions/status.rs b/server/packages/sandbox-agent/tests/sessions/status.rs index ce2faae..cdd9b28 100644 --- a/server/packages/sandbox-agent/tests/sessions/status.rs +++ b/server/packages/sandbox-agent/tests/sessions/status.rs @@ -28,8 +28,13 @@ async fn status_events_present() { install_agent(&app.app, config.agent).await; let session_id = format!("status-{}", config.agent.as_str()); - create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent)) - .await; + create_session( + &app.app, + config.agent, + &session_id, + test_permission_mode(config.agent), + ) + .await; let status = send_status( &app.app, Method::POST, @@ -39,13 +44,11 @@ async fn status_events_present() { .await; assert_eq!(status, StatusCode::NO_CONTENT, "send status prompt"); - let events = poll_events_until_match( - &app.app, - &session_id, - Duration::from_secs(120), - |events| events_have_status(events) || events.iter().any(is_error_event), - ) - .await; + let events = + poll_events_until_match(&app.app, &session_id, Duration::from_secs(120), |events| { + events_have_status(events) || events.iter().any(is_error_event) + }) + .await; assert!( events_have_status(&events), "expected status events for {}", diff --git a/server/packages/sandbox-agent/tests/ui/inspector_ui.rs b/server/packages/sandbox-agent/tests/ui/inspector_ui.rs index f98a758..dd5e5b1 100644 --- a/server/packages/sandbox-agent/tests/ui/inspector_ui.rs +++ b/server/packages/sandbox-agent/tests/ui/inspector_ui.rs @@ -1,9 +1,9 @@ use axum::body::Body; use axum::http::{Request, StatusCode}; use http_body_util::BodyExt; -use sandbox_agent_agent_management::agents::AgentManager; use sandbox_agent::router::{build_router, AppState, AuthConfig}; use sandbox_agent::ui; +use sandbox_agent_agent_management::agents::AgentManager; use tempfile::TempDir; use tower::util::ServiceExt; @@ -22,10 +22,7 @@ async fn serves_inspector_ui() { .uri("/ui") .body(Body::empty()) .expect("build request"); - let response = app - .oneshot(request) - .await - .expect("request handled"); + let response = app.oneshot(request).await.expect("request handled"); assert_eq!(response.status(), StatusCode::OK); diff --git a/server/packages/universal-agent-schema/src/agents/amp.rs b/server/packages/universal-agent-schema/src/agents/amp.rs index ab96765..75326fc 100644 --- a/server/packages/universal-agent-schema/src/agents/amp.rs +++ b/server/packages/universal-agent-schema/src/agents/amp.rs @@ -4,20 +4,9 @@ use serde_json::Value; use crate::amp as schema; use crate::{ - ContentPart, - ErrorData, - EventConversion, - ItemDeltaData, - ItemEventData, - ItemKind, - ItemRole, - ItemStatus, - SessionEndedData, - SessionEndReason, - TerminatedBy, - UniversalEventData, - UniversalEventType, - UniversalItem, + ContentPart, ErrorData, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole, + ItemStatus, SessionEndReason, SessionEndedData, TerminatedBy, UniversalEventData, + UniversalEventType, UniversalItem, }; static TEMP_ID: AtomicU64 = AtomicU64::new(1); @@ -27,7 +16,9 @@ fn next_temp_id(prefix: &str) -> String { format!("{prefix}_{id}") } -pub fn event_to_universal(event: &schema::StreamJsonMessage) -> Result, String> { +pub fn event_to_universal( + event: &schema::StreamJsonMessage, +) -> Result, String> { let mut events = Vec::new(); match event.type_ { schema::StreamJsonMessageType::Message => { @@ -49,12 +40,17 @@ pub fn event_to_universal(event: &schema::StreamJsonMessage) -> Result text, schema::ToolCallArguments::Variant1(map) => { - serde_json::to_string(&Value::Object(map)).unwrap_or_else(|_| "{}".to_string()) + serde_json::to_string(&Value::Object(map)) + .unwrap_or_else(|_| "{}".to_string()) } }; (call.name, arguments, call.id) } else { - ("unknown".to_string(), "{}".to_string(), next_temp_id("tmp_amp_tool")) + ( + "unknown".to_string(), + "{}".to_string(), + next_temp_id("tmp_amp_tool"), + ) }; let item = UniversalItem { item_id: next_temp_id("tmp_amp_tool_call"), @@ -83,16 +79,16 @@ pub fn event_to_universal(event: &schema::StreamJsonMessage) -> Result { - let message = event.error.clone().unwrap_or_else(|| "amp error".to_string()); + let message = event + .error + .clone() + .unwrap_or_else(|| "amp error".to_string()); events.push(EventConversion::new( UniversalEventType::Error, UniversalEventData::Error(ErrorData { diff --git a/server/packages/universal-agent-schema/src/agents/claude.rs b/server/packages/universal-agent-schema/src/agents/claude.rs index 3a62285..5e5c7bc 100644 --- a/server/packages/universal-agent-schema/src/agents/claude.rs +++ b/server/packages/universal-agent-schema/src/agents/claude.rs @@ -3,21 +3,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; use serde_json::Value; use crate::{ - ContentPart, - EventConversion, - ItemEventData, - ItemDeltaData, - ItemKind, - ItemRole, - ItemStatus, - PermissionEventData, - PermissionStatus, - QuestionEventData, - QuestionStatus, - SessionStartedData, - UniversalEventData, - UniversalEventType, - UniversalItem, + ContentPart, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus, + PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, SessionStartedData, + UniversalEventData, UniversalEventType, UniversalItem, }; static TEMP_ID: AtomicU64 = AtomicU64::new(1); @@ -56,8 +44,11 @@ fn system_event_to_universal(event: &Value) -> EventConversion { let data = SessionStartedData { metadata: Some(event.clone()), }; - EventConversion::new(UniversalEventType::SessionStarted, UniversalEventData::SessionStarted(data)) - .with_raw(Some(event.clone())) + EventConversion::new( + UniversalEventType::SessionStarted, + UniversalEventData::SessionStarted(data), + ) + .with_raw(Some(event.clone())) } fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec { @@ -97,12 +88,15 @@ fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec Vec Vec Result, S .get("request") .and_then(Value::as_object) .ok_or_else(|| "missing request".to_string())?; - let subtype = request - .get("subtype") - .and_then(Value::as_str) - .unwrap_or(""); + let subtype = request.get("subtype").and_then(Value::as_str).unwrap_or(""); if subtype != "can_use_tool" { - return Err(format!("unsupported Claude control_request subtype: {subtype}")); + return Err(format!( + "unsupported Claude control_request subtype: {subtype}" + )); } let tool_name = request @@ -387,10 +378,7 @@ fn control_request_to_universal(event: &Value) -> Result, S .get("permission_suggestions") .cloned() .unwrap_or(Value::Null); - let blocked_path = request - .get("blocked_path") - .cloned() - .unwrap_or(Value::Null); + let blocked_path = request.get("blocked_path").cloned().unwrap_or(Value::Null); let metadata = serde_json::json!({ "toolName": tool_name, diff --git a/server/packages/universal-agent-schema/src/agents/codex.rs b/server/packages/universal-agent-schema/src/agents/codex.rs index 78dc917..470e406 100644 --- a/server/packages/universal-agent-schema/src/agents/codex.rs +++ b/server/packages/universal-agent-schema/src/agents/codex.rs @@ -2,22 +2,9 @@ use serde_json::Value; use crate::codex as schema; use crate::{ - ContentPart, - ErrorData, - EventConversion, - ItemDeltaData, - ItemEventData, - ItemKind, - ItemRole, - ItemStatus, - ReasoningVisibility, - SessionEndedData, - SessionEndReason, - SessionStartedData, - TerminatedBy, - UniversalEventData, - UniversalEventType, - UniversalItem, + ContentPart, ErrorData, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole, + ItemStatus, ReasoningVisibility, SessionEndReason, SessionEndedData, SessionStartedData, + TerminatedBy, UniversalEventData, UniversalEventType, UniversalItem, }; /// Convert a Codex ServerNotification to universal events. @@ -30,14 +17,12 @@ pub fn notification_to_universal( let data = SessionStartedData { metadata: serde_json::to_value(¶ms.thread).ok(), }; - Ok(vec![ - EventConversion::new( - UniversalEventType::SessionStarted, - UniversalEventData::SessionStarted(data), - ) - .with_native_session(Some(params.thread.id.clone())) - .with_raw(raw), - ]) + Ok(vec![EventConversion::new( + UniversalEventType::SessionStarted, + UniversalEventData::SessionStarted(data), + ) + .with_native_session(Some(params.thread.id.clone())) + .with_raw(raw)]) } schema::ServerNotification::ThreadCompacted(params) => Ok(vec![status_event( "thread.compacted", @@ -77,28 +62,24 @@ pub fn notification_to_universal( )]), schema::ServerNotification::ItemStarted(params) => { let item = thread_item_to_item(¶ms.item, ItemStatus::InProgress); - Ok(vec![ - EventConversion::new( - UniversalEventType::ItemStarted, - UniversalEventData::Item(ItemEventData { item }), - ) - .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]) + Ok(vec![EventConversion::new( + UniversalEventType::ItemStarted, + UniversalEventData::Item(ItemEventData { item }), + ) + .with_native_session(Some(params.thread_id.clone())) + .with_raw(raw)]) } schema::ServerNotification::ItemCompleted(params) => { let item = thread_item_to_item(¶ms.item, ItemStatus::Completed); - Ok(vec![ - EventConversion::new( - UniversalEventType::ItemCompleted, - UniversalEventData::Item(ItemEventData { item }), - ) - .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]) + Ok(vec![EventConversion::new( + UniversalEventType::ItemCompleted, + UniversalEventData::Item(ItemEventData { item }), + ) + .with_native_session(Some(params.thread_id.clone())) + .with_raw(raw)]) } - schema::ServerNotification::ItemAgentMessageDelta(params) => Ok(vec![ - EventConversion::new( + schema::ServerNotification::ItemAgentMessageDelta(params) => { + Ok(vec![EventConversion::new( UniversalEventType::ItemDelta, UniversalEventData::ItemDelta(ItemDeltaData { item_id: String::new(), @@ -107,10 +88,10 @@ pub fn notification_to_universal( }), ) .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]), - schema::ServerNotification::ItemReasoningTextDelta(params) => Ok(vec![ - EventConversion::new( + .with_raw(raw)]) + } + schema::ServerNotification::ItemReasoningTextDelta(params) => { + Ok(vec![EventConversion::new( UniversalEventType::ItemDelta, UniversalEventData::ItemDelta(ItemDeltaData { item_id: String::new(), @@ -119,10 +100,10 @@ pub fn notification_to_universal( }), ) .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]), - schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => Ok(vec![ - EventConversion::new( + .with_raw(raw)]) + } + schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => { + Ok(vec![EventConversion::new( UniversalEventType::ItemDelta, UniversalEventData::ItemDelta(ItemDeltaData { item_id: String::new(), @@ -131,10 +112,10 @@ pub fn notification_to_universal( }), ) .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]), - schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => Ok(vec![ - EventConversion::new( + .with_raw(raw)]) + } + schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => { + Ok(vec![EventConversion::new( UniversalEventType::ItemDelta, UniversalEventData::ItemDelta(ItemDeltaData { item_id: String::new(), @@ -143,10 +124,10 @@ pub fn notification_to_universal( }), ) .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]), - schema::ServerNotification::ItemFileChangeOutputDelta(params) => Ok(vec![ - EventConversion::new( + .with_raw(raw)]) + } + schema::ServerNotification::ItemFileChangeOutputDelta(params) => { + Ok(vec![EventConversion::new( UniversalEventType::ItemDelta, UniversalEventData::ItemDelta(ItemDeltaData { item_id: String::new(), @@ -155,10 +136,10 @@ pub fn notification_to_universal( }), ) .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]), - schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => Ok(vec![ - EventConversion::new( + .with_raw(raw)]) + } + schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => { + Ok(vec![EventConversion::new( UniversalEventType::ItemDelta, UniversalEventData::ItemDelta(ItemDeltaData { item_id: String::new(), @@ -167,33 +148,34 @@ pub fn notification_to_universal( }), ) .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]), + .with_raw(raw)]) + } schema::ServerNotification::ItemMcpToolCallProgress(params) => Ok(vec![status_event( "mcp.progress", serde_json::to_string(params).ok(), Some(params.thread_id.clone()), raw, )]), - schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => Ok(vec![ - status_event( + schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => { + Ok(vec![status_event( "reasoning.summary.part_added", serde_json::to_string(params).ok(), Some(params.thread_id.clone()), raw, - ), - ]), + )]) + } schema::ServerNotification::Error(params) => { let data = ErrorData { message: params.error.message.clone(), code: None, details: serde_json::to_value(params).ok(), }; - Ok(vec![ - EventConversion::new(UniversalEventType::Error, UniversalEventData::Error(data)) - .with_native_session(Some(params.thread_id.clone())) - .with_raw(raw), - ]) + Ok(vec![EventConversion::new( + UniversalEventType::Error, + UniversalEventData::Error(data), + ) + .with_native_session(Some(params.thread_id.clone())) + .with_raw(raw)]) } schema::ServerNotification::RawResponseItemCompleted(params) => Ok(vec![status_event( "raw.item.completed", @@ -239,7 +221,11 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers content: vec![ContentPart::Text { text: text.clone() }], status, }, - schema::ThreadItem::Reasoning { content, id, summary } => { + schema::ThreadItem::Reasoning { + content, + id, + summary, + } => { let mut parts = Vec::new(); for line in content { parts.push(ContentPart::Reasoning { @@ -295,7 +281,11 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers status, } } - schema::ThreadItem::FileChange { changes, id, status: file_status } => UniversalItem { + schema::ThreadItem::FileChange { + changes, + id, + status: file_status, + } => UniversalItem { item_id: String::new(), native_item_id: Some(id.clone()), parent_id: None, @@ -339,7 +329,8 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers output, }); } else { - let arguments = serde_json::to_string(arguments).unwrap_or_else(|_| "{}".to_string()); + let arguments = + serde_json::to_string(arguments).unwrap_or_else(|_| "{}".to_string()); parts.push(ContentPart::ToolCall { name: format!("{server}.{tool}"), arguments, @@ -433,18 +424,12 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers }], status, }, - schema::ThreadItem::EnteredReviewMode { id, review } => status_item_internal( - id, - "review.entered", - Some(review.clone()), - status, - ), - schema::ThreadItem::ExitedReviewMode { id, review } => status_item_internal( - id, - "review.exited", - Some(review.clone()), - status, - ), + schema::ThreadItem::EnteredReviewMode { id, review } => { + status_item_internal(id, "review.entered", Some(review.clone()), status) + } + schema::ThreadItem::ExitedReviewMode { id, review } => { + status_item_internal(id, "review.exited", Some(review.clone()), status) + } } } @@ -463,7 +448,12 @@ fn status_item(label: &str, detail: Option) -> UniversalItem { } } -fn status_item_internal(id: &str, label: &str, detail: Option, status: ItemStatus) -> UniversalItem { +fn status_item_internal( + id: &str, + label: &str, + detail: Option, + status: ItemStatus, +) -> UniversalItem { UniversalItem { item_id: String::new(), native_item_id: Some(id.to_string()), diff --git a/server/packages/universal-agent-schema/src/agents/opencode.rs b/server/packages/universal-agent-schema/src/agents/opencode.rs index b1bc3d9..ee0941c 100644 --- a/server/packages/universal-agent-schema/src/agents/opencode.rs +++ b/server/packages/universal-agent-schema/src/agents/opencode.rs @@ -2,46 +2,42 @@ use serde_json::Value; use crate::opencode as schema; use crate::{ - ContentPart, - EventConversion, - ItemDeltaData, - ItemEventData, - ItemKind, - ItemRole, - ItemStatus, - PermissionEventData, - PermissionStatus, - QuestionEventData, - QuestionStatus, - SessionStartedData, - UniversalEventData, - UniversalEventType, - UniversalItem, + ContentPart, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus, + PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, SessionStartedData, + UniversalEventData, UniversalEventType, UniversalItem, }; pub fn event_to_universal(event: &schema::Event) -> Result, String> { let raw = serde_json::to_value(event).ok(); match event { schema::Event::MessageUpdated(updated) => { - let schema::EventMessageUpdated { properties, type_: _ } = updated; + let schema::EventMessageUpdated { + properties, + type_: _, + } = updated; let schema::EventMessageUpdatedProperties { info } = properties; let (mut item, completed, session_id) = message_to_item(info); - item.status = if completed { ItemStatus::Completed } else { ItemStatus::InProgress }; + item.status = if completed { + ItemStatus::Completed + } else { + ItemStatus::InProgress + }; let event_type = if completed { UniversalEventType::ItemCompleted } else { UniversalEventType::ItemStarted }; - let conversion = EventConversion::new( - event_type, - UniversalEventData::Item(ItemEventData { item }), - ) - .with_native_session(session_id) - .with_raw(raw); + let conversion = + EventConversion::new(event_type, UniversalEventData::Item(ItemEventData { item })) + .with_native_session(session_id) + .with_raw(raw); Ok(vec![conversion]) } schema::Event::MessagePartUpdated(updated) => { - let schema::EventMessagePartUpdated { properties, type_: _ } = updated; + let schema::EventMessagePartUpdated { + properties, + type_: _, + } = updated; let schema::EventMessagePartUpdatedProperties { part, delta } = properties; let mut events = Vec::new(); let (session_id, message_id) = part_session_message(part); @@ -122,11 +118,16 @@ pub fn event_to_universal(event: &schema::Event) -> Result, schema::Part::Variant4(tool_part) => { let tool_events = tool_part_to_events(&tool_part, &message_id); for event in tool_events { - events.push(event.with_native_session(session_id.clone()).with_raw(raw.clone())); + events.push( + event + .with_native_session(session_id.clone()) + .with_raw(raw.clone()), + ); } } schema::Part::Variant1 { .. } => { - let detail = serde_json::to_string(part).unwrap_or_else(|_| "subtask".to_string()); + let detail = + serde_json::to_string(part).unwrap_or_else(|_| "subtask".to_string()); let item = status_item("subtask", Some(detail)); events.push( EventConversion::new( @@ -160,7 +161,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result, Ok(events) } schema::Event::QuestionAsked(asked) => { - let schema::EventQuestionAsked { properties, type_: _ } = asked; + let schema::EventQuestionAsked { + properties, + type_: _, + } = asked; let question = question_from_opencode(properties); let conversion = EventConversion::new( UniversalEventType::QuestionRequested, @@ -171,7 +175,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result, Ok(vec![conversion]) } schema::Event::PermissionAsked(asked) => { - let schema::EventPermissionAsked { properties, type_: _ } = asked; + let schema::EventPermissionAsked { + properties, + type_: _, + } = asked; let permission = permission_from_opencode(properties); let conversion = EventConversion::new( UniversalEventType::PermissionRequested, @@ -182,7 +189,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result, Ok(vec![conversion]) } schema::Event::SessionCreated(created) => { - let schema::EventSessionCreated { properties, type_: _ } = created; + let schema::EventSessionCreated { + properties, + type_: _, + } = created; let metadata = serde_json::to_value(&properties.info).ok(); let conversion = EventConversion::new( UniversalEventType::SessionStarted, @@ -193,8 +203,12 @@ pub fn event_to_universal(event: &schema::Event) -> Result, Ok(vec![conversion]) } schema::Event::SessionStatus(status) => { - let schema::EventSessionStatus { properties, type_: _ } = status; - let detail = serde_json::to_string(&properties.status).unwrap_or_else(|_| "status".to_string()); + let schema::EventSessionStatus { + properties, + type_: _, + } = status; + let detail = + serde_json::to_string(&properties.status).unwrap_or_else(|_| "status".to_string()); let item = status_item("session.status", Some(detail)); let conversion = EventConversion::new( UniversalEventType::ItemCompleted, @@ -205,7 +219,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result, Ok(vec![conversion]) } schema::Event::SessionIdle(idle) => { - let schema::EventSessionIdle { properties, type_: _ } = idle; + let schema::EventSessionIdle { + properties, + type_: _, + } = idle; let item = status_item("session.idle", None); let conversion = EventConversion::new( UniversalEventType::ItemCompleted, @@ -216,8 +233,12 @@ pub fn event_to_universal(event: &schema::Event) -> Result, Ok(vec![conversion]) } schema::Event::SessionError(error) => { - let schema::EventSessionError { properties, type_: _ } = error; - let detail = serde_json::to_string(&properties.error).unwrap_or_else(|_| "session error".to_string()); + let schema::EventSessionError { + properties, + type_: _, + } = error; + let detail = serde_json::to_string(&properties.error) + .unwrap_or_else(|_| "session error".to_string()); let item = status_item("session.error", Some(detail)); let conversion = EventConversion::new( UniversalEventType::ItemCompleted, @@ -270,7 +291,11 @@ fn message_to_item(message: &schema::Message) -> (UniversalItem, bool, Option (UniversalItem, bool, Option (Option, String) { match part { - schema::Part::Variant0(text_part) => { - (Some(text_part.session_id.clone()), text_part.message_id.clone()) - } + schema::Part::Variant0(text_part) => ( + Some(text_part.session_id.clone()), + text_part.message_id.clone(), + ), schema::Part::Variant1 { session_id, message_id, diff --git a/server/packages/universal-agent-schema/src/lib.rs b/server/packages/universal-agent-schema/src/lib.rs index 428e7a5..f4735f0 100644 --- a/server/packages/universal-agent-schema/src/lib.rs +++ b/server/packages/universal-agent-schema/src/lib.rs @@ -1,13 +1,16 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use schemars::JsonSchema; use utoipa::ToSchema; pub use sandbox_agent_extracted_agent_schemas::{amp, claude, codex, opencode}; pub mod agents; -pub use agents::{amp as convert_amp, claude as convert_claude, codex as convert_codex, opencode as convert_opencode}; +pub use agents::{ + amp as convert_amp, claude as convert_claude, codex as convert_codex, + opencode as convert_opencode, +}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct UniversalEvent { @@ -221,14 +224,38 @@ pub enum ItemStatus { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentPart { - Text { text: String }, - Json { json: Value }, - ToolCall { name: String, arguments: String, call_id: String }, - ToolResult { call_id: String, output: String }, - FileRef { path: String, action: FileAction, diff: Option }, - Reasoning { text: String, visibility: ReasoningVisibility }, - Image { path: String, mime: Option }, - Status { label: String, detail: Option }, + Text { + text: String, + }, + Json { + json: Value, + }, + ToolCall { + name: String, + arguments: String, + call_id: String, + }, + ToolResult { + call_id: String, + output: String, + }, + FileRef { + path: String, + action: FileAction, + diff: Option, + }, + Reasoning { + text: String, + visibility: ReasoningVisibility, + }, + Image { + path: String, + mime: Option, + }, + Status { + label: String, + detail: Option, + }, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]