orrery's README, made a man page by copal-readme-man
| ORRERY(1) | Copal: the programs in ~/code | ORRERY(1) |
NAME
orrery - The Copal fleet’s console — the wall, the seat, the museum interface, and the verbs that act on a room full of machines
DESCRIPTION
An orrery is a clockwork model that shows many bodies at once and lets you turn a handle to see where they will be. That is the wall. The rest of this program grew out of wanting to reach through it.
What it is
§12 of copal-alpine-linux/docs/fleet-plan.md names three faces on one read model. This repository is two of them and then some:
| the wall | orrery --listen | the web console: a page the gallery screen and a phone can open |
| the seat | orrery --seat ID | one node, full screen, in the terminal you are already in |
| the museum interface | orrery --gui | a native window: a room of machines, a row of verbs, and the panes they open |
All three render the same read model, and that model is copal fleet state and nothing else.
The rule it inherits
copal-fleet-console.py’s acceptance test was architectural: it opens no socket, holds no credential, knows no subject names, and every screen is a rendering of a command a person could have typed.
Two of those four are no longer true, and the change was deliberate. The seat opens a socket to one node’s screen; the museum interface opens SSH, SFTP and RDP with the operator’s own certificate. What is intact is the part that mattered: how the console learns things. Every node id, address and fact comes out of copal fleet state; a node that is not in that document cannot be dialled, and there is no second way of knowing anything. A console that discovered nodes for itself would be a second fleet.
Two postures
Read-mostly is a default, not a ceiling. A gallery screen and a phone on a lanyard are two audiences with different rights.
| gallery | default | State, thumbnails. No verb that changes anything. |
| operator | --operator TOKEN | The write verbs as well. |
Without a token the write route does not exist rather than answering 403 — a route that says “forbidden” tells a scanner it is there. In the native window the same rule takes a different shape: the gallery posture’s dispatch table is empty, so a keystroke that would have been a verb finds nothing to call.
The museum interface
./target/release/orrery --gui --demo --operator x
A room of machines. Click tiles, or click a scene or a tag in the rail to select everything running it. The bar under the lab shows the verbs that can apply to that selection, and the footer says how many are not built.
| verb | what it opens | |
| Control c | one node | the node’s desktop, over RDP with mutual TLS |
| Terminal t | one node | a shell, in a pane, with no password asked |
| Exchange e | one node | a two-pane file browser over SFTP |
| Send s | many | one file to every selected node, with a per-node result |
| Message m | many | a banner on every selected screen |
| Run r · Scene S | many | the CLI’s own verbs, per node |
| Snapshot k · Power p | many | as above, and power on is the supply rather than ssh |
| Observe o | — | refused, and §5 of docs/console.md says why |
A verb with no transport is absent rather than greyed: the status line says how many, and the bar never draws a button that cannot work. There is one pane at a time and never a window inside the window.
Cmd-1 and Cmd-2 switch between the Lab and Media, which is the SD-card ledger: which card carries which token, which have been written, and a pane that runs copal-prep.sh on a real terminal so that its two typed ERASE confirmations happen where they always did. Nothing is answered on the operator’s behalf and there is no --yes.
--frame PATH renders any of it to a PPM and exits — no compositor, no window, no node. Every screenshot in docs/ is a file this produced.
The wire
Five protocols, written by hand, each proved against somebody else’s implementation rather than against a fake of its own assumptions.
| proved against | ||
| src/crypto.rs | the primitives | published test vectors — RFC 7748, 8032, 8439, FIPS 180-4 and 197, the NIST GCM cases |
| src/ssh.rs | SSH-2, certificates only | a real sshd in a container, plus a hand-written server for the lies a real one will not tell |
| src/sftp.rs | SFTP v3, pipelined | OpenSSH’s own sftp-server over a pipe — no fake at all |
| src/tls.rs + src/x509.rs | TLS 1.3, one suite | openssl s_server, including mutual TLS and an AES-only server being refused |
| src/rdp.rs | RDP over TLS | xrdp — 1024×768, 137 rectangles, all of them RLE compressed |
src/profile.rs is the whitelist: one key exchange, one host-key algorithm, one signature algorithm, one cipher offered. A test fails the build if sha1, cbc, rc4, ssh-rsa or ecdsa ever appears on a list, and orrery --profile-sshd prints the same list as sshd_config lines — which copal-prep.sh writes into the node and make lint checks for drift. A whitelist that exists in a Rust file and again in a shell heredoc is two whitelists.
The host key must be a certificate signed by the fleet CA, naming that node, valid now, with the far end proving it holds the key. There is no trust-on-first-use, no prompt, and no bare-key branch to fall back to.
The seat
The wall is eight nodes at a glance. The seat is one node, full screen, in the terminal you are already in.
./target/release/orrery --seat museum-01
| Observe | o | the node’s screen, read-only |
| Control | c | keyboard and pointer as well; Ctrl-] lets go |
| Exchange | e | not built here — the native window has it |
| Terminal | t | not built here — the native window has it |
It renders with half-blocks (U+2580 and 24-bit colour) anywhere, and with sixel where the terminal has it. --fps asks the node for a frame rate; the default is 6, because §III-B’s finding is that frame rate is the expensive axis.
The seat speaks RFB security type none only — VNC’s own authentication is 56-bit DES and an eight-character password, which is not security — and asks for Raw and CopyRect only, because every compressed encoding wants zlib.
The native window, underneath
--gui is a Wayland client on a node and an AppKit window on a Mac, behind one Surface trait. The drawing is all software and there is no renderer underneath it: a shared-memory surface is a flat array of pixels, so draw.rs writes them. On a Zero 2 that is the right trade twice — a wall of flat rectangles and small text costs almost nothing to rasterise, and the EGL stack it avoids is both a dependency and a thing that fails differently on every board.
The letters are baked in. src/font.rs carries two faces of the X.Org misc-fixed font, generated from the PCF by tools/mkfont.py and committed, because copal-build on a node must not need Python or a font package. Past Latin-1 exactly seven characters are baked, each one the console actually prints — and a test fails the build if the interface reaches for an eighth.
The session word decides the protocol, and that used to be unresolved. /etc/copal/session holds one word: x11 means x11vnc and RFB, wayland means an RDP server and rdp.rs. Neither is a fallback for the other. The console reads the node’s own remote reading rather than probing a port — off, not installed, vnc:5900 and rdp:3389 are four different sentences.
Running it
cargo build --release # the lab report's museum, from a fixture -- no fleet required ./target/release/orrery --demo # the wall, on a port ./target/release/orrery --gui --demo --operator x # the museum interface # against a fleet ./target/release/orrery --listen 0.0.0.0:8080 ./target/release/orrery --gui --operator x --copal ~/code/copal-alpine-linux ./target/release/orrery --seat museum-01 # the node's sshd policy, rendered from src/profile.rs ./target/release/orrery --profile-sshd cargo test
The suites that need somebody else’s server run in containers, one script each, and they step aside when what they need is absent:
docker run --rm --platform linux/arm64 -v "$PWD":/w -w /w \
-e CARGO_TARGET_DIR=/tmp/t rust:alpine sh /w/tools/wl-check.sh # weston # ... /w/tools/ssh-check.sh # sshd # ... /w/tools/tls-check.sh # openssl # ... /w/tools/rdp-check.sh # xrdp
What it does not do, deliberately
- The wall does not proxy VNC. §III-B priced eight VNC sessions on 512 MB boards that are also computing and refused them. The page hands the browser the node’s own server; a console that tunnelled pixel traffic would put itself in the exhibit’s frame budget.
- It does not cache state. copal fleet state is the answer, and a cache is a second place the fleet’s state lives and a thing that goes stale.
- It does not offer Control on a selection. Broadcasting keystrokes to eight machines produces divergent state nobody can see; the operation actually wanted is Scene or Run, which are declarative and report per node.
- It never mints a credential. The fleet CA signs; this console distributes and verifies. A program holding both would be a program worth stealing.
- It does not decode H.264. A video codec has no small correct subset and libavcodec is not on an Alpine node, so RDP negotiates plain bitmaps with RLE. docs/wire.md §6 has the arithmetic.
| 2026-09-11 | orrery |