From a442efe4bd30e3cf724612502bf3bd9ba1baf229 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 1 Feb 2026 23:25:43 -0800 Subject: [PATCH] chore: update examples to use bypass permissions and remove inspect.sandboxagent.dev (#51) * chore: update examples to use bypass permissions and remove inspect.sandboxagent.dev * chore: simplify examples and print UI URL in runPrompt - Remove logInspectorUrl calls from all examples - Remove isMainModule checks from e2b, docker, vercel examples - Simplify e2b, docker, vercel to match daytona's direct execution style - Print UI URL at start of runPrompt in shared module --- examples/daytona/src/daytona-with-snapshot.ts | 10 +- examples/daytona/src/daytona.ts | 10 +- examples/docker/src/docker.ts | 120 ++---- examples/e2b/src/e2b.ts | 89 ++-- examples/shared/src/sandbox-agent-client.ts | 392 ++---------------- examples/vercel/.gitignore | 2 + examples/vercel/src/vercel.ts | 120 ++---- 7 files changed, 160 insertions(+), 583 deletions(-) create mode 100644 examples/vercel/.gitignore diff --git a/examples/daytona/src/daytona-with-snapshot.ts b/examples/daytona/src/daytona-with-snapshot.ts index 447957a..d0d1ce8 100644 --- a/examples/daytona/src/daytona-with-snapshot.ts +++ b/examples/daytona/src/daytona-with-snapshot.ts @@ -1,5 +1,5 @@ import { Daytona, Image } from "@daytonaio/sdk"; -import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared"; +import { runPrompt } from "@sandbox-agent/example-shared"; const daytona = new Daytona(); @@ -23,7 +23,6 @@ await sandbox.process.executeCommand( ); const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url; -logInspectorUrl({ baseUrl }); const cleanup = async () => { await sandbox.delete(60); @@ -32,10 +31,5 @@ const cleanup = async () => { process.once("SIGINT", cleanup); process.once("SIGTERM", cleanup); -// When running as root in a container, Claude requires interactive permission prompts (bypass mode is not supported). Set autoApprovePermissions: true to auto-approve, -// or leave false for interactive prompts. -await runPrompt({ - baseUrl, - autoApprovePermissions: process.env.AUTO_APPROVE_PERMISSIONS === "true", -}); +await runPrompt(baseUrl); await cleanup(); diff --git a/examples/daytona/src/daytona.ts b/examples/daytona/src/daytona.ts index 8a51d48..4fe6a3b 100644 --- a/examples/daytona/src/daytona.ts +++ b/examples/daytona/src/daytona.ts @@ -1,5 +1,5 @@ import { Daytona } from "@daytonaio/sdk"; -import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared"; +import { runPrompt } from "@sandbox-agent/example-shared"; const daytona = new Daytona(); @@ -24,7 +24,6 @@ await sandbox.process.executeCommand( ); const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url; -logInspectorUrl({ baseUrl }); const cleanup = async () => { await sandbox.delete(60); @@ -33,10 +32,5 @@ const cleanup = async () => { process.once("SIGINT", cleanup); process.once("SIGTERM", cleanup); -// When running as root in a container, Claude requires interactive permission prompts (bypass mode is not supported). Set autoApprovePermissions: true to auto-approve, -// or leave false for interactive prompts. -await runPrompt({ - baseUrl, - autoApprovePermissions: process.env.AUTO_APPROVE_PERMISSIONS === "true", -}); +await runPrompt(baseUrl); await cleanup(); diff --git a/examples/docker/src/docker.ts b/examples/docker/src/docker.ts index 431c5ad..20fafe4 100644 --- a/examples/docker/src/docker.ts +++ b/examples/docker/src/docker.ts @@ -1,90 +1,56 @@ import Docker from "dockerode"; -import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; +import { runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -// Alpine is required because Claude Code binary is built for musl libc const IMAGE = "alpine:latest"; const PORT = 3000; -export async function setupDockerSandboxAgent(): Promise<{ - baseUrl: string; - token?: string; - cleanup: () => Promise; -}> { - const docker = new Docker({ socketPath: "/var/run/docker.sock" }); +const docker = new Docker({ socketPath: "/var/run/docker.sock" }); - // Pull image if needed - try { - await docker.getImage(IMAGE).inspect(); - } catch { - console.log(`Pulling ${IMAGE}...`); - await new Promise((resolve, reject) => { - docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => { - if (err) return reject(err); - docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve()); - }); +// Pull image if needed +try { + await docker.getImage(IMAGE).inspect(); +} catch { + console.log(`Pulling ${IMAGE}...`); + await new Promise((resolve, reject) => { + docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) return reject(err); + docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve()); }); - } - - console.log("Starting container..."); - const container = await docker.createContainer({ - Image: IMAGE, - Cmd: ["sh", "-c", [ - // Install dependencies (Alpine uses apk, not apt-get) - "apk add --no-cache curl ca-certificates libstdc++ libgcc bash", - "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", - "sandbox-agent install-agent claude", - "sandbox-agent install-agent codex", - `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, - ].join(" && ")], - Env: [ - process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "", - process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "", - ].filter(Boolean), - ExposedPorts: { [`${PORT}/tcp`]: {} }, - HostConfig: { - AutoRemove: true, - PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] }, - }, }); - await container.start(); - - const baseUrl = `http://127.0.0.1:${PORT}`; - await waitForHealth({ baseUrl }); - - const cleanup = async () => { - console.log("Cleaning up..."); - try { await container.stop({ t: 5 }); } catch {} - try { await container.remove({ force: true }); } catch {} - }; - - return { baseUrl, cleanup }; } -// Run interactively if executed directly -const isMainModule = import.meta.url === `file://${process.argv[1]}`; -if (isMainModule) { - if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { - throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); - } +console.log("Starting container..."); +const container = await docker.createContainer({ + Image: IMAGE, + Cmd: ["sh", "-c", [ + "apk add --no-cache curl ca-certificates libstdc++ libgcc bash", + "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", + "sandbox-agent install-agent claude", + "sandbox-agent install-agent codex", + `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, + ].join(" && ")], + Env: [ + process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "", + process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "", + ].filter(Boolean), + ExposedPorts: { [`${PORT}/tcp`]: {} }, + HostConfig: { + AutoRemove: true, + PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] }, + }, +}); +await container.start(); - const { baseUrl, cleanup } = await setupDockerSandboxAgent(); - logInspectorUrl({ baseUrl }); +const baseUrl = `http://127.0.0.1:${PORT}`; +await waitForHealth({ baseUrl }); - process.once("SIGINT", async () => { - await cleanup(); - process.exit(0); - }); - process.once("SIGTERM", async () => { - await cleanup(); - process.exit(0); - }); +const cleanup = async () => { + try { await container.stop({ t: 5 }); } catch {} + try { await container.remove({ force: true }); } catch {} + process.exit(0); +}; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); - // When running as root in a container, Claude requires interactive permission prompts - // (bypass mode is not supported). Set autoApprovePermissions: true to auto-approve, - // or leave false for interactive prompts. - await runPrompt({ - baseUrl, - autoApprovePermissions: process.env.AUTO_APPROVE_PERMISSIONS === "true", - }); - await cleanup(); -} +await runPrompt(baseUrl); +await cleanup(); diff --git a/examples/e2b/src/e2b.ts b/examples/e2b/src/e2b.ts index 47d5760..8d54c88 100644 --- a/examples/e2b/src/e2b.ts +++ b/examples/e2b/src/e2b.ts @@ -1,71 +1,40 @@ import { Sandbox } from "@e2b/code-interpreter"; -import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; +import { runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -export async function setupE2BSandboxAgent(): Promise<{ - baseUrl: string; - token?: string; - cleanup: () => Promise; -}> { - const envs: Record = {}; - if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; - if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const envs: Record = {}; +if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; - const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); - const run = async (cmd: string) => { - const result = await sandbox.commands.run(cmd); - if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`); - return result; - }; +console.log("Creating E2B sandbox..."); +const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); - console.log("Installing sandbox-agent..."); - await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); +const run = async (cmd: string) => { + const result = await sandbox.commands.run(cmd); + if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`); + return result; +}; - console.log("Installing agents..."); - await run("sandbox-agent install-agent claude"); - await run("sandbox-agent install-agent codex"); +console.log("Installing sandbox-agent..."); +await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); - console.log("Starting server..."); - await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); +console.log("Installing agents..."); +await run("sandbox-agent install-agent claude"); +await run("sandbox-agent install-agent codex"); - const baseUrl = `https://${sandbox.getHost(3000)}`; +console.log("Starting server..."); +await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); - // Wait for server to be ready - console.log("Waiting for server..."); - await waitForHealth({ baseUrl }); +const baseUrl = `https://${sandbox.getHost(3000)}`; - const cleanup = async () => { - console.log("Cleaning up..."); - await sandbox.kill(); - }; +console.log("Waiting for server..."); +await waitForHealth({ baseUrl }); - return { baseUrl, cleanup }; -} +const cleanup = async () => { + await sandbox.kill(); + process.exit(0); +}; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); -// Run interactively if executed directly -const isMainModule = import.meta.url === `file://${process.argv[1]}`; -if (isMainModule) { - if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { - throw new Error("E2B_API_KEY and (OPENAI_API_KEY or ANTHROPIC_API_KEY) required"); - } - - const { baseUrl, cleanup } = await setupE2BSandboxAgent(); - logInspectorUrl({ baseUrl }); - - process.once("SIGINT", async () => { - await cleanup(); - process.exit(0); - }); - process.once("SIGTERM", async () => { - await cleanup(); - process.exit(0); - }); - - // When running as root in a container, Claude requires interactive permission prompts - // (bypass mode is not supported). Set autoApprovePermissions: true to auto-approve, - // or leave false for interactive prompts. - await runPrompt({ - baseUrl, - autoApprovePermissions: process.env.AUTO_APPROVE_PERMISSIONS === "true", - }); - await cleanup(); -} +await runPrompt(baseUrl); +await cleanup(); diff --git a/examples/shared/src/sandbox-agent-client.ts b/examples/shared/src/sandbox-agent-client.ts index ff80f3b..8258ee8 100644 --- a/examples/shared/src/sandbox-agent-client.ts +++ b/examples/shared/src/sandbox-agent-client.ts @@ -1,14 +1,19 @@ -import { createInterface, Interface } from "node:readline/promises"; +/** + * Simple shared utilities for sandbox-agent examples. + * Provides minimal helpers for connecting to and interacting with sandbox-agent servers. + */ + +import { createInterface } from "node:readline/promises"; import { randomUUID } from "node:crypto"; import { setTimeout as delay } from "node:timers/promises"; import { SandboxAgent } from "sandbox-agent"; -import type { PermissionReply, PermissionEventData, QuestionEventData } from "sandbox-agent"; +import type { PermissionEventData, QuestionEventData } from "sandbox-agent"; -export function normalizeBaseUrl(baseUrl: string): string { +function normalizeBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/, ""); } -export function ensureUrl(rawUrl: string): string { +function ensureUrl(rawUrl: string): string { if (!rawUrl) { throw new Error("Missing sandbox URL"); } @@ -51,16 +56,16 @@ export function logInspectorUrl({ console.log(`Inspector: ${buildInspectorUrl({ baseUrl, token, headers })}`); } -type HeaderOptions = { +export function buildHeaders({ + token, + extraHeaders, + contentType = false, +}: { token?: string; extraHeaders?: Record; contentType?: boolean; -}; - -export function buildHeaders({ token, extraHeaders, contentType = false }: HeaderOptions): HeadersInit { - const headers: Record = { - ...(extraHeaders || {}), - }; +}): HeadersInit { + const headers: Record = { ...(extraHeaders || {}) }; if (token) { headers.Authorization = `Bearer ${token}`; } @@ -70,37 +75,6 @@ export function buildHeaders({ token, extraHeaders, contentType = false }: Heade return headers; } -async function fetchJson( - url: string, - { - token, - extraHeaders, - method = "GET", - body, - }: { - token?: string; - extraHeaders?: Record; - method?: string; - body?: unknown; - } = {} -): Promise { - const headers = buildHeaders({ - token, - extraHeaders, - contentType: body !== undefined, - }); - const response = await fetch(url, { - method, - headers, - body: body === undefined ? undefined : JSON.stringify(body), - }); - const text = await response.text(); - if (!response.ok) { - throw new Error(`HTTP ${response.status} ${response.statusText}: ${text}`); - } - return text ? JSON.parse(text) : {}; -} - export async function waitForHealth({ baseUrl, token, @@ -117,11 +91,17 @@ export async function waitForHealth({ let lastError: unknown; while (Date.now() < deadline) { try { - const data = await fetchJson(`${normalized}/v1/health`, { token, extraHeaders }); - if (data?.status === "ok") { - return; + const headers = buildHeaders({ token, extraHeaders }); + const response = await fetch(`${normalized}/v1/health`, { headers }); + if (response.ok) { + const data = await response.json(); + if (data?.status === "ok") { + return; + } + lastError = new Error(`Unexpected health response: ${JSON.stringify(data)}`); + } else { + lastError = new Error(`Health check failed: ${response.status}`); } - lastError = new Error(`Unexpected health response: ${JSON.stringify(data)}`); } catch (error) { lastError = error; } @@ -130,250 +110,24 @@ export async function waitForHealth({ throw (lastError ?? new Error("Timed out waiting for /v1/health")) as Error; } -export async function createSession({ - baseUrl, - token, - extraHeaders, - agentId, - agentMode, - permissionMode, - model, - variant, - agentVersion, -}: { - baseUrl: string; - token?: string; - extraHeaders?: Record; - agentId?: string; - agentMode?: string; - permissionMode?: string; - model?: string; - variant?: string; - agentVersion?: string; -}): Promise { - const normalized = normalizeBaseUrl(baseUrl); - const sessionId = randomUUID(); - const body: Record = { - agent: agentId || detectAgent(), - }; - const envAgentMode = agentMode || process.env.SANDBOX_AGENT_MODE; - const envPermissionMode = permissionMode || process.env.SANDBOX_PERMISSION_MODE; - const envModel = model || process.env.SANDBOX_MODEL; - const envVariant = variant || process.env.SANDBOX_VARIANT; - const envAgentVersion = agentVersion || process.env.SANDBOX_AGENT_VERSION; - - if (envAgentMode) body.agentMode = envAgentMode; - if (envPermissionMode) body.permissionMode = envPermissionMode; - if (envModel) body.model = envModel; - if (envVariant) body.variant = envVariant; - if (envAgentVersion) body.agentVersion = envAgentVersion; - - await fetchJson(`${normalized}/v1/sessions/${sessionId}`, { - token, - extraHeaders, - method: "POST", - body, - }); - return sessionId; -} - -function extractTextFromItem(item: any): string { - if (!item?.content) return ""; - const textParts = item.content - .filter((part: any) => part?.type === "text") - .map((part: any) => part.text || "") - .join(""); - if (textParts.trim()) { - return textParts; - } - return JSON.stringify(item.content, null, 2); -} - -export async function sendMessageStream({ - baseUrl, - token, - extraHeaders, - sessionId, - message, - onText, -}: { - baseUrl: string; - token?: string; - extraHeaders?: Record; - sessionId: string; - message: string; - onText?: (text: string) => void; -}): Promise { - const normalized = normalizeBaseUrl(baseUrl); - const headers = buildHeaders({ token, extraHeaders, contentType: true }); - - const response = await fetch(`${normalized}/v1/sessions/${sessionId}/messages/stream`, { - method: "POST", - headers, - body: JSON.stringify({ message }), - }); - - 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 (delta can be a string or an object with type: "text") - if (event.type === "item.delta" && event.data?.delta) { - const delta = event.data.delta; - const text = typeof delta === "string" ? delta : delta.type === "text" ? delta.text || "" : ""; - if (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 - } - } - } - - return fullText; -} - function detectAgent(): string { - // Prefer explicit setting if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT; - // Select based on available API key if (process.env.ANTHROPIC_API_KEY) return "claude"; if (process.env.OPENAI_API_KEY) return "codex"; return "claude"; } -export type PermissionHandler = ( - data: PermissionEventData -) => Promise | PermissionReply; +export async function runPrompt(baseUrl: string): Promise { + console.log(`UI: ${buildInspectorUrl({ baseUrl })}`); -export type QuestionHandler = ( - data: QuestionEventData -) => Promise | string[][] | null; + const client = await SandboxAgent.connect({ baseUrl }); -export interface RunPromptOptions { - baseUrl: string; - token?: string; - extraHeaders?: Record; - agentId?: string; - agentMode?: string; - permissionMode?: string; - model?: string; - /** Auto-approve all permissions with "once" (default: false, prompts interactively) */ - autoApprovePermissions?: boolean; - /** Custom permission handler (overrides autoApprovePermissions) */ - onPermission?: PermissionHandler; - /** Custom question handler (return null to reject) */ - onQuestion?: QuestionHandler; -} - -async function promptForPermission( - rl: Interface, - data: PermissionEventData -): Promise { - console.log(`\n[Permission Required] ${data.action}`); - if (data.metadata) { - console.log(` Details: ${JSON.stringify(data.metadata, null, 2)}`); - } - while (true) { - const answer = await rl.question(" Allow? [y]es / [n]o / [a]lways: "); - const lower = answer.trim().toLowerCase(); - if (lower === "y" || lower === "yes") return "once"; - if (lower === "a" || lower === "always") return "always"; - if (lower === "n" || lower === "no") return "reject"; - console.log(" Please enter y, n, or a"); - } -} - -async function promptForQuestion( - rl: Interface, - data: QuestionEventData -): Promise { - console.log(`\n[Question] ${data.prompt}`); - if (data.options.length > 0) { - console.log(" Options:"); - data.options.forEach((opt, i) => console.log(` ${i + 1}. ${opt}`)); - const answer = await rl.question(" Enter option number (or 'skip' to reject): "); - if (answer.trim().toLowerCase() === "skip") return null; - const idx = parseInt(answer.trim(), 10) - 1; - if (idx >= 0 && idx < data.options.length) { - return [[data.options[idx]]]; - } - console.log(" Invalid option, rejecting question"); - return null; - } - const answer = await rl.question(" Your answer (or 'skip' to reject): "); - if (answer.trim().toLowerCase() === "skip") return null; - return [[answer.trim()]]; -} - -export async function runPrompt(options: RunPromptOptions): Promise { - const { - baseUrl, - token, - extraHeaders, - agentId, - agentMode, - permissionMode, - model, - autoApprovePermissions = false, - onPermission, - onQuestion, - } = options; - - const client = await SandboxAgent.connect({ - baseUrl, - token, - headers: extraHeaders, - }); - - const agent = agentId || detectAgent(); + const agent = detectAgent(); console.log(`Using agent: ${agent}`); const sessionId = randomUUID(); - await client.createSession(sessionId, { - agent, - agentMode, - permissionMode, - model, - }); + await client.createSession(sessionId, { agent }); console.log(`Session ${sessionId}. Press Ctrl+C to quit.`); - // Create readline interface for interactive prompts const rl = createInterface({ input: process.stdin, output: process.stdout }); let isThinking = false; @@ -381,47 +135,8 @@ export async function runPrompt(options: RunPromptOptions): Promise { let turnResolve: (() => void) | null = null; let sessionEnded = false; - // Handle permission request - const handlePermission = async (data: PermissionEventData): Promise => { - try { - let reply: PermissionReply; - if (onPermission) { - reply = await onPermission(data); - } else if (autoApprovePermissions) { - console.log(`\n[Auto-approved] ${data.action}`); - reply = "once"; - } else { - reply = await promptForPermission(rl, data); - } - await client.replyPermission(sessionId, data.permission_id, { reply }); - } catch (err) { - console.error("Failed to reply to permission:", err instanceof Error ? err.message : err); - } - }; - - // Handle question request - const handleQuestion = async (data: QuestionEventData): Promise => { - try { - let answers: string[][] | null; - if (onQuestion) { - answers = await onQuestion(data); - } else { - answers = await promptForQuestion(rl, data); - } - if (answers === null) { - await client.rejectQuestion(sessionId, data.question_id); - } else { - await client.replyQuestion(sessionId, data.question_id, { answers }); - } - } catch (err) { - console.error("Failed to reply to question:", err instanceof Error ? err.message : err); - } - }; - - // Stream events in background using SDK const processEvents = async () => { for await (const event of client.streamEvents(sessionId)) { - // Show thinking indicator when assistant starts if (event.type === "item.started") { const item = (event.data as any)?.item; if (item?.role === "assistant") { @@ -431,12 +146,11 @@ export async function runPrompt(options: RunPromptOptions): Promise { } } - // Print text deltas (only during assistant turn) if (event.type === "item.delta" && isThinking) { const delta = (event.data as any)?.delta; if (delta) { if (!hasStartedOutput) { - process.stdout.write("\r\x1b[K"); // Clear line + process.stdout.write("\r\x1b[K"); hasStartedOutput = true; } const text = typeof delta === "string" ? delta : delta.type === "text" ? delta.text || "" : ""; @@ -444,7 +158,6 @@ export async function runPrompt(options: RunPromptOptions): Promise { } } - // Signal turn complete if (event.type === "item.completed") { const item = (event.data as any)?.item; if (item?.role === "assistant") { @@ -455,61 +168,36 @@ export async function runPrompt(options: RunPromptOptions): Promise { } } - // Handle permission requests if (event.type === "permission.requested") { const data = event.data as PermissionEventData; - // Clear thinking indicator if shown if (isThinking && !hasStartedOutput) { process.stdout.write("\r\x1b[K"); } - await handlePermission(data); + console.log(`[Auto-approved] ${data.action}`); + await client.replyPermission(sessionId, data.permission_id, { reply: "once" }); } - // Handle question requests if (event.type === "question.requested") { const data = event.data as QuestionEventData; - // Clear thinking indicator if shown if (isThinking && !hasStartedOutput) { process.stdout.write("\r\x1b[K"); } - await handleQuestion(data); + console.log(`[Question rejected] ${data.prompt}`); + await client.rejectQuestion(sessionId, data.question_id); } - // Handle errors if (event.type === "error") { const data = event.data as any; console.error(`\nError: ${data?.message || JSON.stringify(data)}`); } - // Handle session ended if (event.type === "session.ended") { const data = event.data as any; const reason = data?.reason || "unknown"; - const exitCode = data?.exit_code; - const message = data?.message; - const stderr = data?.stderr; - if (reason === "error") { - console.error(`\nAgent exited with error:`); - if (exitCode !== undefined) { - console.error(` Exit code: ${exitCode}`); - } - if (message) { - console.error(` Message: ${message}`); - } - if (stderr) { - console.error(`\n--- Agent Stderr ---`); - if (stderr.head) { - console.error(stderr.head); - } - if (stderr.truncated && stderr.tail) { - const omitted = stderr.total_lines - ? stderr.total_lines - 70 // 20 head + 50 tail - : "..."; - console.error(`\n... ${omitted} lines omitted ...\n`); - console.error(stderr.tail); - } - console.error(`--- End Stderr ---`); + console.error(`\nAgent exited with error: ${data?.message || ""}`); + if (data?.exit_code !== undefined) { + console.error(` Exit code: ${data.exit_code}`); } } else { console.log(`Agent session ${reason}`); @@ -520,13 +208,13 @@ export async function runPrompt(options: RunPromptOptions): Promise { } } }; + processEvents().catch((err) => { if (!sessionEnded) { console.error("Event stream error:", err instanceof Error ? err.message : err); } }); - // Read user input and post messages while (true) { const line = await rl.question("> "); if (!line.trim()) continue; diff --git a/examples/vercel/.gitignore b/examples/vercel/.gitignore new file mode 100644 index 0000000..c8a7336 --- /dev/null +++ b/examples/vercel/.gitignore @@ -0,0 +1,2 @@ +.vercel +.env*.local diff --git a/examples/vercel/src/vercel.ts b/examples/vercel/src/vercel.ts index 4135ba8..ed2d836 100644 --- a/examples/vercel/src/vercel.ts +++ b/examples/vercel/src/vercel.ts @@ -1,87 +1,51 @@ import { Sandbox } from "@vercel/sandbox"; -import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; +import { runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; -export async function setupVercelSandboxAgent(): Promise<{ - baseUrl: string; - token?: string; - cleanup: () => Promise; -}> { - // Build env vars for agent API keys - const envs: Record = {}; - if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; - if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const envs: Record = {}; +if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; - // Create sandbox with port 3000 exposed - const sandbox = await Sandbox.create({ - runtime: "node24", - ports: [3000], - }); +console.log("Creating Vercel sandbox..."); +const sandbox = await Sandbox.create({ + runtime: "node24", + ports: [3000], +}); - // Helper to run commands and check exit code - const run = async (cmd: string, args: string[] = []) => { - const result = await sandbox.runCommand({ cmd, args, env: envs }); - if (result.exitCode !== 0) { - const stderr = await result.stderr(); - throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${stderr}`); - } - return result; - }; - - console.log("Installing sandbox-agent..."); - await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"]); - - console.log("Installing agents..."); - await run("sandbox-agent", ["install-agent", "claude"]); - await run("sandbox-agent", ["install-agent", "codex"]); - - console.log("Starting server..."); - await sandbox.runCommand({ - cmd: "sandbox-agent", - args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"], - env: envs, - detached: true, - }); - - const baseUrl = sandbox.domain(3000); - - console.log("Waiting for server..."); - await waitForHealth({ baseUrl }); - - const cleanup = async () => { - console.log("Cleaning up..."); - await sandbox.stop(); - }; - - return { baseUrl, cleanup }; -} - -// Run interactively if executed directly -const isMainModule = import.meta.url === `file://${process.argv[1]}`; -if (isMainModule) { - // Check for Vercel auth - if (!process.env.VERCEL_OIDC_TOKEN && !process.env.VERCEL_ACCESS_TOKEN) { - throw new Error("Vercel authentication required. Run 'vercel env pull' or set VERCEL_ACCESS_TOKEN"); +const run = async (cmd: string, args: string[] = []) => { + const result = await sandbox.runCommand({ cmd, args, env: envs }); + if (result.exitCode !== 0) { + const stderr = await result.stderr(); + throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${stderr}`); } + return result; +}; - if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { - throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); - } +console.log("Installing sandbox-agent..."); +await run("sh", ["-c", "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"]); - const { baseUrl, cleanup } = await setupVercelSandboxAgent(); - logInspectorUrl({ baseUrl }); +console.log("Installing agents..."); +await run("sandbox-agent", ["install-agent", "claude"]); +await run("sandbox-agent", ["install-agent", "codex"]); - process.once("SIGINT", async () => { - await cleanup(); - process.exit(0); - }); - process.once("SIGTERM", async () => { - await cleanup(); - process.exit(0); - }); +console.log("Starting server..."); +await sandbox.runCommand({ + cmd: "sandbox-agent", + args: ["server", "--no-token", "--host", "0.0.0.0", "--port", "3000"], + env: envs, + detached: true, +}); - await runPrompt({ - baseUrl, - autoApprovePermissions: process.env.AUTO_APPROVE_PERMISSIONS === "true", - }); - await cleanup(); -} +const baseUrl = sandbox.domain(3000); + +console.log("Waiting for server..."); +await waitForHealth({ baseUrl }); + +const cleanup = async () => { + await sandbox.stop(); + process.exit(0); +}; +process.once("SIGINT", cleanup); +process.once("SIGTERM", cleanup); + +await runPrompt(baseUrl); +await cleanup();