# openalice-pty

> PTY multiplexer microservice — spawns shell sessions and streams ANSI output to xterm.js over WebSocket.

## At a glance
| field | value |
|---|---|
| kind | `service` |
| priority | `P4` |
| default branch | `mvp` |
| last activity | 2026-06-16T01:57:13+00:00 |
| features catalogued | **4** |
| runtime deps declared | **1** |

## Manifest
`.atlas-deps.yml` — parsed cleanly: **yes**

### Runtime deps
- **`openalice-axum-common`** · `other` · via `cargo` — Shared axum plumbing — verify_internal_secret, CORS helpers, and metrics middleware.

### Consumed env
- `INTERNAL_SECRET`
- `PTY_PORT`
- `DEFAULT_SHELL`
- `PTY_RECONNECT_TTL_SECS`
- `OA_PTY_SHELLS`
- `RUST_LOG`

### Exposes
- `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)

## Env drift

**Code reads, manifest omits (1):**
- `INTERNAL_BROADCAST_SECRET`

**Manifest declares, code unused (1):**
- `RUST_LOG`

## Live reachability
- last probe: **up** · `https://pty.blal.pro/health` · HTTP 200 · 0ms
- uptime 24h: **100.0%**

## Features (4)

### pty/control
- **`pty.control.http_dispatcher`** — Agent-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)]  
  *src/main.rs:233*

### pty/session
- **`pty.session.broadcast`** — Drains 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).  
  *src/session.rs:289*
- **`pty.session.spawn`** — Spawn 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()`.  
  *src/session.rs:152*

### pty/transport
- **`pty.transport.websocket`** — Bidirectional 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.  
  *src/main.rs:504*

---

*Generated by [openalice-atlas](https://atlas.blal.pro) — single source of truth for the openalicelabs ecosystem.*
