Terminal I/O uses gRPC bidirectional streaming for low-latency interactive sessions. The server maintains full VT state for each session, enabling instant reconnect with screen snapshot.
Defined in proto/terminal.proto, package relay.terminal.v1.
rpc AttachSession(stream ClientMessage) returns (stream ServerMessage);Bidirectional stream for interactive terminal I/O. The client sends an AttachRequest as the first message, then sends stdin data and resize events. The server streams back stdout data and session events.
| Field | Description |
|---|---|
attach_request |
First message — handshake with session_id |
stdin_data |
Raw bytes forwarded to the PTY |
resize_request |
Terminal resize (cols, rows) |
detach_request |
Graceful detach without killing the session |
| Field | Description |
|---|---|
attach_response |
First response — screen snapshot, size, title, cwd, input mode |
stdout_data |
Raw bytes from the PTY |
session_event |
State changes: title, cwd, size, terminated |
error_response |
Protocol-level error with ErrorCode |
On successful attach, the server sends:
screen_snapshot— full VT screen state as bytessize— current terminal dimensionstitle— terminal window titlecwd— current working directoryinput_mode— READ_WRITE (session creator) or READ_ONLY (other clients)version_info— runner and protocol versions
- READ_WRITE — session creator, can send stdin
- READ_ONLY — attached observer, stdin is rejected
Determined by session_auth.rs: the session creator gets read-write access, all other clients get read-only.
Server-side VT state tracking via the vte crate (runner/src/vt_parser.rs).
- Full screen buffer (rows x cols)
- Cursor position and attributes
- Scrollback buffer (configurable, default 10,000 lines)
- Terminal title (from OSC sequences)
- Current working directory (from OSC 7)
- Terminal size
ArcSwap provides lock-free snapshots of the VT state. On each PTY read:
- Bytes are fed through the VT parser
- Screen state is updated
- A new snapshot is atomically published
Clients attaching mid-session receive the latest snapshot, rendering a complete screen immediately.
- OSC 52 (clipboard write) — stripped from output to prevent clipboard hijacking
- DCS/APC sequences — stripped from input to prevent terminal escape attacks
The SessionBackend trait (runner/src/backend.rs) abstracts the I/O source:
pub trait SessionBackend: Send + Sync {
fn read(&self, buf: &mut [u8]) -> Pin<Box<dyn Future<Output = Result<usize, io::Error>> + Send>>;
fn write_stdin(&self, data: &[u8]) -> Result<usize, io::Error>;
fn resize(&self, cols: u16, rows: u16) -> Result<(), io::Error>;
fn is_alive(&self) -> bool;
fn kill(&self) -> Result<(), io::Error>;
}Implementations:
- LocalPtyBackend (
runner/src/backends/local_pty.rs) — direct PTY vianix(openpty/fork/execvp) - DockerExecBackend (
runner/src/backends/docker_exec.rs) — Docker exec attach viabollard
The read method returns Pin<Box<dyn Future>> because TerminalSession stores backends as Box<dyn SessionBackend>. See ADR-005.
ClientRegistry (runner/src/client_registry.rs) manages per-session client connections:
- Each client gets a bounded
mpscchannel for output delivery - Backpressure: when a client's channel is full, messages are dropped (slow client doesn't block others)
- Client count tracked per session (exposed in
SessionInfo.client_count) - Broadcast: PTY output is fanned out to all attached clients
Direct PTY management (runner/src/pty.rs):
- Uses
nixcrate for POSIX PTY operations:openpty,fork,execvp - Shell resolved from configured allowlist (default:
/bin/bash,/bin/zsh,/bin/sh) - Environment filtering: blocks
LD_PRELOAD,DYLD_*, and other injection vectors - Async I/O via tokio's
AsyncFd SIGHUPfor graceful shutdown,SIGKILLas fallback
rpc GetScrollback(GetScrollbackRequest) returns (GetScrollbackResponse);Returns buffered scrollback lines. max_lines = 0 returns all buffered lines. Configurable buffer size via scrollback_lines in config (default: 10,000).
| RPC | Description |
|---|---|
CreateSession |
Spawn a new PTY session (local mode only; cloud mode returns FAILED_PRECONDITION) |
ListSessions |
List all active sessions |
TerminateSession |
Kill a session by ID |
GetScrollback |
Retrieve scrollback buffer |
RefreshToken |
Issue a new auth token before expiry |
Source: runner/src/grpc_service.rs