Concept · web terminal

Web terminal — a real shell, in a browser, per tenant.

xterm.js attached to a sandboxed node-pty. Persistent across reconnects. Embed in your own UI or use ours.

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

  1. Browser opens wss://gateway.example.com/terminal with the tenant Bearer token in the WebSocket subprotocol field.
  2. Gateway verifies the token (SHA-256 + constant-time compare), resolves the tenant, and spawns a node-pty child inside the tenant's bubblewrap or Docker sandbox.
  3. 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.
  4. SIGWINCH-style resize messages keep the pty's column count in sync with the browser viewport.
  5. 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.

TypeScript example using the OpenClawMU client
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):

html
<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.

~/.openclaw/tenants/acme/config.yaml
terminal:
  recording: enabled        # off | enabled | always
  recording_path: recordings
  retention_days: 30

EXFOLIATE!

Run your own gateway today.

Apache-2.0, self-hosted, no SaaS layer between you and your users. Install the CLI, create your first tenant, mint a token — you're routing traffic in 60 seconds.