Review: The serving plane
This is the honest ledger for the serving plane: where the desired design and the shipped implementation agree, where they are in live tension, and where the design is genuinely unfinished. It is written to be disagreed with — each gap names options, tradeoffs, and who decides.
Read the chapter first. The serving plane describes what is built. This review assumes it. Citations are
agent54/xenon-launcheratmain @56e9457(crates/launcher/assets/route-worker.js,crates/launcher/src/origin.rs,boot_service.rs), plus the round-2 review docagent54-hq/docs/serving-design-review-round2.md.
Desired vs current vs gap — the shape of it
| Dimension | Desired | Current (as-built) | Gap |
|---|---|---|---|
| App reach | one memorable origin per app | http://<app>.localhost:5454, three route types | none — shipped, d/100 |
| Isolation | browser-enforced same-origin, no ambient CORS | same-origin floor + per-request/per-chunk grants | none — shipped |
| Transport scheme | plain http(s) for regular apps (d/101) | https-by-default with name-constrained CA | live tension — G3 |
| Port custody | Prism can trust what it frames | advisory flock, first-come bind | G2 |
| Whole-origin (compose) apps | UI is a plane origin like any app | backend-proxy empty-digest overload (P-203) | route-type ratification — G1 / d/103-B |
| Multi-user host | works for more than one OS user | single global 5454 | G4 |
| Table evolution | safe, versioned schema growth | additive + fail-closed, no explicit migration | G5 |
| Upstream errors | distinguish plane-can’t-reach vs upstream-errored | one synthesized 503, no 502 | open question |
The good news up top: the isolation core is strong and shipped. Same-origin by default, zero ambient cross-origin access, a Fetch-Metadata CSRF floor, grants that are re-read per request and per streamed chunk, a backend a grant can never reach, and a route discriminator that fails closed on anything unknown. The open items below are the edges around that core — trust of the port, the scheme on loopback, and how the table grows — not the core itself.
The gap register
G1 — Whole-origin route type ratification (d/103-B)
What exists. P-207 proved a sound Immich compose package but hit a real
limit: the route model could not express “the whole UI is a container service.”
d/103 splits this. Item A — the rendering model — is DECIDED: a compose
app’s UI is a whole-origin proxy route on the plane
(http://<app>.localhost:5454), framed by Prism exactly like every other app —
no per-app controlled-frame special-casing, no swbn/COI/CSP. This follows
directly from d/100 + d/101. Item B — the route type — is PROPOSED, pending
Luke.
Why it needs design. Two fair shapes, and they trade schema-conservatism against semantic clarity:
| backend-proxy overload (shipped by P-203) | container-origin new type (proposed) | |
|---|---|---|
| Shape | empty digest + prefix:"/" on a backend-proxy route | additive 4th OriginRouteType variant |
| Frozen schema (P-148) | untouched — type discriminator unchanged | is the schema change requiring ratification |
| Semantics | overloads “empty digest means whole-origin” | clean invariant: backend-proxy stays static-digest + proxied-prefix |
| Status | merged as-is (schema-conservative, tested, receipt-backed) | optional refinement for Luke to ratify or reject |
The ruling in place. d/103’s amendment (2026-07-18, after P-203 review):
P-203’s overload merges as the documented shape; container-origin stays an
optional refinement. If Luke rejects it, the overload is the shape and P-210
builds on it. P-210 is gated — it dispatches only after P-203 merges, Luke
ratifies (or rejects) item B, and the connector socket custody is reconciled
(the stale BackendProbe::Ready(SocketAddr) TCP assumption must move to the
d/094 unix-socket shape). It is an opus-lane, review-required packet with a
reviewer-run live Immich end-to-end.
PROPOSED — pending Luke. Ratify the container-origin type, or bless the
backend-proxy empty-digest overload as the permanent documented shape.
Decides: Luke (frozen-schema addition).
G2 — Port custody / local-trust boundary
What exists. The plane binds 5454 first-come. The only mutual-exclusion is
an advisory flock under the data root (boot_service.rs:938-951, used by
up.rs:178). The round-2 review is explicit (NEW-6): this “protects against a
second cooperating Xe, not against a hostile or buggy local process that binds
first.” If the plane is down, another same-UID process can bind 5454 and be
framed by Prism as if it were the plane. There is also an IPv4/IPv6 split
hazard — 127.0.0.1 and ::1 are separate binds, so one can be held while the
other is free.
Why it needs design. This touches the sovereignty litmus directly (inspect what it sees, auditable actions) and Prism’s trust in what it frames. A shell that frames a squatter has been quietly compromised without any transport being broken.
Options.
- (a) Accept same-UID loopback DoS as v1-scoped. The review’s stated
position: a same-UID actor “can already DoS loopback.” Harden by binding both
IP stacks atomically and failing closed if either is held, plus a
xe doctorpath that names the PID holding the port. Cheap; does not close framing-a-squatter. - (b) Prism verifies the plane’s identity before framing it. A pinned serving leaf / mTLS / a handshake secret handed to Prism at launch. This is the strongest control — it closes framing-a-squatter outright. Open sub-question: where does the secret handoff live (boot service?).
- (c) OS-assisted reservation of the port. Platform-specific, heaviest.
Decides: Luke + Jan (security / litmus → propose, Luke disposes).
G3 — HTTPS-on-loopback
The live tension with d/101.
What exists. The launcher defaults to https with a short-lived leaf from
a name-constrained local CA (origin.rs:585); plain http is gated behind
XE_SERVING_HTTP_DEV=1 (origin.rs:819-830). Meanwhile d/101 frames
regular apps as “just http/s,” and every proof that worked to date ran
plain-HTTP guests inside the frame. This is a genuine, unresolved tension in
the tree — the code says https-default, the decision says plain-http-default —
and it must be stated as such, not smoothed over.
Why it needs design. Round-2 NEW-7: name-constraint enforcement holds only
on enforcing Chromium builds; a non-browser local client (e.g. curl trusting
the OS store) is unprotected by a leaked leaf. And RFC 6761 + Secure Contexts
already treat localhost as potentially-trustworthy — so is TLS strictly
needed on loopback?
Options.
- Keep TLS. Secure-context parity for any code that does not special-case
localhost; a real cert chain. Backstop the enforcement gap with a per-OS release gate + short leaf lifetimes. Cost: CA ceremony for every regular app, cutting against d/101’s “no packaging friction.” - Plain http + rely on the browser’s
localhostsecure-context treatment. Removes the CA ceremony for regular apps. Cost: loses protection for non-localhost-aware clients and any future non-loopback bind. The dev-signed leaf already exists, so either direction is reachable from here.
Decides: Luke (litmus / security). This is the top session question on this beat.
G4 — Multi-user machines / per-user plane
What exists. 5454 is a single global loopback port; round-2 §8 scoped v1 to “one Xe per user.” Data roots are already per-user, but the port is fixed, so two OS users on one host collide on 5454.
Why it needs design. Shared workstations and fast-user-switching.
Options. Per-user port derivation (hash of UID into a range), with the origin string carrying it — but the origin is the storage/service-worker key, and SEQ-1 flags it as the single most expensive thing to change later; or first-user-wins + an honest doctor for the second user.
Decides: Luke + Jan.
G5 — Route-table versioning / migration under the freeze
What exists. Schema tags xe/origin-routes/v0, xe/peer-route/v0,
xe/origin-grants/v0; unknown route types fail closed; no explicit v0→v1
migration path.
Why it needs design. The first real type addition (container-origin) or
field change needs a defined upgrade story — an old plane reading a new table,
and vice-versa.
Options. Additive-only + fail-closed (the current de-facto rule), or
explicit schema-version negotiation + a migration journal — there is already a
migrations vector precedent in the tombstone file (origin.rs:67).
Decides: orchestrator for reversible field additions; Luke for any
type addition (the P-148 freeze).
The no-502 question
Not a numbered gap, but a deliberate design choice worth ratifying. There is
no 502 today. Upstream backend/peer failures are synthesized as 503 (a
plane-side “can’t reach” condition), and a genuine upstream 5xx passes through
unchanged. If the honest-error contract wants to distinguish “the plane could
not reach the upstream” (503) from “the upstream itself errored” (502),
that is a deliberate addition to the contract — not a bug to be patched
silently. Decides: orchestrator (contract refinement), surfaced to Luke if it
changes user-visible copy.
Sovereignty litmus — the six criteria
The serving plane against the litmus (from CLAUDE.md): honest scoring, gaps named.
| Criterion | Status | Evidence / caveat |
|---|---|---|
| Inspect what it sees | Strong | Every route is a plaintext row in routes.json; every refusal is a text/plain body naming the cause; /xe/api/catalog exposes the merged view. Caveat: G2 — a squatter on 5454 could be inspected-as-the-plane. |
| Fork permissions | Strong | Cross-origin access is only ever a user-consented grant, re-read per request and per chunk, revocable live (guardStream); no ambient CORS to fork around. |
| BYO models + keys | N/A here | The plane serves origins; model/key custody lives in the agent layer and engines. |
| Movable memory | Partial | App bytes are content-addressed (digest) and portable; but the origin string is the storage/service-worker key (SEQ-1) — moving to a per-user port (G4) would move the memory key, which is expensive. |
| Auditable actions | Strong | Supervisor emits serving_plane_failed; workerd binary is size+SHA-256 verified before run; route generation is deterministic and diff-able. Caveat: no per-request audit log surfaced yet. |
| Exit without loss | Strong | Static apps are just verified byte bundles on disk; no proprietary bundle format is required for regular apps (d/101). Nothing gatekeeps export. |
The plane earns strong marks on the criteria it owns. Its one litmus-relevant weakness is G2 (trust of the port), and its one movability caveat is G4 / SEQ-1 (origin-as-key).
Session questions for Luke + Jan
- G3 / TLS-on-loopback (Luke): keep https-by-default with the
name-constrained CA (accepting Chromium-build-dependent enforcement, NEW-7),
or lean on the browser’s
localhostsecure-context treatment and drop the CA ceremony for regular apps? The code and d/101 currently disagree — which wins? - G2 / port custody + framing (Luke + Jan): accept same-UID loopback squat/DoS as v1-scoped, or make Prism verify the plane’s identity before framing (pinned leaf / handshake secret)? If the latter, where does the secret handoff live — the boot service?
- G1 / d/103-B (Luke): ratify the
container-origintype, or bless thebackend-proxyempty-digest overload as the permanent documented shape? P-210 is gated on this. - G4 / multi-user (Luke + Jan): is one-Xe-per-user-per-host an acceptable v1 boundary, or do we need per-user port derivation now — knowing the origin string is the storage/service-worker key and the most expensive thing to change later (SEQ-1)?
- G5 / versioning (Luke): confirm additive-only + fail-closed as the
route-table evolution rule under the freeze, and whether the first
typeaddition should introduce explicit schema-version negotiation rather than relying on 404-on-unknown. - The no-502 question (orchestrator → Luke if copy changes): should the
contract distinguish
503(plane can’t reach upstream) from502(upstream errored)?
Port drift note.
serving-design-review-round2.mdpredates d/100 and reasons about:5196. Every finding it raises — NEW-6 squatting, NEW-7 CA enforcement, §8 custody — transfers directly to:5454. Only the number changed.
Candidate specs distilled from this layer — the Plane Origin Convention, Route Table v1, and the Honest-error contract v1 — are collected in Part V.