Skip to content

WebSocket Proxy & Terminal Component #288

@jrf0110

Description

@jrf0110

Part of #271 — Gastown Cloud Proposal A (Sandbox-per-Town)

Goal

Build the cloud-side WebSocket proxy and React terminal component so users can observe and interact with gastown agent sessions from the dashboard.

Context

The terminal proxy (PR 7) runs inside the sandbox. This PR adds the cloud app layer that authenticates users, proxies WebSocket connections to the sandbox, and renders terminal output using xterm.js.

Requirements

WebSocket Proxy (Cloud App)

Auth flow:

  1. Browser requests stream ticket via tRPC: gastown.getStreamTicket({ townId, sessionName })
  2. Cloud app mints short-lived JWT (60s expiry):
    { "userId": "...", "townId": "...", "sessionName": "...", "exp": ... }
    Same pattern as signStreamTicket in cloud/src/lib/cloud-agent/.
  3. Browser connects to WebSocket endpoint: wss://app.kilo.ai/api/gastown/terminal?ticket=<jwt>
  4. Cloud app validates ticket, extracts townId and sessionName
  5. Cloud app opens upstream WebSocket to sandbox terminal proxy: ws://<fly-proxy>:8081/sessions/<sessionName>
  6. Bidirectional proxy: browser ↔ cloud app ↔ sandbox terminal proxy

Route: cloud/src/app/api/gastown/terminal/route.ts — WebSocket upgrade handler

tRPC Additions

Add to the gastown router:

Route Type Description
gastown.getStreamTicket mutation Mint short-lived JWT for WebSocket auth
gastown.listSessions query Proxy to sandbox internal API GET /sessions

React Component: GastownTerminal

Props:

  • townId: string
  • sessionName: string

Behavior:

  • Renders xterm.js terminal with fit addon (auto-resize to container)
  • Connects via WebSocket using stream ticket
  • Displays terminal output from the agent session
  • Read-only mode for polecats, witnesses, refineries (observation only)
  • Read-write mode for Mayor session (user can type instructions)
  • Reconnects automatically on disconnect (exponential backoff)
  • Shows connection status indicator (connected/reconnecting/disconnected)

Session Picker Sidebar: GastownSessionPicker

Props:

  • townId: string
  • onSelectSession: (sessionName: string) => void

Behavior:

  • Fetches session list from gastown.listSessions
  • Color-coded by role:
    • Mayor: gold
    • Witness: blue
    • Refinery: green
    • Polecat: gray
  • Shows session status: active/idle/dead
  • Click to switch terminal view
  • Auto-refreshes every 5 seconds

Files

  • cloud/src/app/api/gastown/terminal/route.ts (new) — WebSocket upgrade handler
  • cloud/src/components/gastown/GastownTerminal.tsx (new)
  • cloud/src/components/gastown/GastownSessionPicker.tsx (new)
  • Updates to cloud/src/server/api/routers/gastown.tsgetStreamTicket, listSessions

Dependencies (npm)

  • xterm — terminal emulator
  • @xterm/addon-fit — auto-resize addon

Acceptance Criteria

  • getStreamTicket mints a valid 60s JWT with correct payload
  • WebSocket upgrade handler validates ticket and rejects expired/invalid tokens
  • WebSocket proxy correctly forwards output from sandbox to browser
  • WebSocket proxy correctly forwards input from browser to sandbox (Mayor only)
  • GastownTerminal renders xterm.js terminal with proper sizing
  • Terminal auto-resizes when container dimensions change
  • Read-only mode prevents input for non-Mayor sessions
  • Read-write mode allows input for Mayor session
  • Auto-reconnect on disconnect with exponential backoff
  • Connection status indicator shows current state
  • GastownSessionPicker lists sessions with correct role colors
  • Clicking a session in the picker switches the terminal view
  • Session list auto-refreshes every 5 seconds

Dependencies

  • PR 2 (Provisioning API) — town ownership verification
  • PR 3 (Sandbox Internal API) — GET /sessions endpoint
  • PR 7 (Terminal Proxy) — sandbox-side WebSocket server

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions