Skip to content

Latest commit

 

History

History
225 lines (173 loc) · 11.7 KB

File metadata and controls

225 lines (173 loc) · 11.7 KB

pty specification

This document specifies the current pty system. It builds on requirements.md.

Status

Draft until every mapped contract is present on the default branch. The test matrix below is the executable validation boundary.

Scope

This specification defines persistent session execution, ordered terminal transport, registry state, and supported CLI/package surfaces. It does not define a shell, window manager, multi-tenant authorization boundary, or product orchestration policy.

Composition

CLI / package / testing / remote surfaces
                  |
        +---------+---------+
        |                   |
 ordered client stream   durable registry
        |                   |
        +---------+---------+
                  |
          per-session runtime
                  |
          child process + PTY

The runtime owns the child process and headless terminal model. The stream projects one ordered terminal to ephemeral clients. The registry owns stable identity and durable observations. Every public surface composes these three sources rather than defining alternate semantics (R01, R09, R11).

Runtime and launch

A session is a detached daemon containing one child PTY and one headless terminal emulator. The daemon owns the socket and survives client disconnects (R01).

Launch environment assembly is ordered (R02):

replacement mode: copy env

inherited mode:   process.env -> remove internal server config
isolated mode:    allowlisted process.env + LC_*

policy modes only: base -> unsetEnv[] -> extraEnv{}
all modes:         -> force PTY_SESSION + generation token
                   -> TERM absent/empty ? xterm-256color : preserve value

Replacement mode and the inherited/isolate policy options are mutually exclusive. Metadata persists the selected mode and its removals/assignments. Explicit restart reuses it. Permanent reconciliation re-reads a current manifest declaration when available and otherwise uses persisted metadata. Metadata predating unsetEnv retains historical ambient inheritance.

On child exit, the runtime drains accepted output, records final screen and exit state, emits the lifecycle event, and applies cleanup policy. Every mutating cleanup/restart path compares stable id plus generation so stale work cannot change a replacement session (R03).

Ordered client stream

Packets use a five-byte header followed by a bounded payload:

[type: uint8][length: uint32BE][payload: length bytes]

The reader reassembles partial input and rejects declared payloads above 32 MiB (R07). Unknown bounded message types are ignored for additive compatibility; capability-specific surfaces fail closed when required packets are absent.

Synchronization

For each admitted attach/peek generation (R04):

parser bytes before cut | parser bytes after cut | process exit
          |                       |                    |
          v                       v                    v
GEOMETRY -> SCREEN -----------> queued DATA -------> EXIT

The screen callback is the causal cut: SCREEN represents all earlier parser writes; later data and exit queue behind it. A newer valid mode request invalidates the unfinished generation. Reconnect creates a fresh generation. A local machine detach may end with DETACH before a baseline is emitted.

Roles and geometry

Each socket begins in the command role. Role frames replace, rather than accumulate, socket state (R05):

Frame Resulting role Geometry membership Input/resize
complete ATTACH(rows, cols) writable-attached requested rows/cols enabled
recognized PEEK(flags) readonly none disabled
malformed ATTACH unchanged unchanged unchanged
STATUS unchanged unchanged unchanged

Client-to-server data, status, and resize behavior is role-specific:

Role DATA STATUS RESIZE
command accepted accepted ignored
writable-attached accepted accepted accepted
readonly ignored accepted ignored

Command sockets do not receive a screen baseline and do not participate in geometry. They may receive baseline-less live DATA or EXIT broadcasts; those packets do not constitute reconstructable terminal state. A consumer that needs reconstructable terminal state must first send ATTACH or PEEK. Public stats omit command sockets and expose the writable-attached role with the compatibility string "writable".

For writable-attached request set W, shared geometry is (R06):

rows = min(client.rows for client in W)
cols = min(client.cols for client in W)

The dimensions are minimized independently. A changed GEOMETRY notification precedes terminal output produced after the corresponding PTY resize. Removing the last writable-attached client leaves the last effective geometry stable.

Machine attach

attach --attach-stream-fd-v1 <fd> <ref> requires an inherited writable descriptor fd >= 3. The packaged CLI runs without a wrapper child so the descriptor, controlling terminal, signals, and process identity reach the adapter unchanged (R08, R11).

The adapter reframes only GEOMETRY, SCREEN, DATA, and terminal outcomes to the descriptor; terminal interaction stays on stdin/stdout and diagnostics use stderr. It flushes exactly one clean outcome before EOF:

Outcome Meaning
EXIT(code) the session process ended
empty DETACH this local client intentionally detached
EOF without either transport loss, reconnect give-up, descriptor failure, or abrupt administrative destruction

The last row is a non-zero truncation, not a third clean outcome (R07, R08).

Registry and lifecycle state

PTY_ROOT selects one registry. A stable id owns socket, metadata, events, and generation locks; display names and tags remain mutable lookup/presentation fields (R09).

Inventory is observational: it derives running/exited/vanished state and enriches it with live status when available, but does not restart, reap, or attach. Status reports client roles, requested/effective geometry, process resources, and terminal modes.

Metadata and events form two compatibility tiers (R10):

Record Contract
metadata JSON durable launch/lifecycle source; atomic generation-aware updates preserve unknown fields
event JSONL externally readable observation stream; serialized append and bounded retention
socket packets internal bounded protocol with documented legacy decoding fallbacks

Explicit lifecycle commands and gc own mutation. Cleanup is authorized by the observed generation; removal wins over late daemon finalization, and permanent respawn cannot overwrite a replacement (R03, R10).

Live registry recovery

A supporting daemon may publish an opaque recovery capability only when it can prove its process-start identity and both PTY_ROOT and .recovery are private directories owned by the daemon user. If an external cleanup unlinks that live session's socket, pid, and metadata paths, recover --snapshot authenticates a complete retained metadata snapshot and asks the original daemon to rebind its listener. It preserves the daemon generation, child process, provider launch, terminal state, and attached clients; it does not probe by signal, restart, relaunch, or replace an occupied pathname (R03, R09).

The request/result exchange binds stable id, daemon pid and process-start token, generation, launch identity, root and recovery-directory device/inode identity, and the daemon's signed metadata revision. Metadata mutation advances the signed revision before publishing the replacement record. Recovery therefore fails closed after a partial publication and rejects missing, legacy, stale, replayed, tampered, wrong-root, permission-downgraded, or path-replacement state. Success republishes the socket, pid, and metadata with no-replace and owned-rollback semantics and rotates the recovery secret. An authenticated lock left by an interrupted recoverer may resume; other creation locks remain authoritative and are never displaced (R10).

Surfaces

The CLI, package entrypoint, exported client/server/protocol modules, testing library, and remote route call the same behavioral core (R11). Completion schemas preserve required option values. Remote streaming preserves local packet order and fails explicitly when the peer lacks a capability. The testing library drives real processes and PTYs and exposes screen, cursor, scrollback, input, resize, and multi-client geometry without mocks.

Ownership and validation matrix

Requirement Owning source Primary executable evidence
R01 server, spawn integration, exit reap, shutdown
R02 server, spawn, sessions, ptyfile spawn options, restart parity, restart scrub, ptyfile
R03 server, sessions, recovery kill, immediate reuse, generation guard, exit signal, recovery
R04 server, connection integration, alternate screen, scrollback
R05 server integration
R06 server, protocol effective geometry, resize, status
R07 protocol, connection, remote protocol, connection, remote reconnect
R08 client, CLI, entrypoint attach stream, signals
R09 sessions, server, recovery, CLI root, display name, status, list purity, recovery
R10 sessions, events, recovery, protocol atomic writes, metadata events, events, recovery, disk layout
R11 CLI, client API, remote, testing API help, completions, remote, screenshots, keys

node scripts/verify-docs.ts --vrs-only validates this two-document shape, sequential requirement IDs, links, and complete requirement references.