Skip to content

feat(trios-server): UART endpoint at /api/uart for remote serial-port access - #1082

Draft
gHashTag wants to merge 1 commit into
mainfrom
feat/uart-endpoint
Draft

feat(trios-server): UART endpoint at /api/uart for remote serial-port access#1082
gHashTag wants to merge 1 commit into
mainfrom
feat/uart-endpoint

Conversation

@gHashTag

@gHashTag gHashTag commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Adds three HTTP endpoints under /api/uart on trios-server to give a remote agent controlled access to the host's USB-serial adapters. Motivation is direct: two of three P201Mini boards ship with an identical MAC/hostname/IP image (see docs/LOCAL_FLASH.md Β§1.4), and their boot state can only be diagnosed through a serial console. Paired with tri-tunnel (Tailscale Funnel), this endpoint lets a cloud agent read the UART on the host Mac without opening SSH.

Endpoints

Method Path Purpose
GET /api/uart/ports List serial ports with metadata (no port opened).
GET /api/uart/stream SSE stream of base64-encoded bytes read from a port.
POST /api/uart/write One-shot write of base64-encoded bytes.

Full contract, event schema, and shell examples: docs/UART_ENDPOINT.md.

Security

  • Endpoint gated by a separate bearer token (TRIOS_UART_TOKEN), not TRIOS_API_KEY. Git access must not automatically grant serial-port access.
  • Fail-closed: if TRIOS_UART_TOKEN is unset, the router is not mounted at all. Startup logs UART endpoint DISABLED β€” set TRIOS_UART_TOKEN to enable /api/uart/*.
  • Global auth middleware bypasses /api/uart/* (see security.rs change), so clients only present one credential; UART's own middleware then enforces the UART token.
  • Base64-only body for /write β€” control characters (Ctrl-C, Ctrl-A, ANSI escapes) survive JSON without shell-escaping games.

Design decisions

  • Read = SSE, Write = one-shot. Keeps server state minimal; concurrency is easy to reason about; clients compose stream + write on their side.
  • Bounded broadcast channel (capacity 1024) for the reader. Slow clients see explicit lag events instead of silent drops.
  • Dedicated OS thread for the sync serialport reader. Auto-stops when all SSE subscribers disconnect (no fd leaks).
  • 30s idle timeout with heartbeat idle events so clients can distinguish "port silent" from "connection dead".
  • Generic router state: router<S>() so it can be nested into Router<AppState> without UART handlers knowing about AppState. UART handlers are stateless.

Tests

39/39 unit tests pass locally (cargo test -p trios-server --bin trios-server):

  • router_disabled_without_token β€” fail-closed guard
  • router_enabled_with_token
  • base64_write_body_roundtrip
  • control_chars_survive_base64 β€” Ctrl-C, Ctrl-A, ESC[B survive
  • default_baud_is_115200
  • env_var_name_is_stable β€” regression guard on TRIOS_UART_TOKEN name
  • test_uart_paths_bypass_global_auth_prefix in security.rs

Existing 32 tests untouched.

Deliberately NOT included

  • No convenience write-then-read endpoint β€” compose on client.
  • No multi-viewer port sharing β€” serialport opens exclusive; if needed, put fan-out in front locally.
  • No RTS/CTS/DTR flow-control knobs β€” defaults match P201Mini U-Boot/Linux console. Extend query params later if a target needs otherwise.

How to try locally

export TRIOS_UART_TOKEN="$(openssl rand -hex 32)"
cargo run -p trios-server

# in another shell:
curl -s http://127.0.0.1:9005/api/uart/ports \
  -H "Authorization: Bearer $TRIOS_UART_TOKEN" | jq

curl -N -s "http://127.0.0.1:9005/api/uart/stream?port=/dev/cu.usbmodemXXXX&baud=115200" \
  -H "Authorization: Bearer $TRIOS_UART_TOKEN"

Files changed

  • crates/trios-server/src/uart.rs β€” new module, 414 lines
  • crates/trios-server/src/main.rs β€” mount /api/uart router if token set
  • crates/trios-server/src/security.rs β€” bypass /api/uart/* in global auth
  • crates/trios-server/Cargo.toml β€” add serialport 4.7 (no default features) + base64 0.22; enable tokio-stream/time
  • docs/UART_ENDPOINT.md β€” new, 173 lines

Anchor: phi^2 + phi^-2 = 3.

… access

Adds three HTTP endpoints under /api/uart, fail-closed on TRIOS_UART_TOKEN:

  GET  /api/uart/ports   list serial ports (metadata; no port opened)
  GET  /api/uart/stream  SSE stream of bytes read from a port (base64-encoded)
  POST /api/uart/write   one-shot write of base64-encoded bytes

Motivation
==========
See docs/UART_ENDPOINT.md and docs/LOCAL_FLASH.md sec 1.4. Two of three
P201Mini boards ship with an identical MAC/hostname/IP image, and their
boot state cannot be diagnosed without a serial console. This endpoint,
paired with tri-tunnel (Tailscale Funnel), lets a remote agent read the
UART on the host Mac without SSH.

Security
========
- Uses a separate bearer token (TRIOS_UART_TOKEN) from TRIOS_API_KEY.
  Git access should not automatically grant serial-port access.
- The router is not mounted at all when TRIOS_UART_TOKEN is unset
  (fail-closed default). Startup logs the enabled/disabled state.
- Global auth middleware bypasses /api/uart/* so clients only need one
  credential; UART's own middleware then enforces the UART token.

Design notes
============
- Read = SSE with a bounded broadcast channel (capacity 1024). Slow
  clients see explicit `lag` events instead of silent drops.
- Write = one-shot to keep server state minimal; compose with stream on
  the client side.
- All wire bytes are base64 so control chars (Ctrl-C, Ctrl-A, ANSI
  escapes) survive JSON transport without shell-escaping.
- Idle timeout 30s per stream; SSE emits `idle` heartbeat.
- Reader lives in a dedicated OS thread (serialport is sync) and stops
  when all SSE subscribers disconnect.

Tests
=====
- router_disabled_without_token       (fail-closed)
- router_enabled_with_token
- base64_write_body_roundtrip
- control_chars_survive_base64
- default_baud_is_115200
- env_var_name_is_stable
- test_uart_paths_bypass_global_auth_prefix (in security.rs)

39/39 unit tests pass.

Deliberately NOT included
=========================
- No convenience write-then-read endpoint (compose on client).
- No multi-viewer port sharing (serialport opens exclusive).
- No RTS/CTS/DTR flow-control knobs (defaults match P201Mini console).

Anchor: phi^2 + phi^-2 = 3.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant