Why it exists
Bot platforms always need an escape hatch. The model misbehaves, a cron job needs nudging, a tenant's data is in an odd shape — someone needs a shell. The "someone" might be your support team, the tenant themselves, or an automated agent.
A web terminal is the lowest-friction way to give that access without putting SSH keys in someone's inbox or building a bespoke admin UI for every routine maintenance task. It's also the easiest way for tenants to inspect their own sandbox state.
How it works
-
Browser opens
wss://gateway.example.com/terminalwith the tenant Bearer token in the WebSocket subprotocol field. -
Gateway verifies the token (SHA-256 + constant-time compare),
resolves the tenant, and spawns a
node-ptychild inside the tenant's bubblewrap or Docker sandbox. - The browser sends keystrokes as binary WebSocket frames; the server returns the pty's stdout/stderr as binary frames. xterm.js renders them with the full VT100 escape-sequence support.
-
SIGWINCH-style resize messages keep the pty's column count in sync with the browser viewport. - On reconnect, the WebSocket re-attaches to the same pty if the sandbox is still alive. Your work survives a tab refresh.
JSON-RPC methods
The terminal is a four-method JSON-RPC surface you can call from any client. The hosted xterm.js UI uses the same methods — there's no private API.
import { OpenClawClient } from "@neul-labs/openclawmu-client";
const client = new OpenClawClient({
url: "wss://gateway.example.com",
token: process.env.OPENCLAWMU_TENANT_TOKEN!,
});
// 1. Spawn a pty (returns a terminal ID)
const { id } = await client.terminal.spawn({
cmd: "bash",
cols: 120,
rows: 32,
cwd: "/work",
});
// 2. Stream output
client.terminal.onData(id, (chunk) => process.stdout.write(chunk));
// 3. Write to stdin
await client.terminal.write(id, "ls -la\n");
// 4. Resize on viewport change
window.addEventListener("resize", () => {
client.terminal.resize(id, term.cols, term.rows);
});
// 5. Close
await client.terminal.close(id); Embedding the UI
The hosted xterm.js UI is served by the gateway at /terminal.
To embed in your own app, render an iframe with the tenant token
passed as a URL fragment (so it never hits server logs):
<iframe
src="https://gateway.example.com/terminal/embed#token=tk_acme_9F2A..."
width="100%"
height="600"
allow="clipboard-write"
style="border: 0; border-radius: 8px;"
></iframe> Logging and replay
Every terminal session can optionally be recorded in
asciinema v2
format and saved to the tenant's recordings/
directory. Replays are useful for support hand-offs, incident
forensics, and showing a customer what their agent did.
terminal:
recording: enabled # off | enabled | always
recording_path: recordings
retention_days: 30