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) ? (