mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 04:02:25 +00:00
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>
This commit is contained in:
parent
6dbc871db9
commit
be32cf04a2
6 changed files with 296 additions and 9 deletions
|
|
@ -79,7 +79,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "System",
|
"group": "System",
|
||||||
"pages": ["file-system"]
|
"pages": ["file-system", "processes"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Orchestration",
|
"group": "Orchestration",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ console.log(url);
|
||||||
- Event JSON inspector
|
- Event JSON inspector
|
||||||
- Prompt testing
|
- Prompt testing
|
||||||
- Request/response debugging
|
- Request/response debugging
|
||||||
|
- Process management (create, stop, kill, delete, view logs)
|
||||||
|
- One-shot command execution
|
||||||
|
|
||||||
## When to use
|
## When to use
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "Observability"
|
title: "Observability"
|
||||||
description: "Track session activity with OpenTelemetry."
|
description: "Track session activity with OpenTelemetry."
|
||||||
icon: "terminal"
|
icon: "chart-line"
|
||||||
---
|
---
|
||||||
|
|
||||||
Use OpenTelemetry to instrument session traffic, then ship telemetry to your collector/backend.
|
Use OpenTelemetry to instrument session traffic, then ship telemetry to your collector/backend.
|
||||||
|
|
|
||||||
273
docs/processes.mdx
Normal file
273
docs/processes.mdx
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
<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;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interactive terminals
|
||||||
|
|
||||||
|
For TTY processes, you can open a bidirectional WebSocket for full terminal access.
|
||||||
|
|
||||||
|
### 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>
|
||||||
|
|
||||||
|
### Resize terminal
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```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}'
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
@ -179,6 +179,10 @@ const ProcessesTab = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTerminalExit = useCallback(() => {
|
||||||
|
void loadProcesses("refresh");
|
||||||
|
}, [loadProcesses]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="processes-container">
|
<div className="processes-container">
|
||||||
{/* Create form */}
|
{/* Create form */}
|
||||||
|
|
@ -389,9 +393,7 @@ const ProcessesTab = ({
|
||||||
<GhosttyTerminal
|
<GhosttyTerminal
|
||||||
client={getClient()}
|
client={getClient()}
|
||||||
processId={selectedProcess.id}
|
processId={selectedProcess.id}
|
||||||
onExit={() => {
|
onExit={handleTerminalExit}
|
||||||
void loadProcesses("refresh");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : canOpenTerminal(selectedProcess) ? (
|
) : canOpenTerminal(selectedProcess) ? (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1494,12 +1494,15 @@ async fn get_v1_process_logs(
|
||||||
since,
|
since,
|
||||||
};
|
};
|
||||||
|
|
||||||
let entries = runtime.logs(&id, filter).await?;
|
|
||||||
let response_entries: Vec<ProcessLogEntry> =
|
|
||||||
entries.iter().cloned().map(map_process_log_line).collect();
|
|
||||||
|
|
||||||
if query.follow.unwrap_or(false) {
|
if query.follow.unwrap_or(false) {
|
||||||
|
// Subscribe before reading history to avoid losing entries between the
|
||||||
|
// two operations. Entries are deduplicated by sequence number below.
|
||||||
let rx = runtime.subscribe_logs(&id).await?;
|
let rx = runtime.subscribe_logs(&id).await?;
|
||||||
|
let entries = runtime.logs(&id, filter).await?;
|
||||||
|
let response_entries: Vec<ProcessLogEntry> =
|
||||||
|
entries.iter().cloned().map(map_process_log_line).collect();
|
||||||
|
let last_replay_seq = response_entries.last().map(|e| e.sequence).unwrap_or(0);
|
||||||
|
|
||||||
let replay_stream = stream::iter(response_entries.into_iter().map(|entry| {
|
let replay_stream = stream::iter(response_entries.into_iter().map(|entry| {
|
||||||
Ok::<axum::response::sse::Event, Infallible>(
|
Ok::<axum::response::sse::Event, Infallible>(
|
||||||
axum::response::sse::Event::default()
|
axum::response::sse::Event::default()
|
||||||
|
|
@ -1515,6 +1518,9 @@ async fn get_v1_process_logs(
|
||||||
async move {
|
async move {
|
||||||
match item {
|
match item {
|
||||||
Ok(line) => {
|
Ok(line) => {
|
||||||
|
if line.sequence <= last_replay_seq {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let entry = map_process_log_line(line);
|
let entry = map_process_log_line(line);
|
||||||
if process_log_matches(&entry, requested_stream_copy) {
|
if process_log_matches(&entry, requested_stream_copy) {
|
||||||
Some(Ok(axum::response::sse::Event::default()
|
Some(Ok(axum::response::sse::Event::default()
|
||||||
|
|
@ -1539,6 +1545,10 @@ async fn get_v1_process_logs(
|
||||||
return Ok(response.into_response());
|
return Ok(response.into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let entries = runtime.logs(&id, filter).await?;
|
||||||
|
let response_entries: Vec<ProcessLogEntry> =
|
||||||
|
entries.iter().cloned().map(map_process_log_line).collect();
|
||||||
|
|
||||||
Ok(Json(ProcessLogsResponse {
|
Ok(Json(ProcessLogsResponse {
|
||||||
process_id: id,
|
process_id: id,
|
||||||
stream: requested_stream,
|
stream: requested_stream,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue