GitHub
Concept

Terminal & SSH

WebSocket-bridged terminal access to running agent containers, plus VNC for GUI agents and dynamic subdomain routing for any service the agent exposes.

Every running agent container ships with sshd (in the shared base image), and the platform exposes it through a couple of bridges so you can reach the agent’s working environment from a browser tab — or any SSH client — without exposing the container directly to the internet.

Web terminal

GET /terminal/{instanceId} is the WebSocket endpoint the web UI’s terminal tab connects to (internal/server/router/router.go). The bridge:

  1. Authenticates the request as the user (or workflow) that owns the instance
  2. Opens an SSH connection to the container’s :22 (the password is generated server-side per execution and never shown to the user)
  3. Pipes stdin/stdout/stderr through the WebSocket as raw bytes
  4. Tears the SSH session down on disconnect

Client-side rendering is xterm.js with the fit addon (the web/ package depends on @xterm/xterm plus @xterm/addon-fit). The session lands inside the container’s auto-attached tmux session — the agent’s running workflow output is visible immediately, and you can split panes, scroll back, or just watch.

Multiple terminals can attach to the same instance simultaneously. Every attach lands in the same agent tmux session, so everyone sees the same screen — useful for pair debugging.

SSH directly into a container

For agents you want to drive from a desktop SSH client (VS Code Remote-SSH, vim, your shell), the instance API surfaces SSH connection info:

pfai api GET /api/v1/instances/{id}/ssh

Response:

{
  "host": "127.0.0.1",
  "port": 32768,
  "username": "root",
  "password": "auto-generated-per-execution"
}

The host + port are reachable depending on your deployment topology:

DeploymentWhat’s reachable
Docker Compose (local)Host’s 127.0.0.1:<random> — Docker publishes the container’s :22 to a random host port
Single binary with DockerSame as above
Kubernetes (Enterprise)Cluster-internal IP/port — use pfai cloud port-forward to tunnel

pfai cloud port-forward <instance-id> 22 creates a local TCP tunnel to the container’s SSH port — the same UX as kubectl port-forward, no network policy edits needed.

SSH passwords are randomly generated per agent execution and rotated on container teardown. They’re not user passwords — so there’s nothing to manage and nothing to leak permanently. For repeatable access, configure SSH key auth via deploy keys on the repo the agent clones.

Remote desktop (VNC)

For agents that need a graphical environment — Cursor, browser-driving agents, computer-use scenarios — the container can run an X server with a VNC bridge. The web UI uses noVNC (@novnc/novnc 1.7.0-beta) to render the desktop in a <canvas>:

  • Mouse + keyboard input streamed over WebSocket
  • Screenshot-based for the model’s vision tools (screenshot, computer, browser_action — see Chat Modes)
  • Multiple viewers can attach; the agent and you both see the same desktop

Reachable from the agent execution detail page → Desktop tab when the underlying agent image is one that ships an X server. Currently that’s cursor and claude-code with the GUI variant. The minimal agent images (aider, gemini-cli, opencode, copilot) are headless.

Subdomain routing

When an agent starts a service inside its container — a dev server, a database, a webhook test endpoint — the platform’s port detector notices the new listener and registers an Envoy route. The service then reaches:

{instance-id}-{port}.{BASE_DOMAIN}
LayerWhat it does
Port detector (internal/instances/portdetect)Watches container netstat for new LISTEN sockets
WS-TCP bridge (:8082)Tunnels arbitrary TCP through WebSockets — used by the web UI and pfai cloud port-forward
xDS gRPC (:18000)Pushes Envoy route updates dynamically
Envoy (:8080)The actual public proxy in front of agent containers

BASE_DOMAIN defaults to localhost in OSS (so the URL is inst-abc-3000.localhost — works locally without DNS). In production, point a wildcard *.dev.example.com at your Envoy and set BASE_DOMAIN=dev.example.com.

WebSocket connections survive the same routing — connect to inst-abc-3000.dev.example.com and the WS handshake passes through Envoy unchanged. TLS termination happens on Envoy.

When to use what

You want to…Use
Watch the agent run, occasionally type a commandWeb terminal (Terminal tab)
Edit the agent’s files in your local VS CodeSSH + Remote-SSH extension
See the agent’s GUI sessionWeb Desktop tab (noVNC)
Test a dev server the agent startedSubdomain ({id}-{port}.{BASE_DOMAIN})
Pull a one-off file or run one commandpfai exec command --instance <id> -- <cmd>

See also