orrery's README, made a man page by copal-readme-man

ORRERY(1) Copal: the programs in ~/code ORRERY(1)

orrery - The Copal fleet’s console — the wall, the seat, the museum interface, and the verbs that act on a room full of machines

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.

§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.

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.

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.

./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.

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 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.

--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.

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

  • 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