Files
tines/docs/superpowers/specs/2026-10-24-tines-fleet-console-design.md

231 lines
11 KiB
Markdown

# tines fleet console + simulated fleet — design
Date: 2026-10-24
Status: approved by Kyle (2026-10-24) — decision C: add
`NodeEvent::Contact`/`NodeEvent::Lost` to core (see "The how")
## Intent
A desktop GUI (new `tines-gui` crate) that operates tines nodes in two
containerized modes:
1. **Bluetooth**: a container with the host Bluetooth adapter passed
through runs the full robot stack; its node talks to real tines
robots over BLE.
2. **Simulated fleet**: containers on the host run the full stack with a
new IPC-bus transport (new `tines-bus` crate); they exchange tines
frames with each other over a host bus.
The GUI is a console: a launcher screen plus one window per container.
Each window exposes the whole node control surface — observe (including
addressed), belief, store listing, events (contacts, drops, confirmed
deliveries), status — so tines can be exercised without hand-rolled JSON.
## Why
- Testing tines today means hand-crafting length-prefixed JSON to a Unix
socket; contacts and custody can only be watched in stderr.
- The two-device BLE check (`tines-bt/README.md`) has never been
recorded; a BT container plus a console makes it a routine manual test.
- The only exchange transports are BLE (real radio) and tines-sim's
in-process memory transport; nothing lets separate processes exchange
frames. A simulated fleet on one machine needs a host IPC transport.
- No runtime container image exists (`Dockerfile.ros2` is a CI
build-check).
- Kyle wants to test tines functionality and interact with tines-enabled
robots (hexapod on a Jetson running ROS2, Pi-class robots).
## Goals
- G1. `tines-gui`: an egui/eframe Wayland app following this desktop's
conventions (the `wayland-gui` skill): a launcher, plus one GUI process
per container.
- G2. BT mode: a containerized full stack with host Bluetooth passthrough;
the container's node is a real radio peer for robots in range.
- G3. Sim mode: a new `tines-bus` host transport crate over a host IPC
bus; containers plug in and exchange frames; fleet size is
operator-chosen.
- G4. The `tines` service config gains a transport choice (`bt` default,
`bus`); existing config files keep working.
- G5. Widened service IPC: addressed observe (`to` + `lifetime_ms` via
`Node::observe_to`), contact/lost events, dropped-frame events, and a
store listing; the existing four operations are unchanged.
- G6. GUI windows expose every node operation; the docker lifecycle
(create, start, stop, remove, logs) is managed from the GUI.
- G7. A runtime container image in the repo (new Dockerfile); both modes
run from the same image with different runtime flags.
## Non-goals
- `tines-core` changes limited to one additive pair (decision C, approved
2026-10-24): `NodeEvent::Contact`/`NodeEvent::Lost` variants, so the
console's event feed can show live contacts. Nothing else in core
changes: it stays `no_std`, and the 1.0 branch is unaffected (the enum
is non-exhaustive). The bus is a host crate implementing the existing
`Transport` trait; the IPC widening is in the `tines` service crate.
- No `tines-sim` changes: the simulator remains the protocol test
harness; the simulated fleet runs real services over the bus transport.
- No ROS2 integration this round: containers run the service (Pi-class
load). `tines-ros2` is untouched; a ROS2-backed container is future
work.
- Not a fleet deployment tool: the console is for development, testing
and interacting; systemd units and production deployment stay out of
scope.
- No node in the GUI process: the `Node` is not `Send` and the GUI never
hosts one; it only speaks IPC to containerized services.
## Anti-goals
- No weakening of the trust boundary: the widened IPC keeps the existing
posture — malformed input gets an `Error` response, never a panic;
frame caps stay; hex-on-the-wire stays.
- No silent config breakage: `transport` defaults to `bt`; existing
configs parse unchanged; unknown keys are still rejected.
- No radio in the GUI process; no BLE access outside the BT container.
- No X11: eframe features `glow`/`default_fonts`/`wayland` only.
- No docker SDK dependency: docker is driven as a subprocess (CLI).
## Constraints
- AGENTS.md applies unchanged: spec-first, TDD with
`sh scripts/ci.sh` before commit, conventional commits. New crates go
in the workspace and the AGENTS.md crate layout.
- The `Node` is not `Send`; the service's inline poll architecture is
unchanged. `tines-bus` is a `Transport` implementation and must not
require `Send` of the node (same as `BtTransport`).
- The bus carries the core's wire frames (`MAX_FRAME` = 1500 and friends
are unchanged).
- Docker is available on this desktop; the GUI shells out to it.
- The console follows `wayland-gui` conventions: theme watch on
`~/.local/state/omarchy/current`, `vsync = false`, opaque window
painted with the theme background.
## The how (decided with Kyle)
### Core change (decision C, approved 2026-10-24)
Add `NodeEvent::Contact` / `NodeEvent::Lost` variants to core, emitted
from the exchange's `on_event` when the transport reports a contact
starting or ending. The enum is non-exhaustive and the variants ride the
existing drop-oldest event queue, so this is additive and changes no
wire format. This is the only core change in this work.
### Modes
- **Launcher**: two choices — Bluetooth (real robots) and Simulated
fleet. From it: create containers (name them, choose or generate node
ids), see the running fleet, open any node's window.
- **One GUI process per container** (Kyle: "one GUI instance per
container"): the launcher spawns a child process of the same binary in
node-window mode, so a hung container can't freeze the fleet view.
- **BT mode**: `docker run` privileged, host network, D-Bus access
(bluer/BlueZ needs the system bus and the hci device). Service config:
node id, peer (the service still takes one — F3 remainder; the GUI
exposes exactly what the service supports), log path on a named
volume, socket path on a mounted directory. The GUI connects to the
socket on the host filesystem.
- **Sim mode**: the launcher starts a bus daemon on the host, then N
containers, each running the service with the bus transport and the
socket directory mounted. GUI windows drive each node.
- **Bus access control** (wntrmute's call): the bus daemon's socket lives
in a per-fleet directory under the operator's XDG state directory,
created `0700`. Same-user local processes are already inside the trust
boundary (THREAT_MODEL); the bus adds no new posture.
- **Status strip transport** (wntrmute's call): the launcher knows the
transport because it wrote the container's config; it passes it to the
window process. `Status` on the wire is unchanged.
- **Addressed panel in BT mode** (wntrmute's call): the panel stays in
every window — `observe_to` is a valid node operation whatever the
transport. BT windows show a hint that the transport currently
exchanges with exactly one configured peer, so an addressed
observation to anyone else will sit until its lifetime expires.
### The bus transport (`tines-bus`, new crate)
- Implements `Transport` with `Addr = NodeId`, exactly like `tines-bt`:
frames are the core wire frames; a frame's declared sender is checked
against the bound peer (same address-spoofing rule).
- **Topology** (wntrmute's call; Kyle can override): hub-and-spoke. A
small bus daemon (a bin in the crate) binds a Unix socket in a
per-fleet directory on the host; each node connects once. The bus fans
every frame out to all connected nodes — a shared medium, like radio —
and reports joins/leaves so the transport can emit `Contact`/`Lost`.
- **Peers**: the bus transport enforces core's `MAX_PEERS` (8), as
tines-bt does; an empty allowlist means any bus peer (the bus is a
test medium, so it drops bt's single-peer F3 limit).
- **Framing**: length-prefixed core frames over the socket; no new wire
format.
- **Contact model**: any frame from a bus peer is an implicit contact
(core already treats unknown addresses that way); daemon disconnect →
`Lost` for every peer.
### Service changes (`tines` crate)
- **Config**: new optional `transport` key, `"bt"` (default) or `"bus"`;
with `bus`, a `bus_socket` path key. `peer` stays required for `bt`;
for `bus` it is an optional allowlist (empty = any peer).
- **IPC widening** (backward-compatible: new tagged variants; the
existing four operations are untouched):
- `Observe` gains optional `to` (hex `NodeId`) and `lifetime_ms`
(present together or not at all) → `Node::observe_to`; without them,
the existing `observe` path.
- `Events` also returns contact/lost events (peer hex + kind) and
dropped-frame events (reason + count), alongside confirmed
deliveries. This changes the current D3 surfacing (drops to stderr
only) so the console can show them; the stderr line stays.
- New `List` request → the stored observations (id, observer, topic,
payload, addressed destination, age), capped so a response fits the
1 MiB frame cap.
- `Status` unchanged.
- Trusted-time behavior is unchanged; the console shows service stderr
via `docker logs`.
### The GUI (`tines-gui`, new crate)
- **Window layout**: status strip (node id, next seq, stored count,
transport, socket path); observe panel (topic, payload, optional
recipient + lifetime, broadcast default; text mode with a hex toggle —
the wire is hex, the operator should not have to hex-encode by hand);
belief panel (query a topic; list known topics from `List`); store
browser (`List` view with refresh); events feed (contacts, lost,
delivered, dropped, live); container panel (`docker logs` tail,
stop/remove/restart).
- **Config generation**: the launcher writes each container's service
config JSON (matching the `deny_unknown_fields` schema), generates
random node ids unless the operator supplies them, and manages
volume/socket paths under a per-fleet state directory.
- **IPC client**: the GUI speaks the widened IPC through a client module
implementing the length-prefixed JSON framing. The request/response
types are public in the `tines` crate (lib + bin), so the GUI can
depend on the lib and share the types (builder's call, leaning yes).
### Docker image
- New `Dockerfile.tines`: builds the workspace, installs BlueZ runtime
dependencies; the entrypoint runs the `tines` service with a mounted
config. Both modes use this image; BT mode adds the privileged/host
network and D-Bus/hci plumbing at run time.
## What's left to the builder
- Exact JSON schemas for the new IPC variants (within the constraints
above).
- Bus daemon protocol details (framing, join/leave messages, fan-out).
- egui layout specifics and the theme integration.
- Docker run flags per mode (device passthrough, volume layout, image
build/tag flow).
- Launcher supervision of child processes and daemons (PID tracking,
cleanup on exit).
## Success looks like
- Sim mode: N containers exchange over the bus; broadcast observations
converge in every node; an addressed observation with custody relay is
watchable in the events feeds.
- BT mode: the container's node exchanges with a real tines robot over
BLE — the two-device check from `tines-bt/README.md` is finally run
and recorded from the console.
- Every operation in the widened IPC is reachable from a window without
hand-rolled JSON.