PandaStack

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
ViewersExactly one.Many subscribers, one PTY.
Stdin holderAlways the connected client.One holder at a time; can be requested / handed off.
Use it forA solo developer's shell.Multiplayer debugging, AI watcher + human driver, classroom.
LifecyclePTY 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: websocket

Wire protocol after the upgrade:

DirectionFrameMeaning
client → serverbinary frameBytes typed into stdin.
client → server{"type":"resize","rows":40,"cols":120} (text JSON)Resize the PTY.
server → clientbinary framePTY 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 command

Shared 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: websocket

Extra messages on top of the single-user protocol:

DirectionFrameMeaning
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:

  1. First subscriber opens the PTY and is auto-promoted to holder.
  2. Holder leaves → holder = ""; the next request_stdin claims it.
  3. 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/pty connections.
  • 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.

On this page