diff --git a/README.md b/README.md
index c502b8b..adc9e0a 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,15 @@ curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
```
+Optional: preinstall agent binaries (no server required; they will be installed lazily on first use if you skip this):
+
+```bash
+sandbox-agent install-agent claude
+sandbox-agent install-agent codex
+sandbox-agent install-agent opencode
+sandbox-agent install-agent amp
+```
+
To disable auth locally:
```bash
@@ -147,14 +156,14 @@ npm install -g @sandbox-agent/cli
Create a session and send a message:
```bash
-sandbox-agent sessions create my-session --agent codex --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
-sandbox-agent sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
-sandbox-agent sessions send-message-stream my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
+sandbox-agent api sessions create my-session --agent codex --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
+sandbox-agent api sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
+sandbox-agent api sessions send-message-stream my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
```
Docs: https://rivet.dev/docs/cli
-### Extract credentials
+### Tip: Extract credentials
```bash
sandbox-agent credentials extract-env --export
diff --git a/docs/cli.mdx b/docs/cli.mdx
index 938a8e0..16cea87 100644
--- a/docs/cli.mdx
+++ b/docs/cli.mdx
@@ -3,7 +3,7 @@ title: "CLI"
description: "CLI reference and server flags."
---
-The `sandbox-agent` CLI mirrors the HTTP API so you can script everything without writing client code.
+The `sandbox-agent api` subcommand mirrors the HTTP API so you can script everything without writing client code.
## Server flags
@@ -17,39 +17,57 @@ sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
- `--cors-allow-origin`, `--cors-allow-method`, `--cors-allow-header`, `--cors-allow-credentials`: configure CORS.
- `--no-telemetry`: disable anonymous telemetry.
-## Agent commands
+## Install agent (no server required)
-agents list
+install-agent
```bash
-sandbox-agent agents list --endpoint http://127.0.0.1:2468
+sandbox-agent install-agent claude --reinstall
+```
+
+
+## API agent commands
+
+
+api agents list
+
+```bash
+sandbox-agent api agents list --endpoint http://127.0.0.1:2468
```
-agents install
+api agents install
```bash
-sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:2468
+sandbox-agent api agents install claude --reinstall --endpoint http://127.0.0.1:2468
```
-agents modes
+api agents modes
```bash
-sandbox-agent agents modes claude --endpoint http://127.0.0.1:2468
+sandbox-agent api agents modes claude --endpoint http://127.0.0.1:2468
```
-## Session commands
+## API session commands
-sessions create
+api sessions list
```bash
-sandbox-agent sessions create my-session \
+sandbox-agent api sessions list --endpoint http://127.0.0.1:2468
+```
+
+
+
+api sessions create
+
+```bash
+sandbox-agent api sessions create my-session \
--agent claude \
--agent-mode build \
--permission-mode default \
@@ -58,64 +76,64 @@ sandbox-agent sessions create my-session \
-sessions send-message
+api sessions send-message
```bash
-sandbox-agent sessions send-message my-session \
+sandbox-agent api sessions send-message my-session \
--message "Summarize the repository" \
--endpoint http://127.0.0.1:2468
```
-sessions send-message-stream
+api sessions send-message-stream
```bash
-sandbox-agent sessions send-message-stream my-session \
+sandbox-agent api sessions send-message-stream my-session \
--message "Summarize the repository" \
--endpoint http://127.0.0.1:2468
```
-sessions events
+api sessions events
```bash
-sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468
+sandbox-agent api sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468
```
-sessions events-sse
+api sessions events-sse
```bash
-sandbox-agent sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468
+sandbox-agent api sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468
```
-sessions reply-question
+api sessions reply-question
```bash
-sandbox-agent sessions reply-question my-session QUESTION_ID \
+sandbox-agent api sessions reply-question my-session QUESTION_ID \
--answers "yes" \
--endpoint http://127.0.0.1:2468
```
-sessions reject-question
+api sessions reject-question
```bash
-sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468
+sandbox-agent api sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468
```
-sessions reply-permission
+api sessions reply-permission
```bash
-sandbox-agent sessions reply-permission my-session PERMISSION_ID \
+sandbox-agent api sessions reply-permission my-session PERMISSION_ID \
--reply once \
--endpoint http://127.0.0.1:2468
```
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index 435ab0a..9162441 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -38,7 +38,18 @@ sandbox-agent server \
--cors-allow-credentials
```
-## 2. Create a session
+## 2. Install agents (optional)
+
+Agents install lazily on first use. To preinstall everything up front:
+
+```bash
+sandbox-agent install-agent claude
+sandbox-agent install-agent codex
+sandbox-agent install-agent opencode
+sandbox-agent install-agent amp
+```
+
+## 3. Create a session
```bash
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session" \
@@ -47,7 +58,7 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session" \
-d '{"agent":"claude","agentMode":"build","permissionMode":"default"}'
```
-## 3. Send a message
+## 4. Send a message
```bash
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
@@ -56,7 +67,7 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
-d '{"message":"Summarize the repository and suggest next steps."}'
```
-## 4. Read events
+## 5. Read events
```bash
curl "http://127.0.0.1:2468/v1/sessions/my-session/events?offset=0&limit=50" \
@@ -79,14 +90,14 @@ curl -N -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages/stream" \
-d '{"message":"Hello"}'
```
-## 5. CLI shortcuts
+## 6. CLI shortcuts
-The CLI mirrors the HTTP API:
+The `sandbox-agent api` subcommand mirrors the HTTP API:
```bash
-sandbox-agent sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
+sandbox-agent api sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
-sandbox-agent sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
+sandbox-agent api sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
-sandbox-agent sessions send-message-stream my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
+sandbox-agent api sessions send-message-stream my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
```
diff --git a/examples/daytona/daytona.ts b/examples/daytona/daytona.ts
deleted file mode 100644
index aba4e53..0000000
--- a/examples/daytona/daytona.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Daytona } from "@daytonaio/sdk";
-import { pathToFileURL, fileURLToPath } from "node:url";
-import { dirname, resolve } from "node:path";
-import {
- ensureUrl,
- logInspectorUrl,
- runPrompt,
- waitForHealth,
-} from "@sandbox-agent/example-shared";
-
-const DEFAULT_PORT = 3000;
-const BINARY_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "../../target/release/sandbox-agent");
-
-export async function setupDaytonaSandboxAgent(): Promise<{
- baseUrl: string;
- token: string;
- extraHeaders: Record;
- cleanup: () => Promise;
-}> {
- const token = process.env.SANDBOX_TOKEN || "";
- const port = Number.parseInt(process.env.SANDBOX_PORT || "", 10) || DEFAULT_PORT;
- const language = process.env.DAYTONA_LANGUAGE || "typescript";
-
- const daytona = new Daytona();
- console.log("Creating sandbox...");
- const sandbox = await daytona.create({ language });
-
- // Daytona sandboxes can't reach releases.rivet.dev, so upload binary directly
- console.log("Uploading sandbox-agent...");
- await sandbox.fs.uploadFile(BINARY_PATH, "/home/daytona/sandbox-agent");
- await sandbox.process.executeCommand("chmod +x /home/daytona/sandbox-agent");
-
- console.log("Starting server...");
- const tokenFlag = token ? `--token ${token}` : "--no-token";
- await sandbox.process.executeCommand(
- `nohup /home/daytona/sandbox-agent server ${tokenFlag} --host 0.0.0.0 --port ${port} >/tmp/sandbox-agent.log 2>&1 &`
- );
-
- const preview = await sandbox.getPreviewLink(port);
- const extraHeaders: Record = {
- "x-daytona-skip-preview-warning": "true",
- };
- if (preview.token) {
- extraHeaders["x-daytona-preview-token"] = preview.token;
- }
-
- const baseUrl = ensureUrl(preview.url);
- console.log("Waiting for health...");
- await waitForHealth({ baseUrl, token, extraHeaders });
- logInspectorUrl({ baseUrl, token });
-
- return {
- baseUrl,
- token,
- extraHeaders,
- cleanup: async () => {
- try { await sandbox.delete(60); } catch {}
- },
- };
-}
-
-async function main(): Promise {
- const { baseUrl, token, extraHeaders, cleanup } = await setupDaytonaSandboxAgent();
-
- process.on("SIGINT", () => void cleanup().then(() => process.exit(0)));
- process.on("SIGTERM", () => void cleanup().then(() => process.exit(0)));
-
- await runPrompt({ baseUrl, token, extraHeaders });
-}
-
-if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
- main().catch((error) => {
- console.error(error);
- process.exit(1);
- });
-}
diff --git a/examples/daytona/package.json b/examples/daytona/package.json
index 8bd1404..ea9f37a 100644
--- a/examples/daytona/package.json
+++ b/examples/daytona/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "start": "tsx daytona.ts"
+ "start": "tsx src/daytona.ts"
},
"dependencies": {
"@daytonaio/sdk": "latest",
diff --git a/examples/daytona/daytona.test.ts b/examples/daytona/src/daytona.test.ts
similarity index 100%
rename from examples/daytona/daytona.test.ts
rename to examples/daytona/src/daytona.test.ts
diff --git a/examples/daytona/src/daytona.ts b/examples/daytona/src/daytona.ts
new file mode 100644
index 0000000..9e5ed36
--- /dev/null
+++ b/examples/daytona/src/daytona.ts
@@ -0,0 +1,65 @@
+import { Daytona, Image } from "@daytonaio/sdk";
+import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared";
+
+if (
+ !process.env.DAYTONA_API_KEY ||
+ (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY)
+) {
+ throw new Error(
+ "DAYTONA_API_KEY and (OPENAI_API_KEY or ANTHROPIC_API_KEY) required",
+ );
+}
+
+const SNAPSHOT = "sandbox-agent-ready";
+const BINARY = "/usr/local/bin/sandbox-agent";
+
+const daytona = new Daytona();
+
+const hasSnapshot = await daytona.snapshot.get(SNAPSHOT).then(
+ () => true,
+ () => false,
+);
+if (!hasSnapshot) {
+ console.log(`Creating snapshot '${SNAPSHOT}' (one-time setup, ~1-2min)...`);
+ await daytona.snapshot.create(
+ {
+ name: SNAPSHOT,
+ image: Image.base("ubuntu:22.04").runCommands(
+ "apt-get update && apt-get install -y curl ca-certificates",
+ `curl -fsSL -o ${BINARY} https://releases.rivet.dev/sandbox-agent/latest/binaries/sandbox-agent-x86_64-unknown-linux-musl`,
+ `chmod +x ${BINARY}`,
+ ),
+ },
+ { onLogs: (log) => console.log(` ${log}`) },
+ );
+ console.log("Snapshot created. Future runs will be instant.");
+}
+
+console.log("Creating sandbox...");
+const sandbox = await daytona.create({
+ snapshot: SNAPSHOT,
+ envVars: {
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
+ },
+ autoStopInterval: 0,
+});
+
+console.log("Starting server...");
+await sandbox.process.executeCommand(
+ `nohup ${BINARY} server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &`,
+);
+
+const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
+logInspectorUrl({ baseUrl });
+
+const cleanup = async () => {
+ console.log("Cleaning up...");
+ await sandbox.delete(60);
+ process.exit(0);
+};
+process.once("SIGINT", cleanup);
+process.once("SIGTERM", cleanup);
+
+await runPrompt({ baseUrl });
+await cleanup();
diff --git a/examples/docker/package.json b/examples/docker/package.json
index f3eb6bd..28d126d 100644
--- a/examples/docker/package.json
+++ b/examples/docker/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "start": "tsx docker.ts"
+ "start": "tsx src/docker.ts"
},
"dependencies": {
"dockerode": "latest",
diff --git a/examples/docker/docker.test.ts b/examples/docker/src/docker.test.ts
similarity index 100%
rename from examples/docker/docker.test.ts
rename to examples/docker/src/docker.test.ts
diff --git a/examples/docker/docker.ts b/examples/docker/src/docker.ts
similarity index 100%
rename from examples/docker/docker.ts
rename to examples/docker/src/docker.ts
diff --git a/examples/e2b/package.json b/examples/e2b/package.json
index 5db786f..001851c 100644
--- a/examples/e2b/package.json
+++ b/examples/e2b/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "start": "tsx e2b.ts"
+ "start": "tsx src/e2b.ts"
},
"dependencies": {
"@e2b/code-interpreter": "latest",
diff --git a/examples/e2b/e2b.test.ts b/examples/e2b/src/e2b.test.ts
similarity index 100%
rename from examples/e2b/e2b.test.ts
rename to examples/e2b/src/e2b.test.ts
diff --git a/examples/e2b/e2b.ts b/examples/e2b/src/e2b.ts
similarity index 100%
rename from examples/e2b/e2b.ts
rename to examples/e2b/src/e2b.ts
diff --git a/examples/shared/package.json b/examples/shared/package.json
index f3b54c0..30a9ff5 100644
--- a/examples/shared/package.json
+++ b/examples/shared/package.json
@@ -3,6 +3,6 @@
"private": true,
"type": "module",
"exports": {
- ".": "./sandbox-agent-client.ts"
+ ".": "./src/sandbox-agent-client.ts"
}
}
diff --git a/examples/shared/sandbox-agent-client.ts b/examples/shared/src/sandbox-agent-client.ts
similarity index 75%
rename from examples/shared/sandbox-agent-client.ts
rename to examples/shared/src/sandbox-agent-client.ts
index 9f5423a..2fad2a0 100644
--- a/examples/shared/sandbox-agent-client.ts
+++ b/examples/shared/src/sandbox-agent-client.ts
@@ -140,7 +140,7 @@ export async function createSession({
const normalized = normalizeBaseUrl(baseUrl);
const sessionId = randomUUID();
const body: Record = {
- agent: agentId || process.env.SANDBOX_AGENT || "codex",
+ agent: agentId || process.env.SANDBOX_AGENT || "claude",
};
const envAgentMode = agentMode || process.env.SANDBOX_AGENT_MODE;
const envPermissionMode = permissionMode || process.env.SANDBOX_PERMISSION_MODE;
@@ -163,28 +163,6 @@ export async function createSession({
return sessionId;
}
-export async function sendMessage({
- baseUrl,
- token,
- extraHeaders,
- sessionId,
- message,
-}: {
- baseUrl: string;
- token?: string;
- extraHeaders?: Record;
- sessionId: string;
- message: string;
-}): Promise {
- const normalized = normalizeBaseUrl(baseUrl);
- await fetchJson(`${normalized}/v1/sessions/${sessionId}/messages`, {
- token,
- extraHeaders,
- method: "POST",
- body: { message },
- });
-}
-
function extractTextFromItem(item: any): string {
if (!item?.content) return "";
const textParts = item.content
@@ -197,53 +175,81 @@ function extractTextFromItem(item: any): string {
return JSON.stringify(item.content, null, 2);
}
-export async function waitForAssistantComplete({
+export async function sendMessageStream({
baseUrl,
token,
extraHeaders,
sessionId,
- offset = 0,
- timeoutMs = 120_000,
+ message,
+ onText,
}: {
baseUrl: string;
token?: string;
extraHeaders?: Record;
sessionId: string;
- offset?: number;
- timeoutMs?: number;
-}): Promise<{ text: string; offset: number }> {
+ message: string;
+ onText?: (text: string) => void;
+}): Promise {
const normalized = normalizeBaseUrl(baseUrl);
- const deadline = Date.now() + timeoutMs;
- let currentOffset = offset;
+ const headers = buildHeaders({ token, extraHeaders, contentType: true });
- while (Date.now() < deadline) {
- const data = await fetchJson(
- `${normalized}/v1/sessions/${sessionId}/events?offset=${currentOffset}&limit=100`,
- { token, extraHeaders }
- );
+ const response = await fetch(`${normalized}/v1/sessions/${sessionId}/messages/stream`, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({ message }),
+ });
- for (const event of data.events || []) {
- if (typeof event.sequence === "number") {
- currentOffset = Math.max(currentOffset, event.sequence);
+ if (!response.ok || !response.body) {
+ const text = await response.text();
+ throw new Error(`HTTP ${response.status} ${response.statusText}: ${text}`);
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = "";
+ let fullText = "";
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split("\n");
+ buffer = lines.pop() || "";
+
+ for (const line of lines) {
+ if (!line.startsWith("data: ")) continue;
+ const data = line.slice(6);
+ if (data === "[DONE]") continue;
+
+ try {
+ const event = JSON.parse(data);
+
+ // Handle text deltas
+ if (event.type === "item.delta" && event.data?.delta?.type === "text") {
+ const text = event.data.delta.text || "";
+ fullText += text;
+ onText?.(text);
+ }
+
+ // Handle completed assistant message
+ if (
+ event.type === "item.completed" &&
+ event.data?.item?.kind === "message" &&
+ event.data?.item?.role === "assistant"
+ ) {
+ const itemText = extractTextFromItem(event.data.item);
+ if (itemText && !fullText) {
+ fullText = itemText;
+ }
+ }
+ } catch {
+ // Ignore parse errors
}
- if (
- event.type === "item.completed" &&
- event.data?.item?.kind === "message" &&
- event.data?.item?.role === "assistant"
- ) {
- return {
- text: extractTextFromItem(event.data.item),
- offset: currentOffset,
- };
- }
- }
-
- if (!data.hasMore) {
- await delay(300);
}
}
- throw new Error("Timed out waiting for assistant response");
+ return fullText;
}
export async function runPrompt({
@@ -258,7 +264,6 @@ export async function runPrompt({
agentId?: string;
}): Promise {
const sessionId = await createSession({ baseUrl, token, extraHeaders, agentId });
- let offset = 0;
console.log(`Session ${sessionId} ready. Type /exit to quit.`);
@@ -280,16 +285,15 @@ export async function runPrompt({
}
try {
- await sendMessage({ baseUrl, token, extraHeaders, sessionId, message: trimmed });
- const result = await waitForAssistantComplete({
+ await sendMessageStream({
baseUrl,
token,
extraHeaders,
sessionId,
- offset,
+ message: trimmed,
+ onText: (text) => process.stdout.write(text),
});
- offset = result.offset;
- process.stdout.write(`${result.text}\n`);
+ process.stdout.write("\n");
} catch (error) {
console.error(error instanceof Error ? error.message : error);
}
diff --git a/examples/vercel/package.json b/examples/vercel/package.json
index 0c50a97..b89c3c8 100644
--- a/examples/vercel/package.json
+++ b/examples/vercel/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "start": "tsx vercel-sandbox.ts"
+ "start": "tsx src/vercel-sandbox.ts"
},
"dependencies": {
"@vercel/sandbox": "latest",
diff --git a/examples/vercel/vercel-sandbox.test.ts b/examples/vercel/src/vercel-sandbox.test.ts
similarity index 100%
rename from examples/vercel/vercel-sandbox.test.ts
rename to examples/vercel/src/vercel-sandbox.test.ts
diff --git a/examples/vercel/vercel-sandbox.ts b/examples/vercel/src/vercel-sandbox.ts
similarity index 100%
rename from examples/vercel/vercel-sandbox.ts
rename to examples/vercel/src/vercel-sandbox.ts
diff --git a/server/packages/extracted-agent-schemas/build.rs b/server/packages/extracted-agent-schemas/build.rs
index 6ab8f0c..cdf328b 100644
--- a/server/packages/extracted-agent-schemas/build.rs
+++ b/server/packages/extracted-agent-schemas/build.rs
@@ -56,10 +56,10 @@ fn main() {
fs::write(&out_path, formatted)
.unwrap_or_else(|e| panic!("Failed to write {}: {}", out_path.display(), e));
- emit_stdout(&format!(
- "cargo:warning=Generated {} types from {}",
- name, file
- ));
+ // emit_stdout(&format!(
+ // "cargo:warning=Generated {} types from {}",
+ // name, file
+ // ));
}
}
diff --git a/server/packages/sandbox-agent/src/main.rs b/server/packages/sandbox-agent/src/main.rs
index fea4e6a..072063f 100644
--- a/server/packages/sandbox-agent/src/main.rs
+++ b/server/packages/sandbox-agent/src/main.rs
@@ -6,7 +6,7 @@ use std::sync::Arc;
use clap::{Args, Parser, Subcommand};
use reqwest::blocking::Client as HttpClient;
use reqwest::Method;
-use sandbox_agent_agent_management::agents::AgentManager;
+use sandbox_agent_agent_management::agents::{AgentId, AgentManager, InstallOptions};
use sandbox_agent_agent_management::credentials::{
extract_all_credentials, AuthType, CredentialExtractionOptions, ExtractedCredentials,
ProviderCredentials,
@@ -16,7 +16,9 @@ use sandbox_agent::router::{
PermissionReply, PermissionReplyRequest, QuestionReplyRequest,
};
use sandbox_agent::telemetry;
-use sandbox_agent::router::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse};
+use sandbox_agent::router::{
+ AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse, SessionListResponse,
+};
use sandbox_agent::router::{build_router_with_state, shutdown_servers};
use sandbox_agent::ui;
use serde::Serialize;
@@ -47,10 +49,10 @@ struct Cli {
enum Command {
/// Run the sandbox agent HTTP server.
Server(ServerArgs),
- /// Manage installed agents and their modes.
- Agents(AgentsArgs),
- /// Create sessions and interact with session events.
- Sessions(SessionsArgs),
+ /// Call the HTTP API without writing client code.
+ Api(ApiArgs),
+ /// Install or reinstall an agent without running the server.
+ InstallAgent(InstallAgentArgs),
/// Inspect locally discovered credentials.
Credentials(CredentialsArgs),
}
@@ -80,15 +82,9 @@ struct ServerArgs {
}
#[derive(Args, Debug)]
-struct AgentsArgs {
+struct ApiArgs {
#[command(subcommand)]
- command: AgentsCommand,
-}
-
-#[derive(Args, Debug)]
-struct SessionsArgs {
- #[command(subcommand)]
- command: SessionsCommand,
+ command: ApiCommand,
}
#[derive(Args, Debug)]
@@ -98,13 +94,11 @@ struct CredentialsArgs {
}
#[derive(Subcommand, Debug)]
-enum AgentsCommand {
- /// List all agents and install status.
- List(ClientArgs),
- /// Install or reinstall an agent.
- Install(InstallAgentArgs),
- /// Show available modes for an agent.
- Modes(AgentModesArgs),
+enum ApiCommand {
+ /// Manage installed agents and their modes.
+ Agents(AgentsArgs),
+ /// Create sessions and interact with session events.
+ Sessions(SessionsArgs),
}
#[derive(Subcommand, Debug)]
@@ -116,8 +110,32 @@ enum CredentialsCommand {
ExtractEnv(CredentialsExtractEnvArgs),
}
+#[derive(Args, Debug)]
+struct AgentsArgs {
+ #[command(subcommand)]
+ command: AgentsCommand,
+}
+
+#[derive(Args, Debug)]
+struct SessionsArgs {
+ #[command(subcommand)]
+ command: SessionsCommand,
+}
+
+#[derive(Subcommand, Debug)]
+enum AgentsCommand {
+ /// List all agents and install status.
+ List(ClientArgs),
+ /// Install or reinstall an agent.
+ Install(ApiInstallAgentArgs),
+ /// Show available modes for an agent.
+ Modes(AgentModesArgs),
+}
+
#[derive(Subcommand, Debug)]
enum SessionsCommand {
+ /// List active sessions.
+ List(ClientArgs),
/// Create a new session for an agent.
Create(CreateSessionArgs),
#[command(name = "send-message")]
@@ -156,7 +174,7 @@ struct ClientArgs {
}
#[derive(Args, Debug)]
-struct InstallAgentArgs {
+struct ApiInstallAgentArgs {
agent: String,
#[arg(long, short = 'r')]
reinstall: bool,
@@ -164,6 +182,13 @@ struct InstallAgentArgs {
client: ClientArgs,
}
+#[derive(Args, Debug)]
+struct InstallAgentArgs {
+ agent: String,
+ #[arg(long, short = 'r')]
+ reinstall: bool,
+}
+
#[derive(Args, Debug)]
struct AgentModesArgs {
agent: String,
@@ -277,7 +302,7 @@ struct CredentialsExtractArgs {
provider: Option,
#[arg(long, short = 'd')]
home_dir: Option,
- #[arg(long, short = 'n')]
+ #[arg(long)]
no_oauth: bool,
#[arg(long, short = 'r')]
reveal: bool,
@@ -290,7 +315,7 @@ struct CredentialsExtractEnvArgs {
export: bool,
#[arg(long, short = 'd')]
home_dir: Option,
- #[arg(long, short = 'n')]
+ #[arg(long)]
no_oauth: bool,
}
@@ -407,12 +432,19 @@ fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
Command::Server(_) => Err(CliError::Server(
"server subcommand must be invoked as `sandbox-agent server`".to_string(),
)),
- Command::Agents(subcommand) => run_agents(&subcommand.command, cli),
- Command::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
+ Command::Api(subcommand) => run_api(&subcommand.command, cli),
+ Command::InstallAgent(args) => install_agent_local(args),
Command::Credentials(subcommand) => run_credentials(&subcommand.command),
}
}
+fn run_api(command: &ApiCommand, cli: &Cli) -> Result<(), CliError> {
+ match command {
+ ApiCommand::Agents(subcommand) => run_agents(&subcommand.command, cli),
+ ApiCommand::Sessions(subcommand) => run_sessions(&subcommand.command, cli),
+ }
+}
+
fn run_agents(command: &AgentsCommand, cli: &Cli) -> Result<(), CliError> {
match command {
AgentsCommand::List(args) => {
@@ -440,6 +472,11 @@ fn run_agents(command: &AgentsCommand, cli: &Cli) -> Result<(), CliError> {
fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
match command {
+ SessionsCommand::List(args) => {
+ let ctx = ClientContext::new(cli, args)?;
+ let response = ctx.get(&format!("{API_PREFIX}/sessions"))?;
+ print_json_response::(response)
+ }
SessionsCommand::Create(args) => {
let ctx = ClientContext::new(cli, &args.client)?;
let body = CreateSessionRequest {
@@ -674,6 +711,24 @@ fn redact_key(key: &str) -> String {
format!("{prefix}...{suffix}")
}
+fn install_agent_local(args: &InstallAgentArgs) -> Result<(), CliError> {
+ let agent_id = AgentId::parse(&args.agent).ok_or_else(|| {
+ CliError::Server(format!("unsupported agent: {}", args.agent))
+ })?;
+ let manager =
+ AgentManager::new(default_install_dir()).map_err(|err| CliError::Server(err.to_string()))?;
+ manager
+ .install(
+ agent_id,
+ InstallOptions {
+ reinstall: args.reinstall,
+ version: None,
+ },
+ )
+ .map_err(|err| CliError::Server(err.to_string()))?;
+ Ok(())
+}
+
fn select_token_for_agent(
credentials: &ExtractedCredentials,
agent: CredentialAgent,
diff --git a/server/packages/sandbox-agent/src/router.rs b/server/packages/sandbox-agent/src/router.rs
index 520c648..e967c69 100644
--- a/server/packages/sandbox-agent/src/router.rs
+++ b/server/packages/sandbox-agent/src/router.rs
@@ -2329,47 +2329,63 @@ impl SessionManager {
}
};
let event_type = value.get("type").and_then(Value::as_str).unwrap_or("");
- if event_type == "assistant" {
+ let native_session_id = value
+ .get("session_id")
+ .and_then(Value::as_str)
+ .or_else(|| value.get("sessionId").and_then(Value::as_str))
+ .map(|id| id.to_string());
+ if event_type == "assistant" || event_type == "result" || native_session_id.is_some() {
let mut sessions = self.sessions.lock().await;
if let Some(session) = Self::session_mut(&mut sessions, session_id) {
- let id = value
- .get("message")
- .and_then(|message| message.get("id"))
- .and_then(Value::as_str)
- .map(|id| id.to_string())
- .unwrap_or_else(|| {
- session.claude_message_counter += 1;
- let generated =
- format!("{}_message_{}", session.session_id, session.claude_message_counter);
- if let Some(message) = value.get_mut("message").and_then(Value::as_object_mut)
- {
- message.insert("id".to_string(), Value::String(generated.clone()));
- } else if let Some(map) = value.as_object_mut() {
- map.insert(
- "message".to_string(),
- serde_json::json!({
- "id": generated
- }),
- );
- }
- generated
- });
- session.last_claude_message_id = Some(id);
- }
- } else if event_type == "result" {
- let has_message_id = value.get("message_id").is_some() || value.get("messageId").is_some();
- let mut sessions = self.sessions.lock().await;
- if let Some(session) = Self::session_mut(&mut sessions, session_id) {
- if !has_message_id {
- let id = session.last_claude_message_id.take().unwrap_or_else(|| {
- session.claude_message_counter += 1;
- format!("{}_message_{}", session.session_id, session.claude_message_counter)
- });
- if let Some(map) = value.as_object_mut() {
- map.insert("message_id".to_string(), Value::String(id));
+ if let Some(native_session_id) = native_session_id.as_ref() {
+ if session.native_session_id.is_none() {
+ session.native_session_id = Some(native_session_id.clone());
+ }
+ }
+ if event_type == "assistant" {
+ let id = value
+ .get("message")
+ .and_then(|message| message.get("id"))
+ .and_then(Value::as_str)
+ .map(|id| id.to_string())
+ .unwrap_or_else(|| {
+ session.claude_message_counter += 1;
+ let generated = format!(
+ "{}_message_{}",
+ session.session_id, session.claude_message_counter
+ );
+ if let Some(message) =
+ value.get_mut("message").and_then(Value::as_object_mut)
+ {
+ message.insert("id".to_string(), Value::String(generated.clone()));
+ } else if let Some(map) = value.as_object_mut() {
+ map.insert(
+ "message".to_string(),
+ serde_json::json!({
+ "id": generated
+ }),
+ );
+ }
+ generated
+ });
+ session.last_claude_message_id = Some(id);
+ } else if event_type == "result" {
+ let has_message_id =
+ value.get("message_id").is_some() || value.get("messageId").is_some();
+ if !has_message_id {
+ let id = session.last_claude_message_id.take().unwrap_or_else(|| {
+ session.claude_message_counter += 1;
+ format!(
+ "{}_message_{}",
+ session.session_id, session.claude_message_counter
+ )
+ });
+ if let Some(map) = value.as_object_mut() {
+ map.insert("message_id".to_string(), Value::String(id));
+ }
+ } else {
+ session.last_claude_message_id = None;
}
- } else {
- session.last_claude_message_id = None;
}
}
}
@@ -3958,6 +3974,7 @@ fn normalize_permission_mode(
return Ok("bypass".to_string());
}
let supported = match agent {
+ AgentId::Claude => false,
AgentId::Codex => matches!(mode, "default" | "plan" | "bypass"),
AgentId::Amp => matches!(mode, "default" | "bypass"),
AgentId::Opencode => matches!(mode, "default"),
diff --git a/todo.md b/todo.md
index 6986730..3bdcd8e 100644
--- a/todo.md
+++ b/todo.md
@@ -9,4 +9,6 @@
- [x] Add Docker/Vercel/Daytona/E2B examples with basic prompt scripts and tests.
- [x] Add unified AgentServerManager for shared agent servers (Codex/OpenCode).
- [x] Expose server status details in agent list API (uptime/restarts/last error/base URL).
+- [x] Add local agent install CLI command and document optional preinstall step.
+- [x] Move API CLI commands under the api subcommand.
- [ ] Regenerate TypeScript SDK from updated OpenAPI (blocked: Node/pnpm not available in env).