Review: Peer serving & the fleet
Review of the layer as described, under the
sovereignty litmus. Verdicts cite xenon-launcher main @56e94575; gap IDs
(G1, G2, G5) are this beat’s register and feed
Part IV. The identity-side gaps G3/G4 are held
in the transport review.
The headline
The serving path is membership-blind. Authentication is a 32-byte bearer secret in a ticket file, while a fully built, signed, revocation-capable membership plane sits unwired one crate away. The sovereign “device group” story and the serving story are not yet the same graph — a device removed from the group keeps serving and consuming for as long as it holds a live ticket. Everything else in this review is downstream of that one honest sentence.
What the layer does well, it does with unusual care: fresh secret per session, constant-time comparison, 0600/DACL ticket custody, digest-pinned catalog indexes that never overwrite last-good on mismatch, honest liveness states, and hash-chained audit records on both publish and verify. The craftsmanship is real; the open question is the authorization model, not the plumbing.
Desired vs current
| Want (decisions / packets) | Have (code @56e94575) | Delta |
|---|---|---|
| Peer topology, every platform a full peer (P-206) | Windows publish+consume native (P-206, PR#62); macOS proven (P-208) | Auth is bearer-secret, not the membership plane that already exists — the two graphs are unjoined (G4). |
| Litmus: fork permissions · auditable · exit without loss | Catalog + index publication hash-chain audited; liveness inspectable; tickets 0600/DACL | Route secret has no revocation list, epoch, or bounded lifetime (P-209 STATE). “Fork permissions” for a served origin = restart the publisher — blunt. |
| Preserve restart-as-revocation until a real revocation story exists (P-209 follow-on) | Exactly implemented: fresh secret per session, durable reuse refused (peer_serve.rs:428) | A correct stop, not a design. --rotate-secret is inert scaffolding awaiting the G1 decision. |
| Browser → plane (5454) → internal forward (5198); plane owns Host normalization (d/100, d/102) | Consumer rewrites upstream Host from ticket origin_port (P-209 item 4) | The runtime seam where the plane owns Host normalization is still queued (d/102 follow-up seam). |
Gap register (this beat — serving slice)
G1 — Route-secret revocation / lifetime (primary)
Exists: a fresh 32-byte secret per publish session; no persistence, no
revocation list, no epoch, no expiry (peer_serve.rs:428-433; P-209 STATE is
explicit that persisting would remove restart-as-revocation on a route with
none of those).
Why this needs design: restart-as-revocation is all-or-nothing — any publisher restart strands every consumer — and durable secrets are blocked because no finer-grained revocation exists. The layer is boxed in until this is decided.
Options:
(a) bounded-lifetime ticket (exp field + refresh) — cheap, bounds exposure,
still bearer;
(b) per-consumer secret + a revocation epoch/list in the published index —
real per-peer revocation, needs a distribution channel;
(c) bind serving to existing group membership so group revoke already
cuts the tunnel (reuses MembershipRegistry; collapses G1 into
G4);
(d) UCAN capabilities with exp + revocation (below).
Decides: Luke — litmus- and custody-touching, effectively irreversible once tickets are in the wild.
G2 — Windows exposure-tripwire asymmetry
Exists: on Unix, opening an over-shared ticket refuses with a
chmod 600 instruction (peer_serve.rs:295-302); on Windows, the same
condition is silently repaired — the ACL is narrowed and reading proceeds
(:308-323).
Why this needs design: refusal is a tripwire — “something widened access to your credential” is a signal the operator should see. Repair hides it. P-206 queued exactly this (“repair-vs-reject asymmetry”).
Options: (a) refuse by default on a non-owner ACE, with an explicit
--repair opt-in (Unix-symmetric); (b) refuse + audit the observed ACL;
(c) keep repair but emit a loud audit record.
Decides: Fable (reversible, low blast radius) with a custody re-review — but it wants a taste call.
G5 — Ticket distribution channel
Exists: publish prints only ticket_path= (peer_serve.rs:463); the
operator moves the file out of band. No in-product handoff exists.
Why this needs design: the bearer secret’s entire security rests on the transfer channel — which is undefined. Custody is meticulous at both endpoints and unspecified in the middle.
Options: (a) deliver tickets over the already-authenticated group gossip lane (member→member, in-band, encrypted); (b) QR handoff reusing the invite QR machinery; (c) keep out-of-band and document the threat model honestly.
Decides: Luke/Jan — UX + custody. Note: G4 option (a) + G1 option (c) dissolve this gap for the member-to-member case entirely.
The UCAN question
What is UCAN? UCAN (User-Controlled Authorization Networks) is a capability token format: a signed JWT-like token where the issuer (
iss) and audience (aud) are keys expressed as did:key, carrying attenuable capabilities, expiry (exp), and delegation proof chains (prf).
A UCAN-ified ticket would replace the opaque bearer secret with a signed
capability: iss = publisher key, aud = consumer key (device keys are
already Ed25519, so did:key is free), att = {with: "xe://<origin_id>/<app>", can: "serve/consume"}, plus exp and caveats.
Gives: audience-binding (only the named consumer key can present it — removes the pure-bearer exposure at the heart of G1/G5); bounded lifetime for free; delegation chains with attenuation (publisher → consumer → sub-share); per-app/per-port caveats; a revocation-by-CID story.
Costs: DID resolution + signature verification per session (vs one constant-time compare); an Xe resource namespace and capability semantics must be defined; revocation still needs a distribution channel; net-new machinery.
The tension worth naming: the group membership plane is already a
capability system — a signed document says who may connect, and revocation
closes live connections. Adopting UCAN for serving would duplicate much of
crates/net/group. Verdict this review offers the session: gate serving
on the existing membership plane first (G4 option a); reserve UCAN for the
one thing membership cannot express — delegation to a non-member — and
decide unify-vs-bolt-on before adopting any of it.
Sovereignty litmus check
| Criterion | Verdict for this layer |
|---|---|
| Inspect what it sees | Holds. Liveness states map 1:1 to observations; path receipts printed per tunnel; the 503 for offline peers even admits what it cannot override (a service-worker cache). |
| Fork permissions | Weakest point. One secret per publisher session — no per-consumer grant, attenuation, or revoke. The only permission change is restarting the world. (G1) |
| BYO models + keys | Partial. Keys are local; relays configurable but unsigned on this plane (G3, transport review). |
| Movable memory | Partial. Catalog indexes are cached, digest-pinned, and survive publisher outage (last-good rule) — the view moves; app data itself is single-homed (site-wide, Part IV). |
| Auditable actions | Holds for the catalog (hash-chained publish/verify records). Fails for handoff: ticket distribution is out-of-band and leaves no record (G5). |
| Exit without loss | Consumer: yes — remove the peer, keep your data. Publisher restart strands all consumers until tickets are re-carried (G1) — availability loss, not data loss. |
Questions for the Luke + Jan session
- Unify serving with membership, or keep bearer + add UCAN? (G1+G4+G5)
Gating the serve ALPN on the existing
MembershipRegistryreuses built code and yields real revocation for free; UCAN is heavier and partly duplicates the group plane. Which spine? - Route-secret lifetime — adopt a bounded-lifetime ticket (
exp) now to unblock durable secrets, or wait for full membership gating? (P-209 explicitly parked durable secrets on this.) - Windows tripwire — refuse-by-default on a non-owner ACE
(Unix-symmetric) with
--repairopt-in, or keep silent repair + loud audit? (G2 — reversible, but wants a taste call.)