diff --git a/docs/docs.json b/docs/docs.json index b2b3a6a..afccb80 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -79,7 +79,7 @@ }, { "group": "System", - "pages": ["file-system"] + "pages": ["file-system", "processes"] }, { "group": "Orchestration", diff --git a/docs/inspector.mdx b/docs/inspector.mdx index f3b3dc6..2d15971 100644 --- a/docs/inspector.mdx +++ b/docs/inspector.mdx @@ -34,6 +34,8 @@ console.log(url); - Event JSON inspector - Prompt testing - Request/response debugging +- Process management (create, stop, kill, delete, view logs) +- One-shot command execution ## When to use diff --git a/docs/observability.mdx b/docs/observability.mdx index 770fe8b..5b5751b 100644 --- a/docs/observability.mdx +++ b/docs/observability.mdx @@ -1,7 +1,7 @@ --- title: "Observability" description: "Track session activity with OpenTelemetry." -icon: "terminal" +icon: "chart-line" --- Use OpenTelemetry to instrument session traffic, then ship telemetry to your collector/backend. diff --git a/docs/processes.mdx b/docs/processes.mdx new file mode 100644 index 0000000..ace9242 --- /dev/null +++ b/docs/processes.mdx @@ -0,0 +1,273 @@ +--- +title: "Processes" +description: "Run commands and manage long-lived processes inside the sandbox." +sidebarTitle: "Processes" +icon: "terminal" +--- + +The process API lets you run one-shot commands, spawn long-lived processes, stream logs, and open interactive terminal sessions over WebSocket. + +- **One-shot execution** — run a command to completion and capture stdout, stderr, and exit code +- **Managed processes** — spawn, list, stop, kill, and delete long-lived processes +- **Log streaming** — fetch buffered logs or follow live output via SSE +- **Interactive terminals** — full PTY support with bidirectional WebSocket I/O +- **Configurable limits** — control concurrency, timeouts, and buffer sizes per runtime + +## Run a command + +Execute a command to completion and get its output. + + +```ts TypeScript +import { SandboxAgent } from "sandbox-agent"; + +const sdk = await SandboxAgent.connect({ + baseUrl: "http://127.0.0.1:2468", +}); + +const result = await sdk.runProcess({ + command: "ls", + args: ["-la", "/workspace"], +}); + +console.log(result.exitCode); // 0 +console.log(result.stdout); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"ls","args":["-la","/workspace"]}' +``` + + +You can set a timeout and cap output size: + + +```ts TypeScript +const result = await sdk.runProcess({ + command: "make", + args: ["build"], + timeoutMs: 60000, + maxOutputBytes: 1048576, +}); + +if (result.timedOut) { + console.log("Build timed out"); +} +if (result.stdoutTruncated) { + console.log("Output was truncated"); +} +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/run" \ + -H "Content-Type: application/json" \ + -d '{"command":"make","args":["build"],"timeoutMs":60000,"maxOutputBytes":1048576}' +``` + + +## Managed processes + +Create a long-lived process that you can interact with, monitor, and stop later. + +### Create + + +```ts TypeScript +const proc = await sdk.createProcess({ + command: "node", + args: ["server.js"], + cwd: "/workspace", +}); + +console.log(proc.id, proc.pid); // proc_1, 12345 +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes" \ + -H "Content-Type: application/json" \ + -d '{"command":"node","args":["server.js"],"cwd":"/workspace"}' +``` + + +Set `tty: true` to allocate a pseudo-terminal (required for interactive programs like vim or tmux): + +```ts TypeScript +const proc = await sdk.createProcess({ + command: "bash", + tty: true, + interactive: true, +}); +``` + +### List and get + + +```ts TypeScript +const { processes } = await sdk.listProcesses(); + +for (const p of processes) { + console.log(p.id, p.command, p.status); +} + +const proc = await sdk.getProcess("proc_1"); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/processes" + +curl "http://127.0.0.1:2468/v1/processes/proc_1" +``` + + +### Stop, kill, and delete + + +```ts TypeScript +// SIGTERM with optional wait +await sdk.stopProcess("proc_1", { waitMs: 5000 }); + +// SIGKILL +await sdk.killProcess("proc_1", { waitMs: 1000 }); + +// Remove exited process record +await sdk.deleteProcess("proc_1"); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/stop?waitMs=5000" + +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/kill?waitMs=1000" + +curl -X DELETE "http://127.0.0.1:2468/v1/processes/proc_1" +``` + + +## Logs + +### Fetch buffered logs + + +```ts TypeScript +const logs = await sdk.getProcessLogs("proc_1", { + tail: 50, + stream: "combined", +}); + +for (const entry of logs.entries) { + console.log(entry.stream, atob(entry.data)); +} +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/processes/proc_1/logs?tail=50&stream=combined" +``` + + +### Follow logs via SSE + +Stream log entries in real time. The subscription replays buffered entries first, then streams new output as it arrives. + +```ts TypeScript +const sub = await sdk.followProcessLogs("proc_1", (entry) => { + console.log(entry.stream, atob(entry.data)); +}); + +// Later, stop following +sub.close(); +await sub.closed; +``` + +## Interactive terminals + +For TTY processes, you can open a bidirectional WebSocket for full terminal access. + +### Write input + + +```ts TypeScript +await sdk.sendProcessInput("proc_1", { + data: "echo hello\n", + encoding: "utf8", +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/input" \ + -H "Content-Type: application/json" \ + -d '{"data":"echo hello\n","encoding":"utf8"}' +``` + + +### Resize terminal + + +```ts TypeScript +await sdk.resizeProcessTerminal("proc_1", { + cols: 120, + rows: 40, +}); +``` + +```bash cURL +curl -X POST "http://127.0.0.1:2468/v1/processes/proc_1/terminal/resize" \ + -H "Content-Type: application/json" \ + -d '{"cols":120,"rows":40}' +``` + + +### WebSocket terminal + +Open a WebSocket for bidirectional PTY I/O. The server sends raw terminal output as binary frames and JSON control frames (`ready`, `exit`, `error`). The client sends JSON frames for `input`, `resize`, and `close`. + +```ts TypeScript +const ws = sdk.connectProcessTerminalWebSocket("proc_1"); + +ws.binaryType = "arraybuffer"; + +ws.addEventListener("message", (event) => { + if (typeof event.data === "string") { + const frame = JSON.parse(event.data); + // { type: "ready" } | { type: "exit", exitCode: 0 } | { type: "error", message: "..." } + console.log("control:", frame); + } else { + // Raw PTY output + const text = new TextDecoder().decode(event.data); + process.stdout.write(text); + } +}); + +// Send input +ws.send(JSON.stringify({ type: "input", data: "ls\n" })); + +// Resize +ws.send(JSON.stringify({ type: "resize", cols: 120, rows: 40 })); +``` + +Since the browser WebSocket API cannot send custom headers, the endpoint accepts an `access_token` query parameter for authentication. The SDK handles this automatically. + +## Configuration + +Adjust runtime limits like max concurrent processes, timeouts, and buffer sizes. + + +```ts TypeScript +const config = await sdk.getProcessConfig(); +console.log(config); + +await sdk.setProcessConfig({ + ...config, + maxConcurrentProcesses: 32, + defaultRunTimeoutMs: 60000, +}); +``` + +```bash cURL +curl "http://127.0.0.1:2468/v1/processes/config" + +curl -X POST "http://127.0.0.1:2468/v1/processes/config" \ + -H "Content-Type: application/json" \ + -d '{"maxConcurrentProcesses":32,"defaultRunTimeoutMs":60000,"maxRunTimeoutMs":300000,"maxOutputBytes":1048576,"maxLogBytesPerProcess":10485760,"maxInputBytesPerRequest":65536}' +``` + diff --git a/frontend/packages/inspector/src/components/debug/ProcessesTab.tsx b/frontend/packages/inspector/src/components/debug/ProcessesTab.tsx index e3b2903..0d11844 100644 --- a/frontend/packages/inspector/src/components/debug/ProcessesTab.tsx +++ b/frontend/packages/inspector/src/components/debug/ProcessesTab.tsx @@ -179,6 +179,10 @@ const ProcessesTab = ({ } }; + const handleTerminalExit = useCallback(() => { + void loadProcesses("refresh"); + }, [loadProcesses]); + return (
{/* Create form */} @@ -389,9 +393,7 @@ const ProcessesTab = ({ { - void loadProcesses("refresh"); - }} + onExit={handleTerminalExit} /> ) : canOpenTerminal(selectedProcess) ? (