Terminal
Interactive PTY into a sandbox over WebSocket. Single-user and shared (multi-viewer) modes.
The terminal endpoint hands you a real Linux PTY inside the sandbox over a WebSocket. It's what powers the dashboard's "Connect" tab, and it's the right primitive to embed if you're building an IDE, a CI live-tail, or a pair-debugging tool.
Two flavors:
/exec/pty | /exec/pty/shared | |
|---|---|---|
| Viewers | Exactly one. | Many subscribers, one PTY. |
| Stdin holder | Always the connected client. | One holder at a time; can be requested / handed off. |
| Use it for | A solo developer's shell. | Multiplayer debugging, AI watcher + human driver, classroom. |
| Lifecycle | PTY closes when client disconnects. | PTY survives subscriber churn; closes when last viewer leaves. |
Both endpoints upgrade an Authorization: Bearer … HTTP GET to a WebSocket.
Single-user PTY
GET /v1/sandboxes/{id}/exec/pty?rows=40&cols=120
Upgrade: websocketWire protocol after the upgrade:
| Direction | Frame | Meaning |
|---|---|---|
| client → server | binary frame | Bytes typed into stdin. |
| client → server | {"type":"resize","rows":40,"cols":120} (text JSON) | Resize the PTY. |
| server → client | binary frame | PTY stdout/stderr bytes. |
| server → client | {"type":"exit","code":N} | Shell exited; connection closes. |
The shell defaults to bash -li (login + interactive). Override with ?cmd= (URL-encoded full command line) when you want a single program: ?cmd=htop, ?cmd=python3, etc.
Browser example (xterm.js)
import { Terminal } from "@xterm/xterm";
const ws = new WebSocket(
`wss://api.pandastack.ai/v1/sandboxes/${id}/exec/pty?rows=40&cols=120`,
["pds-pty", `bearer.${token}`], // subprotocol-based auth for browsers
);
ws.binaryType = "arraybuffer";
const term = new Terminal({ rows: 40, cols: 120 });
term.open(document.getElementById("term")!);
ws.onmessage = (ev) => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
if (msg.type === "exit") term.write(`\r\n[process exited ${msg.code}]\r\n`);
} else {
term.write(new Uint8Array(ev.data));
}
};
term.onData((data) => ws.send(new TextEncoder().encode(data)));
term.onResize(({ rows, cols }) => ws.send(JSON.stringify({ type: "resize", rows, cols })));CLI
pandastack sandbox shell <sandbox-id> # opens a PTY in your terminal
pandastack sandbox shell <sandbox-id> -- htop # one-off commandShared PTY (multiplayer)
/exec/pty/shared is the multiplayer terminal — many viewers around one shell. The first connection opens the PTY and becomes the stdin holder; subsequent connections only see output until they ask for the keyboard.
GET /v1/sandboxes/{id}/exec/pty/shared?name=alice&rows=40&cols=120
Upgrade: websocketExtra messages on top of the single-user protocol:
| Direction | Frame | Meaning |
|---|---|---|
| s → c | {"type":"hello","you":{...},"holder":"<id>","participants":[…],"rows":40,"cols":120} | Sent immediately on connect. |
| s → c | {"type":"join","participant":{...}} | Someone joined. |
| s → c | {"type":"leave","id":"<id>"} | Someone left. |
| s → c | {"type":"stdin_holder","id":"<id>"} | Keyboard moved to <id> (broadcast). |
| c → s | {"type":"request_stdin"} | Ask to become the holder. |
| c → s | {"type":"release_stdin"} | Hand the keyboard back to the floor. |
| c → s | {"type":"grant_stdin","to":"<id>"} | Current holder explicitly gives to another participant. |
| c → s | {"type":"force_take"} | Steal the keyboard (only works with ?resolve=1 capability — admin-only). |
| s → c | {"type":"error","message":"not stdin holder"} | You wrote a binary frame without holding stdin — UI should grey out the input. |
Lifecycle:
- First subscriber opens the PTY and is auto-promoted to holder.
- Holder leaves →
holder = ""; the nextrequest_stdinclaims it. - PTY's underlying shell exits → server broadcasts
{exit, code}and closes every connection.
This is exactly the multiplayer primitive the dashboard ships — see Multiplayer for the higher-level CRDT / cursor sync that rides alongside it.
Use cases
Developer tool: in-browser shell. A "Connect" button in your app opens a tab with a full bash shell into the user's sandbox. xterm.js + 30 lines of WS glue.
Agent watcher. Your agent runs pytest -x via exec. You drop a teammate into the same shared PTY with read-only viewer privileges so they can watch in real time without yanking the keyboard.
Human-in-the-loop confirmation. The agent runs a git push that prompts for a passphrase. The dashboard pops "an interactive prompt is waiting" — a human takes over stdin for a moment, types the secret, hands it back.
Classroom / pair programming. Instructor opens the shared PTY, several students join, one student is granted stdin to attempt an exercise, others watch.
Limits
- One PTY per sandbox per shared-hub. Multiple
?cmd=shells need separate/exec/ptyconnections. - Idle disconnect after 10 minutes of no traffic in either direction (keep-alive with an empty resize at least every 5 min if needed).
- Default capacity: 64 viewers per shared PTY. Past that, new connections get
1013 Try Again Later.