Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-launcher at main @56e9457 (crates/launcher/assets/route-worker.js, crates/launcher/src/origin.rs, boot_service.rs), plus the round-2 review doc agent54-hq/docs/serving-design-review-round2.md.

Desired vs current vs gap — the shape of it

DimensionDesiredCurrent (as-built)Gap
App reachone memorable origin per apphttp://<app>.localhost:5454, three route typesnone — shipped, d/100
Isolationbrowser-enforced same-origin, no ambient CORSsame-origin floor + per-request/per-chunk grantsnone — shipped
Transport schemeplain http(s) for regular apps (d/101)https-by-default with name-constrained CAlive tension — G3
Port custodyPrism can trust what it framesadvisory flock, first-come bindG2
Whole-origin (compose) appsUI is a plane origin like any appbackend-proxy empty-digest overload (P-203)route-type ratification — G1 / d/103-B
Multi-user hostworks for more than one OS usersingle global 5454G4
Table evolutionsafe, versioned schema growthadditive + fail-closed, no explicit migrationG5
Upstream errorsdistinguish plane-can’t-reach vs upstream-erroredone synthesized 503, no 502open 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)
Shapeempty digest + prefix:"/" on a backend-proxy routeadditive 4th OriginRouteType variant
Frozen schema (P-148)untouchedtype discriminator unchangedis the schema change requiring ratification
Semanticsoverloads “empty digest means whole-origin”clean invariant: backend-proxy stays static-digest + proxied-prefix
Statusmerged 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 doctor path 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 localhost secure-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.

CriterionStatusEvidence / caveat
Inspect what it seesStrongEvery 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 permissionsStrongCross-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 + keysN/A hereThe plane serves origins; model/key custody lives in the agent layer and engines.
Movable memoryPartialApp 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 actionsStrongSupervisor 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 lossStrongStatic 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

  1. 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 localhost secure-context treatment and drop the CA ceremony for regular apps? The code and d/101 currently disagree — which wins?
  2. 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?
  3. G1 / d/103-B (Luke): ratify the container-origin type, or bless the backend-proxy empty-digest overload as the permanent documented shape? P-210 is gated on this.
  4. 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)?
  5. G5 / versioning (Luke): confirm additive-only + fail-closed as the route-table evolution rule under the freeze, and whether the first type addition should introduce explicit schema-version negotiation rather than relying on 404-on-unknown.
  6. The no-502 question (orchestrator → Luke if copy changes): should the contract distinguish 503 (plane can’t reach upstream) from 502 (upstream errored)?

Port drift note. serving-design-review-round2.md predates 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.