openalice-pty
[Atlas card](https://atlas.blal.pro/repos/openalice-pty)
Architecture rating
Manifest present+valid (manifest 100), AGENTS.md present, sits in openalice zone with standard docker/Cargo layout; but drift 80 with two env-drift items (RUST_LOG unused, INTERNAL_BROADCAST_SECRET undeclared) keeps it below solid.
Strong manifest (valid, summary, 3 exposes declared) plus AGENTS.md and roadmap 95, but README is a one-line Atlas-card stub and 19 missing docstrings across the source — thin in-code/architecture prose.
Clean AST (code_health 100, 0 bare excepts, 0 cycles) and 100% 24h uptime, but one complexity-24 hotspot and 2 god modules; no evidence of SIGTERM graceful shutdown, PTY session reaping, WS read/write timeouts, or idempotency on the control API.
Owns no tables, no migrations dir, no DB dep (runtime_deps_count=1) — sessions held in-memory by design. No schema/RLS/FK/GDPR signal to grade; conservative score reflects absence of any persistence layer, not a broken one.
/health probe live (200, matched_path=/health) is the only confirmed signal; RUST_LOG is declared-but-unused (drift), suggesting log config not wired. No metrics endpoint, tracing, or structured-log evidence.
Spawns real shells over WS — high-risk surface; only signal is INTERNAL_BROADCAST_SECRET shared-secret, and it's undeclared/undrifted (env_drift_undeclared_used). No evidence of rate-limiting, security headers, origin checks, or SSRF/injection guards on the agent control API.
No tests/ dir and empty migrations/tests presence; inspector reports zero test signal and features=52 flagged as weakest axis. No regression coverage for PTY spawn, WS streaming, or the broadcast-secret auth path.
source · auto-grader-2026-06-16
Ecosystem manifest
.atlas-deps.ymlPTY multiplexer microservice — spawns shell sessions and streams ANSI output to xterm.js over WebSocket.
- via cargoShared axum plumbing — verify_internal_secret, CORS helpers, and metrics middleware.
- INTERNAL_SECRET
- PTY_PORT
- DEFAULT_SHELL
- PTY_RECONNECT_TTL_SECS
- OA_PTY_SHELLS
- RUST_LOG
- docker_image: openalice-pty
- http_endpoint: http://openalice-pty:3995/v1/sessions (agent control API)
- http_endpoint: ws://openalice-pty:3995/v1/sessions/:id/ws (PTY WebSocket stream)
Atlas grepped the repo's source for env reads (rust env::var · ts process.env · py os.environ · shell $VAR) and diffed against the manifest's consumes_env. Two lists below: stale declarations and undeclared reads.
- INTERNAL_BROADCAST_SECRET
- RUST_LOG
Roadmap · what's planned next
- session-reconnect-resumein progress23%Honour PTY_RECONNECT_TTL_SECS by buffering stdout for disconnected clients and replaying on reconnect, so a flaky network doesn't kill a TUI session.
- signal-delivery-m2planned2%Promote /v1/sessions/:id/signal from logged-only to real delivery (INT/HUP/TERM) so agents can actually interrupt long-running TUI processes.
- agent-control-apidone100%Expose POST /v1/sessions, /input, /resize, /signal, and a /v1/shells discovery route so agents can spawn and drive TUI processes without going through the WebSocket.
- prometheus-metricsdone100%Export pty_up, pty_active_sessions, pty_spawned_total, and pty_errors_total on /metrics so the monitoring stack can alarm on stuck or crashing PTY children.
Code composition
tokei · loc- Rust89.6%
- YAML4.2%
- TOML3.3%
- Dockerfile2.9%
- Rust89.6%
- YAML4.2%
- TOML3.3%
- Dockerfile2.9%
- Rust89.6%· 862
- YAML4.2%· 40
- TOML3.3%· 32
- Dockerfile2.9%· 28
| Language | % code | code | comments | blanks | files |
|---|---|---|---|---|---|
| Rust | 89.6% | 862 | 93 | 83 | 2 |
| YAML | 4.2% | 40 | 15 | 2 | 1 |
| TOML | 3.3% | 32 | 16 | 13 | 1 |
| Dockerfile | 2.9% | 28 | 16 | 13 | 1 |
| Markdown | 0.0% | 0 | 90 | 41 | 2 |
Telemetry ribbon · last 90 days
3 eventspty/control
1 feature- pty.control.http_dispatchersrc/main.rs:233Agent-side stdin path: POST /v1/sessions/{id}/input accepts base64-encoded bytes so the openalice-live-control dispatcher can drive a PTY session entirely over HTTP without holding a WebSocket open. Agents and human xterm.js clients share the same Session::write path — the running TUI cannot distinguish keystrokes from the two sources. `POST /v1/sessions/{id}/input` — write bytes to the PTY master. Body: `{"bytes_b64": "<base64>"}`. Decoded bytes flow into `Session::write` exactly like a WS Binary frame would — agents and humans share the same stdin path, so a TUI can't tell whether keystrokes came from xterm.js or the dispatcher. Returns 204 on success, 404 when the session is missing, 400 on malformed base64, 500 on a PTY write error. [derive(Deserialize)]
pty/session
2 features- pty.session.broadcastsrc/session.rs:289Drains the PTY master on a dedicated blocking thread and fans every byte chunk out over a tokio::sync::broadcast channel (capacity 1024). Frames with no active subscribers are silently dropped so the child process never stalls waiting on a full pipe buffer. Blocking read loop: pull bytes off the PTY master and broadcast them. Exits cleanly when the master returns EOF (child exited and its stdout closed).
- pty.session.spawnsrc/session.rs:152Spawn a child process inside a real PTY pair (openpty on Unix, ConPTY on Windows via portable-pty). The blocking read loop starts immediately on a dedicated std::thread; callers get an Arc<Session> ready for WebSocket subscription without any further setup. Spawn a new PTY + child process. On success the read pump is already running on a blocking thread; subscribers attach via `stdout_tx.subscribe()`.
pty/transport
1 feature- pty.transport.websocketsrc/main.rs:504Bidirectional WebSocket handler: outbound pump broadcasts PTY stdout as Binary frames (xterm.js calls Terminal.write() directly on the bytes); inbound pump routes Binary frames to PTY stdin and Text frames to the ControlFrame dispatcher (resize / signal / ping). A 60-second reconnect TTL keeps the session alive after the client drops, enabling seamless browser refreshes without killing the running process. Drive a single client connection: subscribe to PTY stdout broadcast and forward chunks; receive client frames and dispatch them to the PTY (binary → stdin; text → control). Reconnect TTL: when the last subscriber goes away, the session is *not* killed immediately. A delayed cleanup task fires after `reconnect_ttl`; if no new subscriber attaches by then, the session is removed and the child killed.