85 lines
3.6 KiB
Markdown
85 lines
3.6 KiB
Markdown
# ADR-001 — Shell / Terminal Boundary and Transport Model
|
||
|
||
**Status:** Accepted
|
||
**Date:** 2026-04-22
|
||
**Issue:** [#1695 — ATLAS Cockpit: operator inspector rail and session shell patterns](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/1695)
|
||
|
||
---
|
||
|
||
## Context
|
||
|
||
The Nexus operator cockpit needs a real shell/terminal surface so the operator can run commands, inspect logs, and interact with the system without leaving the 3D world UI.
|
||
|
||
Three transport models were evaluated:
|
||
|
||
| Option | Description |
|
||
|---|---|
|
||
| **A. Native local PTY** | `server.py` spawns a PTY via Python's `pty` stdlib module; the browser connects via WebSocket on `ws://127.0.0.1:8766/pty` |
|
||
| **B. Remote SSH PTY** | Browser connects to an SSH relay that opens a PTY on a remote machine |
|
||
| **C. Browser pseudo-terminal** | Pure JavaScript terminal emulation (e.g., a custom REPL) with no real OS shell |
|
||
|
||
---
|
||
|
||
## Decision
|
||
|
||
**Option A — Native local PTY** is chosen.
|
||
|
||
The Nexus is explicitly a **local-first** system (CLAUDE.md: "local-first training ground for Timmy"). The operator is the same person sitting at the machine. A local PTY bridged through `server.py` is:
|
||
|
||
- Architecturally consistent with the existing `server.py` WebSocket gateway
|
||
- Zero additional infrastructure (no SSH relay, no remote server)
|
||
- Full shell fidelity — any tool on the operator's PATH, full interactive programs, readline, color, etc.
|
||
- Bounded: PTY_PORT (8766) binds to `127.0.0.1` only; no external exposure
|
||
|
||
---
|
||
|
||
## Transport Detail
|
||
|
||
```
|
||
Browser (xterm.js) ←→ ws://127.0.0.1:8766/pty ←→ server.py pty_handler ←→ OS PTY ($SHELL)
|
||
```
|
||
|
||
- Each WebSocket connection gets its own `pty.openpty()` pair and subprocess.
|
||
- Input from xterm.js `onData` → `_ptyWs.send(data)` → `os.write(master_fd, data)`
|
||
- Output from PTY → `os.read(master_fd)` → `websocket.send(text)` → `_term.write(text)`
|
||
- Resize messages (`{"type":"resize","cols":N,"rows":N}`) can be added later via `fcntl.ioctl(TIOCSWINSZ)`.
|
||
|
||
---
|
||
|
||
## Why not Option B (Remote SSH PTY)?
|
||
|
||
Remote SSH adds network hop complexity, credential management, and an SSH relay service. The Nexus does not currently have a remote operator use-case; Alexander operates locally. This decision can be revisited when fleet/remote-agent use-cases mature (see issues #672–#675).
|
||
|
||
---
|
||
|
||
## Why not Option C (Browser pseudo-terminal)?
|
||
|
||
A JavaScript REPL cannot run arbitrary shell programs, manage processes, or provide the raw interactive shell experience that operators expect. It would be a toy, not a tool.
|
||
|
||
---
|
||
|
||
## Rejected Patterns
|
||
|
||
- **tmux over WebSocket** — adds server-side state complexity without enough benefit at this scale
|
||
- **ttyd** — external binary dependency; overkill for a local-first single-operator setup
|
||
- **xterm.js + websocketd** — external binary dependency; `server.py` already owns the WebSocket gateway
|
||
|
||
---
|
||
|
||
## Consequences
|
||
|
||
- `server.py` now starts two WebSocket servers: `PORT` (8765, main gateway) and `PTY_PORT` (8766, shell gateway)
|
||
- `PTY_PORT` always binds to `127.0.0.1` regardless of `NEXUS_WS_HOST`
|
||
- `cockpit-inspector.js` loads xterm.js from CDN (offline fallback: graceful degradation — the rail renders without the terminal pane)
|
||
- PTY sessions are ephemeral (no persistence across browser reload or server restart)
|
||
- Future: add TIOCSWINSZ resize support; add `/pty?shell=zsh` query param selection
|
||
|
||
---
|
||
|
||
## Related
|
||
|
||
- `cockpit-inspector.js` — implements the browser side (xterm.js + WebSocket)
|
||
- `server.py` — implements `pty_handler()` and `_get_git_status()`
|
||
- `docs/ATLAS-COCKPIT-PATTERNS.md` — documents source patterns adopted
|
||
- Issues: #1695, #686, #687
|