mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 04:03:31 +00:00
feat: add process management support (#207)
* feat: improve inspector UI for processes and fix PTY terminal
- Simplify ProcessRunTab layout: compact form with collapsible Advanced section for timeout/maxOutputBytes
- Rewrite ProcessesTab: collapsible create form, lightweight list items with status dots, clean detail panel with tabs
- Extract error details: use problem.detail instead of generic "Stream Error" title for better error messages
- Fix GhosttyTerminal binary frame parsing: handle server's binary ArrayBuffer control frames (ready/exit/error)
- Enable WebSocket proxying in Vite dev server with ws: true
- Set TERM=xterm-256color default for TTY processes so tools like tmux, vim, htop work out of the box
- Remove orange gradient background from terminal container for cleaner look
- Remove orange left border from selected process list items
- Update inspector CSS with new process/terminal styles
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix: address review issues and add processes documentation
- Fix unstable onExit callback in ProcessesTab (useCallback)
- Fix SSE follow stream race condition (subscribe before history read)
- Update inspector.mdx with new process management features
- Change observability icon to avoid conflict with processes
- Add docs/processes.mdx covering the full process management API
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: simplify processes doc — rename sections, remove low-level protocol
- Rename "Interactive terminals" to "Terminals" with "Connect to a terminal" sub-heading
- Add TTY process creation step at top of Terminals section
- Remove low-level WebSocket protocol table and raw WebSocket example
- Keep browser terminal emulator reference with Ghostty link
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update GhosttyTerminal permalink to latest commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: use main branch permalink for GhosttyTerminal reference
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: refine process API — WebSocket binary protocol, SDK terminal session, updated tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update GhosttyTerminal permalink to 636eefb
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* inspector: use websocket terminal API
* sdk: restore high-level terminal session
* docs: update inspector terminal permalink
* inspector: update run once placeholder
* Fix lazy install v1 API test fixture
* Add reusable React terminal component
* Fix terminal WebSocket ready state checks
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e7656d78f0
commit
febe8601f6
28 changed files with 2098 additions and 83 deletions
|
|
@ -51,6 +51,7 @@
|
|||
"pages": [
|
||||
"quickstart",
|
||||
"sdk-overview",
|
||||
"react-components",
|
||||
{
|
||||
"group": "Deploy",
|
||||
"icon": "server",
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
},
|
||||
{
|
||||
"group": "System",
|
||||
"pages": ["file-system"]
|
||||
"pages": ["file-system", "processes"]
|
||||
},
|
||||
{
|
||||
"group": "Orchestration",
|
||||
|
|
|
|||
|
|
@ -34,9 +34,18 @@ console.log(url);
|
|||
- Event JSON inspector
|
||||
- Prompt testing
|
||||
- Request/response debugging
|
||||
- Process management (create, stop, kill, delete, view logs)
|
||||
- Interactive PTY terminal for tty processes
|
||||
- One-shot command execution
|
||||
|
||||
## When to use
|
||||
|
||||
- Development: validate session behavior quickly
|
||||
- Debugging: inspect raw event payloads
|
||||
- Integration work: compare UI behavior with SDK/API calls
|
||||
|
||||
## Process terminal
|
||||
|
||||
The Inspector includes an embedded Ghostty-based terminal for interactive tty
|
||||
processes. The UI uses the SDK's high-level `connectProcessTerminal(...)`
|
||||
wrapper via the shared `@sandbox-agent/react` `ProcessTerminal` component.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"license": {
|
||||
"name": "Apache-2.0"
|
||||
},
|
||||
"version": "0.2.1"
|
||||
"version": "0.2.2"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
|
|
|||
258
docs/processes.mdx
Normal file
258
docs/processes.mdx
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
title: "Processes"
|
||||
description: "Run commands and manage long-lived processes inside the sandbox."
|
||||
sidebarTitle: "Processes"
|
||||
icon: "terminal"
|
||||
---
|
||||
|
||||
The process API supports:
|
||||
|
||||
- **One-shot execution** — run a command to completion and capture stdout, stderr, and exit code
|
||||
- **Managed processes** — spawn, list, stop, kill, and delete long-lived processes
|
||||
- **Log streaming** — fetch buffered logs or follow live output via SSE
|
||||
- **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.
|
||||
|
||||
<CodeGroup>
|
||||
```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"]}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
You can set a timeout and cap output size:
|
||||
|
||||
<CodeGroup>
|
||||
```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}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Managed processes
|
||||
|
||||
Create a long-lived process that you can interact with, monitor, and stop later.
|
||||
|
||||
### Create
|
||||
|
||||
<CodeGroup>
|
||||
```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"}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### List and get
|
||||
|
||||
<CodeGroup>
|
||||
```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"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Stop, kill, and delete
|
||||
|
||||
<CodeGroup>
|
||||
```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"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Logs
|
||||
|
||||
### Fetch buffered logs
|
||||
|
||||
<CodeGroup>
|
||||
```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"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### 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;
|
||||
```
|
||||
|
||||
## Terminals
|
||||
|
||||
Create a process with `tty: true` to allocate a pseudo-terminal, then connect via WebSocket for full bidirectional I/O.
|
||||
|
||||
```ts TypeScript
|
||||
const proc = await sdk.createProcess({
|
||||
command: "bash",
|
||||
tty: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Write input
|
||||
|
||||
<CodeGroup>
|
||||
```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"}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Connect to a terminal
|
||||
|
||||
Use `ProcessTerminalSession` unless you need direct frame access.
|
||||
|
||||
```ts TypeScript
|
||||
const terminal = sdk.connectProcessTerminal("proc_1");
|
||||
|
||||
terminal.onReady(() => {
|
||||
terminal.resize({ cols: 120, rows: 40 });
|
||||
terminal.sendInput("ls\n");
|
||||
});
|
||||
|
||||
terminal.onData((bytes) => {
|
||||
process.stdout.write(new TextDecoder().decode(bytes));
|
||||
});
|
||||
|
||||
terminal.onExit((status) => {
|
||||
console.log("exit:", status.exitCode);
|
||||
});
|
||||
|
||||
terminal.onError((error) => {
|
||||
console.error(error instanceof Error ? error.message : error.message);
|
||||
});
|
||||
|
||||
terminal.onClose(() => {
|
||||
console.log("terminal closed");
|
||||
});
|
||||
```
|
||||
|
||||
Since the browser WebSocket API cannot send custom headers, the endpoint accepts an `access_token` query parameter for authentication. The SDK handles this automatically.
|
||||
|
||||
### Browser terminal emulators
|
||||
|
||||
The terminal session works with any browser terminal emulator like ghostty-web or xterm.js. For a drop-in React terminal, see [React Components](/react-components).
|
||||
|
||||
## Configuration
|
||||
|
||||
Adjust runtime limits like max concurrent processes, timeouts, and buffer sizes.
|
||||
|
||||
<CodeGroup>
|
||||
```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}'
|
||||
```
|
||||
</CodeGroup>
|
||||
103
docs/react-components.mdx
Normal file
103
docs/react-components.mdx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
title: "React Components"
|
||||
description: "Drop-in React components for Sandbox Agent frontends."
|
||||
icon: "react"
|
||||
---
|
||||
|
||||
`@sandbox-agent/react` exposes small React components built on top of the `sandbox-agent` SDK.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @sandbox-agent/react@0.2.x
|
||||
```
|
||||
|
||||
## Full example
|
||||
|
||||
This example connects to a running Sandbox Agent server, starts a tty shell, renders `ProcessTerminal`, and cleans up the process when the component unmounts.
|
||||
|
||||
```tsx TerminalPane.tsx expandable highlight={5,32-36,71}
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { ProcessTerminal } from "@sandbox-agent/react";
|
||||
|
||||
export default function TerminalPane() {
|
||||
const [client, setClient] = useState<SandboxAgent | null>(null);
|
||||
const [processId, setProcessId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let sdk: SandboxAgent | null = null;
|
||||
let createdProcessId: string | null = null;
|
||||
|
||||
const cleanup = async () => {
|
||||
if (!sdk || !createdProcessId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sdk.killProcess(createdProcessId, { waitMs: 1_000 }).catch(() => {});
|
||||
await sdk.deleteProcess(createdProcessId).catch(() => {});
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
sdk = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
});
|
||||
|
||||
const process = await sdk.createProcess({
|
||||
command: "sh",
|
||||
interactive: true,
|
||||
tty: true,
|
||||
});
|
||||
|
||||
if (cancelled) {
|
||||
createdProcessId = process.id;
|
||||
await cleanup();
|
||||
await sdk.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
createdProcessId = process.id;
|
||||
setClient(sdk);
|
||||
setProcessId(process.id);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to start terminal.";
|
||||
setError(message);
|
||||
}
|
||||
};
|
||||
|
||||
void start();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
void cleanup();
|
||||
void sdk?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div>{error}</div>;
|
||||
}
|
||||
|
||||
if (!client || !processId) {
|
||||
return <div>Starting terminal...</div>;
|
||||
}
|
||||
|
||||
return <ProcessTerminal client={client} processId={processId} height={480} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Component
|
||||
|
||||
`ProcessTerminal` attaches to a running tty process.
|
||||
|
||||
- `client`: a `SandboxAgent` client
|
||||
- `processId`: the process to attach to
|
||||
- `height`, `style`, `terminalStyle`: optional layout overrides
|
||||
- `onExit`, `onError`: optional lifecycle callbacks
|
||||
|
||||
See [Processes](/processes) for the lower-level terminal APIs.
|
||||
|
|
@ -29,6 +29,12 @@ The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgent` class.
|
|||
npm install @sandbox-agent/persist-indexeddb@0.2.x @sandbox-agent/persist-sqlite@0.2.x @sandbox-agent/persist-postgres@0.2.x
|
||||
```
|
||||
|
||||
## Optional React components
|
||||
|
||||
```bash
|
||||
npm install @sandbox-agent/react@0.2.x
|
||||
```
|
||||
|
||||
## Create a client
|
||||
|
||||
```ts
|
||||
|
|
@ -206,4 +212,3 @@ Parameters:
|
|||
- `fetch` (optional): Custom fetch implementation used by SDK HTTP and ACP calls
|
||||
- `waitForHealth` (optional, defaults to enabled): waits for `/v1/health` before HTTP helpers and ACP session setup proceed; pass `false` to disable or `{ timeoutMs }` to bound the wait
|
||||
- `signal` (optional): aborts the startup `/v1/health` wait used by `connect()`
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue