The Big Picture
Read this first. It holds the shape of the whole system on one screen. Everything else in this book is a zoom-in on a piece of what follows.
What the Xe Computer is
The web has apps but no computer. In a browser you can open an app, but you cannot really install it, own its identity, own the trust chain that says it is what it claims to be, compose two apps together, or sync your own state across your own devices. A native operating system gives you all of that — install, identity, trust, composition, sync — but it is closed: the vendor owns the enclosure.
Xe Computer is the missing layer between the browser and the OS. It makes web apps first-class citizens of a machine the user actually controls. The one-line north star is “a universal app package manager for the web” — but sovereign: you can inspect it, fork it, and leave it without losing anything.
The wider bet: within a few years something will act in every person’s name across their whole digital life. The only question that matters is whose side it is on. The platform answer is the closed super-app. Xe Computer is the open, user-sovereign alternative — the super user agent. What that commits us to is spelled out in Mission & principles; read that next, it is the lens for the rest of the site.
How it fits together, in one pass
Every one of your devices runs its own Xe Computer instance. There is no central hub — devices are peers that sync to each other. On each device:
- A small Boot Service wakes the runtime on demand, then lets it sleep. Nothing heavy runs when you are not using it.
- The runtime stands up a local plane bound to
127.0.0.1:5454. Every app you have — installed locally or served by one of your other devices — appears at a stable web address likehttp://<app>.localhost:5454. Because browsers treat*.localhostas a secure origin, each app gets exactly the isolation the web already provides. No per-app packaging required. - Behind the plane sits an origin router — a small, pinned web server (workerd, Cloudflare’s runtime for serving code) whose routing table is generated from your installed-app records, never hand-edited. Apps that need a real backend reach an app engine (a hidden container/VM layer) through a socket the engine itself publishes.
- Prism is our shell: a super-app that frames each app in an isolated
Controlled Frame (a web embedding
surface stronger than an
<iframe>), composes them, and journals every action an agent takes. darc → Reagent is the resident agent layer that acts through Prism, with the boundary, grants, and audit trail we build around it. - Xenon is our browser — a fork of Helium (itself a de-Googled Chromium) that makes these apps work without developer flags. Until Xenon ships, any Chromium-family browser runs the same apps via a flag recipe.
Devices connect as peers over iroh (a peer-to-peer transport that dials a device directly by its public key). One device can serve an app to another, byte-for-byte; a phone companion is a remote control and consent surface, not a sync peer. Trust runs top to bottom from our forge and a signed catalog — install checks the signature and content hash before anything runs; the console witnesses the receipts.
The whole system, one diagram
Edge and node labels mark what is proven today versus designed-only. Legend: ● proven (verified in the 2026-07-18 build-and-run matrix) · ◇ designed-only / blocked (has a design of record, not yet demonstrated end to end).
flowchart TB
legend["● proven (2026-07-18 matrix) · ◇ designed-only / blocked"]:::note
subgraph DIST["Forge + Distribution — Agent54 (cluster-internal, d/065)"]
FJ["Forgejo forge — origin of record (d/007, d/013)"]
CAT["Signed catalog<br/>Ed25519, serial-anchored (d/067)"]
FJ --> CAT
end
subgraph DEVA["Device A — Xe Computer instance (macOS)"]
direction TB
BSA["Boot Service — on-demand wake (d/053)"]
PLANEA["Local plane 127.0.0.1:5454<br/>origins http://<app>.localhost:5454 (d/100)"]
RTA["Origin router = workerd, pinned + launcher-managed (d/078)<br/>route type: Origin / backend-proxy / container-origin* (d/103)"]
ENGA["App engine — smolvm / colima / docker (d/051)<br/>backend via engine unix socket (d/094)"]
FWDA["peer forward :5198 — internal (d/102)"]
BROWA["Xenon (Helium fork, IWA-enabled, d/061)<br/>—or— stock Chromium + flag recipe (d/057)"]
PRISMA["Prism — super web app / shell (d/063, d/075)<br/>Controlled Frames + agency ladder (d/082, d/091)"]
DARCA["darc → Reagent — resident agent (d/062)"]
CONA["Console — witness: receipts + update ledger (d/064)"]
BSA --> PLANEA --> RTA
RTA --> ENGA
RTA --> FWDA
BROWA --> PRISMA
PRISMA -->|frames plane origins| PLANEA
DARCA -->|acts through| PRISMA
CONA -.->|reads journal ◇ open seam| PRISMA
end
subgraph DEVB["Device B — Xe Computer instance (Linux)"]
PLANEB["Local plane :5454"]
RTB["workerd router"]
FWDB["peer forward :5198"]
PLANEB --> RTB --> FWDB
end
PHONE["Phone companion — remote client + consent surface,<br/>NOT a sync peer (d/042, d/062)"]
CAT -->|"install: verify sig + digest ● proven"| RTA
CAT -->|install ●| RTB
FWDA <-->|"iroh peer serving ● byte-identical (P-184)"| FWDB
RTA <-->|"peer-sync v1: metadata only ◇ designed (d/068)"| RTB
PHONE -->|"plane binds + iroh direct ● (P-202)"| DEVB
PHONE -.->|"in-frame render ◇ blocked on d/104"| PRISMA
classDef note fill:#f5f5f5,stroke:#bbb,color:#333,font-style:italic;
What is proven vs designed-only
Drawn from the build-and-run matrix in status/2026-07-18-night.md; the full table
lives in the proving matrix.
| Capability | Status | Evidence / what gates it |
|---|---|---|
| Serve and consume an app on Linux, macOS, and Windows | ● proven | Night matrix, all three OSes (Windows in a native VM) |
| All four app kinds: static, live-backend, single-page app, compose | ● proven | Fleet catalog merged and exercised |
| Real production app inside Prism’s frame | ● proven | Excalidraw runs the whole peer pipeline |
| Two-device story end to end | ● proven | Device B publishes over iroh; payload arrives byte-identical (matching sha256); source drop shows an honest offline notice; recovery on return |
| Android consume (on-device plane) | ● proven | App renders on-device; cross-origin isolation verified on the emulator |
| Android in-frame render + in-frame offline honesty | ◇ blocked | Pending the PROPOSED d/104 launch-seam decision |
| Peer-sync v1 (two desktops, metadata only) | ◇ designed | Design of record landed; not yet wired |
| Compose whole-origin route (e.g. an Immich UI) | ◇ gated | Pending the PROPOSED d/103-B route-type decision |
| Xenon fork artifact | ◇ designed | Not yet built; today = stock browser + flag recipe |
| Console reads Prism’s journal (same device) | ◇ open seam | No wiring yet; named in the system spine |
How to read this site
This book is a zoom ladder. Each part descends one level: big picture → layers → subsystems → open questions. You can stop at any rung.
- The Big Picture (you are here) — the whole system on one screen.
- Part I — Mission & Vision — the why and the rules to judge every design against: the mission and sovereignty litmus, and the decisions ledger recording what we chose and why. Read Part I before Part II — it is the lens.
- Part II — The Stack — the layers, bottom to top: transport & identity, the serving plane, peer serving, the app model, engines & backends, the shell (Prism/Xenon), devices, and the agent layer. Each layer chapter opens with a short “What is X?” primer, so you do not need to know iroh, Isolated Web Apps, or Controlled Frame going in.
- Part III — Design Reviews — a critical read of each layer against the litmus: what is strong, what is thin, where the seams are.
- Part IV — The Design Frontier — the honest gaps. Where the mission demands something no decision covers yet (movable memory, exit-without-loss, multi-user), it is recorded here as first-class work, not hidden. The External Review (sol) section that follows Part IV holds the six verbatim memos from an independent frontier-model critique; the frontier’s External review input lines point into them.
- Part V — Emerging Specs — protocol and format candidates as they firm up.
- Appendix — a glossary, a repo & link directory, and the proving matrix (the full ●/◇ table).
A note on honesty: proven and designed-only are kept visibly separate everywhere, and decisions still awaiting Luke’s sign-off are marked PROPOSED wherever they appear. If something is aspirational, the text says so.
Mission & principles
This chapter is the lens. Every layer, review, and open question in the rest of the book is meant to be read against the commitments set out here. If a design cannot be checked against the sovereignty litmus below, that is itself a finding.
The mission: the super user agent
The name carries the whole thesis in two old meanings:
- super-user — in the Unix sense, root over your own computing. Full authority on your own machine.
- user agent — in the original web sense, software that acts for you, on your behalf, under your direction.
Put together: a piece of software that acts across your whole digital life, with full authority, and is unambiguously on your side.
The premise is simple and near-term. Within a few years something will act in every person’s name across their entire digital life — booking, buying, corresponding, remembering, deciding. That much is not in question. The only question that matters is whose side it is on. The dominant answer being built today is the closed super-app: an agent inside a vendor-owned enclosure, assembled out of open pieces (Chromium, Electron, webviews) but sealed so the user cannot see in, change the rules, or leave with their data. The counter-move — the mission — is an open, user-sovereign delegation layer.
The missing-layer thesis
Stated concretely: the web has apps but no computer. A browser can open an app but
cannot let you truly own its install, its identity, the trust chain that vouches for
it, the composition of one app with another, or the sync of your state across your own
devices. A native OS has all of those, but closed. Xe Computer is the missing layer
between the browser and the OS — it gives web apps first-class citizenship on a
machine the user actually controls. The working north star, from docs/state.md, is
“a universal app package manager for the web”: install, run, trust, compose, and
sync web apps, on infrastructure you own, on any of your devices.
The sovereignty litmus
The mission is not a slogan; it is an operating constraint with six clauses. Every design in this book is checked against them, and where current code fails a clause, that is tracked as debt rather than glossed over. An agent is yours only if you can:
- Inspect what it sees — you can look at the context, inputs, and state the agent acts on.
- Fork its permission model — you can change the rules of what it may do, not just toggle vendor-provided switches.
- Bring your own models and keys — you are not locked to one provider’s model or one provider’s credentials. (This is a live litmus test: hardcoded model IDs anywhere in the code are a known violation and tracked.)
- Move its memory — your state and data are portable, not held hostage.
- Audit its actions — every action leaves an inspectable trail.
- Leave without losing everything — exit is possible without forfeiting your data, your identity, or your history.
Three of these six are only half-met today, by our own honest accounting, and they
lead Part IV — The Design Frontier: movable memory
(app data is single-home; sync moves metadata, not the data inside apps),
exit-without-loss (xe export exists as intent, not a proven whole-machine
round-trip), and audit (some grant/trust journals sit outside the hash-chained audit
log, so the console cannot yet witness them). The site names these plainly rather than
implying the litmus is fully satisfied.
Power Design
Power Design is the conduct standard. It binds all output of the system and the team’s own conduct — it applies to the racks and the fleet, not only to user interface. An agent that touches a machine’s baseboard management controller is held to a stricter audit standard than one that restyles a page. Its clauses:
- Honest — no performed confidence; name uncertainty; if something is aspirational, say so.
- Inspectable — state is visible and provable, not asserted.
- Zero-trust — the system assumes nothing and proves its own state rather than trusting that it is correct.
- Audit + undo — anything done unattended leaves an audit trail and a persistent undo.
- Doesn’t belittle — the user is treated as capable; no condescension.
- Minimal — the smallest surface that does the job.
One binding rule reaches all the way down into copy: every failure string ends with an action. A user (or a reviewer) who hits an error is always told what to do next.
Calm vocabulary
The product uses a small, canonical vocabulary. It matters here because words carry the architecture — the names below are used precisely throughout the site, and the distinction between an app’s install level and an agent’s rung (the two axes below) depends on keeping them straight. The full list lives in the glossary; the load-bearing names:
- Xe Computer — the cross-platform runtime (home: xe.computer). Not “the launcher.”
- Xe Computer Boot Service — the on-demand wake mechanism; zero resident processes. Not a “daemon,” not an “agent.”
- Xe Computer console — the management UI.
xe— the CLI. - Xenon — our Isolated-Web-App-enabled
Chromium fork (the browser), not the runtime. (Repo names like
xenon-*are historical.) - app engine — the substrate layer inside Xe Computer that runs backends. The concrete engine names (colima, lima, docker, smolvm, WSL2) never appear in user copy.
- apps — web apps on the spectrum below. “Install an app,” never “install a bundle.”
- super web app / super user agent — an app granted fabric-level capabilities; interchangeable terms, and never framed as “privileged.”
- Reagent — the coming rename of darc. Luke and Jan execute the rename; new copy should not bake “darc” in. Until then this site writes darc → Reagent.
- Folknet — the community compute fabric that backs the fleet.
A style note the site inherits from doctrine/vocab.md: we describe design work with
calm words — probe, stress-test, critical review — never the adversarial register.
The two capability axes
This is the most important conceptual tool in the system, and the easiest to get wrong. There are two separate axes, and they move independently.
Axis 1 — the install spectrum (a property of the app)
What is this app granted? Defined by d/076. Every surface Xe renders is a web app on some level of one spectrum — there are no cliffs, no separate “native app” class:
| Level | Meaning |
|---|---|
| L0 | Visited — opened, not installed. |
| L1 | Installed — has a home, an icon, a stable origin. Installed unmodified and un-Xe-aware: this is a hard promise. Any design that requires an app to “integrate with Xe” to install violates the model. |
| L2 | Capability-enhanced — granted extra platform capabilities on request. |
| L3 | Super — the Controlled-Frame-hosting, agent-grade tier. |
Axis 2 — the agency ladder (a property of the agent’s action)
What is my agent doing on this surface, right now? Defined by d/082 and revised in d/091. It runs from doing nothing up to being the shell itself:
nothing → allowed / denied / shielded → observed → enhanced → mutated → controlled → driven → composed / generated / delegated → super user agent
A few rungs to know: shielded means the agent actively guards a surface (a first-class state — permission denied is not an error, it is a valid outcome); driven means the agent operates a live surface over the Chrome DevTools Protocol (the same wire protocol browser automation uses); generated composition means the agent reads pane A and pane B and produces a new artifact C. Every rung is consented, receipted, and undoable.
The two axes move independently. An app’s install level and the agent’s rung are orthogonal — a plain L1 app can be driven or composed by your agent; a super L3 app can just sit there being observed. No new named level joins either axis without a decision. Keeping these apart is what prevents the system from collapsing back into “privileged native apps vs sandboxed web pages.”
The two-tier app model
This is the load-bearing recent decision, d/101, and it is what keeps the install spectrum from turning into a store-like gate. Apps come in two tiers:
- Regular apps (the default, overwhelmingly). Ordinary web apps served plain
http(s)at the device plane originhttp://<app>.localhost:5454, framed and isolated by Prism’s Controlled Frame. No signing, no bundle format, no manifest requirement. The isolation you would otherwise pay for with per-app packaging is already delivered by the browser’s same-origin policy plus the Controlled Frame. - Super web apps / super user agents (rare, opt-in). The Signed-Web-Bundle / Isolated-Web-App tier, reserved for shells that themselves need IWA-class powers — Controlled Frame hosting and agent-grade device surface. Prism is the canonical example.
The reasoning matters: requiring every app to be packaged as an Isolated Web App would rebuild exactly the store-like gate the litmus rejects. So an ordinary app never needs IWA packaging to be published, listed, or loaded. (This is current truth as of d/100/d/101/d/102/d/103; earlier serving decisions that assumed HTTPS-by-default and a different port are lineage, and the decisions ledger says so explicitly.)
Primer — Isolated Web Apps (IWA) & Controlled Frame: an
Isolated Web App is a Chromium
mechanism for a web app packaged and signed as a self-contained bundle, running in a
stronger isolation context than an ordinary tab, and permitted to use powerful
capabilities. A Controlled Frame
(spec) is an embedding element available
inside an IWA — a much stronger, fully partitioned cousin of the <iframe> — that
lets a host app (Prism) frame and mediate another app. These are the primitives the L3
tier is built on; the rest of the site’s shell chapter goes deeper.
Peer topology
Multi-device is peer, not hub-and-spoke (d/062). Every device runs its own Xe Computer instance; there is no central server that owns your fleet. State syncs between instances as a first-class deliverable — the sync layer is not an add-on, it is part of the computer. This is called out as the costliest steer to reverse once sync formats ship, so format decisions are flagged loudly wherever they appear. A phone is a companion — a remote client and consent surface — not a sync peer (d/042).
Internet Archive & the DWeb context
The public stakes are raised by who is watching. The Internet Archive and the decentralized-web (DWeb) community are the first audience — Brewster Kahle has seen the work and the concept resonated (d/041); the backing is “warm.” The near-term community also includes the User & Agents circle. This is why the litmus is not negotiable marketing: the audience that would use this first is precisely the audience that will check whether you can inspect, fork, bring your own keys, move your memory, audit, and leave. The decisions ledger records how each of these commitments was turned into a concrete choice — read it next.
The decisions ledger
This is a curated index of the load-bearing decisions — roughly thirty of the ~104 on
record — grouped by the layer they govern. Each entry gives, in one line: what it
decided · why · and its reversal condition where that is notable. The full decision
files live in decisions/NNN-*.md in the base repo; this ledger is the map, not the
territory.
How to read this ledger
Three conventions carry a lot of weight, so read them first:
- Current truth vs lineage. The app-model and serving spine moved decisively in
mid-July 2026. Present d/100 / d/101 / d/102 / d/103 as current truth (plane on
127.0.0.1:5454, plain-http default, super-apps opt-in). Earlier serving decisions d/078 and d/080 are lineage — superseded on the port and the HTTPS-by-default ruling. Where they conflict, the higher number wins; the ledger says so at each entry. - Proposed markers. Two decisions are PROPOSED / pending Luke and gate real work: d/103-B and d/104. They are flagged inline and collected in Proposed / pending Luke below. Nothing downstream of them dispatches until Luke disposes.
- Tension with code. A short list of decisions currently in tension with the code that is supposed to implement them is kept honest in Decisions in tension with code.
Foundational / mission
- d/041 — Dogfood Xe as the daily driver; unsigned personal packages allowed behind strong consent. Decided: run Xe as Luke’s main OS on a fresh machine; self-signed / AI-authored personal packages are first-class, gated by a strong consent surface (not a gatekeeper). Why: real use beats demo; the safety layer is consent, not a store. Catalog-source signature enforcement is unchanged. The Internet Archive / Brewster backing is “warm.”
- d/062 — Delivery scope: peer topology, mobile both-track, resident agent, weeks-not-months. Decided: multi-device is peer (not hub-and-spoke); mobile ships in two tracks (v1 phone-as-remote-client, v2 Android runtime); darc → Reagent is the resident agent layer; the P0 line is the IA/DWeb circle on mac + Linux, in weeks. Reversal: peer topology is the costliest steer to reverse once sync formats ship — flag format decisions loudly.
- d/068 — Finish line = the demo-day bar; peer-sync v1 is metadata-only. Decided: “done” means Luke and Jan can run install → open → wake → update on a fresh mac and a fresh Linux box from the quickstart alone; peer-sync v1 scope is source pins + serial anchors + receipts/ledger only — volumes stay single-home. Why: a checkable, unambiguous finish line; a deliberately narrow first sync.
Runtime / browser
- d/008 — Xenon is a fork of Helium. Decided: our browser forks
Helium (a de-Googled Chromium); GPL-3.0
inherited; patches layered in
patches/xenon/; build order Linux → macOS → Windows → Android; no GitHub Actions (Folknet is the build infra); Android is a separate greenfield track. The Isolated-Web-App gates are three Chromium flags to flip on desktop. Why: start from a maintained de-Googled base, add only what IWAs need. - d/061 — Helium is the confirmed Xenon base. Reversal: if Helium stalls against IWA needs, fall back to a vanilla Chromium fork.
- d/057 — Browser provider matrix; Xenon’s value is “IWAs without flags.” Decided: the launcher runs IWAs in stock Chromium-family browsers via a CLI-flag recipe; Xenon’s differentiator is that IWAs work without flags, not exclusivity; browser choice is a user-visible setting. Reversal: if Chromium breaks the flag path, flagless Xenon becomes primary.
- d/003 — Blessed install path = launcher + bundled Helium. Decided: the narrated primary path is the launcher with bundled Helium; own-Chrome is the secondary path. Empirically IWAs install on every engine ≥ 140; older engines fail; unmanaged install is silent but two-step (install, then launch).
- d/053 — Naming and Boot Service architecture; darc → Reagent rename decided. Decided: the canonical names (runtime / Boot Service / console / Xenon / app engine) and a zero-resident-process Boot Service; the darc → Reagent rename is decided but executed later by Jan. Open: the IWA → Boot Service wake-channel mechanism is still undecided — an unauthenticated loopback TCP socket would be disqualifying by our own standard (see the design frontier).
App model / spectrum
The July-16 wave that set the current app model. See Mission & principles for the two axes these encode.
- d/075 — “Super web app” is a class, not a product. Decided: no Prism-privileged APIs; every surface Prism uses must be a grantable platform contract; super web apps are interchangeable like browsers or window managers; darc is the proof of concept, Prism the first platform-native instance.
- d/076 — The app spectrum L0–L3. Decided: one substance (a web app), one axis (granted capability), no cliffs; L1 = installable unmodified and un-Xe-aware as a hard promise. Why: if installing required “integrate with Xe,” the model would be broken. First real-app target: Patchwork.
- d/082 — The agency ladder. Decided: the rungs from nothing up to super user agent; every rung is consented, receipted, and undoable; permission denied is a first-class state, not an error.
- d/091 — Agency ladder v3. Decided: the driven rung goes live over the Chrome DevTools Protocol; composition becomes generative (pane A + pane B → new artifact C); the canonical demo is a live re-skin.
- d/081 — Xe-generated IWA wrapper as a native-window shim. Decided: an Xe-generated IWA wrapper can give an L1/L2 app its own window, icon, and dock presence; the wrapped app stays unmodified and gains no capabilities. Status: recorded, not scheduled; watching Chromium’s IWA sub-apps API.
Serving plane / origins
- d/077 — Each installed app gets its own subdomain off localhost. Decided: apps
present at
<app>.localhost, origin-isolated locally and over iroh; composition panes load origin-served subdomain apps, not other IWAs. Why: use the web’s own origin isolation instead of inventing one. - d/078 — Serving ratifications (partly lineage — see note). Decided: the router
is workerd, pinned and launcher-managed, with
a route table generated from installed-app records;
<name>.localhostnaming with collision suffixing; no cross-app CORS, ever (same-origin policy is the isolation model); router privacy default (lifecycle events only, no per-request logging); every device runs its own router, iroh connects the routers. Note: its ingress:5196is superseded by d/100/d/102; the rest stands. - d/080 — Local app origins over HTTPS via a name-constrained local CA (partly
superseded). Decided: local origins served
https://<name>.localhost:5196via a launcher-managed, name-constrained local certificate authority (DNS.localhostonly). Status: the HTTPS-by-default ruling is narrowed by d/101 (plain-http default); the name-constrained-CA machinery remains valid for cases that need it. - d/092 — Bundle-header policy: honor-by-default. Decided: the platform parses an
app’s
_headersfile (the Netlify/Cloudflare grammar) and applies it to that app’s own origin; the one refusal class is headers whose effect would escape onto sibling apps or the platform (e.g. a parent-scopeSet-Cookie Domain). Apps run unmodified. - d/079 — swbn/IWA packaging reserved for L3; L1/L2 delivered as catalog rows. Decided: signed web bundles are for L3 Controlled-Frame apps only; L1/L2 apps ship as a signed catalog row + content-addressed static payload; example apps live in their own monorepo, never inside a super-app’s repo (that would re-privilege them, forbidden by d/075).
- d/100 — The Xe local plane port is
127.0.0.1:5454. Decided: the plane binds5454(Agent54 → 54:54); guest origins arehttp://<app>.localhost:5454; it is a local-only consume plane. Reversal: collision with common software → it stays configurable. Current truth. - d/101 — Two app tiers; plain http(s) is the default. Decided: regular apps are served plain http(s) and isolated by the Controlled Frame; swbn/IWA is reserved for super-apps. Why: isolation is already delivered by the Controlled Frame, so a per-app IWA rebuild would just be a store-like gate against the litmus. Load-bearing — the current app-model spine.
- d/102 —
5454is the user-facing plane; the internal peer forward stays5198. Decided: the plane origin is5454; the internal workerd → iroh plumbing keepsPEER_FORWARD_PORT = 5198— using the same value causes an address collision in the integrated fleet path. Why: a “fix” that merely narrowed the failing test was rejected; the port split is the real fix. Current truth. - d/103 — Compose-driver app UIs are plane-origin proxy routes. Decided (Item A):
a compose app’s UI is a whole-origin proxy route on the plane, framed by Prism like
any app — not a per-app Controlled-Frame target. Proposed (Item B — see below):
whether that becomes a distinct
container-originroute type or overloads the shippedbackend-proxytype. Note: a code comment on the rendering model still contradicts this (see tension list); reconciliation is part of the gated implementation.
Isolation / backends / engine
- d/028 — Untrusted workloads get a VM boundary (amended 2026-07-18). Decided:
third-party / untrusted workloads run behind a VM boundary; weaker rootless containers
must be labeled visibly (an
isolation_classfield). Amendment: the standalone microVM workstream is retired and rescoped onto the smolvm-backend track (d/051 + d/094); the VM-boundary-for-untrusted principle stands. - d/094 — Backends reached through a socket the app engine itself publishes. Decided: the backend connector consumes a host unix socket provided by the app engine — no per-app sidecar, no loopback TCP; smolvm gets the capability first as an upstream-quality contribution. Why: fewer moving parts, a cleaner trust boundary; retires the earlier sidecar options.
- d/050 — macOS packaging discipline. Decided: never
codesign --deep; sign inside-out; two-layer pinning (download pins in git, installed pins stamped after signing); only Mach-O binaries in code locations, data trees under Resources. - d/027 — UCAN-compatible delegation semantics. Decided: capability delegations
carry UCAN-compatible semantics (signed,
attenuable, decentralized chains, attenuation-only), while invocations and receipts
follow a Xenon profile; the full UCAN wire format is deferred, with the profile
documented in
ucan-mapping.md. Primer — UCAN: User-Controlled Authorization Networks, a way to express “you may do X on my behalf, but no more than X” as a signed, chainable token that needs no central server to verify.
Transport / peer
- d/014 — iroh terminates in a native local worker; self-host the relay before
production. Decided: iroh runs in a native local
worker (not inside the IWA); the public n0 relays are dev-only; a self-hosted
iroh-relayis required before production; iroh-blobs first. Primer — iroh: a peer-to-peer transport that dials a device directly by its public key, using relays only to help two peers find each other and to fall back when a direct connection is not possible. - d/046 — Minimal iroh fork with a single visibility commit. Decided: our
agent54/irohfork is upstream + one commit exposing a network-change hook; explicitly temporary. Reversal: upstream the commit, then repin vanilla iroh.
Distribution / forge / governance
- d/007 / d/025 — Forgejo on the NAS is the forge of record. “The forge is home” — the git host we own is the origin of record.
- d/013 — Forge-first push policy. Decided: agents never push to GitHub; GitHub pushes are Luke-approval-gated, per push. Why: work products live on infrastructure we control.
- d/098 — HARD RULE: no AI drive-by PRs to external repos, ever. Decided: agents never open PRs on non-Agent54 repos; humans publish contributions; the agent role stops at a reviewed branch + drafted PR text. Why: precipitated by an external PR withdrawn the same day; publishing is a human act.
- d/067 / d/064 / d/065 / d/066 — The trust chain. Publishing is a signed static catalog (d/067, Ed25519, serial-anchored); the console is the witness (d/064); distribution stays cluster-internal until Luke flips it public (d/065); key custody: the orchestrator holds catalog/release seeds for the build phase, with the production key ceremony pending (d/066). Primer — Ed25519: a widely used public-key signature scheme (RFC 8032); a serial anchor ties each catalog release to a monotonically increasing number so rollbacks are detectable.
- d/099 — Retire
docs/map.html. Decided: the old structural map is replaced bydocs/state.md(narrative) and an orchestration-board artifact (structure). This documentation site is the successor review surface.
Mobile
- d/042 — iOS companion: full member and management surface, app-opening delegated. Decided: the iOS companion is a full group member and consent surface (consent-on-the-go, Devices/Activity, notifications) sharing the Rust core via Swift FFI; the IWA / Controlled-Frame shell is the platform gap, so opening apps is delegated to Safari. Android companion is the parity target. Note: iOS is client-only by Apple policy, not by our architecture (see the design frontier).
Proposed / pending Luke
These are not yet decided. They are recorded here because downstream work is gated on them and the review session must dispose of them.
- d/103-B — Distinct
container-originroute type, or overload the shippedbackend-proxy? Security-critical: it widens the connector boundary from a single/apiprefix to an entire third-party app’s origin. Gates P-210 (the compose / Immich UI). Luke ratifies at the P-210 charter. - d/104 — The Controlled-Frame guest launch seam = the catalog launch descriptor. The proposal: the guest a frame launches is resolved from a runtime/catalog-owned launch descriptor (app label → plane origin) on every platform — no per-app baked guest URLs. Escalated when an installed Android Prism could not repoint from its baked guest to the plane origin. Unblocks Android in-frame render and offline honesty. Luke disposes; nothing dispatches until ratified.
Also awaiting Luke, though not numbered decisions: the authed-wake questions (AW-1..AW-5) and the d/053 wake-channel mechanism.
Decisions in tension with code
Kept honest so the review does not mistake intent for reality:
- d/080 / d/078 (HTTPS-default,
:5196) vs d/100 / d/101 / d/102 (plane:5454, plain-http default). The scheme moved from HTTPS-default to plain-http-default and the port from5196to5454. d/080’s name-constrained-CA machinery is still valid, but its “HTTPS is the product default” ruling is narrowed, and d/078’s:5196ingress is superseded. The site presents d/100/d/101/d/102 as current truth and d/078/d/080 as lineage. - d/103-A rendering-model contradiction (inside the decision). A code comment on the
MarkUirecipe operation still says “Controlled Frame UI” (a CF-per-app intent), while d/100/d/101/d/103 mandate plane-origin rendering. Reconciliation is part of the gated compose implementation. - d/094 vs launcher code. A
BackendProbe::Ready(SocketAddr)path is stale TCP and must be reconciled to the engine-unix-socket contract; the related sidecar PR is held pending this. - d/028 amendment residue. Any code or doc still describing a standalone firecracker/microVM lane is stale — read d/051 + d/094 as the living successors.
The review agenda
Ten questions for the Luke + Jan review session, each with one paragraph of context. The Big Picture and this ledger are the reading for these. (These ten are the ledger-level cut; the consolidated running order for the sitting — these plus every Part III session question, de-duplicated — is Part IV’s review agenda.)
-
Ratify d/104? The Controlled-Frame guest launch seam as a catalog launch descriptor. Ratifying unblocks Android in-frame render, offline honesty, and the dependent mobile work. The alternative on the table is an explicit Prism/IWA launch contract with install-time guest manifests. This is the single architectural proposed item blocking the mobile in-frame story.
-
Rule on d/103-B. A distinct
container-originroute type versus overloading the already-shippedbackend-proxytype. This decides whether the frozen route-table schema changes, and it gates P-210 (the Immich compose UI). It is security-relevant because the container-origin path exposes a whole third-party app origin through the connector, not just an/apiprefix. -
Movable memory and exit-without-loss — commit a format now, or stay single-home? Two sovereignty-litmus clauses are only half-met today: app data is single-home (sync moves metadata, not the data inside apps), and
xe exportexists as intent, not a proven whole-machine round-trip. Deciding whether to commit a per-app export/import format now is a litmus-touching call for Luke. -
Schedule the production key ceremony. The trust chain is closed in code, but the production signing keys do not exist yet — the orchestrator holds only dev/build-phase seeds. The offline split-custody ceremony (Luke + Jan) gates all public distribution. When does it happen — after Thailand?
-
Pick the Boot Service wake-channel mechanism. d/053 named the IWA → Boot Service wake channel but left the mechanism and its authentication open; an unauthenticated loopback TCP socket is disqualifying by our own standard. Candidates: loopback + a per-install token; IWA Direct Sockets with an authenticated handshake; or OS socket activation (launchd/systemd/Windows service triggers). A design packet should precede implementation.
-
Multi-user / groups / sharing — in near-term scope, or explicitly post-camp? Everything today is one user’s own device group under a single-writer group doc. There is no decision covering sharing an app or data with another person, multi-writer docs, or multiplayer. The DWeb / local-first audience will ask first; the current plan defers it (“recruit the CRDT people at camp”). Confirm the deferral, or scope it.
-
Confirm the HTTPS-vs-plain-http local-origin truth. Confirm that d/101’s plain-http default fully supersedes d/080’s HTTPS-default ruling for regular apps, and that the name-constrained CA is reserved for the cases that actually need it — so the site and the code present one current truth rather than two.
-
darc → Reagent rename timing. The rename is decided (d/053) but executed by Luke + Jan. Does the review-session material and near-term copy say “Reagent” now, or keep “darc” until the rename is actually performed?
-
Schedule the self-hosted iroh relay. d/014 requires a self-hosted
iroh-relaybefore any production peer traffic; the public n0 relays are dev-only by policy. This needs a human packet (a container + DNS + TLS) before production peer serving. -
Approve journal unification. Some grant/trust records are standalone JSON-lines logs sitting outside the hash-chained audit journal, so the console’s Activity view cannot witness them — an inspectability-litmus gap. Approve folding them into the hash-chained journal as a work packet?
Transport & identity (iroh)
This layer is how two Xe Computer devices find each other, prove who they are, and move bytes — with no server in the middle deciding who may talk to whom. Everything above it (peer serving, the fleet catalog, remote engines) rides on the primitives described here.
As-built truth. Code citations are repo-relative in
agent54/xenon-launcheratmain @56e94575(“P-215: deterministic router conformance tests”). Thexenon-*repo name is historical; the product is the Xe Computer (see Glossary). Where design and code diverge, this chapter says so and points at the design review.
Primer: the words this chapter assumes
What is iroh? iroh is a peer-to-peer networking library (from the n0 / number0 team) built on QUIC. Each node has an Ed25519 keypair; the public key is the node’s address. iroh dials by key, hole-punches through NATs where it can, and falls back to relays when it can’t. Docs: iroh.computer/docs.
What is QUIC / ALPN? QUIC (RFC 9000) is an encrypted, multiplexed transport over UDP; its handshake is TLS 1.3 (RFC 9001). ALPN (RFC 7301) is the handshake field that names which application protocol a connection speaks — Xe uses one ALPN string per plane, so a single device key can serve several distinct protocols without confusion.
What is a relay? A rendezvous-and-fallback server: peers register their presence, coordinate hole-punching through it, and — when a direct path fails — carry their (still end-to-end encrypted) traffic through it. See iroh’s relay docs.
What is Ed25519? A signature scheme (RFC 8032). One 32-byte seed yields a signing key; in Xe that one seed is simultaneously the device’s identity and its network address.
Zoom 1 — one key, three planes
A single 32-byte Ed25519 seed is both the Xe device identity and the iroh
signing key — the launcher identity seed is fed directly into iroh via
SecretKey::from_bytes (crates/net/src/iroh_provider.rs:41-45, :94-104).
On top of that one key, three planes move bytes today:
- App-serving plane (peer-serve). A publisher binds an iroh endpoint and forwards each authenticated tunnel to a named local TCP port. Authentication is a 32-byte bearer secret carried in a ticket — not group membership. This is the plane the browser actually talks through. Detail: Peer serving & the fleet.
- Membership plane (group). A fully built, signed, hash-chained
group-document system with a join ceremony, head gossip, revocation, and
restart anti-rollback (
crates/net/src/group/). It is not wired into the serving plane — the honest seam this book keeps returning to. - Fleet catalog plane. No new protocol at all: an immutable JSON app index travels as ordinary HTTP over the already-authenticated serving tunnel, pinned by a SHA-256 digest carried in the ticket. Detail: Peer serving & the fleet.
flowchart TB
KEY["One Ed25519 device key<br/>(identity seed = iroh node key)"]
KEY --> EP1["Peer-serve endpoint<br/>ALPN xenon/serve/tcp/1 (+ echo)"]
KEY --> EP2["Group daemon endpoint<br/>ALPNs group-sync · group-join · substrate"]
EP1 -->|"bearer secret (ticket)"| T["byte tunnel → local app port"]
EP2 -->|"signed membership"| G["group document · gossip · join · revocation"]
T -.->|"not membership-checked today"| G
The two endpoints are separate binaries with separate accept loops: an
IrohProvider endpoint advertises the echo + serve ALPNs
(iroh_provider.rs:96), while a GroupNode advertises the group-sync,
group-join, and any extra ALPNs (crates/net/src/group/node.rs:539-543).
Zoom 2 — subsystems
The endpoint, and honest path receipts
The endpoint is iroh 1.0.2 userspace QUIC. On bind it waits up to 10 s for
endpoint.online() (iroh_provider.rs:105-109). For every live connection,
selected_path reads connection.paths(), finds the selected transport, and
maps it to a three-valued receipt (iroh_provider.rs:307-317, enum at
crates/net/src/lib.rs:64-72):
Direct— a hole-punched or LAN IP path,Relay— bytes are being carried through a relay,Unknown— the library reported something else; say so rather than guess.
Peer-serve prints the observed path per tunnel and per index probe — a receipt
of what actually happened, never an optimistic claim. For testing, a
force_relay switch strips IP transports so relay carriage can be proved
rather than assumed (iroh_provider.rs:98-100).
Membership enforcement (where it is wired)
When a provider is built with_membership, every inbound connection is
authorized against the current signed group head
(iroh_provider.rs:200-226), and every dial authorizes the remote key before
bytes flow (:158-179). Live connections are registered so that revocation
closes them immediately — with QUIC error code 0x5845 (“XE”) and the
reason string xenon: membership revoked (:242-245). Removal from the group
is not merely “no new connections”; it is an active disconnect.
The serving plane constructs its provider with IrohProvider::new — the
no-membership path — so none of this applies to app-serving tunnels today
(crates/launcher/src/peer_serve.rs:435, :591). That gap is
G4 in the review.
The signed group document
Group state is a single signed document, hash-chained across versions
(crates/net/src/group/mod.rs:198-296):
- monotonic
version+previous_hashchain, over a canonical big-endian binary encoding (crates/net/src/group/canonical.rs) — two implementations that agree on bytes agree on hashes; - one Ed25519 authority signs successors (
AuthorityMode::Single— a single authority key, not a quorum, is the current model); revocationsis append-only;- opaque
statementsgive forward compatibility for records the current code does not understand.
Succession is deliberately strict: a successor must be exactly one version
newer and must name the prior envelope’s hash; gaps, rollbacks, and forks are
rejected; the first valid successor wins — there is no quorum or conflict
resolution (mod.rs:11-16, DocumentChain::accept, :540-590). Across
restarts, a persisted MembershipLowWaterMark refuses to load any document
older than one already seen (:473-509) — an anti-rollback floor.
Join and gossip
The join ceremony (crates/net/src/group/protocol.rs) uses a single-use
signed invite plus an Ed25519 proof-of-possession from the joining
device. The invite is durably consumed before the successor document adding
the member is minted (serve_join_inner, :321-368) — a crashed join burns
the invite rather than leaving a replayable one.
Head gossip is a symmetric hello / need / document / done exchange in which
the behind side pulls one verified successor at a time (run_sync,
:422-479) — each step re-validated against the chain rules above.
Recovery statements — headroom, not a lifecycle
crates/net/src/group/recovery.rs defines five typed, signed statement
families — successor, takeover, counter, freeze, rotation — with a 72-hour
default challenge window (:74), plus reserved types for a future FROST
(threshold-signature) v2. This is statement vocabulary only: no ceremony,
no UX, and no networking drives these statements yet. The review treats this
as designed headroom, not a working key-recovery lifecycle.
Zoom 3 — module detail
ALPN registry
Every ALPN string in the codebase, and whether the connection it names is membership-gated:
| ALPN | Constant / site | Purpose | Membership-gated? |
|---|---|---|---|
xenon/net/echo/1 | ALPN, crates/net/src/lib.rs:25 | Original echo experiment; NetworkProvider::dial default | Only if built with_membership |
xenon/serve/tcp/1 | SERVE_ALPN, crates/net/src/lib.rs:31 | Peer-serve bearer-ticket byte tunnel | No — bearer secret only |
xenon/net/group-sync/1 | GROUP_SYNC_ALPN, crates/net/src/group/protocol.rs:31 | Membership-gated head gossip | Yes |
xenon/net/group-join/1 | GROUP_JOIN_ALPN, crates/net/src/group/protocol.rs:33 | Invite-gated join ceremony | Pre-membership (invite + proof-of-possession) |
xenon/substrate/1 | ALPN, crates/substrate-proto/src/wire.rs:14 | Remote substrate multi-lane session (dial_session, group/node.rs:414) | Yes (member-checked at dial) |
xenon/consent/test/1 | EXTRA_ALPN, crates/net/tests/extra_alpn.rs:25 | Test-only — proves extra ALPNs inherit the membership gate | Yes |
A hardened form of this table is a Part V candidate: ALPN & signing-domain registry.
Signing domains (not ALPNs)
Domain-separation strings prefix what gets signed, so a signature made for
one purpose can never be replayed as another. They are listed here so they are
not confused with the wire registry above: xenon/group-document/v1\0,
xenon/group-envelope/v1\0, xenon/group-join-proof/v1\0,
xenon/recovery/{successor,takeover,counter,freeze,rotation,rotation-accept}/v1\0,
xenon/wireguard-key/v1\0, xenon/interface-*, xenon/mod-manifest/v1\0,
xenon/profile-*; and in crates/xe-sync: xenon/group-document/v2\0,
xenon/group-envelope/v2\0, xenon/member-endpoint-binding/v1\0.
Frames and limits
Group join/sync frames are 4-byte big-endian length + strict JSON, capped at
64 KiB (group/protocol.rs:36, :123-153). Small, boring, auditable.
Two group-document lineages — an honest fork
Two parallel membership lineages exist in-tree:
- v1 in
crates/net/src/group/— what gossip, join, and the membership-gated provider compile against today; - v2 in
crates/xe-sync/src/group.rs— newer:MemberKind{Device, Companion}, member endpoint bindings, and v2 canonical signing domains.
Which lineage is canonical for the shipped serving path is undecided
in-tree. MemberKind::Companion has no serving semantics at all yet (may a
companion consume but never publish?). This fork blocks the central design
move of this beat — gating serving on membership — and is questions 1 and 3 on
the review’s session agenda.
Where identity does not reach yet
To keep the seam explicit before you read on: the membership plane above is built, signed, and revocation-capable — and the app-serving plane does not use it. Serving auth today is a bearer secret in a ticket file. What that means in practice, and the options for closing the seam, are the subject of Peer serving & the fleet and the two reviews (transport, peer serving).
Links
- iroh: iroh.computer · docs · relays
- QUIC: RFC 9000 · TLS handshake RFC 9001 · ALPN RFC 7301
- Ed25519: RFC 8032 · did:key method
- UCAN (discussed in the review): spec
The serving plane
Every app on an Xe Computer — a static site, a container-backed service, an
app that actually lives on another device in your fleet — is reached through
one local address: http://<app>.localhost:5454. The serving plane is
the piece of the machine that answers that address. It is a small HTTP router
that runs on your own device, on loopback only, and decides — per request —
what to serve, what to refuse, and how to say so honestly.
This chapter sits one layer above Transport & identity (how bytes move between devices) and one layer below Shell & rendering (the browser surface, Prism, that frames what the plane serves). It is the request/response plane: the thing your browser talks to.
As-built truth. Citations are repo-relative in
agent54/xenon-launcheratmain @56e9457. The router source lives atcrates/launcher/assets/route-worker.js(request logic) andcrates/launcher/src/origin.rs(config generation + supervision); citations below are to those two files unless noted. Ports follow the current decisions d/100 (browser plane on 5454) and d/102 (internal peer-forward stays 5198, distinct). Where the design is honestly unfinished, this chapter says so and links the design review.
Primer: the words this chapter assumes
What is loopback /
localhost?127.0.0.1(and its IPv6 twin::1) is the address a machine uses to talk to itself; traffic never leaves the device. The namelocalhost— and, by RFC 6761, any name ending in.localhostsuch asphotos.localhost— always resolves to loopback, with no DNS lookup. That is why every Xe app gets its own<app>.localhostorigin for free: separate origins, all on your own machine.What is an origin? The web’s unit of isolation: scheme + host + port together (e.g.
http://photos.localhost:5454). Two pages from different origins cannot read each other’s data unless one explicitly grants access. The plane gives every app a distinct origin so the browser isolates them for us.What is workerd? The open-source JavaScript/Wasm server runtime that powers Cloudflare Workers — github.com/cloudflare/workerd. The Xe serving plane is a workerd process running one worker (
route-worker.js) as the origin router. We pin an exact workerd binary and supervise it; the launcher, not a cloud, owns it.What is Fetch Metadata? A set of browser-sent request headers (
Sec-Fetch-Site,Sec-Fetch-Mode, …) that tell the server how a request was initiated — same-site, cross-site, navigation, websocket. See W3C Fetch Metadata and this practical guide. The plane uses them as a cross-site-request-forgery floor.What is IWA / a controlled frame? An Isolated Web App is a signed, bundled web app the browser grants extra powers, running under a strict content-security policy. A controlled frame (WICG proposal) is the embedding primitive a super-app shell like Prism uses to host guest apps. Per d/101, ordinary Xe apps are not IWAs — they are plain http(s) framed by Prism. IWA headers are opt-in and reserved for super-app shells. This chapter’s “two-tier” section returns to it.
Zoom 1 — the big picture: one origin, one router, honest refusals
Three ideas carry the whole layer:
- One user-facing origin per app, all on 5454. The browser only ever
talks to
http://<app>.localhost:5454. The plane binds that port on loopback and nothing else is browser-facing (d/100). - A pinned, launcher-managed router. A supervised workerd process runs
route-worker.js. It is not a general web server you configure — it reads a generated route table and serves exactly what the table describes. - Fail closed, explain in plain language. Anything the router cannot
safely serve becomes a small
text/plainresponse that names the next action (“Runxe app install PACKAGE, then retry.”). There is no ambient cross-origin access and no silent success.
flowchart LR
Br[Browser / Prism frame] -->|http://app.localhost:5454| Plane
subgraph Plane["Serving plane — workerd on 127.0.0.1:5454"]
RW["route-worker.js<br/>origin router + refusal ladder"]
RT[(routes.json<br/>route table)]
RW --- RT
end
RW -->|static-digest| Store[["static-store<br/>read-only disk, no dotfiles"]]
RW -->|backend-proxy| Conn["backend-connector<br/>127.0.0.1:5199 → container"]
RW -->|peer| Fwd["peer-forward<br/>127.0.0.1:5198 → iroh tunnel"]
Sup["origin supervisor (origin.rs)<br/>pins + health-probes + restarts workerd"] -. manages .-> Plane
The launcher (origin.rs) generates the workerd config and the route
table, downloads and verifies the pinned workerd binary, and runs a supervisor
that health-probes the plane and restarts it on failure. workerd runs the
router. The router routes. That separation — a Rust supervisor owning a
JavaScript router — is the shape of the whole layer.
The ports, at a glance (all loopback)
| Port | Role | Source |
|---|---|---|
| 5454 | The Xe local plane — the single browser-facing origin http://<app>.localhost:5454 | fleet_catalog.rs:24 LOCAL_PLANE_PORT; origin.rs:34 |
| 5199 | Launcher-owned backend connector (workerd → container) | origin.rs:708 BACKEND_CONNECTOR_ADDR |
| 5198 | Peer-forward (workerd → iroh loopback forward), present but dormant until a peer route exists | origin.rs:728 PEER_FORWARD_PORT |
| per-peer | Fleet-catalog consumer forwards, one binding each | fleet_catalog.rs:133-138 |
d/100 fixes 5454 as the local consume plane only. d/102 keeps 5198 distinct
from 5454: a branch that unified them hit EADDRINUSE when the plane and a peer
consumer were co-resident (PR#57, closed unmerged). Only 5454 is browser-facing;
5199 and 5198 are launcher-internal plumbing and never an app origin
(origin.rs:704-714).
Zoom 2 — the route table: three frozen types
The router does not guess. For each request it looks up the app’s label in a
route table (routes.json, schema xe/origin-routes/v0) and finds a row whose
type tells it how to serve. There are exactly three types, and the
discriminator is frozen — this is decision-grade, set by P-148 (PR#36),
which also added an idempotence guarantee and a real-loopback HTTP conformance
suite:
flowchart TD
RT["RouteTable — schema xe/origin-routes/v0<br/>port + routes sorted by name"]
RT --> OR["OriginRoute<br/>name · package_id · type · digest? · static_serving? · backend? · peer? · iwa?"]
OR --> T{"type discriminator<br/>FROZEN by P-148"}
T --> SD["static-digest<br/>digest → read-only disk store<br/>SPA fallback, declared headers"]
T --> BP["backend-proxy<br/>BackendArm prefix/idempotent_get/connector<br/>→ connector 5199"]
T --> PR["peer<br/>PeerArm device/connector/origin_port<br/>→ forward 5198 / fleet forwards"]
BP -. "d/103-B: whole-origin overload<br/>empty digest + prefix '/'" .-> WO(("compose UI<br/>as backend-proxy"))
T -. "proposed additive 4th type<br/>(needs Luke ratification)" .-> CO(["container-origin"])
T ==>|unknown / missing| FC[["fail closed: 404 unsupported"]]
static-digest— the app is a bundle of files pinned by a SHA-256 digest. The router serves them from a read-only disk store (static-store,writable=false, allowDotfiles=false,origin.rs:803), with/→/index.html, optional single-page-app fallback, and MIME +X-Content-Type-Options: nosniffon every asset. This was the only populated arm at the freeze.backend-proxy— the app has a container backend. A declared path prefix is proxied through the launcher-owned connector on 5199; everything else can be served from a static frontend digest in the same row.peer— the app actually lives on another device. The router forwards to the iroh tunnel (see Peer serving & the fleet); if the publisher is offline the user gets an honest503naming the device.
Two properties make this safe to evolve. First, an unknown type fails closed
— any type not in {static-digest, backend-proxy, peer} yields a 404, never
an accidental serve (route-worker.js:272). Second, the digest field is
omitted when empty (skip_serializing_if, origin.rs:111), so adding backend
or peer arms leaves a pure static route’s bytes identical — which is exactly what
P-148’s idempotence tests guard.
The full field tables for the route table, the backend/peer arms, and the grant record are written up as a spec candidate — Route Table v1 in Part V.
Zoom 3 — the module: the life of a request and the refusal ladder
Inside route-worker.js, every request runs a fixed ladder. Each rung either
refuses with an honest error or passes the request down. This is the security
boundary as-built — the isolation model is same-origin with zero ambient
cross-origin access; the only way a cross-origin read materializes is a
short-lived, user-consented grant that is re-checked on every request and
every streamed chunk.
flowchart TD
A[Browser to app.localhost:5454] --> B["fetch(): strip x-xe-peer-request-origin"]
B --> C{"method CONNECT or TRACE?"}
C -- yes --> R400a[400 Bad Request]
C -- no --> D{"loopback host + /xe/api/index or /catalog?"}
D -- yes --> SVC[serve published index / merged catalog · 200]
D -- no --> E{"Host matches .localhost grammar<br/>AND == URL authority?"}
E -- no --> R400b[400 Bad Request]
E -- yes --> F{"path decode ok & no .. \\ NUL?"}
F -- no --> R400c[400 Bad Request]
F -- yes --> G["compute Fetch-Metadata:<br/>sameOrigin / crossSite / safe / upgrade"]
G --> H{OPTIONS preflight?}
H -- yes --> H1{grant or declared-static CORS?}
H1 -- grant --> P204[204 preflight]
H1 -- declared safe --> P204b[204 declared]
H1 -- neither --> R403a[403 no grant authorizes preflight]
H -- no --> I["routeFor(): read routes.json"]
I --> J{"route type known?<br/>static-digest / backend-proxy / peer"}
J -- unknown/missing --> R404u[404 unsupported / 503 missing table]
J -- known --> K{"cross-origin? origin && !sameOrigin"}
K -- yes --> K1{matching live grant?}
K1 -- yes --> K2{credentials ok?}
K2 -- no --> R403c[403 credentials not permitted]
K2 -- yes --> SG["serveGranted: static reads only,<br/>byte-ceiling + live revoke per chunk"]
K1 -- no --> K3{"declared static header covers<br/>safe non-upgrade non-backend path?"}
K3 -- yes --> STAT
K3 -- no --> R403x[403 cross-origin refused]
K -- no --> L{"same-origin but cross-site<br/>unsafe or websocket?"}
L -- yes --> R403f[403 cross-site refused · Fetch-Metadata]
L -- no --> M{"route.type == peer?"}
M -- yes --> PEER["servePeer: rebase origin/host,<br/>stamp; offline to 503"]
M -- no --> N{"path under backend.prefix?"}
N -- yes --> BE["serveBackend via connector 5199<br/>503 + Retry-After on failure"]
N -- no --> O{safe method?}
O -- no --> R405[405 · Allow: GET, HEAD]
O -- yes --> STAT["serveRouteStatic from digest<br/>SPA fallback; 404 if absent"]
STAT --> IWA{route.iwa?}
PEER --> IWA
IWA -- yes --> WRAP[wrap COOP/COEP/CSP]
IWA -- no --> DONE[response]
WRAP --> DONE
Walking the rungs (all route-worker.js unless noted):
- Entry & peer-origin unwrap (
:453). An internalx-xe-peer-request-originheader is read and stripped before any downstream code sees it (:455-460); it only selects response stamping for peer-served apps, and its hostname must match the request host and end in.localhostor it is ignored (:462-467). Authorization for a peer tunnel is the iroh bearer-ticket handshake, not this header (:458-459). - Method floor —
CONNECT/TRACE→400(:381). - Reserved service paths — on the loopback host (
127.0.0.1[:port]),/xe/api/indexserves the published app index and/xe/api/catalogthe merged fleet catalog (:384-385). These are the only non-.localhosthost the router answers. - Hostname guard —
Hostmust match the strict grammar^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.localhost(?::PORT)?$, andurl.hostnamemust equallabel + ".localhost"(:386-388). This closes host/URL-authority mismatch. IPv4 and IPv6 are separate binds. - Path safety —
decodeURIComponentin a try/catch (400on malformed percent-encoding), then reject\, NUL, or any..segment (:390-391). - Fetch-Metadata — derive
sameOrigin,crossSite,safe(GET/HEAD), andupgrade(websocket) fromOriginandSec-Fetch-Site(:392-399). - OPTIONS preflight — a matching live grant →
204; else a declared static CORS allowance for a safe GET/HEAD off a non-backend prefix → synthetic204; else403 No capability grant authorizes this cross-origin preflight(:400-413). - Route lookup —
routeFor()reads the table and fails closed on the frozen discriminator (:262-274): unknown type → unsupported; missing table → missing; no row and no dynamic peer → not-installed. - Cross-origin arm — with an
Originthat is not same-origin: a matching grant runs a credential check (cookies/Authorizationrefused unless the grant sayscredentials:"include") then serves static reads only with a live byte-ceiling; no grant means allow only a declared-static safe path, else403 Cross-origin request refused(:417-429). - Same-origin CSRF floor — a same-origin-looking but cross-site unsafe or
websocket request →
403 Cross-site request refused (Fetch Metadata)(:430-432). - Dispatch — honest
503/404for missing/not-installed/unsupported;peer→servePeer; a backend-prefix path →serveBackend; a non-safe method on a static route →405withAllow: GET, HEAD; otherwiseserveRouteStatic(:436-451). If (and only if) the route is IWA-marked, the response is wrapped with COOP/COEP/CSP headers (:214-220).
Two boundary facts worth stating plainly:
- A grant can never reach a backend. Grants cover static reads only; a
grant-authorized request to a backend prefix is refused
403(serveGranted,:304-306). The connector on 5199 is launcher-owned and never an app origin. - A grant owns its whole CORS envelope. Declared CORS headers cannot widen
what a request-level grant permits (
:204,:209-211), andguardStreamre-reads the grant table on every chunk, aborting on revocation, expiry, or a ceiling breach (:221-246).
The two-tier app model touchpoint (d/101)
Isolation between apps is delivered by giving each app its own origin and letting the browser (and Prism’s controlled frame) enforce the boundary. That is enough for the common case, so regular apps get plain http(s) from the plane and no COOP/COEP/CSP headers at all — this is d/101: “most apps should be regular apps that are just http/s.”
Only IWA-marked packages — today a short allowlist:
xenon/installer-sample and agent54/prism (is_iwa_marked_package,
origin.rs:683-685) — receive iwaHeaders: IWA_CSP
(frame-ancestors 'none', default-src 'self'), COOP same-origin, COEP
require-corp, and a dotfile manifest store. Prism, the super-app-tier shell,
is the canonical IWA; it frames the plane origin in its controlled frame and
the plane serves the framed guest as ordinary http. No ordinary app needs
signing, bundling, or IWA packaging to be served, listed, or loaded. See
App model & packaging and
Shell & rendering for the framing side.
There is a live tension here: the launcher still defaults to https on loopback with a name-constrained local CA, while d/101 frames regular apps as plain http(s). The design review treats this honestly as an open session question — it is not settled.
Honest-error behaviour today
The refusal ladder never throws an opaque failure at the user. Every refusal is
text/plain; charset=utf-8 with X-Content-Type-Options: nosniff, and every
body names the next action. A few load-bearing examples:
- Not installed →
404“This app is not installed. Runxe app install PACKAGE…”. - Backend cold or unreachable →
503withRetry-After: 2(“backend connector unavailable” / “backend not reachable”). - Peer offline →
503“This app runs on<device>, which is offline. Retry, or open it on that device.” plusCache-Control: no-store,Vary: *,xe-peer-liveness: offline(peerOffline,:39-44).
One deliberate absence: there is no 502 today. When the plane cannot reach
an upstream backend or peer, it synthesizes a 503 (a plane-side condition), and
a genuine upstream 5xx passes through unchanged. Whether the contract should
distinguish “plane can’t reach upstream” (503) from “upstream errored” (502)
is an open question the review
carries. The full status/trigger/body/header table is written up as the
Honest-error contract v1
spec candidate in Part V.
Supervision & pinning (why the plane is trustworthy to frame)
The router is only as trustworthy as the process behind the port. The supervisor
(run_origin_supervisor, origin.rs:978-1046) generates the config, calls
ensure_workerd(), and runs workerd under a health probe on 127.0.0.1:port
with an on-failure restart policy (max 10 retries, 1s→30s backoff). A Failed
service raises serving_plane_failed. ensure_workerd downloads the pinned
artifact, verifies size and SHA-256, extracts the binary, verifies the binary’s
SHA-256, and atomic-renames it into place; only linux-x86_64 and
darwin-arm64 are pinned, and any other target refuses honestly rather than
running an unverified binary (origin.rs:207-213, :832-906).
There is one seam this chapter must not gloss: port custody. The only
mutual-exclusion on 5454 is an advisory flock under the data root
(boot_service.rs:938-951) — it guards against a second cooperating Xe, not
against a foreign same-user process that binds 5454 first. If the plane is down,
another local process could hold the port and be framed by Prism as if it were
the plane. This is a real, named gap; the
review lays
out the options, including having Prism verify the plane’s identity before
framing it.
Next: the design review — desired design vs current implementation vs gap, the sovereignty-litmus table, and the open questions for Luke and Jan.
Peer serving & the fleet
This layer is one Xe Computer device serving another’s apps — a publisher exposes locally served apps over an authenticated tunnel, a consumer materializes them as ordinary local origins its browser can visit — and the fleet catalog that lets a device see every peer’s apps in one place. It rides directly on Transport & identity.
As-built truth. Citations are repo-relative in
agent54/xenon-launcheratmain @56e94575. Ports follow the current decisions d/100 / d/102 (browser plane on 5454; internal peer forward on 5198); earlier port choices in d/078/d/080 are lineage, superseded. Where the design is honestly unfinished, this chapter says so and links the review.
Primer: the words this chapter assumes
What is peer serving here? Not file sync and not a CDN: a live TCP tunnel from consumer to publisher over iroh QUIC, carrying whatever HTTP the consumer’s browser plane sends. The publisher’s app never knows it is being reached from another device.
What is a bearer secret? A credential that works for whoever holds it — like a coat-check token. No identity is checked; possession is the whole proof. Today’s serving tickets are bearer credentials, which is the central honest limitation of this layer.
Lineage. Peer-serve replaced an earlier experiment built on n0’s dumbpipe (P-158). The current implementation is native:
crates/launcher/src/peer_serve.rs+fleet_catalog.rs.
Zoom 1 — the big picture: publish → ticket → consume → browse
sequenceDiagram
participant Op as Operator (out-of-band)
participant Pub as Publisher (xe peer publish)
participant Con as Consumer (xe peer consume)
participant Br as Browser / workerd
Pub->>Pub: mint fresh 32-byte secret (getrandom)
Pub->>Pub: publish_index → published-index.json + .sha256
Pub->>Pub: write ticket (node_addr, secret, index_sha256, origin_id, device, origin_port)
Pub->>Pub: 0600/DACL file; print ticket_path only
Op-->>Con: carry ticket file (OUT OF BAND — G5)
Con->>Con: read ticket (refuse group/other-readable file, unix)
Con->>Pub: dial SERVE_ALPN + length-prefixed secret
Pub->>Pub: constant-time compare vs expected
Pub-->>Con: ACCEPT(1) / REJECT(0)
Con->>Pub: GET /xe/api/index (Host rewritten to origin_port)
Pub-->>Con: index bytes
Con->>Con: verify sha256 == ticket.index_sha256; cache; register_peer
Con->>Con: bind 127.0.0.1:port; liveness = live
Br->>Con: HTTP to peer-backed localhost origin
Con->>Pub: authenticated tunnel → 127.0.0.1:target_port
Pub-->>Br: response bytes (raw pipe)
Note over Pub,Con: Publisher restart → new secret → all tickets void (the only revocation today — G1)
In words: xe peer publish mints a one-session secret, writes it into a
tightly permissioned ticket file, and starts accepting tunnels on the
xenon/serve/tcp/1 ALPN. The operator carries the ticket to another device.
xe peer consume dials the publisher by its iroh key, presents the secret,
verifies the app index against the digest pinned in the ticket, and binds a
local port. From then on the consumer’s browser plane treats the remote apps
as local origins.
Zoom 2 — subsystems
The ticket: mint and custody
- Fresh secret per publish. Every publish mints a new 32-byte secret via
getrandom(peer_serve.rs:430-433). The code comment is explicit that persisting or reusing it “would remove restart-as-revocation” (:428-429); the--rotate-secretparameter exists but is deliberately inert (_rotate_secret,:426) until a real revocation design lands. - Unix custody. Ticket files are created
0600(:216-225). On read,open_private_ticket_filerefuses any ticket whose mode has group/other bits set (mode & 0o077 != 0) and tells the operator tochmod 600(:287-304). Refusal is a tripwire: an over-shared credential is a signal, not something to silently paper over. - Windows custody. Creation resolves the current-user SID (validated,
:147-175), replaces the parent-directory and file DACLs with a fresh owner-only rule (restrict_windows_acl,:180-213), openscreate_newwithshare_mode(0), and deletes the half-written file if ACL setup fails (:266-284). Filenames carry PID + a 16-byte nonce against path reuse (:236-262). On read, however, Windows silently repairs a broad ACL rather than refusing (:308-323) — an asymmetry with the Unix tripwire, registered as G2 in the review. - The secret never hits stdout. Publish prints
ticket_path=…plus non-secret status only (node_key,relay,target,device,origin_id,index_sha256,ready) (:463-477). Distribution of the file itself is out of band — undefined by the product today (G5).
The forward path and the two ports
The publisher forwards each accepted tunnel to 127.0.0.1:target_port
(:557); target_port is required (P-209 item 1). The consumer binds
127.0.0.1:port for its side (:634). Two constants keep the planes
distinct (d/100, d/102):
| Port | Constant | Role |
|---|---|---|
| 5454 | LOCAL_PLANE_PORT (fleet_catalog.rs:24) | The browser-facing local plane — what URLs carry |
| 5198 | PEER_FORWARD_PORT (origin.rs:728) | Internal peer-forward target — never user-visible |
Host rewrite: decoupling the two authorities
The ticket carries an optional origin_port (P-209 item 4): the consumer’s
index probe rewrites the upstream Host: header to
127.0.0.1:<origin_port or 5454> (:804-808), so the publisher’s HTTP
authority port and the consumer’s browser port need not match. Tickets minted
before this field behave as a plain byte pipe (PeerTicket.origin_port doc,
:101-104). The fuller seam — the plane owning Host normalization rather
than the consume forward — is queued as the d/102 follow-up seam.
Restart-as-revocation
Because the secret is per-session and never persisted, restarting the
publisher invalidates every outstanding ticket. That is the only
revocation mechanism today (:428-429; P-209 STATE). It is a correct stop
— a deliberate refusal to ship durable secrets on a route with no revocation
list, epoch, or lifetime — but it is all-or-nothing: any restart strands every
consumer. The design space is
G1 in the review.
Liveness
A side channel, not part of the wire protocol
(crates/launcher/src/peer_liveness.rs): replace-in-place JSON per origin
with four states — unknown / live / degraded / offline — mapped 1:1 from
observations: a real tunnel served, or a 5-second probe cycle with a
10-second dial budget (peer_serve.rs:570-573, probe_loop, :741-783).
A data-freshness stamp is deferred (P-169 Q3); the code carries an honest
note that “reachable” only approximates “fresh”
(FRESHNESS_DEFERRAL_NOTE).
Zoom 2 — the fleet catalog
The catalog adds no new protocol. The publisher writes an immutable JSON app index into its origin store; its SHA-256 rides in the ticket; the consumer fetches the index as ordinary HTTP over the already-authenticated tunnel, verifies the digest, caches it; a route worker merges every cached index with liveness into one view.
flowchart TD
subgraph Publisher
REG[App registry] -->|publish_index| IDX[published-index.json + .sha256]
IDX -->|sha256 into ticket| TIX[PeerTicket.index_sha256]
end
subgraph Consumer store
TIX -->|register_peer| PEERS[peers.json + mirror xe/peers.json]
PROBE[probe loop, 5 s] -->|GET /xe/api/index over tunnel| VER{sha256 == ticket?}
VER -->|yes| CACHE[xe/indexes/peer_key.json]
VER -->|no| KEEP[keep last-good copy]
PROBE -->|observation| LIVE[xe/liveness/*.json<br/>unknown/live/degraded/offline]
end
subgraph Route worker
PEERS --> MERGE[serveMergedCatalog]
CACHE --> MERGE
LIVE --> MERGE
MERGE --> CAT[/xe/api/catalog: peers + apps + status + last_seen/]
end
- Publisher index.
publish_index(fleet_catalog.rs:158-231) enumerates the app registry deterministically (name-sorted), writesxe/published-index.jsonplus a.sha256sidecar, and appends a hash-chained audit recordpeer_catalog_index_published. - Peer identity, derived not asserted.
publisher_origin_id = "iroh-" + sha256(json(node_addr))(:152-155) — only public address metadata is hashed, never the secret. From it:peer_key = sha256(origin_id)andconnector = "peer_" + peer_key[..16](:254-260). - register_peer (
:234-294) rejectsport == 0andport == 5454(never squat the plane port), rejects a port already owned by a different peer, upserts byid, sorts, and writes durablepeers.jsonand the workerd mirrorxe/peers.jsonatomically. - cache_verified_index (
:298-343) bounds the index at 1 MiB (MAX_INDEX_BYTES), validates schema/identity/label/order (validate_index,:410-440), auditspeer_catalog_index_verified— and on digest mismatch never overwrites the last-good copy (:308-326). That last rule is what keeps offline peers’ apps visible in the catalog. - Merged view. The route worker (
assets/route-worker.js:102-130) merges each peer’s cached index with its liveness snapshot into/xe/api/catalog.binaryPeerStatuscollapseslive|degraded → "live", else"offline"(:74-76). Offline peers are retained withlast_seen_unix_ms— nothing prunes them (a deliberate visibility choice; retention policy is an open edge). Fetching an offline peer’s app returns a 503 that honestly admits it cannot defeat the app’s own service-worker cache (:35-44).
Zoom 3 — the wire, precisely
Consumer Publisher (ALPN xenon/serve/tcp/1)
dial_alpn(node_addr, SERVE_ALPN) ── QUIC bi-stream open ──►
u8 len (=32) ‖ secret[len] ──────────────────────► read u8 len (1..=64)
read len bytes
constant-time compare vs 32-byte secret
◄──────────── u8 ACK: ACCEPT(1) | REJECT(0) ─────── (≤10 s preamble deadline)
if ACCEPT: copy_bidirectional(local TCP ⇄ iroh stream) ⇄ connect 127.0.0.1:target_port
A 1-byte length-prefixed secret preamble, then a raw untyped byte pipe
carrying whatever HTTP the browser plane sends. Constants: ACCEPT=1,
REJECT=0, MAX_SECRET_LEN=64, MAX_TUNNELS=64. The index probe reuses the
same authenticated stream but writes a literal GET /xe/api/index HTTP/1.1
with the rewritten Host (:788-839) — HTTP-over-tunnel for the catalog,
opaque pipe for apps.
Full field tables for the ticket, the app index, and this protocol are Part V candidates: Xe Peer Ticket v1, Xe Fleet App-Index v1, Peer-serve wire protocol v1.
Honest edges
Named here, argued in the review:
- Serving auth is membership-blind — a bearer secret, while a full signed membership plane sits unwired one crate away (G4, held in the transport review).
- Revocation is publisher-restart only;
--rotate-secretis inert scaffolding awaiting the G1 decision. - The ticket’s whole security rests on an undefined out-of-band transfer channel (G5).
- Windows repairs an over-shared ticket where Unix refuses (G2).
- Peer proof: Windows publish+consume native (P-206, PR#62); macOS proven (P-208); the fleet is real across all three desktop platforms.
Links
- dumbpipe (predecessor): dumbpipe.dev · github.com/n0-computer/dumbpipe
- iroh: iroh.computer · relay concepts
- Related chapters: Transport & identity · The serving plane (browser-side of port 5454)
App model & packaging
What is an app on the Xe Computer? This chapter answers that from the top down: first the spectrum an app can occupy (from a page you merely visit to a shell that hosts the whole computer), then the two serving tiers, then the on-disk package format (XAM) and the pipeline that turns a package into a running, routed app. The design review asks the harder question — desired versus built versus missing.
Everything here cites code at xenon-launcher main @56e94575 and the example
apps at xe-example-apps @5752a3e. Where a claim is a decision rather than
code, it cites d/NNN in the decisions ledger.
Primer: the words this chapter assumes
You do not need to know browser or container internals to read this. A few terms recur:
- Web app — an app whose interface is HTML/JS/CSS served over HTTP, the same substance a browser renders. On the Xe Computer, everything rendered is a web app (d/076); the differences are about capability, not technology.
- Origin — a web app’s address-and-trust boundary, e.g.
http://notes.localhost:5454. The browser isolates one origin from another. - Manifest — a small declarative file describing an app: its identity, where its code comes from, what it needs. Xe’s manifest format is XAM.
- Controlled Frame / Prism — Prism is Xe’s shell (an app itself). It loads other apps into controlled frames: partitioned, unprivileged panes, the way a browser isolates a tab. See shell & rendering.
- IWA / swbn — an Isolated Web App is a browser packaging format with stronger isolation and install-grade guarantees; swbn (Signed Web Bundle) is its signed on-disk bundle. Xe reserves this tier for super apps; ordinary apps do not need it (d/101).
- docker compose — a text file (
compose.yaml) describing one or more containers that make up a backend service. Some Xe apps carry one. See engines & backends for how they run. - Digest — a SHA-256 hash of some bytes, used as their tamper-evident name (e.g. a locked release, a pinned image).
External references, used on first mention: the compose specification, the OCI image spec, and Isolated Web Apps.
Zoom 1 — what an app is
The spectrum: one substance, one axis (d/076)
The Xe Computer does not have “web pages” on one side and “native apps” on the other. It has one substance — the web app — and one axis — granted capability (d/076). Every rendered thing sits somewhere on a ladder, and moving up the ladder is a matter of grants, never a rewrite:
flowchart TB
L0["<b>L0 — Visited</b><br/>loaded from origin, untouched<br/>(what browsers do today)"]
L1["<b>L1 — Installed</b><br/>unmodified, NOT Xe-aware<br/>gains identity · pinning · offline · membership"]
L2["<b>L2 — Enhanced</b><br/>declared capabilities granted at the gates<br/>(read Xe data, stats) · journaled · revocable"]
L3["<b>L3 — Super</b><br/>fabric-level grants: frames.host + shell (Prism)<br/>apps.manage (package manager) · carry.broker"]
L0 --> L1 --> L2 --> L3
caption["No cliffs: L0 to L3 are grants, not rewrites.<br/>The same consent/trust machinery at every rung."]
L3 -.- caption
Three consequences bind the design (d/076):
- No cliffs on the ladder. The same consent and trust machinery applies at every rung. An app does not get “ported” to move up; it gets granted.
- Placement and capability are one decision seen from two sides. An app’s level determines both where it runs (a tab, a controlled-frame partition, an IWA profile) and what it may touch. They are designed together.
- L1 is a hard promise. Apps must be installable unmodified and un-Xe-aware. Any design that requires an app to “integrate with Xe” just to be installed violates the model. The first real-app target named in d/076 is Patchwork (Ink & Switch), L1 first.
The level is a property of the app’s grants, not of its packaging. That distinction is the whole point of the next section.
The two serving tiers (d/101)
If capability is the axis, packaging is a separate question — and here Xe made the default deliberately boring. There are two tiers, and the default is the plain one (d/101):
| Tier | Who | How it is served | Requirements |
|---|---|---|---|
| Regular apps (default, ~all apps) | ordinary web apps | plain HTTP on the plane origin http://<app>.localhost:5454, framed and isolated by Prism’s controlled frame | none beyond what the app itself wants — no signing, no bundle format, no manifest floor |
| Super web apps (rare, opt-in) | shells that need IWA-class powers themselves (controlled-frame hosting, agent-grade device surface) | the swbn/IWA tier | signed bundle; Prism is the canonical example |
The reasoning (d/101, from Luke: “most apps should be regular apps that are just http/s”): requiring per-app swbn packaging adds signing and bundling friction and quietly rebuilds a store-like gate — against the sovereignty litmus (exit without loss, no gatekeeper). Isolation is already delivered by the controlled frame; app-side IWA adds nothing for the common case. An ordinary app never needs the super tier to be published, listed, or loaded.
Honest note on residue. The super-tier machinery (
swbn/Iwa) is fully built and tested in the tree, and two example apps still ship as dev-signed swbn. Under d/101 those are the deprioritized super tier, not the default. The review tracks this as a live cleanup item.
Zoom 2 — the package format (XAM v0.2)
The manifest, as built
An Xe app is described by an XAM manifest — XenonAppManifest in
crates/apps/src/model.rs:127. Two things are worth knowing before the field
tables:
- Runtime validation is authoritative, not the JSON Schema.
validate()(model.rs:564) is the source of truth; the emitted JSON Schema (xam_json_schema,model.rs:674) is explicitly advisory — the code itself says cross-field invariants “are not honestly expressible as a small structural schema.” xam_versionis"0.2". This is a moving target; treat the field tables as an as-built snapshot, not a frozen spec.
Top-level shape (model.rs:127-165):
| Field | Required | Meaning |
|---|---|---|
xam_version | yes | "0.2" |
identity | yes | who this app is (below) |
source | yes | where its code comes from (below) |
components | yes | the parts that run (below) |
endpoints | no | declared network surfaces |
capabilities | no | requested grants, with reasons |
config_schema | no | declared config keys (also gates compose interpolation) |
secret_bindings | no | named secret slots |
instances | no | instance shaping |
serving | no | only a narrow SPA-fallback subset is recognized today |
handlers, interfaces, notifications | no | reserved — parsed and stored, not yet acted on |
Identity (model.rs:168): package_id, release.version, publisher,
display_name, optional icon. Publisher is a tagged enum (model.rs:191):
Signed { key_id }— a keyed publisher.UnsignedDerived { canonical_url, subdir }— identity is derived from the source URL. Deterministically:derived_package_id = sha256("xenon.unsigned-package.v1\0" + url + "\0" + subdir)(model.rs:736). Every compose app is currently unsigned-derived — see the review’s signing gap.
Source (model.rs:210): kind, locator, pinned, optional size,
adapter, detection, optional compose_path. SourceKind (model.rs:230)
is Compose | Git | Image | Web | Static | Swbn — with the explicit caveat in
code that these are “supported source kinds in the format, not necessarily
implemented adapters.”
Component (model.rs:249): id, type (Ui | Service | Task),
runtime { driver, entrypoint }, optional, depends_on, run_policy,
resources, optional task, optional frontend. The RuntimeDriver
(model.rs:299) is Cf | Iwa | Origin | Oci | Compose — the driver decides
how a component is rendered or run.
Endpoints (model.rs:357): Http { port, routes[] } | Tcp { port } |
Udp { port } | LanDiscovery { port }. An HttpRoute (model.rs:377)
carries the flags websocket, sse, range, no_buffer, large_body, long_timeout.
Non-HTTP classes “require separate consent.”
Capabilities (model.rs:396): each is { resource, ability, reason },
and the list must be sorted and unique or validation rejects it.
Resources (model.rs:407): a ResourceEnvelope of request,
required_minimum, and a breach { memory_pids, disk, cpu }. Validation only
checks that request ≥ required_minimum (model.rs:699) — it does not
yet push these numbers into container limits (see the
review).
Two on-disk package shapes — a conflation hazard, honestly
This is the one thing most likely to mislead a reader of the example apps: a
file named package.json (or *.xam.json) on disk can be one of three
different things. The recon found all three in xe-example-apps @5752a3e:
| App(s) | On-disk file | What it actually is |
|---|---|---|
excalidraw | package.json | a full XAM v0.2 (kind=static, adapter=static-v0, driver=origin, serving.spa_fallback=true) |
scratchpad, appdex | *.xam.json | a full XAM v0.2 (kind=swbn, driver=iwa, publisher signed, key_id=agent54-dev) |
guestbook, compose-board | package.json | a build-input descriptor (schema_version:1: package, version, display_name, compose, data_classes, gates) — the compose adapter compiles this into a XAM |
xe-notes, vitehello | package.json | ordinary npm manifests (Vite build inputs) — not Xe metadata at all |
So “the package.json” is not a single format. When you read one, check its
keys: a xam_version means it is a real manifest; a schema_version:1 with a
compose key means it is a recipe the adapter turns into a manifest; an npm
dependencies/scripts block means it is not Xe’s at all.
A naming artifact worth flagging.
guestbook/guestbook.xam.jsonis a byte-for-byte copy of the app’scompose.yaml, not a compiled XAM. It looks like a manifest because of its name; it is not one. Do not treat the file extension as proof of format.
Also note: the seven example apps that exist at 5752a3e are scratchpad,
appdex, guestbook, vitehello, xe-notes, compose-board, and
excalidraw. There is no paperlog, and Immich is not in this repo — it
exists only as a launcher live-test (crates/apps/tests/live_immich_manager.rs)
and on the p-207-immich branch.
The adapter / driver matrix
A source kind is turned into an installable release by an adapter, and the resulting components are rendered or run by a driver. Only three adapters are implemented today; the matrix below is honest about what is proven versus merely expressible:
source.kind | adapter | driver | Concrete instance | Proven? |
|---|---|---|---|---|
static | static-v0 | Origin | excalidraw (v0.18.1) | Yes — P-211 merged, live render, byte-identical on both peers |
swbn | swbn | Iwa | scratchpad, appdex (dev-signed); catalog console/hello-xe | swbn adapter tested; shells pinned (swbn.rs:434) — the d/101 super tier |
compose (frontend) | compose | Cf | guestbook frontend | Yes — frontend maps to Cf (compose.rs:1225) |
compose (service) | compose | Compose | guestbook backend, compose-board | Yes, live — P-141 lifecycle + P-203 full auto-wired end-to-end |
compose (whole-origin UI) | compose | Compose | Immich | No — package sound (P-207), render blocked → d/103 + P-210 (gated) |
git | via compose | Compose | git-sourced compose | path exists; not separately proven in examples |
image / web / oci | — | — | — | No standalone adapter |
Git is handled inside the compose adapter (GitComposeSource +
stage_source_tree, compose.rs:458, 685), not as its own adapter. The
Image, Web, and Oci source kinds are expressible in the format but have
no adapter behind them yet — a gap the review records.
Zoom 3 — from package to running app
The install pipeline, step by step
Installing an app is a small state machine with two human consent gates.
InstallState (crates/apps/src/lifecycle.rs:23; transitions at
lifecycle.rs:137):
Probing → Resolved → AwaitingPlanReview →[gate 1]→ Fetching → Building
→ AwaitingGrantConsent →[gate 2]→ Staging → Installed (any → Failed)
- Analyze. The adapter’s
analyzestep produces aPlan. If itsunsupported_featureslist is non-empty, materialization is prevented (adapter.rs:45, enforcedcompose.rs:1498). The install fails closed before anything is fetched or run. - Gate 1 — Plan Review (
approve_plan,lifecycle.rs:83). The user reviews the declared build effects: network hosts, context roots, resource summary, emulation (adapter.rs:69). The consent’s subject issha256(plan)— it approves exactly this plan, nothing else. - Materialize (fetch + build), gated by a
BuildGrantthat can only be minted from a matching gate-1 consent. - Gate 2 — Grant Consent (
approve_grants,lifecycle.rs:109). The subject is the exact locked release digest. The two gates are deliberately non-interchangeable — approving the plan does not approve the result. - Route registration.
origin::generatederives the route table from registry rows (crates/launcher/src/origin.rs:495). An install with a payload digest gets aStaticDigestroute; a backend-carrying install gets aBackendProxyroute. Either way the app is reachable athttp://<name>.localhost:5454.
The consent machinery — how a request is filed, claimed, and decided, and why the Console web bridge cannot approve — is a cross-cutting mechanism shared with backends, so it lives in engines & backends → consent gates. The install sequence diagram is there too.
Where the app model does not reach yet
- Half the source kinds have no adapter (
image,web,oci). They are expressible but not installable. - The reserved manifest families (
handlers,interfaces,notifications) are parsed and stored but not acted on. - Compose apps have no publisher key — they are always
UnsignedDerived, identity from URL + subdir. The real signing ceremony is deferred. - Resource envelopes are declared, not enforced into container limits.
Each of these is picked up with a decider and options in the app-model review.
Links
- App-model review · Engines & backends · Emerging specs: XAM v0.2
- d/076 (app spectrum) · d/101 (two tiers) · d/103 (compose UI rendering) — decisions ledger
- compose spec · OCI image spec · Isolated Web Apps
- Forge: xenon-launcher · xe-example-apps
Engines & backends
Some Xe apps are just files — HTML, JS, CSS — served over HTTP. Others carry a backend: one or more containers that must actually run somewhere. This chapter is about that “somewhere” (the engine) and about how a running backend is reached, started, gated, and updated without ever handing untrusted code a port on your machine.
The headline architecture is d/094’s third path: the launcher owns a single connector that consumes a unix socket published by the engine itself — no per-app sidecar container, no loopback TCP publication of backend ports. That one sentence drives most of this chapter.
Citations are code at xenon-launcher main @56e94575 unless noted; decisions
cite d/NNN in the ledger. For what an app
is and how it installs, read app model & packaging
first.
Primer: the words this chapter assumes
- Engine — the thing that actually runs containers. On the Xe Computer the productized default is a pinned Colima/Lima/Docker stack; an experimental alternative is smolvm.
- microVM — a lightweight virtual machine that boots in well under a second and isolates a workload far more strongly than a bare container. Both Colima and smolvm run Docker inside a microVM, so an app’s containers never touch the host kernel directly.
- smolvm — an upstream open-source microVM engine (smol-machines/smolvm) Xe forks and extends. Its role here is to publish a unix socket the launcher’s connector can consume.
- docker compose — a
compose.yamlfile describing a multi-container service. See the compose spec. - unix socket — a file on disk that programs use to talk to each other locally. Unlike a TCP port, it is not reachable over the network and its access is governed by ordinary file permissions — which is exactly why d/094 chose it.
- docker exec — running a command inside an already-running container. Xe uses it to reach a container’s port from the outside without publishing that port anywhere.
- route worker (workerd) — a small sandboxed JS worker (workerd) that implements the serving plane’s HTTP routing. It decides which app a request belongs to and whether to proxy it to a backend. See the serving plane.
- consent gate — a point where a human must approve something before the system proceeds. Xe’s are cryptographically bound to exactly what was approved.
Zoom 1 — the shape of the whole thing
An app with a backend runs like this, top to bottom:
- The browser (Prism) makes an ordinary HTTP request to the app’s plane
origin,
http://<app>.localhost:5454. - The serving plane’s route worker recognizes a backend route and, rather than serving a file, forwards the request to a launcher-owned connector.
- The connector reaches the app’s container through the engine’s unix
socket, using
docker execto touch the container’s port — never a published port, never a sidecar. - If the backend is not running yet, the launcher’s lifecycle engine starts it on the first request, holding replayable requests briefly and fast-failing the rest with honest HTTP status.
Two design commitments make this trustworthy, both from d/094:
- The launcher owns every boundary. The engine socket, the connector, the route worker’s proxy arm — all launcher-written, launcher-held. Untrusted app code never gets a host port and never holds the socket.
- No sidecar, no loopback publication. The P-141 connector question was resolved against both escalation options (a per-app sidecar container, or publishing backend ports on loopback TCP). The chosen “third path” lives at the engine layer, where the isolation already is.
The rest of this chapter zooms into each of those pieces.
Zoom 2 — subsystems
The engine socket contract (d/094)
This is the load-bearing mechanism, and it is live at HEAD (shipped in P-203). Four rules define it:
| Concern | Rule | Anchor |
|---|---|---|
| Discovery | exactly-one docker.sock under <data-root>/engine/smolvm/ (depth ≤ 8); zero or many ⇒ refuse | smolvm.rs:276 |
| Custody | launcher sets DOCKER_HOST=unix://… (Linux) or tcp://127.0.0.1:2375 (mac); the router never holds the socket | backend.rs:854 |
| Reach container | docker compose exec -T <svc> sh -c 'nc 127.0.0.1 <port>' — never host-published, never a sidecar | backend.rs:827, 846 |
| Connector | one launcher process on 127.0.0.1:5199, started only if the app has a backend | origin.rs:708, manifest.rs:333 |
Walking it through as a sequence — from a browser request to bytes flowing to the container:
sequenceDiagram
participant Browser as Browser (Prism)
participant Worker as Route worker (workerd)
participant Conn as BackendConnectorService 127.0.0.1:5199
participant Bridge as ExecBridge 127.0.0.1:0
participant Engine as Engine (smolvm/colima)
participant Ctr as App container
Browser->>Worker: GET http://app.localhost:5454/api/…
Note over Worker: recognizes BackendProxy route;<br/>grants never cover the backend prefix (403)
Worker->>Conn: fetch external "backend" service
Conn->>Bridge: connect (launcher-resolved svc+port)
Bridge->>Engine: docker compose exec -T svc nc 127.0.0.1 port
Engine->>Ctr: pipe bytes to container port
Ctr-->>Browser: response (proxied back up the chain)
Two details that matter for trust:
- The connector trusts no request input.
ComposeBackendControllerresolves the project, route, and service entirely from launcher-written registry state — never from the HTTP request (backend.rs:416). Thex-xe-appheader is validated to[a-z0-9-]{1,63}(backend.rs:1091), and the route worker’s grants never cover the backend prefix (a403,assets/route-worker.js:304) — so a cross-origin grant can never be used to reach a backend. - The socket path is refused if ambiguous.
SmolvmBackendrefuses zero or more-than-one sockets (smolvm.rs:276), so no other engine can be silently substituted for the one the launcher expects.
The lifecycle engine: lazy start, honest failure
Backends are started on first request, not eagerly. The LifecycleEngine
(crates/launcher/src/backend.rs:188) makes that safe and honest:
stateDiagram-v2
[*] --> NotStarted
NotStarted --> Starting: first request claims the driver (singleflight)
Starting --> Ready: probe healthy
Starting --> Failed: total_start_deadline (120s) OR start error
Ready --> NotStarted: invalidate() on transport failure
Failed --> NotStarted: stop() clears the slot
Ready --> NotStarted: stop()
note right of Starting
Wait class held up to per_request_wait (15s) then 503 Retry-After
Immediate class 503 at once (never held then lost)
waiters capped at 256
end note
note right of Failed
distinct 502 - a failed start is not a slow one
end note
The design choices, plainly:
- Singleflight. The first request wins a compare-exchange and spawns the
one start-and-poll task per cold-start episode (
backend.rs:236). A test fires 200 concurrent first requests and asserts exactly onestart(backend.rs:1573). - Wait class. A
HEADrequest, or a manifest-declared idempotent-GET prefix (matched on segment boundaries), is safe to hold while the backend warms →Wait. Everything else isImmediatefail-fast, so a non-replayable request (a POST) is never held and then lost (classify,backend.rs:96). - Two deadlines, two truths.
per_request_wait(15 s) → an honest, retryable 503 with a jitteredRetry-After.total_start_deadline(120 s) → a 502 Failed — because a backend that failed to start is a different fact from one that is merely slow, and the status code should say so. The 502 namesxe doctoras the next step (backend.rs:1110). - Self-healing edges.
invalidate()drops a staleReadyon transport failure;stop()clears a failed slot for a clean retry.
Compose apps: the safe subset
Most backends today are docker compose apps. The compose adapter does not run
arbitrary compose files: it enforces a safe subset and fails the install
closed if a compose.yaml steps outside it.
The full refusal list is normative and lives in the
spec candidate,
but the shape is: anything that would puncture the microVM isolation boundary
is refused — privileged, raw devices:, host networking, namespace joins,
bind mounts of host paths, mounting the docker socket, publishing host ports
(use expose), unpinned images, and more. Each refusal yields a stable
diagnostic code with a source:line:col pointer (compatibility_diagnostics,
compose.rs:907). Config interpolation is likewise constrained: only keys the
app declared in its config_schema may interpolate, and shell-style default
operators are forbidden (interpolate_declared, compose.rs:856).
A known-holes caveat. The refusal list has three documented gaps tracked by P-174 (open): short-form host ports (
ports: ["8080"]), non-UTF-8 corruption in the interpolation path, andnetwork_mode: container:. These matter because P-174 gates running compose on user-supplied manifests — the review treats it as a sequencing question before third-party apps.
Git-sourced compose is handled inside this same adapter (GitComposeSource +
stage_source_tree), so a compose app can be fetched from a pinned git commit
rather than a registry.
Consent gates: two gates, cryptographically bound
Installing or updating an app that runs code requires human consent, and Xe’s consent design is deliberately strict. It is shared between the install pipeline and backends, so it lives here.
There are two gates, and they are not interchangeable:
- Gate 1 — Plan Review. Subject is
sha256(plan). Approving it mints aBuildGrantthat is unforgeable by construction:BuildGrant::from_consentrequires a gate-1 consent whosesubject_digest == sha256(plan)(adapter.rs:93), and every adapter re-checks it atmaterialize(compose.rs:249,static_v0.rs:155,swbn.rs:145). - Gate 2 — Grant Consent. Subject is the exact locked release digest. Approving the plan does not approve the built result; the second gate binds to the bytes that were actually produced.
sequenceDiagram
actor User
participant CLI as xe app install
participant Adapter
participant Queue as Consent queue (pending/claimed/decided)
participant Surface as Launcher surface (xe consent / tray)
participant Origin as origin::generate
CLI->>Adapter: probe → resolve → analyze
Adapter-->>CLI: Plan (unsupported_features must be empty)
CLI->>Queue: file PlanReview request (inert; subject = sha256(plan))
Surface->>Queue: claim (atomic rename) + decide
Surface-->>CLI: gate-1 consent → BuildGrant
CLI->>Adapter: materialize (under BuildGrant) → compile → locked release
CLI->>Queue: file GrantConsent request (subject = locked digest)
Surface->>Queue: claim + decide
Surface-->>CLI: gate-2 consent
CLI->>CLI: transition Staging → Installed (signed audit)
CLI->>Origin: generate RouteTable at app.localhost:5454
The custody model is the trustworthy part (crates/launcher/src/consent.rs):
- A consent request is an inert JSON record in
pending/<id>.json. It mints no authority by merely existing (ConsentAction::AppInstallGate“mints no runtime authority”,consent.rs:75). - Deciding it means claiming it by an atomic rename to
claimed/(a single executor), thendecided/. There is a bound:MAX_PENDING=8,REQUEST_TTL_SECS=15 min. - The Console / web bridge has no approval endpoint, by construction. Only
a launcher-owned surface —
xe consentor the macOS tray — can mint authority (consent.rs:1-28). A web app, however privileged it renders, cannot approve its own install. - The runtime is the final choke point.
ManagedCompose::authorizedrefuses to construct a runnable project unless gate-2 consent matches the locked release digest and the state is Staging/Installed (runtime.rs:138). Private fields make the ungated path unreachable to callers — you cannot forget to check.
This install consent is distinct from the cross-origin Grant table
(grants.rs, d/086) used for one app to reach another — the
review keeps them from being conflated.
Zoom 3 — module detail
smolvm’s actual scope, plainly
The docs and the code disagree slightly, and the honest version matters:
- The default engine is not smolvm.
docs/smolvm-engine.md:1states “the smolvm substrate is an explicit macOS experiment.” The productized default isAppEngineBackend— Colima + Lima + Docker, pinned and verified, macOS (crates/substrate/src/backends/app_engine.rs) — which downloads and verifies a guest image and never touches host engine installs. - The code has moved past the doc.
SmolvmBackendnow carries a Linux x86_64 path (smolvm.rs:66, pin1.6.1-agent54-engine-socket) that publishes the unix socket the d/094 connector consumes. So the honest statement is: smolvm remains an opt-in experiment (mac-first per the doc), and is now also the Linux engine that publishes the d/094 socket. It verifies a pinned launcher + binary sha256 before exec (tamper-refused,smolvm.rs:301) and brand-sanitizes its output to “app engine.”
The d/094 upstream story: the socket capability builds on smolvm upstream
PR #656 (socket relay,
merged upstream), extended on Xe’s fork (agent54/smolvm on the forge) to
publish a guest TCP port as a host unix socket. Submitting that upstream is a
separate human-gated step (d/013, d/094).
The exec bridge, precisely
The connector’s ExecBridge (backend.rs:521) binds 127.0.0.1:0 (an
ephemeral loopback port the launcher owns, not the app) and pipes each
connection through docker compose exec -T <service> sh -c 'nc 127.0.0.1 <port>'
(compose_exec_command, backend.rs:827). On Linux the engine is reached via
DOCKER_HOST=unix://<data-root>/engine/smolvm/**/docker.sock; on macOS/colima
via tcp://127.0.0.1:2375 or docker --context colima
(backend.rs:854-891). The connector service itself binds
127.0.0.1:5199 (BACKEND_CONNECTOR_ADDR, origin.rs:708) and starts only
when the app actually has a backend (manifest.rs:333, origin.rs:1010).
The update engine: built, but with no user loop yet
App updates are a complete engine in crates/apps/src/update.rs, and this
chapter has to be honest that the machinery is done while the user-facing
loop is not:
discover_update(semver-gated) finds a candidate;compute_diffdetects escalation across permissions, endpoints, config, and source.apply_updatetakes a pre-update snapshot, stages, activates, and rolls back on failure;rollback_updaterestores artifacts and digest-verified volumes.UpdatePolicyisAuto | Notify | Manual;auto_update_actiononly auto-applies patch, non-escalating updates. An escalating update forces fresh gate-2 consent (update.rs:239); a non-escalating one carries the prior consent forward (update.rs:242).
Honest status. The update mechanism is library-complete and correct, but the recon surfaced no CLI or consent-surface wiring driving it. There is no proven user loop for app updates today — see the review.
Where engines & backends do not reach yet
- Windows backend apps are unsolved — the exec bridge exists only for
smolvm and colima; WSL2 and provisioned-VM backends have no bridge
(
backend.rs:878-889). - GPU / ML backends collide with the isolation subset — the safe-compose
list forbids exactly the
devices:+privilegeda GPU needs; Immich dropped its ML service for this reason (a Luke ruling, per the review). - Resource limits are declared, not enforced —
deploy.resources.limitsin a compose file is not mapped to container cgroups. - The update engine has no user loop (above).
Links
- Engines & backends review · App model & packaging · The serving plane
- Emerging specs: engine socket contract, compose refusal list
- d/094 (engine socket) · d/086 (cross-origin grants) · d/013 (upstream push gate) — decisions ledger
- smolvm · smolvm PR #656 · compose spec · workerd
- Forge: xenon-launcher · agent54/smolvm
Shell & rendering (Prism / Xenon)
This layer is the window the user actually looks at. Everything below it — the device key, the serving tunnels, the fleet catalog — exists so that a person can open one trustworthy app and, from inside it, run every other app their computer offers. That one trustworthy app is Prism: the super-app shell. The browser it prefers to run inside is Xenon, Agent54’s Isolated-Web-App-enabled Chromium. This chapter describes both as they are built today, and marks — loudly — the two places where the design has run ahead of the code.
As-built truth. Code citations are repo-relative in
agent54/prismatmain @98acaa3; every quoted header, manifest, and line number below was re-read against that commit. The Xenon fork is described as recorded in the HQ packets (P-171, P-190/191, P-192, P-195/197) andagent54/android-iwa— no Chromium tree was cloned. Where design and code diverge, this chapter says so and points at the design review. Decisions are cited as d/NNN against the decisions ledger.
Primer: the words this chapter assumes
You do not need any prior browser-internals knowledge to read this chapter. The five terms below are all you need, and each links to its upstream source.
What is an Isolated Web App (IWA)? An ordinary website is fetched from a server over HTTPS and its identity is a domain name. An Isolated Web App is instead a bundle of files signed by a key; its identity is the key, and it runs at a special
isolated-app://origin the operating system installs like a native app. Because the whole app is signed and sealed, the browser can safely grant it stronger capabilities than a normal web page. Explainer: WICG/isolated-web-apps.What is a signed web bundle (
.swbn)? The file format an IWA ships in: a single.swbnfile that packs every resource (HTML, JS, CSS, icons) plus a signature. The signing key derives the app’s origin, so the origin cannot be forged without the key. Spec + tooling: WICG/webpackage · signed-web-bundles explainer.What is a Controlled Frame? A
<controlledframe>is like an<iframe>that only an Isolated Web App is allowed to use. It embeds another app, but gives the host app (Prism) far more control over the embedded guest — its own storage partition, the ability to observe or script it, navigation control — while keeping the guest strongly isolated from the host. It is the mechanism by which one super-app can host many ordinary apps. Explainer: WICG/controlled-frame · spec: wicg.github.io/controlled-frame. It is a real Chrome API, currently gated to IWAs.What is a Content-Security-Policy (CSP)? A set of rules, shipped as an HTTP header, that tells the browser exactly which origins a page may load scripts from, connect to, or frame. A tight CSP is a declared trust boundary: anything not on the list is refused by the browser itself. MDN: Content-Security-Policy.
What is Xenon / Helium? Helium is a privacy-forward Chromium build that ships Isolated-Web-App support. Xenon is Agent54’s IWA-enabled Chromium fork in that same family (Helium is its base; d/053) — the browser the Xe Computer offers to download when no IWA-capable browser is already present. Chromium project: chromium.org.
Zoom 1 — the shape of the layer
Three things make up the shell & rendering layer, and they nest:
- Prism — the super-app shell. Prism is itself an Isolated Web App: a signed bundle that installs like a native app and shows no address bar. Its whole job is to host other apps and broker what moves between them.
- Controlled Frames — the panes inside Prism. Every ordinary app the user
runs is loaded into a
<controlledframe>: partitioned, unprivileged, and observed only through named channels. This is where isolation lives. - Xenon — the browser Prism prefers to run in, because hosting Controlled Frames requires IWA support that not every browser guarantees. On Android that support is delivered by a small patch series, not a fork (below).
The design rule that organises all of it is d/101 — most apps are just
http(s). An ordinary app needs no signing, no bundle, no special manifest; it
is served plainly at a local origin (http://<app>.localhost:5454, d/100) and
Prism’s Controlled Frame supplies all the isolation. The heavyweight IWA
machinery — signing, .swbn, cross-origin isolation — is reserved for the
shell itself. Prism is, in the words of its own product copy, “a super web
app: interchangeable, never privileged — swap this shell and keep your apps,
your data, and your journal” (src/spectrum/spectrum.ts:163-165). That single
sentence is the sovereignty litmus’s exit-without-loss criterion written
into the product.
flowchart TB
subgraph OS["Device OS · Xenon (IWA-enabled Chromium)"]
subgraph IWA["Prism — the super-app shell (isolated-app://, signed bundle)"]
direction TB
home["Fleet catalog home<br/>iwa-controlledframe.html"]
jr["Hash-linked journal<br/>(tamper-evident, on-device)"]
ch["FrameChannel<br/>port + nonce + schema"]
cf["controlledframe<br/>(one visible pane)"]
home --> cf
home --> jr
cf <-->|"tab.* verbs only"| ch
end
plane["Xe local plane · 127.0.0.1:5454<br/>/xe/api/catalog"]
end
cf -->|"src = launch_url<br/>http://app.localhost:5454"| appA["ordinary app A<br/>(plain http, unprivileged guest)"]
home -->|"reads catalog (connect-src)"| plane
plane --- appA
Zoom 2 — the subsystems
Prism is an Isolated Web App
Prism ships as a signed web bundle built with the standard Google IWA toolchain
(wbn + wbn-sign). The recipe (packaging/bundle/Makefile:11-24) stages the
web assets, derives the app origin from the signing key, builds an unsigned
bundle, then signs it into evidence/prism-dev.swbn. The proven bundle is
163 297 bytes with a key-derived origin id of
f4ujayjmxhlpwhujmxd63rxuwlmok3xxduygwx3r46y5ad6ltjyqaaic
(evidence/prism-iwa-status.md:12-15).
The signing today is development signing, and the app is honest about it:
its installed name is literally “Prism (DEV-SIGNED)” (web/manifest.webmanifest),
so the trust tier is visible in the app title itself. The packaging README
states the limit plainly: “The development signature proves bundle consistency
only. It does not establish production identity or independent review.
Production signing needs the Xe Computer key ceremony and must not reuse the
development key” (packaging/bundle/README.md:18-21). That matches d/066,
which lets the orchestrator hold development-tier keys for the build phase
only; the real key ceremony (Luke + Jan, offline root) is still ahead. The
review tracks this as G1.
Prism was installed and rendered end-to-end on an Android emulator (P-195):
InstallIsolatedWebAppCommandSuccess, launched standalone with no address bar,
with a bundled demo app rendering inside a real HTMLControlledFrameElement
(merged prism PR#17 @5b2ed70).
Controlled Frames — where isolation lives
Prism’s home page (web/iwa-controlledframe.html) hosts one visible
Controlled Frame, created programmatically in src/iwa-controlledframe-app.ts
(ensureFrame, :196-218). Every guest app is loaded into it. Two properties
make the frame an isolation boundary rather than a mere embed:
- Partitioning. The frame is pinned to the storage partition
persist:prism-fleet-apps(:58). A guest’s cookies, storage, and DOM live in that partition; the shell’s own storage is a different address space the guest cannot reach. (A second, hidden partitionpersist:prism-fleet-catalog,:59, is used only as a transport to read the catalog JSON when Prism runs as an installed IWA and cannotfetch()the loopback plane directly — it navigates a throwaway frame to the endpoint and reads the text back out,:266-316. A clean, if roundabout, seam.) - Named-channel-only privilege. The guest gets a
src, a partition, and nothing else by default. It cannot reach the shell’s control surface except through one authenticated channel (below), and the only capabilities on that channel are four tab verbs. d/101 states the posture directly: “isolation is already delivered by the controlled frame (partitioned, unprivileged guests).”
The privilege that does exist runs the other way: the shell can call
executeScript into the guest (:236) to read it — the “observed” rung of the
agency ladder, below. The guest cannot script the shell.
The baked CSP — the trust boundary in one line
Every response inside Prism’s bundle carries a Content-Security-Policy baked in
at build time (packaging/bundle/headers.json). It reads, verbatim:
content-security-policy:
default-src 'self';
connect-src 'self' http://127.0.0.1:5454;
frame-src 'self' http://*.localhost:5454;
img-src 'self'; style-src 'self'; script-src 'self'
Read plainly, this is the local-trust boundary: Prism’s own scripts, styles
and images are self-only; the only origin it may connect out to is the
local plane http://127.0.0.1:5454 (to read the catalog); and the only
origins it may frame are the per-app plane origins http://*.localhost:5454
(d/100). The same header file sets cross-origin-opener-policy: same-origin
and cross-origin-embedder-policy: require-corp, which together put the shell
into cross-origin isolation — the mode a browser requires before it will
let a page host Controlled Frames at all.
So the shell’s trust reduces to one sentence: Prism trusts whatever answers
on 127.0.0.1:5454 and *.localhost:5454. What binds those ports, and how
that binding is authenticated, is the subject of
G4 in the review.
The fleet catalog — the launch seam (d/104 PROPOSED)
Prism’s home page renders a catalog of apps across the user’s fleet, fetched
from GET http://127.0.0.1:5454/xe/api/catalog (schema xe/fleet-catalog/v1,
validated at iwa-controlledframe-app.ts:111-131). Each entry carries a
launch_url — a plane origin like http://app.localhost:5454. Offline apps
are greyed and disabled with a last-seen time, never hidden
(renderCatalog, :161-193) — honesty over tidiness.
Launching an app is one line: openEntry() sets the frame’s source to the
catalog’s launch URL —
activeFrame.src = entry.launch_url; // iwa-controlledframe-app.ts:258
⚠ PROPOSED, pending Luke — d/104. This “catalog descriptor is the launch seam” is a proposed architecture decision, not yet ratified. The code above already works this way on desktop; d/104 asks Luke to bless the catalog descriptor (
app label → http://<app>.localhost:5454) as the single controlled-frame launch seam on every platform, rather than inventing a separate install-time launch contract per app. It matters because Android’s frame-cell story depends on it: P-202 could not repoint an installed Android Prism from its baked guest URL to a new plane origin through any seam it was allowed to touch — the fix is mechanical once the catalog build is the launch path. Until Luke disposes, treat the Android frame-cell as blocked on this decision. See d/104 and the review’s session questions.
The agency ladder — how much the agent may touch
The shell does not offer a single “run this app” mode. It offers a ladder of increasing agency, from a page the agent never touches to the shell acting on the user’s behalf. The rungs are Luke’s own words (d/082, extended by d/091), and the rule riding along every rung is consented + receipted + undoable, and never faked — a rung that needs installed mode says so; a denied permission is a first-class demonstration state, not a failure.
flowchart TB n["1 · nothing<br/>a plain tab, untouched"] a["2 · allowed / denied / shielded<br/>the page asks; the agent answers —<br/>allow, deny, or a user-chosen value"] o["3 · observed<br/>read out: DOM + requests (copy data OUT)"] e["4 · enhanced<br/>add in: styles, elements, annotations"] m["5 · mutated<br/>filter/rewrite requests on the user's behalf"] c["6 · controlled<br/>the agent acts in the page: navigate, click, fill"] d["7 · driven<br/>full automation over the DevTools (CDP) layer"] s["8 · composed / generated / delegated<br/>carry between apps · A+B→C · resident agent acts"] z["9 · super user agent<br/>the shell itself: hosts, brokers, journals"] n --> a --> o --> e --> m --> c --> d --> s --> z
The middle rungs are implemented in Prism’s Spectrum surface; the “driven” rung runs live over the Chrome DevTools Protocol (d/091). Crucially, each rung above “nothing” leaves a receipt in the journal, and permission answers can be shielded — the agent returns a user-chosen value (an approximate location, a fixed answer) so the page never sees the real device. The design status of that shielding, for installed apps and for capabilities beyond geolocation, is the headline gap of this beat: G6.
The Prism↔app channel — a deliberately tiny surface
An app can talk back to the shell, but only through one narrow, authenticated
door. The transport is a MessageChannel: one port is transferred to the guest
with its origin pinned, and a 128-bit nonce is sent only over that transferred
port. The shell accepts a message only after three checks in order — port
identity → nonce equality → exact {nonce, verb, args} schema
(docs/channel.md; src/channel/message.ts:41-63). The entire verb
vocabulary is four items:
tab.open{url} · tab.close{} · tab.reload{} · key{key} // message.ts:15
Every accepted verb is journaled; every rejection is journaled too, with the
offending origin and the check that failed (src/channel/frame-channel.ts:18-35).
A static lint (scripts/check-channel-boundary.mjs) forbids control ever
riding over console output, so this port is the only control path. The
channel’s own documentation is candid about the limit: “any script that obtains
the transferred port and nonce holds the capability” — the door is small, but
whoever holds the key is trusted. Extending this surface (window titles,
notifications) is G2.
The journal — tamper-evident memory
Prism keeps a hash-linked journal in on-device storage: each record carries the
SHA-256 of its predecessor, so the log is tamper-evident and can be verified
after export by a CLI (journal:verify). Navigations, channel rejections,
permission answers, and leased reads all land here. This is the shell’s
contribution to the sovereignty litmus’s auditable actions criterion — the
user can inspect, and prove, what their agent did.
Xenon — a patch series, not a fork
Hosting Controlled Frames needs IWA support, and not every browser ships it reliably, so the Xe Computer offers Xenon — Agent54’s IWA-enabled Chromium (Helium-based; d/053) — as the download when none is present. It is important to be precise about what “the Xenon fork” actually is on Android today:
It is not a full Chromium fork.
agent54/android-iwais, in the words of the forge registry, “patch files + gn args + validation scripts, NOT a full chromium fork” — a small series of three patches on a stock Chromium base, plus build arguments and test scripts:
- 0181 — a scoped
isolated-appbaseline permissions policy plus a guest-view gate, which fixes the bug where Controlled Frame would not register on Android;- 0182 — scoped IWA header injection (
.well-knownmanifest allow + cross-origin isolation);- 0183 — standalone launch through the existing web-app activity (own task, no address bar).
The choice was to ride Chromium’s desktop-android variant plus a targeted Controlled-Frame carve-out, rather than build the whole extensions stack on Android (P-171). Because Controlled Frame and IWAs are real, upstream Chromium features (currently in origin trial), the divergence is deliberately small — which is the whole point, and the subject of G5, fork maintenance. It was proven with an arm64 APK and a kitchen-sink IWA installed on the emulator, with a rollback APK preserved.
Two engines, one bundle (P-192)
A quiet but strategically large result: the same signed bundle
(hello-xe-0.6.2.swbn) was rendered by two different browser engines — on
Blink (Chromium) via a real IWA install, and on Servo (Mozilla’s
independent Rust engine) via an isolated-app://-style scheme handler that
serves the bundle’s bytes only after verifying its Ed25519 signature offline
(a tampered bundle is refused). This is what turns “a Chromium-fork feature”
into “a web-platform shape with more than one implementation.” It is described
honestly as CF-shaped, not CF-strong: the Servo side has the API surface
only — no process isolation, OS sandbox, or cross-origin isolation yet. Why it
matters for the standards story is drawn out in the
review.
Zoom 3 — module detail
The baked headers (exact)
| Header | Value | Effect |
|---|---|---|
content-security-policy | default-src 'self'; connect-src 'self' http://127.0.0.1:5454; frame-src 'self' http://*.localhost:5454; img-src 'self'; style-src 'self'; script-src 'self' | Self-only code; one connect target (the plane); framing limited to plane app origins |
cross-origin-opener-policy | same-origin | Isolates the browsing-context group |
cross-origin-embedder-policy | require-corp | Every subresource must opt in |
cross-origin-resource-policy | same-origin | Blocks cross-origin resource leakage |
The last three together yield crossOriginIsolated: true, without which the
browser refuses to host a Controlled Frame.
The channel verb set (exact)
| Verb | Args | Journals as |
|---|---|---|
tab.open | {url} | navigate.tab |
tab.close | {} | navigate.tab |
tab.reload | {} | navigate.tab |
key | {key} (non-empty string) | navigate.tab |
Any message failing port / nonce / schema is refused and journaled as
channel.reject with the origin and failing check. There is no verb for
titles, notifications, focus, or liveness — see G2.
Partitions
| Partition | Purpose |
|---|---|
persist:prism-fleet-apps | The visible guest pane; each app’s storage lives here |
persist:prism-fleet-catalog | Hidden; a transport to read the plane catalog when installed |
An honest discrepancy: composition still uses iframes
Prism has a Composition surface (its root /, web/index.html) that frames
two demo apps side by side and carries a consented, receipted, undoable “carry”
record between them. It is a genuine and working demonstration — but it is
built on plain <iframe> elements (web/index.html:28-29), not
Controlled Frames. The fleet home uses real Controlled Frames; the composition
surface has not yet migrated to them. Stated plainly here so the book never
implies uniform Controlled-Frame composition everywhere; the migration path
(d/103, compose UIs as plane origins) is G3 in the review.
Where this layer meets the rest of the stack
- Below: the plane origins Prism frames (
http://<app>.localhost:5454) and the catalog it reads are produced by the serving plane and peer serving & the fleet. The CSP’s two allowed origins are exactly that layer’s outputs. - Beside: what makes an app installable and which tier it belongs to is the app model (d/101 lives on both beats).
- Above: the agency ladder is where the agent layer — darc, becoming Reagent (d/053) — reaches into panes to observe, enhance, and drive.
Links
- Isolated Web Apps: WICG explainer
- Signed web bundles / web packaging: WICG/webpackage · signed-web-bundles explainer · tooling webbundle-plugins
- Controlled Frame: WICG explainer · spec
- Content-Security-Policy: MDN
- Helium: github.com/imputnet/helium · Chromium: chromium.org
- Servo (second engine): servo.org · github.com/servo/servo
Devices (desktop & Android)
This layer is the device fabric: the desktops, laptops, and phones that together make up one person’s Xe Computer, how they join a group, how they serve each other’s apps over iroh, and — on Android — the companion app that carries membership, consent, an audit ledger, and the launcher crossing in one APK. It rides on Transport & identity and Peer serving & the fleet; this chapter is about the devices those planes run on and the phone-shaped surface unique to this beat.
As-built truth. Runtime citations are repo-relative in
agent54/xenon-runtimeatmain @b0cfc90(companion-android + thecompanion-coreRust crate). Desktop proof is drawn from packet reports (packets/done/, laneREPORT.mds). Ports follow d/100 (on-device browser plane on 5454). Where the design is honestly unfinished, this chapter says so and links the review.
Primer: the words this chapter assumes
What is the companion? The Android app
network.xenon.companion. It is not the browser — it is the phone’s membership card, consent screen, audit log, and launcher. It shows you the apps a desktop in your group is serving and, when you tap one, hands off to Xenon (our browser fork) to open it. Mobile is both-track (d/062): v1 is phone-as-remote-client to your own instance; v2 is a real Android browser runtime — a separate, later workstream.What is a group / membership? A device group is a signed, append-only document naming who is a member and who has been revoked. Every device holds a copy; membership is checked before any connection is trusted. There is no server — the document is the authority, verifiable offline.
What is the on-device plane? A single loopback web server on
127.0.0.1:5454that a device runs while its browser is open. Requests to<app>.localhost:5454are bridged, byte-for-byte, over an authenticated iroh session to whichever device is actually hosting the app. No background service; close the browser and every listener drops (d/037).What is a foreground service (FGS)? On Android, a long-running task that must show a notification and declare a type. The companion’s session runs as a
connectedDeviceFGS — chosen because Android 15 time-limits thedataSynctype, and a device session must not be silently killed by a clock.
Zoom 1 — the big picture: one person, many devices, one group
An Xe Computer is not a single machine. It is a fleet: a set of devices enrolled into one group, each able to serve its locally running apps to the others over an authenticated iroh mesh. Desktops and laptops are full peers — they both publish and consume. The phone is a companion — today it consumes (sees and launches a desktop’s apps) and carries the sovereignty machinery (membership, consent, audit), but does not yet host a browser runtime of its own.
graph LR
subgraph Group["Your device group (signed membership · iroh mesh)"]
LX["Linux<br/>reference peer<br/>publish + consume"]
MAC["macOS studio-36<br/>full peer<br/>publish + consume"]
WIN["Windows VM 200<br/>full peer<br/>publish + consume"]
AND["Android companion<br/>consume + launcher<br/>membership · consent · audit"]
IOS["iOS<br/>client-surface only<br/>(Apple policy)"]
end
MAC -- "iroh Direct, byte-identical (P-208)" --> LX
LX -- "publish → consume (P-201m)" --> MAC
WIN -- "iroh Direct, byte-identical (P-206)" --> LX
LX -- "L1-L3 consume in Controlled Frame (P-201w)" --> WIN
AND -- "substrate route_info/open (xenon/substrate/1)" --> LX
AND -. "apps.list dial - responder UNWIRED (P-188)" .-> WIN
classDef gap stroke-dasharray:4 3;
Every solid edge above is a proven serve-and-consume path; the dashed edge is the one structural gap on the launcher route (§3.5). Per-device roles as shipped:
| Device | Role today | Proof |
|---|---|---|
| Linux | Reference platform; every cross-host proof’s consumer. xe peer publish|consume + the on-device plane build & run native; serve+consume byte-identical over iroh. | P-208 REPORT (lane-186 consumed studio’s app byte-identical, iroh Direct). |
| macOS | Full peer (studio-36, arm64) — consume and publish. Prism installed + launched as a real IWA; peer app rendered in a real <controlledframe>; honest offline→recovery. | P-201m REPORT — PARTIAL (strong), LOAD proven E2E on real macOS. P-208 — Mac publishes over iroh Direct, “cleanest receipts of the night.” |
| Windows | Full peer (VM 200) — publish + consume native. A deliberate #[cfg(unix)] gate once blocked it; P-206 made peer publish|consume cross-platform. Full L1→L3 consume ladder incl. real HTMLControlledFrameElement render, in-frame offline+recovery. | P-206 — MERGED PR#62, byte-identical both directions, owner-only DACL custody. P-201w — VERIFIED-WITH-CAVEATS (catalog showed 0 apps; captures headless). |
| Android | The companion APK — consume + launcher + the membership/consent/audit substrate. Two subsystems in one APK (§3). | companion-android/ + crates/companion-core/; M3 (P-180) merged @193f1a7. |
| iOS | Client-surface only, by Apple policy (not our architecture). companion-ios crate exists; out of this beat. | kickoff §2. |
Zoom 2 — subsystems of the fabric
2.1 Device pairing & membership
A device joins a group through two enrollments, both over iroh, both verifiable offline:
- Runtime enrollment (
companion-core/src/lib.rs:120-171): the device redeems an authority-signedInvite(version, runtime identity, authority, token, expiry, signature), builds an enroll request in slotlauncherwith roleDevice, and verifies the returnedEnrollmentAttestation. - Group join (
group.rs:244): a base64urlJoinCode{invite, endpoint}is redeemed over iroh viaGroupNode::join_with_protocols; the verified, signed head of the group document is persisted; the node stays running.
Membership itself is a GroupDocument: a monotonic, hash-chained,
single-authority-signed list of Member{device_key, name, role} where role is
Administrator | Member | Observer, plus an append-only revocations list.
The roster is derived offline from the durable head (roster_from
group.rs:657) — no server call to know who is in your group. Convergence is
by gossip on every accepted connection plus an explicit announce on app-open
(sync_now group.rs:409); there is no background service (d/037).
Revocation is honest in both directions. Removing a live device cuts it
immediately. A device that has been revoked while offline learns the truth the
next time it dials: a member-gate refusal ("not a current group member") is
interpreted as this device’s own revocation, which writes a local
self-revoked.json and stops the device signing anything further
(record_self_revocation group.rs:438). The reverse paths — lost phone,
new laptop, rejoin after self-revocation — are thin, and reviewed as a gap
(review G-3).
2.2 The on-device plane (host-routed single listener)
Once a device is a member, its browser reaches peers’ apps through one loopback server, not a service per app (d/100):
- A single host-routed loopback HTTP plane at
127.0.0.1:5454(DEFAULT_LOCAL_PLANE_ADDR,apps.rs:41). A browser request to<app>.localhost:5454is bridged byte-for-byte over anopenlane on the same authenticated substrate session to the hosting device’s host-agent (apps.rs:1-19). - Grant enforcement stays hosting-side. The host re-checks membership plus
the
net.dialgrant on everyopen, so a revoked phone’s next fetch dies with the host’s own spelled reason surfaced as an HTTP error page (apps.rs:7-12). Authorization is not cached on the consuming device. - Plain HTTP loopback: no TLS, no zone CA, no system proxy, no external DNS
(
apps.rs:14-16) — deliberately minimal (d/101). Lifecycle (d/037): the proxy lives while the app process lives; closing the browser drops every listener (apps.rs:18-19). open(apps.rs:238) first doesrefresh_routes(a realRouteInfoRPC that doubles as a liveness probe), thenstart_plane, then returns the<app>.localhost:5454URL. On a network change,retire_stale_bridges_after_recovery(apps.rs:401) drops pre-recovery browser bridges (RFC 9112 §9.3.1 new-accept discipline).
2.3 Cross-device serving over iroh
The tunnel itself — publisher mints a ticket, consumer dials, verifies, binds a local port — is the subject of its own chapter and is not re-derived here; see Peer serving & the fleet. What matters for the device story is the shape it gives the fabric: any peer’s apps appear as ordinary local origins on any other peer, which is what lets the phone’s launcher list a desktop’s apps and open them as if they were local.
Zoom 3 — the Android companion, module by module
The APK is two overlapping subsystems in one binary, which are easy to conflate. Naming them separately is the whole trick to reading the code.
3.1 Subsystem A — the session/wake layer (Kotlin)
This is the Android incarnation of the Xe Computer Boot Service: the layer that answers “is this device up, and honestly so?”
SessionService(SessionService.kt): aconnectedDeviceforeground service startedSTART_NOT_STICKY— the OS is told not to resurrect it, so “off means off” (:26,68). It binds a loopback readiness endpoint127.0.0.1:47615that answersREADY <device>(:105-111). TheconnectedDeviceFGS type is chosen overdataSyncbecause Android 15 time-limits the latter (AndroidManifest.xml:37-40); Android 14+ requires this FGS to holdCHANGE_NETWORK_STATE(:6-9).- Honest kill vs. off.
onDestroywithout an explicit STOP writes akillaudit line (:161-170); a failed loopback bind tears down askill(:118-141); a clean STOP writesoff(:144-159). The audit ledger tells you how a session ended. WakeActivity(WakeActivity.kt): a no-UI trampoline (Theme.NoDisplay, exported) that gives a cold start a foreground context. Three actions —WAKE(start the FGS),CROSS(the launcher crossing, below),STOP. An unrecognized action is a deliberate no-op (:43-46): an exported activity must never fall through into a wake.SessionStore(SessionStore.kt): cached session state (so astatusprobe can answer without waking the device) plus the hash-chained audit log.deviceNamesanitizes every source to[a-z0-9-]≤32 (:80-92) because the device name reaches the notification, the loopback handshake line, and the audit log — an unsanitized newline could otherwise inject an audit-shaped line.StatusReceiver(StatusReceiver.kt): the doctor/status surface. Two modes, neither of which starts the FGS:probe=falseanswers from cached state;probe=true(“doctor”) does a live loopback probe and reports reconciled truth. Every failure string ends in an action (:59-63) — a Power Design rule made literal.
Session FGS semantics (honesty-first). START_NOT_STICKY means no OS
resurrection. On app launch, maybeRestoreSession (MainActivity.kt:117)
restarts a session only if it was active and the loopback probe returns null
and the cached flag was not cleared by a clean STOP — so a deliberately-ended
session is never silently revived; a genuine restore is audited as resume.
3.2 Subsystem B — the group / companion core (Rust via UniFFI)
The membership, consent, and app-access logic lives in companion-core and is
called from Kotlin over UniFFI. Its screens (MainActivity.kt) map directly to
states:
| Screen | Function | States it renders |
|---|---|---|
| Apps (launcher) — primary surface | renderLauncher (:189) | CHANNEL_UNAVAILABLE · offline · browser-missing · empty · app-missing · populated app cards. Actions: Reconnect · Start device · Refresh · Open Xenon · Connect a peer. Uses apps.list. |
| Join | showJoin (:504) | Paste join code + device name → “Join over iroh”. QR scan disabled (“coming next”). |
| Device roster | render (:524) | members / self / revoked · notice banner · Sync now · “Open apps ▸”. |
| Consent request | showConsent (:663) | FROM DEVICE / ACTION / APP / CAPABILITIES / WHY · Decline / Approve (Approve disabled if self-revoked). |
| Apps (on-device plane) | showApps (:558) | host substrate address + host device key · Connect to host · app cards · Back to roster. |
3.3 First run & the launcher crossing (the CROSS flow)
First run is the Join screen: paste the join code and a device name, tap
“Join over iroh.” After that the primary surface is the launcher — “Installed
apps on <computer>” — populated (when the responder is wired, §3.5)
from the desktop’s apps.list. Tapping an app card is the crossing: the
phone hands the app off to Xenon, our IWA-enabled Chromium fork, so it opens as
its own standalone app task rather than a browser tab.
sequenceDiagram participant U as You participant L as Companion launcher (MainActivity) participant C as companion-core (Rust) participant W as WakeActivity (trampoline) participant X as Xenon (Chromium fork) U->>L: tap app card L->>C: apps.list over xenon/companion/apps/1 C-->>L: LauncherAppsView (isolated_app_url) L->>L: validate isolated-app:// + Xenon installed L->>C: audit "launch outcome=requested" L->>W: Intent CROSS (origin, navigate) W->>W: validIsolatedAppUrl? else audit "cross outcome=refused-invalid-origin" W->>X: Intent(component=chrome/Main)<br/>NEW_TASK | NEW_DOCUMENT | RESET_TASK_IF_NEEDED W->>C: audit "cross outcome=sent" X-->>U: standalone WebappActivity fronts the app origin
The crossing, precisely (WakeActivity.doCross :70):
- Validate the target is an
isolated-app://URL — else auditcross outcome=refused-invalid-origin. - Build the Intent:
ACTION_VIEW(navigate) orsetData(target)(restore, no navigate). setClassName("org.chromium.chrome", "com.google.android.apps.chrome.Main")— an explicit component to Xenon, never an app-chooser.- Flags
NEW_TASK | NEW_DOCUMENT | RESET_TASK_IF_NEEDED(:101-103): document-centric routing so the system re-fronts the installed app’s own task (a standaloneWebappActivity) instead of coalescing into a tabbed-browser task — the P-160 fronting-over-an-existing-tab fix. There is noMULTIPLE_TASK, so an existing app task is re-fronted, never duplicated. - Audit
cross origin=… mode=navigate|restore outcome=sent|browser-missing.
3.4 The audit chain (as-shipped)
Every session and launcher event is appended to a tamper-evident
hash-chained log (SessionStore.audit :95-106):
line = "<seq>|<ts>|<action>|<detail>|<hash>\n"
seq = lines.size + 1
ts = ISO-8601 millis+tz (SimpleDateFormat "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
prevHash = previous line's field after the last '|' ("genesis" if empty)
payload = "<seq>|<ts>|<action>|<detail>"
hash = SHA-256(prevHash + "|" + payload).hex().take(16) // 64-bit truncated
safeAudit collapses \r \n | → - and caps a field at 2048 bytes
(:109-112). Actions observed in-tree: wake, kill, off, cross, list, launch, resume. This is tamper-evident (each line commits to the last), not
append-only or externally anchored — an honest limitation it shares with the
agent-layer ledger; the two should converge on one discipline
(agent-layer review). The full field table is a
Part V candidate:
Companion audit hash-chain v1.
3.5 The ALPN registry — every ALPN accounted for
An ALPN is the protocol name negotiated when two devices open an iroh stream. The companion touches exactly three:
| ALPN string | Const (source) | Direction on the phone | Wired? |
|---|---|---|---|
xenon/net/consent/1 | CONSENT_ALPN (companion-transport/src/lib.rs:27) | Inbound and registered — the only protocol in bind_with_protocols / join_with_protocols (group.rs:233,272); dispatched by adopt_node (:328). | ✅ fully wired |
xenon/companion/apps/1 | LAUNCHER_APPS_ALPN (launcher_apps.rs:23) | Phone dials it outbound (launcher_apps_list → dial_session(&addr, LAUNCHER_APPS_ALPN) group.rs:605). Phone-as-host responder is not wired (below). | ⚠️ dial-only |
xenon/substrate/1 | SUBSTRATE_ALPN (apps.rs:599) | Phone dials it for route_info + open on the on-device plane. | ✅ dial wired |
The known unwired responder (P-188). This is the single most important structural gap on the launcher path, and it is a dispatch omission, not a missing protocol:
adopt_nodenever dispatchesLAUNCHER_APPS_ALPN. Its stream loop iswhile let Some(incoming) = extra.recv().await { if incoming.alpn != CONSENT_ALPN { continue; } … }(group.rs:327-328) — an inbound launcher-apps stream is silently skipped.- The protocol is not even accepted inbound:
bind_node/joinregister onlyvec. - The responder exists but is never called:
serve_list_apps_extra(launcher_apps.rs:271) checksincoming.alpn == LAUNCHER_APPS_ALPNand serves — but nothing ingroup.rsinvokes it. - What serves today is only the debug TCP adapter
bin/launcher-app-host.rs(used by the emulator rig) and the desktopxehost’s own responder. - User-visible face: in production,
MainActivity.fetchLauncherApps(:271) has no wired channel and no debug endpoint, so it throws → the launcher shows the reviewedCHANNEL_UNAVAILABLEcopy (:297-300, wired by P-217). Parked pending d/104 (PROPOSED, pending Luke); tracked as P-188, and argued in the review.
Honest edges
Named here, argued in the review:
- The launcher’s
apps.listresponder is unwired on the phone-as-host and the desktop-serve path (P-188) — the reviewedCHANNEL_UNAVAILABLEcopy is what a real phone shows until d/104 ratifies the launch descriptor. - Device recovery is one-directional: enrollment and revocation are solid; lost-phone / new-laptop / rejoin ceremonies are thin (G-3).
- The phone cannot hold app state offline — the plane is host-routed and session-lifetime only; “movable memory” is not yet a cross-device layer (G-5).
- Wake-a-sleeping-device is honestly refused on most paths: the peer transport is iroh, which cannot be OS-socket-activated, so only same-network zero-resident desktops are wakeable today (G-4, and the agent-layer mission context).
Links
- iroh: iroh.computer · relay concepts
- Controlled Frame / IWA: Controlled Frame explainer · Isolated Web Apps
- Android: foreground service types · Doze & App Standby · UnifiedPush (sovereign push)
- Related chapters: Transport & identity · Peer serving & the fleet · The agent layer · Review
The agent layer (darc → Reagent)
This is the layer the whole system exists for: a resident agent that acts on your behalf across your devices, under your inspection, bounded by capabilities you can fork and revoke. It is also — said plainly, up front — the mission and the least-built layer. Being precise about the line between what is real and what is positioning is the single most important thing this chapter does.
As-built truth. Runtime citations are repo-relative in
agent54/xenon-runtimeatmain @b0cfc90; darc citations are fromagent54/xe-darcdesign docs (docs/darc-cf-alpha.md,docs/injection-boundary-design.md); the capability substrate isxenon-cap/xenon-grantat the same runtime commit. Items marked PROPOSED are design, not code. The review carries the program headline and the gap register.
Primer: the words this chapter assumes
What is a resident agent? A process that lives on your computer and can see a page, decide, and act — click, type, navigate, call a tool — so you can delegate work rather than do it by hand. The sovereignty question is not “can it act” but “under whose authority, visible to whom, revocable how.”
What is a capability? A single unforgeable permission: this holder may do this verb on this resource, within these limits, until this expiry. Capability systems replace “who are you?” (identity + a big rulebook) with “what do you hold?” (a token that is the permission). See capability-based security.
What is UCAN? UCAN (User-Controlled Authorization Networks) is a capability-token format: a signed token whose issuer and audience are public keys, carrying attenuable abilities, an expiry, and a delegation proof chain — so authority can be handed onward, only ever narrowing. It is the natural external anchor for this layer’s grant/lease model.
What is the agency ladder? Our vocabulary for how much an agent is doing on a surface:
observed → enhanced → mutated → controlled → driven, with shielded/permission rungs kept honest (d/082, d/091). It is a property of the agent’s action, distinct from the app install spectrum L0–L3 (d/076), which is a property of the app. The two move independently.What is Reagent? The coming rename of darc into the productized resident agent (
doctrine/vocab.md). Luke and Jan execute that rename; this chapter does not perform it and does not bake “darc” into user-facing product copy.
Zoom 1 — the honest big picture: three layers, one real, one preview, one paper
Read the agent layer as three horizontal bands, and keep them separate:
graph TB
subgraph Future["Reagent - the resident, capability-bounded agent (PAPER)"]
R["Reagent: lives where? sees what? acts how?<br/>enforced leases · agent://act family · BYO model"]
end
subgraph Preview["darc - research preview (REAL but UNBOUNDED)"]
D["Controlled-Frame browser-as-IWA · agent sidebar<br/>only gate = human approves each tool call"]
end
subgraph Substrate["The capability & consent substrate (REAL, shipped)"]
CAP["xenon-cap: Capability = resource x verb x constraints"]
GRANT["xenon-grant: authority-signed Delegation bundles"]
CONSENT["ConsentAttestation / ConsentStatement<br/>(used today by the companion)"]
AUDIT["hash-chained audit ledger"]
end
R -.->|"stands on (not yet wired)"| Substrate
D -.->|"will bind to"| Substrate
- The substrate is real. Signed capabilities, authority-signed grant bundles, consent attestations the companion already mints, and a hash-chained audit ledger all ship today.
- darc is real, but pre-boundary. It is a working research-preview browser that can drive pages — but the only live gate on its actions is a human approving each tool call.
- The resident, capability-bounded agent (Reagent) is paper. The design exists; the enforcement does not.
The rest of this chapter walks those bands bottom-up: the substrate that is real, the preview that is real-but-unbounded, and the delegation model (PROPOSED) that would join them.
Zoom 2 — subsystems
2.1 The capability & consent substrate (real, shipped)
This is the part of the agent layer that exists in enforced code today, and it is genuinely capability-shaped:
xenon-cap::Capability=(resource URI × ability verb × typed constraints)with an audience, an expiry, and an onward-delegation flag, verified byverify_chain/authorize_invocation(xenon-cap/src/lib.rs:30-84). This is the atom of permission.xenon-grantissues runtime-authority-signedDelegationbundles to a per-install principal, grouped in a named, versioned bundle, withonward:false(xenon-grant/src/lib.rs:23-53,233-291). A grant is a long-lived, non-re-delegable permission handed to an installed app.- Consent is not a mock.
ConsentAttestation/ConsentStatementare shipped and used by the companion: the Android consent screen (devices §3.2) signs a realConsentAttestationon approval. - The ledger is the same hash-chained discipline the companion audit log uses — tamper-evident, per-device.
What is not wired yet, even in the substrate: the pipeline
capabilities[] → plan table → minted grant is parsed but not connected to
grant minting (capability-model-design.md §1), and the agent family
(agent://act) is declared and audited only, not enforced. So the substrate
can express agent authority; it does not yet gate agent actions on it. That is
the frontier (review G-2).
2.2 darc today — the research preview (real but unbounded)
darc (Jan’s xe-darc) is a working research-preview browser built as an
Isolated Web App: Controlled Frame with per-tab partitions
(ControlledFrame.svelte:2057), an agent sidebar (Claude models, voice, a task
queue), one-shot executeScript plus persistent addContentScripts injection,
a CDP proxy, and a console.log('iwa:…') IPC channel
(docs/darc-cf-alpha.md). It is the demo, and it is real.
It is also, honestly, unbounded — the capability boundary the mission requires is not there yet:
- The agent sidebar and the untrusted page share one IWA shell with no
capability check between them (
injection-boundary-design.md §1). - The only live gate is a human approving each tool call in the sidebar UI
(
§1.3). That is a real safety property, but it is a person, not a capability. - The CDP proxy forwards
'*', soRuntime.evaluatepasses unconditionally (§1.1). daemon/main.jsis 0 bytes (§1.1, Path C) — there is no separate agent daemon yet; the agent lives in the shell.
None of the shipped runtime or companion code contains a resident AI agent at
all: every “agent” in xenon-runtime is host-agent (the app-hosting
substrate daemon) — a different sense of the word. Nothing there reads a page,
decides, or acts on your behalf.
2.3 darc → Reagent (the coming rename + productization)
Reagent is the productized resident agent that darc becomes — the rename is
Luke-and-Jan’s to execute (doctrine/vocab.md). The design that would give it
a real boundary, injection-boundary-design.md, is paper only (“No code
changed”). So the resident agent layer is largely future: the substrate is
real, the agent that will stand on it is not. The chapter names that rather
than letting the vocabulary imply a bounded resident agent exists today.
2.4 The agency ladder — rungs in vocabulary, zero in code
The ladder — observed → enhanced → mutated → controlled → driven (d/082,
d/091) — is settled vocabulary. In code, zero rungs exist. The closest
code-shaped mapping is the injection-boundary capability enum (read.text / read.dom / read.field / mutate.node / mutate.form / navigate.tab / inject.persistent / execute.script, injection §3.2) — and that too is paper.
The ladder is a language waiting for an agent to exercise it; it is the
ability-verb axis the delegation model (below) would put under agent://act.
Zoom 3 — the module that would join them: grant/lease delegation (PROPOSED)
The delegation model is where the substrate (real) meets the agent (future),
and where UCAN maps almost exactly. It is PROPOSED — an outline built from
the shipped xenon-cap / xenon-grant primitives, not yet a wired pipeline.
Naming note. The product delegation model is
capability-model-design.md
injection-boundary-design.md. It is notdocs/delegation-interfaces.md, which is about an unrelated broker; a reader should not conflate them.
Two kinds of delegation, cleanly separated:
- A grant is a long-lived delegation — runtime-authority-signed, handed
to an install principal,
onward:false(no re-delegation). It says “this app may, standing, do these verbs on these resources.” This isxenon-granttoday. - A lease is a short-lived, attenuated invocation minted under a
grant for one agent action — nonce, a use-binding (single-use / count /
session), a not-before/not-after window, a signer, and an audit reference
(
injection §3.1). It says “for this action, right now, the agent may do this much and no more.”
sequenceDiagram
participant U as You (authority root)
participant G as Grant (long-lived delegation)
participant A as Agent (Reagent)
participant L as Lease (attenuated invocation)
participant S as Surface (page / app / net)
participant Led as Audit ledger
U->>G: sign grant → install principal (onward:false, expiry)
Note over G: standing permission: verbs x resources
A->>G: request to act on a surface
G->>L: mint lease UNDER grant (nonce, use-binding, notBefore/After)
A->>S: invoke action bounded by lease
S-->>A: result
A->>Led: append {leaseId, nonce, before, after, cause, prevHash}
Note over U,Led: you can inspect every lease-use, and revoke the grant to end them all
The UCAN mapping, made explicit (and recommended as a product decision, not an implementation detail):
| This model | UCAN analog |
|---|---|
| Grant | a UCAN delegation — issuer = runtime authority, audience = install principal, capabilities, expiry |
| Lease | an attenuated UCAN invoked once — the nonce is the replay-guard (CID-equivalent) |
onward:false | a leaf with no further delegation proofs |
| agency-ladder rung | the ability-verb axis under agent://act |
The full field outline is a Part V candidate: Grant / Lease delegation model (UCAN-aligned).
Honest edges
Named here, argued in the review:
- The resident agent does not exist as a bounded process. darc is real but
unbounded (human-in-the-loop is the only gate); Reagent is paper; the
daemon/is 0 bytes. - Agent authority is declared, not enforced.
agent://actis audited only; thecapabilities[] → grantpipeline is unwired. - BYO model is litmus debt. Model IDs are hardcoded in darc; sovereign model choice is a mission requirement not yet met.
- The ladder is vocabulary. No rung is enforced in code.
Links
- UCAN: spec · delegation · invocation
- Object-capability prior art: SPKI/SDSI RFC 2693 · Macaroons · capability-based security
- Agent-boundary threat framing: the lethal trifecta
- Related chapters: Devices · App model & packaging · Review
Review: Transport & identity
This review takes the layer as described and
asks three questions: what verifiably holds, what is built but idle, and what
still needs design. The lens throughout is the sovereignty litmus (inspect
what it sees · fork permissions · BYO models + keys · movable memory ·
auditable actions · exit without loss). Verdicts cite code at
xenon-launcher main @56e94575; gap IDs (G3, G4) are this beat’s register and
feed Part IV.
What verifiably holds
- One key, honest receipts. The device identity and the network address
are the same Ed25519 key, and every connection reports its real path —
Direct,Relay, or an honestUnknown— with a test-onlyforce_relayto prove relay carriage rather than assume it (iroh_provider.rs:307-317,:98-100). - Membership is enforced where wired, actively. Inbound and outbound
connections are authorized against the signed head, and revocation closes
live connections immediately with a named QUIC error
(
0x5845,xenon: membership revoked;iroh_provider.rs:200-226,:242-245). Removal is a disconnect, not a hope. - The chain refuses the sharp edges. Succession is exactly-one-version,
prior-hash-named; gaps, rollbacks, and forks are rejected; a persisted
low-water mark refuses pre-seen documents across restarts
(
group/mod.rs:11-16,:473-509). Canonical binary encoding makes hashes implementation-independent (canonical.rs). - The join ceremony burns before it mints. Single-use invite + Ed25519
proof-of-possession, with the invite durably consumed before the member
is added (
group/protocol.rs:321-368) — a crash cannot leave a replayable invite behind. - Gossip validates every step. One verified successor at a time, each
re-checked against the chain rules (
run_sync,:422-479).
Built, but idle
- Recovery statements (successor / takeover / counter / freeze /
rotation, 72 h challenge window, FROST types reserved;
group/recovery.rs) have no ceremony, UX, or networking driving them. This is designed headroom — valuable — but the honest status is: there is no working key-recovery lifecycle today. A lost device key is currently an unrecoverable identity. - The membership plane itself is the biggest idle asset: fully built, signed, revocation-capable — and the serving path does not consult it. That is G4, below.
Gap register (this beat — transport slice)
G3 — Relay trust model
Exists: with no relays configured, RelayMode::Default selects the
iroh/number0 public relay set (iroh_provider.rs:84-85, presets::N0).
Group documents can pin relay_endpoints under the authority signature
(:74-83), but peer-serve takes relays from CLI arguments only
(cli.rs:485-488) — unsigned.
Why this needs design: relays see connection metadata (who talks to whom, when, how much) and are an availability dependency operated by a third party. No fallback policy is stated anywhere.
Options: (a) Agent54-run relay(s), pinned in the signed head; (b) explicitly document acceptance of n0’s relays, direct-preferred with relay fallback; (c) relay-optional (hole-punch-only) mode for LAN-local fleets.
Decides: Luke — infrastructure plus a trust boundary.
G4 — Device identity & membership lifecycle for serving
Exists: a rich lifecycle in crates/net/group — enrollment
(invite + proof-of-possession), removal (append-only revocation with
live-connection closure), recovery statement families. The serving tunnel
bypasses all of it: peer_serve.rs builds IrohProvider::new, never
with_membership (:435, :591).
Why this needs design: a device removed from the group can still serve and consume so long as it holds a live ticket. Enrollment and authorization-to-serve are different graphs today, and the sovereignty story (“my devices, my group, my rules”) only refers to one of them.
Compounding fork: two group-document lineages exist — v1 in
crates/net/group (what everything compiles against) and v2 in
crates/xe-sync (MemberKind{Device, Companion}, endpoint bindings, v2
signing domains). Which is canonical for shipped serving is undecided, and
Companion has no serving semantics at all.
Options: (a) gate the serve ALPN on membership — small change, pass a
MembershipRegistry into publish/consume, and group revocation cuts serving
tunnels for free; (b) keep bearer tickets for the “share to a non-member”
case and add membership as an additional gate; (c) settle the v1-vs-v2
canonical document first, then wire.
Decides: Luke — this is the sovereignty spine of the whole beat. See also G1/G5 in the peer-serving review, which collapse if option (a) lands.
Sovereignty litmus check
| Criterion | Verdict for this layer |
|---|---|
| Inspect what it sees | Mostly holds. Path receipts are honest; frames are small strict JSON. Relay metadata exposure (G3) is real but not yet documented to the user. |
| Fork permissions | Blunt. AuthorityMode::Single means one authority key governs succession; members cannot attenuate or delegate. Recovery/quorum (FROST) is reserved, not built. |
| BYO models + keys | Holds for keys — device keys are locally generated, never escrowed. Relays are BYO-able in the signed doc for the group plane, but only via unsigned CLI for serving (G3). |
| Movable memory | Not this layer’s surface (app data single-homing is a site-wide finding; see Part IV). |
| Auditable actions | Holds in-plane. Hash-chained document, append-only revocations, canonical encoding. (Site-wide: grant/trust journals outside the hash chain — Part IV.) |
| Exit without loss | Partial. Leaving a group costs nothing held here; but a lost device key has no recovery lifecycle yet, which is a loss with no exit. |
Questions for the Luke + Jan session
- v1 vs v2 group document — is
crates/xe-sync(v2: Device/Companion kinds, endpoint bindings) the intended canonical membership layer for shipped serving, or iscrates/net/group(v1: what gossip and the membership gate compile against) the line? This blocks G4. - Relay trust — run Agent54 relays pinned in the signed head, or accept number0’s public relays with direct-preferred + relay-fallback documented? (G3)
- Companion semantics — should
MemberKind::Companion(v2) be allowed to consume but never publish? No serving semantics exist for it today.
(The unify-serving-with-membership question itself is held in the peer-serving review, where the bearer-ticket alternatives live.)
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.
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.)
Review: App model & packaging
This review takes the app model as described and asks
three questions: what verifiably holds, what is built but idle, and what still
needs design. The lens is the sovereignty litmus (inspect what it sees · fork
permissions · BYO models + keys · movable memory · auditable actions · exit
without loss). Verdicts cite code at xenon-launcher main @56e94575 and
xe-example-apps @5752a3e; gap IDs (AM-*) are this beat’s app-model slice and
feed Part IV. The backend/engine half of the beat
is reviewed separately.
What verifiably holds
- One substance, one axis — and the code agrees. The XAM manifest carries
identity, source, and components as first-class fields, and the driver
(
Cf | Iwa | Origin | Oci | Compose) — not the packaging — decides how a component renders (model.rs:299). The spectrum of d/076 is expressible, not aspirational. - The two consent gates are unforgeable by construction. A
BuildGrantcan only be minted from a gate-1 consent whose digest equalssha256(plan)(adapter.rs:93), and every adapter re-checks it atmaterialize. The runtime refuses to build a runnable project without gate-2 consent matching the locked release digest, and private fields make the ungated path unreachable (runtime.rs:138). You cannot forget to check. - Unsupported features fail the install closed, early. A non-empty
unsupported_featureslist prevents materialization before anything is fetched or built (adapter.rs:45,compose.rs:1498). The default is “no,” not “try and see.” static-v0is proven end to end. excalidraw installs viastatic-v0/Origin, renders live, and is byte-identical across both peers (P-211). This is the exemplar of the d/101 default tier.- Compose service apps are proven live. guestbook and compose-board run through the full install → start → serve → recover loop (P-141 lifecycle + P-203 auto-wired end-to-end). See the engines review.
- Exit is a mandatory decision, not an afterthought.
UninstallTransactionforces a data disposition before delete —VerifiedExportThenDelete | RetainDetached | DestructiveDeleteConfirmed(lifecycle.rs:299) — andexport_xaaXAA archives already back update snapshots. Named volumes survive a stop.
Built, but idle
- Half the source kinds have no adapter.
Image,Web, andOciare expressible in the manifest (model.rs:230) but have no adapter behind them. A manifest can declare them; nothing installs them. - Reserved manifest families.
handlers,interfaces, andnotificationsare parsed and stored but not acted on — headroom, honestly labelled. - The swbn / IWA super tier. Fully built and tested (
swbn.rs), but under d/101 it is reserved for shells (console/hello-xe). The two dev-signed swbn example apps (scratchpad,appdex) are residue of the earlier per-app-IWA direction, not the default. Nothing forces swbn on an ordinary app — but the residue is a live cleanup item (AM-2 below).
Gap register (this beat, app-model slice)
IDs are beat-local; Part IV consolidates.
AM-1 — The rendering model still contradicts itself (d/103 item A)
Exists: d/103 item A is DECIDED — a compose app’s browser-facing service renders as a whole-origin proxy route on the plane, framed by Prism like every other app; no per-app Controlled Frame special-casing.
The gap: the tree has not caught up. RecipeOperation::MarkUi’s comment
still says “Controlled Frame UI” (compose.rs:49), and compose_components
maps x-xenon-component:ui → RuntimeDriver::Cf (compose.rs:1225). This is
the residual CF-per-app intent d/103 wants reconciled to plane rendering. Until
it is, a reader of the code sees two contradictory rendering stories.
Options: (a) reconcile MarkUi semantics to “browser-facing UI served via
the plane” and surface its Http endpoint into the runtime manifest — the P-210
work; (b) leave it until P-210 dispatches. Decides: reversible → orchestrator
(the decision is made; this is code hygiene). Gated behind AM-4 and the
engines review’s P-174 sequencing.
AM-2 — swbn residue vs the d/101 default
Exists: d/101 makes plain HTTP the default; swbn/IWA is the rare super tier.
The gap: SourceKind::Swbn, RuntimeDriver::Iwa, and the full swbn.rs
adapter are all present and tested, and two example apps still ship dev-signed
swbn. None of this is wrong — it is the legitimate super-tier machinery — but
the example set does not yet model the default cleanly.
Options: (a) migrate scratchpad/appdex to the plain-HTTP/origin tier so
the examples teach the default; (b) relabel them explicitly as super-tier
demonstrations. Decides: reversible → orchestrator.
AM-3 — App identity / signing, end to end
Exists: the catalog is signed by an ORCH-BUILD Ed25519 key
(package-index/src/lib.rs:32, trust label ORCH-SIGNED), successor-signing
verifies the committed catalog before signing serial N+1
(sign_catalog_successor, :187), and a retired key + catalog_key_migration
record show a rotation was already exercised. Per d/066 the orchestrator holds
keys for the build phase; every trust label shows the interim ORCH-SIGNED / DEV
tier.
The gap: compose apps are always UnsignedDerived (compose.rs:383) —
identity is URL + subdir, with no publisher key. Trust-on-first-install versus
update relies on a package_id match plus carried consent, with no
per-publisher key pinning across updates. The real signing ceremony is deferred
(P-075 rotates all of d/066 out).
Decides: irreversible / security → Luke + Jan (production ceremony). This is not an orchestrator call.
AM-4 — Route type for compose UIs (d/103 item B) — PROPOSED
Exists: P-203 shipped the whole-origin compose case as a backend-proxy
route with an empty digest + prefix / — an overload that does not touch
the P-148-frozen route-table type discriminator. origin::generate already
emits that shape (origin.rs:534).
The gap / open decision: d/103 item B is PROPOSED, not ratified. Accept
P-203’s backend-proxy overload as the documented whole-origin shape, or ratify
a distinct additive container-origin route type? P-210 (the Immich-class
whole-origin render) is blocked on this ruling.
Options: (a) accept the overload as the documented shape (schema-conservative,
already tested); (b) add container-origin as an additive type (cleaner
invariant: backend-proxy keeps meaning “static digest + proxied prefix”).
Decides: Luke (frozen-schema addition). Reversal is free — nothing more
is built until the gates clear.
AM-5 — First-class export UX (exit without loss)
Exists: the uninstall transaction makes data disposition mandatory, and XAA export archives exist for update snapshots.
The gap: there is no first-class per-app export command surfaced, and the
uninstall browser-storage sweep “does not independently prove localStorage
emptiness” (documented limit, serving-and-placement-design.md:93).
Options: (a) add an xe app export command backed by the existing XAA
machinery; (b) add a DOMStorage-receipt follow-up so uninstall can prove the
sweep. Decides: reversible → orchestrator.
Sovereignty litmus check
| Litmus | App-model verdict |
|---|---|
| Inspect what it sees | Strong. XAM is human-readable; Plan Review shows declared build effects (network hosts, context roots, resources, emulation); capabilities carry required reason strings. |
| Fork permissions | Holds. Capabilities are declared, gated, journaled, and revocable (L2, d/076); the two consent gates bind to exact digests. |
| BYO models + keys | Partial. Publisher signing is ORCH-held today (d/066); compose apps are unsigned-derived with no BYO-publisher-key story yet (AM-3). |
| Movable memory | Partial. XAA export archives + mandatory uninstall disposition exist; no first-class per-app export UX (AM-5). |
| Auditable actions | Holds. Install transitions write a signed audit; consent requests move pending → claimed → decided as inspectable records. |
| Exit without loss | Partial. Uninstall forces VerifiedExportThenDelete / RetainDetached / DestructiveDeleteConfirmed; the localStorage sweep cannot yet prove emptiness (AM-5). |
The model scores well on inspect / fork / audit — its consent design is its strongest sovereignty asset. The two soft spots are both about keys (AM-3) and getting your data out cleanly (AM-5).
Questions for the Luke + Jan session
- d/103 item B ratification (AM-4). Accept P-203’s shipped
backend-proxyempty-digest overload as the documented whole-origin shape, or ratify a distinctcontainer-originroute type? P-210 is blocked on this. (Luke) - Signing ceremony timing (AM-3). d/066 has the orchestrator holding
ORCH-SIGNED keys with a demonstrated rotation. When does P-075 (Luke + Jan
offline root) run — and does compose’s always-
UnsignedDerivedidentity get a publisher-key story, or stay URL-derived? (Luke + Jan) - Example-set hygiene (AM-2). Should the dev-signed swbn examples migrate to the plain-HTTP default tier so the example set teaches d/101 cleanly, or be explicitly relabelled super-tier? (Fable to dispatch; confirm intent)
Review: Engines & backends
This review takes engines & backends as described
and asks the three questions: what verifiably holds, what is built but idle, and
what still needs design. The lens is the sovereignty litmus. Verdicts cite code
at xenon-launcher main @56e94575; gap IDs (EB-*) are this beat’s engines
slice and feed Part IV. The app-format half is
reviewed separately.
What verifiably holds
- The d/094 third path is live, not planned. The connector reaches
containers through the engine’s unix socket via
docker exec(backend.rs:827), with no sidecar and no host-published backend port — the two P-141 escalation options are retired in code, not just on paper. This shipped in P-203. - The connector trusts no request input. Project, route, and service are
resolved from launcher-written registry state, never from the HTTP request
(
backend.rs:416); thex-xe-appheader is validated to[a-z0-9-]{1,63}; and the route worker’s grants never cover the backend prefix (a403,route-worker.js:304). A cross-origin grant cannot be turned into backend reach. - The engine is refused if ambiguous.
SmolvmBackendrefuses zero or more-than-onedocker.sockunder the data root (smolvm.rs:276) and verifies a pinned launcher + binary sha256 before exec (smolvm.rs:301). No silent engine substitution. - Failure is told honestly. The lifecycle engine separates slow (503 +
jittered
Retry-After) from failed (502, namingxe doctor), holds only replayable requests, and never holds-then-loses a POST (classify,backend.rs:96;http_error,backend.rs:1110). Singleflight is proven under 200 concurrent first requests (backend.rs:1573). - The compose subset fails closed. Anything that would puncture microVM
isolation is refused at install with a stable code +
source:line:col(compatibility_diagnostics,compose.rs:907).
Built, but idle
- The update engine has no user loop.
crates/apps/src/update.rsis a complete engine — semver-gated discovery, escalation-detecting diff, snapshot → stage → activate → rollback-on-failure, digest-verified volume restore, and anAuto | Notify | Manualpolicy where only patch, non-escalating updates auto-apply and escalations force fresh gate-2 consent. The recon found no CLI or consent-surface wiring driving any of it. The mechanism is correct and idle; there is no proven update loop today (EB-1 below). - Non-default engines are present but partial.
wsl2.rs,firecracker.rs, andmicrosandbox.rsbackends exist alongside smolvm and the ColimaAppEngineBackend— but only smolvm and colima have the connector exec bridge (EB-2 below).
Gap register (this beat, engines slice)
IDs are beat-local; Part IV consolidates.
EB-1 — App updates have no user loop
Exists: a library-complete update engine (above), with the right safety
shape: escalating updates force fresh gate-2 consent (update.rs:239);
non-escalating updates carry prior consent forward (update.rs:242).
The gap: no surfaced command and no consent-surface wiring. The update loop — discover → show the diff at the consent gate → apply or roll back — is unproven end to end.
Options: (a) wire an xe app update command + consent-queue integration and
prove one end-to-end update-with-rollback; (b) leave until an app needs it.
Decides: reversible → orchestrator (surface + one end-to-end proof).
EB-2 — No exec bridge for Windows (WSL2)
Exists: the connector exec bridge is implemented for smolvm and colima only. Linux backend apps work via the smolvm socket.
The gap: WSL2 → “does not yet implement the WSL exec bridge,” ProvisionedVm
→ “no connector exec bridge” (backend.rs:878-889). Windows
backend-carrying apps are unsolved — a genuine hole, not a rough edge.
Options: (a) add a WSL exec bridge; (b) publish a Windows named-pipe socket contract mirroring d/094. Tradeoff: every new engine widens the socket-custody surface the launcher must own. Decides: reversible arch → orchestrator, but whether Windows-with-backend is in scope now is a Luke/Jan placement call (question 3).
EB-3 — Compose validator holes gate the connector (P-174) — sequencing
Exists: the normative refusal list is enforced install-time and fails closed.
The gap: P-174 (open) documents three pre-existing holes in that list —
short-form host ports (ports: ["8080"]), non-UTF-8 corruption in
interpolate_declared, and network_mode: container:. These matter because
P-174 gates running docker compose up on user-supplied manifests. The
guestbook/compose-board proofs used authored manifests; a third-party manifest
is a different trust posture.
Options: (a) dispatch P-174 to close the holes before P-210 (Immich / whole-origin, which runs a third-party compose); (b) accept the holes as known-and-bounded for the near term and sequence P-210 first. This is a sequencing question, not a rendering one. Decides: Fable to dispatch; confirm sequencing with Luke (question 2). Recommendation: P-174 before the connector meets an unauthored manifest.
EB-4 — GPU / ML backend class collides with the isolation subset
Exists: nothing — there is no GPU passthrough in any backend.
The gap: the safe-compose subset forbids exactly what GPU passthrough
needs — raw devices: and privileged are both on the refusal list
(compose.rs:918, 924). Immich dropped its ML service to install at all
(the honest P-207 stop). So a GPU-bearing backend collides head-on with the
isolation validator.
Options: (a) a narrow, separately-consented device-grant carve-out; (b) route ML backends to a dedicated GPU placement outside the container subset. Tradeoff: any device access punctures the compose-subset trust boundary. Decides: Luke — this is litmus / security-touching, not an orchestrator call (question 1).
EB-5 — Resource limits are declared, not enforced
Exists: the XAM ResourceEnvelope (request / minimum / breach) is validated
at install (request ≥ required_minimum).
The gap: the compose adapter sets ResourceEnvelope::default() for every
compose component (compose.rs:1237) and does not map deploy.resources.limits
(which compose-board/compose.yaml declares) into the envelope or into
container cgroups. The envelope is advisory, not runtime-enforced.
Options: map deploy limits → envelope → engine --memory / --cpus.
Decides: orchestrator + a small spec addition.
EB-6 — Documentation staleness (a documentation-program finding)
Exists: docs/serving-and-placement-design.md describes the serving model.
The gap: it is stale. Sections :29, :58-72 still describe
backend-proxy as “dormant / fail-closed end to end … no current launcher
listener owns 127.0.0.1:5199.” That predates P-203; the code at HEAD
contradicts every clause (the connector does bind :5199, the exec bridge
is live). A reader trusting that doc would conclude the opposite of the truth.
Options: (a) refresh the in-repo doc; (b) supersede it with this documentation site as the live truth and mark the old doc historical. Decides: Fable (question 4).
Sovereignty litmus check
| Litmus | Engines/backends verdict |
|---|---|
| Inspect what it sees | Strong. Compose diagnostics name source:line:col; the connector resolves everything from inspectable registry state; failure modes map to honest HTTP status. |
| Fork permissions | Holds. The safe-compose subset is a declared, enforced boundary; device/privilege escalations are refused with stable codes. |
| BYO models + keys | Partial / open. No GPU/ML backend class yet (EB-4) — a BYO local-model backend cannot run inside the current subset without a Luke ruling. |
| Movable memory | Mostly. Named volumes survive stop; update snapshots are digest-verified XAA archives; rollback restores volumes. |
| Auditable actions | Holds. Consent requests are inert-until-decided records; the runtime choke point (runtime.rs:138) leaves no ungated path. |
| Exit without loss | Partial. Volume survival + XAA export are strong; the update user loop that would let you act on an update or roll back is unwired (EB-1). |
The engine layer’s isolation and honesty are its strongest sovereignty assets. The two open edges both touch capability the subset forbids: local ML/GPU (EB-4) and Windows placement (EB-2).
Questions for the Luke + Jan session
- GPU / ML class (EB-4). The isolation subset forbids
devices:+privileged, which GPU passthrough needs. Do we design a narrow, separately-consented device-grant carve-out, or keep ML backends off the container subset entirely? (Luke — litmus) - P-174 before the connector meets real apps (EB-3). The safe-compose validator has three known holes; P-174 gates running compose on user-supplied manifests. Dispatch P-174 before Immich / P-210? (Fable to dispatch; confirm sequencing)
- Windows backend engine (EB-2). No exec bridge for WSL2; backend-carrying apps cannot run on Windows. Is Windows-with-backend in scope now, or explicitly deferred? (Luke / Jan — placement)
- Doc hygiene (EB-6).
serving-and-placement-design.mdis stale (callsbackend-proxy“dormant”; contradicted by shipped P-203). Refresh in-repo, or supersede it with this documentation site as the live truth? (Fable)
Review: Shell & rendering (Prism / Xenon)
This review takes the layer as described and
asks three questions: what verifiably holds, what is built or proven but not
yet the design, and what still needs design. The lens throughout is the
sovereignty litmus (inspect what it sees · fork permissions · BYO models +
keys · movable memory · auditable actions · exit without loss). Verdicts cite
code at agent54/prism main @98acaa3 and the Xenon packet record; gap IDs
(G1–G6) are this beat’s register and feed Part IV.
The one line to carry out of this review: the highest-leverage undesigned sovereignty surface in the whole layer is G6 — the in-frame permission model. Whether a page’s request for the camera, microphone, or location is answered by the user’s own agent (shielded by default, journaled) or by Chrome’s native prompt decides whether the phrase “your agent answers for you” is real or marketing. It is not built for installed apps, and it is Luke’s call.
What verifiably holds
- The trust boundary is one readable line. The baked CSP
(
packaging/bundle/headers.json) reduces the shell’s outbound trust to two origins —127.0.0.1:5454for the catalog and*.localhost:5454for app panes — and nothing else. A reviewer can hold the entire network surface of the shell in their head. That is the sovereignty litmus’s inspect what it sees criterion, met by construction. - Guests are unprivileged by default, and the shell says how. Apps load
into partitioned Controlled Frames (
persist:prism-fleet-apps); the only way back to the shell is aMessageChannelgated by port identity, a 128-bit nonce, and an exact schema, carrying exactly four tab verbs (src/channel/message.ts:15,:41-63). Every accept and every reject is journaled. The boundary is small enough to audit. - Honesty is enforced, not hoped. A static lint forbids control ever
riding over console output (
scripts/check-channel-boundary.mjs); offline apps are shown greyed with a last-seen time rather than hidden (iwa-controlledframe-app.ts:161-193); the dev-signed trust tier is printed in the app’s own name. The journal is hash-linked and separately verifiable (journal:verify). - It runs on a real device. Prism installed and rendered a guest inside a
genuine
HTMLControlledFrameElementon an Android emulator (P-195, merged PR#17) — the Controlled-Frame story is proven, not aspirational.
Built or proven, but not yet the design
- Two engines (P-192). The same signed bundle rendered on Blink and Servo. Real, and strategically large (see below) — but the Servo path is CF-shaped, not CF-strong, so it is inspiration for a standards story, not a shippable second engine yet.
- Composition. A working, receipted carry between two apps exists — on
<iframe>, not Controlled Frames (web/index.html:28-29). The mechanism is proven; its migration onto the Controlled-Frame + catalog path is not built (G3). - The launch seam. The catalog-descriptor launch path is already the code
(
iwa-controlledframe-app.ts:258), but blessing it as the canonical cross-platform seam is a proposal awaiting Luke (d/104). The code has run ahead of the ratified design; the review flags the gap rather than pretending it is closed.
Desired vs current vs gap — the map
| Area | Desired | Current | Gap |
|---|---|---|---|
| App tier (d/101) | Most apps plain http(s); IWA reserved for the shell | Exactly this — only Prism is bundled; every proven guest ran plain http | — (holds) |
| Launch seam (d/104) | One catalog descriptor is the launch path, all platforms | Code already sets frame src = launch_url; Android can’t repoint installed guest | Ratify d/104 (G-seam) |
| In-frame permissions | Shell brokers every permission, shield-by-default, journaled | Only geolocation, only in the dev driver, same-origin | G6 — undesigned |
| Composition (d/103) | Any two fleet apps compose via CF + plane origins | Two fixture apps compose via iframes | G3 |
| Frame↔shell contract | App can title / notify / report liveness safely | Four tab verbs, one-way; no title/notify | G2 |
| Production identity | Real key ceremony, independent review | Dev-signed; name reads “(DEV-SIGNED)” | G1 |
| Local trust | Authenticated wake / bind of the plane | CSP trusts whatever binds the ports | G4 |
| Fork burden | Minimal patches, upstream-aligned | 3-patch series, no rebase cadence set | G5 |
Gap register (this beat — shell & rendering)
G6 — The in-frame permission model (the headline gap)
Exists: only geolocation, and only in the development driver, applied
same-origin (src/spectrum/permission.ts, src/gateway/gateway.ts:128-143).
The installed-driver mechanism is deliberately not built — when it cannot
mint a lease or journal the answer, the gateway refuses rather than fake a
result (permission.ts:4-13, gateway.ts:130-135). Answers can already be
allow, deny, or shield (return a user-chosen value; the page never sees the
real device).
Why design is needed: three questions are open and every one of them is a sovereignty question. Who prompts — the shell, or Chrome’s native dialog inside the partition? Who remembers — is there per-origin persistence the user can inspect and revoke? And the whole non-geolocation capability set (camera, microphone, clipboard, USB) is undesigned for the installed Controlled-Frame driver.
Options and trade-offs — presented for Luke to weigh:
| Option A — shell brokers everything | Option B — delegate to Chrome | |
|---|---|---|
| Who prompts | The user’s agent, via the agency ladder | Chrome’s native per-origin prompt |
| Default answer | Shield by default (user-chosen value) | Whatever Chrome offers (allow / block) |
| Record | Journaled, inspectable, revocable | Chrome’s site settings |
| Litmus fit | Inspect what it sees + auditable both met | Cedes “your agent answers for you” |
| Cost | Real browser-driver engineering per capability | Nearly free — it already works |
Who decides: Luke — this is litmus-touching. Option A is the entire differentiator of the product (“your agent answers for you”); Option B is free but concedes the sovereignty claim at exactly the surface where users feel it most. A design packet should precede any implementation.
G1 — Production signing / the key ceremony
Exists: development signing by key-path (d/066, orchestrator-held build-phase key); the installed name literally reads “(DEV-SIGNED)”. Why design is needed: the ceremony (offline root, Luke + Jan) is unbuilt; nothing yet rotates the dev key out or establishes a world-facing, independently reviewed identity. Options: (a) a single offline-root ceremony at world-release (current plan) — every install shows DEV until then; (b) an intermediate cross-signed release tier before public. Decides: Luke + Jan (key custody is a hard rule; d/066 is a bounded, build-phase-only override). Queued behind the publishing gate.
G2 — The frame↔shell communication contract
Exists: an asymmetric, minimal surface. App→shell is the four-verb nonce
channel; shell→app is a capability-leased gateway reaching the pane, plus an
opt-in cooperative bridge an app may expose (web/dev-origins/site.html).
Why design is needed: there is no contract for an app to set a window
title, raise a notification, request focus, or report liveness. A guest
sets document.title, but the shell has no defined reaction. Options:
(a) extend the nonce-authed verb set with a small app-declared surface
(title / notify / badge); (b) keep apps fully unprivileged and derive what the
shell needs by observation. Trade-off: (a) is ergonomic but every verb is a
capability whoever holds the port then holds (the channel doc’s own caveat);
(b) keeps the boundary tiny but apps cannot cooperate. Decides: Fable
proposes the verb-set delta; Luke on anything touching user-visible identity
(notifications).
G3 — Multi-app composition
Exists: the Composition surface frames two fixture apps and carries a leased
“carry” record between them (read → consent card with a payload fingerprint →
leased write → undo) — on iframes (web/index.html:28-29).
Why design is needed: composition across arbitrary catalog apps in
Controlled Frames (vs two hard-coded embeds) is aspired, not built; d/103
(compose UIs are plane origins) is the intended generalization. Options:
(a) promote composition onto the CF + catalog path so any two fleet apps
compose; (b) keep composition a curated shell feature over fixtures.
Decides: Luke (product surface).
G4 — The local-trust boundary
Exists: the CSP pins trust to 127.0.0.1:5454 + *.localhost:5454.
Why design is needed: the shell trusts whatever binds those ports.
d/053’s own gate says “unauthenticated loopback TCP is disqualifying by our own
standard” — yet the IWA→Boot-Service wake channel’s authentication is explicitly
OPEN (d/053:44-49). Options: per-install token provisioned at install; IWA
Direct Sockets with an authenticated handshake; or OS socket activation
(launchd / systemd / Windows service triggers). Decides: Luke — security and
litmus-touching; a design packet is required before implementation.
G5 — Chromium-fork maintenance burden
Exists: agent54/android-iwa is deliberately a patch series, not a full
fork — three patches (0181 / 0182 / 0183) on a stock Chromium base plus gn
args and validation scripts. Controlled Frame and IWAs are real upstream
Chromium features (origin trial), so the divergence is small and
upstream-aligned. Why design is needed: no rebase cadence, patch-count
budget, or upstreaming path is recorded. Options: (a) ride the
desktop-android variant + origin-trial features and drive patches toward zero as
upstream lands (lowest burden, dependent on upstream timing); (b) maintain a
standing carry-patch set (control at the cost of rebase toil). Decides:
Jan + Luke (fork strategy). Note d/098 — no agent-opened upstream PRs, ever;
upstreaming is a human-published contribution.
P-192 — Two engines, and the standards story
The single result most worth understanding strategically is that the same
signed bundle rendered on two independent engines: Blink via a real IWA install,
and Servo via a Rust isolated-app:// scheme handler that verifies the
bundle’s Ed25519 signature offline before serving a byte (a tampered bundle is
refused, test-pinned). This converts “a Chromium-fork feature” into “a
web-platform shape with more than one implementation” — which is precisely the
argument a standards body wants to hear, and precisely the position that keeps
the Xe Computer from being a single-vendor dependency.
The honesty caveat is load-bearing: the Servo side is CF-shaped, not CF-strong. It reproduces the API surface and the origin-verification discipline, but not the isolation fundamentals — no OS sandbox, no cross-origin-isolation implementation, permissions-policy commented out (P-190/P-191, PARKED). So P-192 is a credible standards and independence story and a reminder that a second production engine is real work, not a flag flip. The verdict on Servo as a base was INSPIRATION / NOT-VIABLE-YET, revisited the week of Jul 20.
Sovereignty litmus check
| Criterion | Verdict for this layer |
|---|---|
| Inspect what it sees | Holds by construction. The whole outbound trust surface is two CSP origins; the shell can executeScript-observe guests, and every observation is leasable and journaled. |
| Fork permissions | Partial — and this is G6. Permission shielding exists in the dev driver (geolocation), but the installed-app broker is unbuilt; the choice between shell-brokered and Chrome-native prompts is undesigned and decides whether this criterion is truly met. |
| BYO models + keys | Holds for shape, pending ceremony (G1). Signing keys are local and never escrowed; the dev key is honestly labelled; the production ceremony is ahead. |
| Movable memory | Structurally present. Guest state lives in a named partition; app data single-homing is a site-wide finding (Part IV), not this layer’s to close. |
| Auditable actions | Holds. Hash-linked journal, separately verifiable; channel rejections recorded with cause; leased reads receipted. |
| Exit without loss | Holds as intent, written into the product. “Swap this shell and keep your apps, your data, and your journal” (spectrum.ts:163-165) — Prism is interchangeable by design; the guarantee firms up once composition and the launch seam are the ratified path (G3, d/104). |
Questions for the Luke + Jan session
- In-frame permission model (G6, the headline). Does the shell broker all app permissions via the agency ladder — shield-by-default, journaled — or delegate to Chrome’s native per-origin prompts inside the partition? This is the highest-leverage undesigned sovereignty surface in the layer.
- Ratify d/104? Bless the catalog descriptor
(
label → http://<app>.localhost:5454) as the single Controlled-Frame launch seam on every platform, or hold out for an explicit install-time guest-manifest launch contract? (Unblocks the Android frame-cell.) - Wake-channel authentication (d/053 OPEN, G4). Per-install token, IWA Direct Sockets handshake, or OS socket activation for IWA→Boot-Service? This is the load-bearing piece of the local-trust boundary.
- Fork strategy (G5). Commit to minimizing
android-iwatoward zero patches as upstream Controlled Frame / IWA lands (accepting an upstream-timing dependency), or maintain a standing carry-patch set? Rebase cadence and patch-count budget are unset. - Composition scope (G3). Promote composition onto the Controlled-Frame + catalog path (any two fleet apps compose), or keep it a curated shell feature over the two fixtures?
- Production signing timing (G1). Confirm the key ceremony triggers at world-release and that nothing ships publicly under DEV-SIGNED.
Review: Devices (desktop & Android)
Review of the device fabric as described, under the
sovereignty litmus. Verdicts cite xenon-runtime main @b0cfc90 and the
authed-wake design doc; gap IDs (G-3…G-7) are this beat’s device-side register
and feed Part IV. The agent-layer gaps (G-1
resident agent, G-2 delegation) are held in the
agent-layer review.
The headline
The device fabric is the most-built layer of this beat, and its launcher crossing is one dispatch call short of complete. Desktops are proven full peers on all three platforms; the phone carries real membership, real consent, and a real audit chain. What is missing is not craftsmanship — it is reach: the phone can dial a desktop’s app list but nothing answers (P-188); a sleeping device usually cannot be woken because the transport that reaches it is the one transport that cannot be OS-socket-activated (G-4); and the reverse lifecycle — lost phone, new laptop — is thin. The fabric holds the device truth honestly; the gaps are all about crossing the gap between two devices.
What the layer does well, it does with unusual care: START_NOT_STICKY
sessions that never lie about being up, kill-vs-off audit distinctions,
device-name sanitization guarding three sinks at once, hosting-side grant
re-checks on every open, and a launcher that shows honest
CHANNEL_UNAVAILABLE copy rather than a fake empty list.
Desired vs current
| Want (decisions / packets) | Have (code @b0cfc90) | Delta |
|---|---|---|
| Mobile both-track: v1 phone-as-client, v2 Android runtime (d/062) | Companion APK: consume + launcher + membership/consent/audit; v2 runtime not in this beat | On track; v1 is real. The launcher list itself is not yet served (G-7). |
| Every desktop a full peer (P-206) | Linux/macOS/Windows all publish + consume native; byte-identical proofs | Holds. Windows caveats disclosed (P-201w headless, 0-app catalog). |
| Catalog IS the launch seam (d/104 PROPOSED) | On-device plane + apps.list protocol exist; no launch descriptor ratified | d/104 unratified — blocks P-202’s two frame cells and unparks P-188. |
| Authorized peer may wake a device it’s allowed to (P-123) | Honest disabled stub (P-169); crypto core designed | No code; the zero-resident-vs-reachability crux is unresolved (G-4). |
| Litmus: movable memory | Group doc syncs; app state + agent memory are per-device, local | No cross-device memory layer; phone can’t hold app state offline (G-5). |
Gap register (device slice)
G-3 — Device enrollment / recovery lifecycle
Exists: the forward path is solid — runtime enrollment + group-join over
iroh (lib.rs:120-171, group.rs:244), an offline-verifiable roster
(roster_from group.rs:657), immediate revocation for a live target, and
self-revocation detected via a dial refusal → self-revoked.json
(group.rs:438). docs/key-recovery-design.md exists on paper.
Why this needs design: the reverse / loss paths are thin. A lost phone’s
key can be revoked, but the group must re-mint invites; a new laptop
re-enrolls from scratch with no grant carryover (by design — reinstall =
new principal, capability-model §4); self-revoked.json is one-way with no
surfaced rejoin ceremony; there is no account- or social-recovery of the group
authority key itself.
Options / tradeoffs: (a) admin-re-invite as the blessed ceremony — simplest,
but a lost authority device is unrecoverable; (b) threshold / social recovery
of the authority key — resilient, heavier ceremony; (c) a surfaced rejoin flow
that clears self-revoked.json after a fresh invite. Ties to AW-2’s
statement-typed policy.
Decides: Luke — key-recovery ratification (litmus: exit without loss).
G-4 — Authed-wake, and the socket-activation crux
Exists: the design is delivered (P-123 → docs/xe-authed-wake-design.md);
no code ships beyond the honest disabled [Wake device] stub (P-169). The
proposal is tight: a roster-member peer may cause a bounded, idempotent
ensure_running on a target it is authorized to wake — nothing more — under
four invariants (W-1 wake carries no authority; W-2 sync never wakes / wake
never syncs; W-3 the runtime learns only a reason, never a payload; W-4
honest availability).
Why this needs design — the crux (§2.1 of the design): the peer transport
that shipped is iroh (QUIC/UDP + relay), and iroh cannot be
OS-socket-activated the way launchd or systemd can hold a plain TCP
socket and start a process on the first connection. The transport a peer uses
to reach a device is precisely the transport that cannot wake a zero-resident
device. So the only honest wake is a same-network, zero-resident desktop
over an OS-held plain socket; everything else the button honestly refuses
with an action. The wakeability matrix (design §2.2):
| Target state | Wakeable? |
|---|---|
| Desktop, same network, zero-resident | Yes (best-effort, OS socket activation) |
| Desktop, internet-only | No (relay cannot socket-activate) |
| Android, session active | Moot (already up) |
| Android, force-stopped | No (needs the unbuilt notifications transport — G-6) |
| Windows | No (no socket-activation primitive) |
| Powered-off | No (out of scope) |
Options / tradeoffs: (a) accept same-network-only for v1 and reserve a
relay-mailbox design for cross-internet wake later — honest, ships, defers the
hard part; (b) design the relay mailbox now — full reach, significant net-new
machinery on a route with subtle replay/amplification concerns; (c)
per-platform wake shims (launchd/systemd now, Android notifications + Windows
later). The crypto core is already designed either way (target-issued
WakeAdmissionV1 ≤24h cert, response-free WakeHintV1, durable at-most-once
replay admission, per-class amplification budgets).
Decides: Luke — AW-1 (accept the crux) and AW-2 (authorization source:
membership-derived + admission, or a group-doc Statement wake-policy type).
AW-3 (≤24h stale-revocation ceiling), AW-4 (Android/Windows non-parity for v1),
AW-5 (button honesty default; AW-5b default OFF off-trusted-LAN) round it out.
G-5 — Cross-device state sync / movable memory
Exists: the group document syncs by gossip/announce; peer-sync-v1-design.md
is a design; the on-device plane bridges live host sessions only.
Why this needs design: what actually syncs is the group doc plus anchors — not app data, agent memory, or the audit ledger, all of which are local per-device. The phone cannot hold app state offline because the plane is host-routed and lives only for the session. “Movable memory” is asserted per-document but there is no cross-device memory layer yet.
Options / tradeoffs: (a) extend peer-sync-v1 to named data anchors with last-writer or CRDT merge — general, needs conflict semantics; (b) a designated home device per data class with pull-on-demand — simpler, weaker offline story; (c) leave app data single-homed and document it (status quo).
Decides: Luke — peer-sync-v1 ratification (litmus: movable memory).
G-6 — Notification / wake paths per OS
Exists: the honest matrix above. Android with an active session is moot; Android force-stopped needs an unbuilt notifications transport (d/037 Q3); Windows has no socket-activation primitive; powered-off is out of scope.
Why this needs design: a sovereign push path for a dead Android process is the missing primitive behind both G-4 (Android row) and any “your agent needs you” prompt. The tradeoff is self-hosted vs. vendor: UnifiedPush (sovereign, self-hostable) vs. FCM (vendor, ubiquitous, opt-in).
Options / tradeoffs: (a) UnifiedPush self-hosted — on-thesis, more to run; (b) FCM opt-in — trivial reach, a Google dependency on the wake path; (c) both, user-selected. Decision 037 Q3 parked exactly this.
Decides: Luke — d/037 Q3, tied to AW-4.
G-7 — The unwired apps.list responder (P-188)
Exists: the concrete, code-shaped gap from
devices §3.5.
adopt_node never dispatches LAUNCHER_APPS_ALPN (group.rs:327-328); the
protocol is not accepted inbound (:233,272); the responder
serve_list_apps_extra (launcher_apps.rs:271) exists but has no caller. Only
the debug TCP adapter serves today. The launcher shows the reviewed
CHANNEL_UNAVAILABLE copy in production.
Why this needs design: this is the difference between a launcher that lists a desktop’s apps and one that shows an honest “not available” card. It is small and mechanical — but it is parked on d/104, because the shape it serves should be the ratified launch descriptor, not an improvised one.
Options / tradeoffs: (a) wire the responder behind the member-gated serve
loop now, using the current LauncherAppEntry shape — unblocks the demo,
risks re-shaping later; (b) ratify d/104 first, then wire once to the
descriptor — one implementation, gated on a decision. P-188 R1/R2 is written
for option (b).
Decides: Luke — d/104 ratification, then a sol lane implements.
The launch seam (d/104, PROPOSED)
decisions/104-cf-guest-launch-seam.md: the catalog IS the launch seam.
The launch descriptor is (app label → plane origin http://<app>.localhost:5454)
— no per-app baked guest URLs, one route on every platform. Prism’s fleet
catalog (P-197) already navigates controlled frames to arbitrary app origins on
desktop; Android’s fix is mechanical. Ratifying it blocks P-202’s two BLOCKED
cells (in-frame render, in-frame offline honesty) and unparks P-188.
Grounded in d/100 (plane origin) + d/103 (compose UIs are plane origins).
Reversal is cheap: if Luke prefers an explicit Prism/IWA install-time
guest-manifest contract, it is discarded before any dispatch. The descriptor
shape is a Part V candidate:
Launch descriptor (d/104).
Sovereignty litmus check
| Criterion | Verdict for the device fabric |
|---|---|
| Inspect what it sees | Holds. status/doctor reconcile cached vs. live truth without waking; the launcher states are honest (incl. CHANNEL_UNAVAILABLE); every failure string ends in an action. |
| Fork permissions | Partial. Membership roles (Admin/Member/Observer) and revocation are real and offline-verifiable; but per-device capability granularity for the agent is not here yet (G-2, agent review). |
| BYO models + keys | Holds for keys — device keys are local, group authority is a local signed document, no server. Models are out of the device beat. |
| Movable memory | Weakest point. Group doc moves; app data + agent memory are single-homed per device; the phone can’t hold app state offline (G-5). |
| Auditable actions | Holds on-device. Hash-chained session/launcher ledger (wake/kill/off/cross/list/launch/resume), tamper-evident. Caveat: 64-bit truncated hash, not externally anchored. |
| Exit without loss | Partial. A consumer removes a peer and keeps its data; but a lost device has no recovery ceremony beyond admin-re-invite, and a new device inherits no grants (G-3). |
Questions for the Luke + Jan session
- d/104 — ratify the catalog descriptor as the single launch seam? It
unblocks P-202’s two BLOCKED Android frame cells and unparks P-188 (the
unwired
apps.listresponder). Reversal is cheap — no code depends on it. - AW-1 / AW-2 — the authed-wake crux: accept “same-network zero-resident
desktop only; cross-internet asleep-wake is out of v1” (relay mailbox
reserved), and membership-derived authorization (Observers cannot wake) vs. a
group-doc
Statementwake-policy type? These are the only AW items needing real interview time. - G-3 recovery — what is the lost-phone / new-laptop ceremony? Admin-
re-invite is the current implicit answer; is threshold / social recovery of
the group authority key in scope? (Ties to AW-2 and
key-recovery-design.) - G-6 push — sovereign push for a force-stopped Android process: UnifiedPush self-hosted, FCM opt-in, or both user-selected? (d/037 Q3.)
Review: The agent layer (darc → Reagent)
Review of the agent layer as described, under the
sovereignty litmus. Verdicts cite xenon-runtime main @b0cfc90, the xenon-cap
/ xenon-grant crates, and the xe-darc design docs. The gap IDs here (G-1
resident agent, G-2 delegation) are this beat’s agent-side register and feed
Part IV. The device-side gaps (G-3…G-7) are held
in the device review.
The headline
The agent layer is the mission and the least-built layer. The whole system — peer serving, the device fabric, the capability substrate — exists so that a resident agent can act for you, under your inspection, bounded by capabilities you can fork and revoke. That agent, as a bounded resident process, does not exist yet. What exists is a real capability-and-consent substrate, and a real research-preview browser (darc) whose only live boundary is a human approving each tool call.
This is not a criticism to soften; it is the program’s central honest fact, and the site should state it plainly rather than let the vocabulary imply that a bounded resident agent ships today. Concretely:
- Real:
xenon-capcapabilities,xenon-grantauthority-signed grant bundles,ConsentAttestation/ConsentStatement(used by the companion), the hash-chained audit ledger. - Real but unbounded: darc — a working Controlled-Frame browser-as-IWA with an agent sidebar; the only gate is human-approves-each-tool-call.
- Paper: Reagent (the productized, bounded resident agent); the
injection-boundary enforcement; the
capabilities[] → grantpipeline; every agency-ladder rung.
Everything below is downstream of that one paragraph.
Desired vs current
| Want (mission / decisions) | Have (code @b0cfc90) | Delta |
|---|---|---|
| A resident agent that sees, decides, acts under capability bounds | darc research preview (real, unbounded); daemon/main.js is 0 bytes | The agent has no daemon and no capability boundary; the human is the boundary. (G-1) |
| Agent authority expressed as capabilities (litmus: fork permissions) | xenon-cap + xenon-grant shipped and capability-shaped | agent://act is declared + audited only, not enforced; capabilities[] → grant unwired. (G-2) |
| BYO models + keys (sovereignty litmus) | Keys local; darc model IDs hardcoded | Sovereign model choice is unmet — litmus debt the kickoff flagged. (G-1) |
| Agency ladder governs agent actions (d/082, d/091) | Settled vocabulary; injection-boundary enum on paper | Zero rungs enforced in code. |
| Auditable agent actions | Hash-chained ledger discipline exists (companion + injection §3.4) | Ready to carry lease-use records; nothing mints them yet. |
Gap register (agent slice)
G-1 — The resident agent itself (darc → Reagent)
Exists: darc, a real research preview — but the only gate on its actions
is a human approving each tool call (injection §1.3); the agent sidebar and
the untrusted page share one IWA shell with no capability check between them
(§1); the CDP proxy forwards '*' so Runtime.evaluate passes
unconditionally (§1.1); daemon/main.js is 0 bytes. The substrate crates it
would bind to (xenon-cap, xenon-grant) are shipped.
Why this needs design: where the resident agent physically lives (in the
shell now, or in a real daemon/), what it can see and do, how consent and
audit extend to agent actions (not just app grants), and how BYO-model wiring
replaces the hardcoded model IDs. Until D-5 (the lease-signing root) is
answered, a “signed Lease” is aspirational: the shell shares an address space
with the forgeable iwa: IPC channel, so nothing prevents a lease being minted
by the very surface it is meant to bound.
Options / tradeoffs: injection-boundary D-1 (ship the boundary in the shell now vs. stand up the daemon first) — shell-now is faster but keeps the agent and the page co-resident; daemon-now is the honest boundary but is net-new. D-5 (lease-signing root) — a separate signer process vs. the OS keystore vs. deferring signed leases entirely. BYO-model — a model-selection capability vs. a config file vs. status quo hardcoding.
Decides: Luke + Jan — the injection-boundary D-1…D-7 burn-down.
G-2 — Delegation model: per-action consent vs. standing grants
Exists: a genuinely UCAN-adjacent substrate. xenon-cap::Capability =
(resource URI × ability verb × typed constraints) with audience + expiry +
onward-delegation flag, verified by verify_chain / authorize_invocation
(xenon-cap/src/lib.rs:30-84). xenon-grant mints runtime-authority-signed
Delegation bundles per install principal with onward:false
(xenon-grant/src/lib.rs:23-53,233-291). ConsentAttestation /
ConsentStatement are shipped and used by the companion.
Why this needs design: the pipeline capabilities[] → plan table → minted grant is parsed but not wired (capability-model-design §1); the agent
family (agent://act) is declared + audited only, not enforced; there is
no cross-device grant sync (out of scope, §9). Leases (the attenuated
per-action invocations, injection §3.1) are designed but not minted.
Why UCAN maps here exactly: the model is naturally both per-action and
standing — a grant is a long-lived delegation, a lease is a short-lived
attenuated delegation minted under it, and onward:false means no
re-delegation. That is a UCAN delegation, a UCAN invocation, and a no-proof
leaf, respectively. The recommendation this review offers the session: make the
UCAN alignment an explicit product decision, not an implementation detail —
decide whether Reagent’s authority is the xenon-cap/xenon-grant substrate
(it should be), and whether agent://act is enforced in v1 or stays
declared+audited.
Options / tradeoffs: CM-1 (the v1 grantable set — how wide); CM-2
(outbound network: declared-only vs. a block-catalog); grant-vs-lease split as
above (standing vs. per-action). Enforcing agent://act in v1 is the honest
choice but couples the agent milestone to the grant-minting pipeline; keeping
it declared+audited ships sooner but leaves the boundary advisory.
Decides: Luke — CM-1…CM-7. The full outline is a Part V candidate: Grant / Lease delegation model (UCAN-aligned).
The one ledger discipline (a convergence note)
The companion’s Kotlin audit chain
(devices §3.4) and the
agent-layer AuditEntry.prevHash (injection §3.4) are the same idea
implemented twice: a tamper-evident, per-device hash chain, 64-bit truncated,
not externally anchored. If lease-use records are going to be the agent’s audit
trail, the two should converge on one ledger discipline (this is
injection-boundary D-5’s neighbor). Naming it now avoids two subtly different
“audit logs” in one product.
Sovereignty litmus check
| Criterion | Verdict for the agent layer |
|---|---|
| Inspect what it sees | Partial. darc surfaces each tool call for human approval — genuine inspection — but there is no capability-scoped record of what the agent may see; the boundary is a person, not a policy. |
| Fork permissions | Weakest point. The substrate can express attenuable grants/leases, but agent authority is unenforced (agent://act audited-only). You cannot yet fork or revoke the agent’s permissions because it holds none. (G-2) |
| BYO models + keys | Fails for models. Model IDs are hardcoded in darc — the sovereignty debt the kickoff named. Keys are local. (G-1) |
| Movable memory | Not yet. Agent memory is local per device; no cross-device grant or memory sync (capability-model §9). |
| Auditable actions | Ready, unwired. The hash-chain discipline exists and the AuditEntry shape is specified; nothing mints lease-use records because no bounded agent acts. |
| Exit without loss | N/A yet. No resident agent state to carry; becomes real the moment the daemon holds memory. |
Questions for the Luke + Jan session
- G-2 / the UCAN decision — is Reagent’s authority the same
xenon-cap/xenon-grantsubstrate (Grant = long-lived UCAN delegation, Lease = attenuated invocation), and doesagent://actget enforced in v1 or stay declared + audited? This is where UCAN alignment should be a product decision, not an implementation detail. - G-1 / D-1 + D-5 — does the boundary ship in the shell now or wait for a
real daemon, and what is the lease-signing root, given the shell shares an
address space with the forgeable
iwa:IPC channel? - BYO model — a model-selection capability, a config file, or status-quo hardcoding for v1? (Litmus: BYO models + keys.)
- Naming hygiene — the site must not let a reader conflate the product
delegation model (
capability-model-design.md+injection-boundary-design.md) withdocs/delegation-interfaces.md(an unrelated broker). Agreed as a glossary fix?
The Design Frontier
This is the honest open edge. Every layer review in Part III ends with a gap register; this chapter consolidates all of them into one place, grouped by theme rather than by beat, so a reader can see the shape of what is undesigned across the whole system at once. Nothing here is hidden debt — where the mission demands something no decision yet covers, it is recorded as first-class work.
Read this against the sovereignty litmus: inspect what it sees · fork permissions · BYO models + keys · movable memory · audit actions · exit without loss. Three of those six clauses are only half-met today, by our own accounting, and they lead the register below. The point of naming them is not to apologise for them — most of this system is built and proven — but to keep proven and still-to-design visibly separate, which is itself a litmus commitment.
Two honest framings before the register:
- The agent layer is the mission and the least-built layer. The whole stack — transport, serving, the device fabric, the capability substrate — exists so a resident agent can act for you under bounds you can fork and revoke. That agent, as a bounded resident process, does not exist yet. This is the program’s central honest fact, carried below unsoftened (theme D).
- Two mission-level openings are owned by no single layer. Multi-person sharing (sharing an app or data with someone who is not you — distinct from the multi-OS-user gap PLAT-5 below) has no decision at all; the current plan defers it to the local-first camp audience. And the P-192 two-engine result is an asset, not a deficiency — the same signed bundle rendered on Blink and Servo, which is the argument that keeps Xe from being a single-vendor dependency, revisited the week of Jul 20. Both are tracked in the review agenda, not as gaps here.
An external review now sits beside this register. On 2026-07-18 an independent frontier
model — gpt-5.6-sol at xhigh — read the whole site adversarially and returned
six memos. Its single loudest ask is a precondition on two of the
entries below: publish an explicit threat model, trusted computing base, and authority
matrix before ratifying d/103-B (F1)
or d/104 (E1), because every security claim in the
stack currently rests on an unnamed boundary. sol’s positions are folded into the entries
below as External review input lines; the two gaps it surfaced that no layer review had
named are collected in Theme G. Where sol disagrees
with one of our own Part III reviews, both voices are kept —
the disagreement is the point.
The frontier at a glance
Weight marks who must decide and how reversible the call is: ▲ heavy — Luke, a litmus-, security-, or frozen-schema-touching call that is expensive to reverse; ◆ medium — Luke + Jan, a ceremony, scheduling, or placement call; ○ light — reversible, an orchestrator/Fable hygiene or surface call. Counts are canonical entries after de-duplication.
flowchart TB DF["The Design Frontier<br/>27 canonical gaps · 7 themes"]:::root DF --> A["A · Sovereignty spine<br/>3 gaps · the half-met litmus"]:::heavy DF --> B["B · Serving & membership<br/>5 gaps"]:::heavy DF --> C["C · Permissions & consent<br/>2 gaps"]:::heavy DF --> D["D · The agent layer<br/>2 gaps · mission & least-built"]:::heavy DF --> E["E · Platform coverage<br/>5 gaps"]:::mixed DF --> F["F · Operational / versioning<br/>8 gaps"]:::light DF --> G["G · Local trust boundary<br/>2 gaps · net-new from sol"]:::heavy A --> A1["A1 movable memory ▲"]:::heavy A --> A2["A2 exit-without-loss ▲"]:::heavy A --> A3["A3 audit / journal unification ▲"]:::heavy B --> B1["B1 membership-blind serving ▲"]:::heavy B --> B2["B2 v1-vs-v2 group doc ▲"]:::heavy B --> B3["B3 relay trust ▲"]:::heavy B --> B4["B4 ticket distribution ◆"]:::med B --> B5["B5 Windows tripwire ○"]:::light C --> C1["C1 in-frame permissions ▲"]:::heavy C --> C2["C2 frame↔shell contract ○/▲"]:::mixed D --> D1["D1 resident agent + BYO model ▲"]:::heavy D --> D2["D2 delegation / agent://act ▲"]:::heavy E --> E1["E1 d/104 launch seam ▲"]:::heavy E --> E2["E2 authed-wake + push ▲"]:::heavy E --> E3["E3 Windows backend engine ◆"]:::med E --> E4["E4 GPU / ML class ▲"]:::heavy E --> E5["E5 multi-OS-user plane ◆"]:::med F --> F1["F1 route-table + d/103-B ▲"]:::heavy F --> F2["F2 no-502 ○"]:::light F --> F3["F3 signing ceremony ◆"]:::med F --> F4["F4 update loop ○"]:::light F --> F5["F5 resource limits ○"]:::light F --> F6["F6 P-174 sequencing ○"]:::light F --> F7["F7 code / doc hygiene ○"]:::light F --> F8["F8 fork-maintenance cadence ◆"]:::med G --> G1["G1 browser request admission ▲"]:::heavy G --> G2["G2 connector / engine-API auth ▲"]:::heavy classDef root fill:#1f2933,stroke:#1f2933,color:#fff,font-weight:bold; classDef heavy fill:#f4c7c3,stroke:#c0392b,color:#3d0f0a; classDef med fill:#fdebd0,stroke:#d68910,color:#4d340a; classDef light fill:#eafaf1,stroke:#27ae60,color:#0e3d24; classDef mixed fill:#eaf2f8,stroke:#2874a6,color:#0a2540;
Sixteen of the twenty-seven carry a ▲ Luke call — fifteen outright, one (C2) split Fable ○ / Luke ▲ — and most of those are litmus-touching, which is the honest signal that the remaining design work is about sovereignty and trust boundaries, not plumbing. The plumbing is largely built. The two net-new Theme G entries are both Luke calls, and both are the same call: is an untrusted same-host process inside the threat model?
Theme A — The sovereignty spine (the three half-met litmus clauses)
These three lead because they are the clauses the first audience will check first. Each is a site-wide finding that individual layer reviews surface locally.
A1 — Movable memory: app data is single-home
Gap. Sync moves the group document and its anchors — not app data, agent memory, or the audit ledger, all of which live local per device (devices G-5). The phone cannot hold app state offline because the plane is host-routed and lives only for the session. “Movable memory” is asserted per-document but there is no cross-device memory layer.
Options. (a) Extend peer-sync-v1 to named data anchors with last-writer or CRDT merge — general, needs conflict semantics; (b) a designated home device per data class with pull-on-demand — simpler, weaker offline story; (c) leave app data single-homed and document it (status quo, honestly labelled). Peer-sync topology is the costliest steer to reverse (d/062) once sync formats ship.
External review input. sol backs a replicated-home collection as the honest v1 (single-writer, signed epoch home-transitions, at least one durable-replica acknowledgement) before any generic multi-writer CRDT; a blanket last-writer-wins rule is unacceptable for opaque app data — it silently discards work and leans on clocks the design otherwise distrusts. — sol · devices §5
Decides: Luke ▲ — peer-sync-v1 ratification (litmus: movable memory). Source: devices review G-5.
A2 — Exit without loss: xe export is intent, not a proven round-trip
Gap. Two faces of the same clause. (i) Per-app data: uninstall forces a data
disposition (VerifiedExportThenDelete | RetainDetached | DestructiveDeleteConfirmed)
and XAA archives back update snapshots, but there is no first-class xe app export
command surfaced, and the uninstall browser-storage sweep “does not independently prove
localStorage emptiness”
(app-model AM-5).
(ii) Device / key recovery: a lost device key has no recovery lifecycle — a new laptop
re-enrolls from scratch with no grant carryover, and self-revoked.json is one-way with
no surfaced rejoin ceremony
(devices G-3).
Options. For (i): add xe app export on the existing XAA machinery + a DOMStorage
receipt so uninstall can prove the sweep. For (ii): admin-re-invite as the blessed
ceremony (simplest; a lost authority device is unrecoverable) vs. threshold / social
recovery of the group authority key (resilient, heavier) vs. a surfaced rejoin flow.
External review input. sol wants the clause held to a proof, not an intent: a versioned portable-state container (catalog pins, identity, config, grants, journal refs, encrypted volume snapshots, per-app export hooks) with a fresh-machine export→import round-trip and an independent reader before exit-without-loss is called met — and re-origining an app is explicitly not user-data migration. — sol · mission §3 · sol · app-model §3
Decides: (i) orchestrator ○ for the export surface; (ii) Luke ▲ for key-recovery ratification. Source: app-model AM-5 · devices G-3.
A3 — Auditable actions: some journals sit outside the hash chain
Gap. Some grant/trust records are standalone JSON-lines logs sitting outside the
hash-chained audit journal, so the console’s Activity view cannot witness them. Worse,
the discipline is implemented twice — the companion’s Kotlin audit chain and the
agent-layer AuditEntry.prevHash are the same tamper-evident, 64-bit-truncated,
un-anchored idea built independently
(agent-layer convergence note).
The product should land one ledger discipline, not two subtly different “audit logs.”
Options. (a) Fold the standalone logs into the hash-chained journal as a work packet and converge both implementations on one discipline; (b) leave them separate and document the seam. Only (a) closes the inspectability gap.
External review input. Unifying the logs is necessary but not sufficient: sol shows a hash chain is tamper-evident only against a trusted head, so without signed, periodically-anchored checkpoints and cross-device head gossip it catches corruption, not adversarial tampering (truncation, rollback, wholesale replacement, and forking all survive it). The 64-bit truncation also needlessly weakens collision resistance — keep the full SHA-256. Converge the companion, agent, and shell journals on one full-hash, signed-checkpoint discipline with per-device chains whose heads devices witness for one another. — sol · devices §4 · sol · shell §1
Decides: Luke ▲ — approve journal unification (litmus: auditable actions). Source: agent-layer one-ledger note.
Theme B — Serving & membership
The serving beat’s honest one-liner: the serving path is membership-blind. A fully built, signed, revocation-capable membership plane sits one crate away from a serving tunnel that authenticates with a bearer secret and never consults it. Everything in this theme is downstream of that.
B1 — Membership-blind serving
Canonical entry — the transport and peer-serving reviews frame the same seam as G4 and G1.
Gap. Authentication for peer serving is a 32-byte bearer secret in a ticket file;
the serve tunnel builds IrohProvider::new, never with_membership. A device removed
from the group keeps serving and consuming for as long as it holds a live ticket —
enrollment and authorization-to-serve are different graphs today. The route secret
has no revocation list, epoch, or bounded lifetime; the only “permission change” is
restarting the publisher, which strands every consumer. (The
transport review
frames this as the identity gap G4; the
peer-serving review
frames the same seam as the revocation gap G1. They are one decision.)
Options. (a) Gate the serve ALPN on the existing MembershipRegistry — a small
change; group revocation then cuts serving tunnels for free, and this collapses B1, B4,
and much of B3 at once; (b) keep bearer tickets for the “share to a non-member” case and
add membership as an additional gate; (c) a bounded-lifetime (exp) ticket now to
unblock durable secrets while the fuller model is decided; (d) UCAN capabilities with
exp + revocation — heavier, and partly duplicates the group plane, which is already
a capability system. The review’s offered verdict: gate on membership first; reserve
UCAN for the one thing membership cannot express, delegation to a non-member.
External review input. sol narrows the review’s verdict: group membership is an
authenticated ACL, not a capability system, so gate on membership and a per-route,
audience-bound signed grant (compare the grant’s audience to the already-authenticated iroh
remote key) — membership alone still lets any member who learns the address reach every
exposed route, and a copied bearer secret stays usable. “Reserve UCAN for non-members” is the
wrong cut; the real boundary is transitive delegation/attenuation. Ship gating on a new
ALPN xenon/serve/tcp/2, never by mutating /1 in place. —
sol · transport §2
Decides: Luke ▲ — the sovereignty spine of the beat; effectively irreversible once tickets are in the wild. Source: transport G4 · peer-serving G1 · the UCAN question.
B2 — Which group document is canonical (v1 vs v2)
Gap. Two group-document lineages exist: v1 in crates/net/group (what gossip and the
membership gate compile against) and v2 in crates/xe-sync (MemberKind{Device, Companion},
endpoint bindings, v2 signing domains). Which is canonical for shipped serving is undecided,
and Companion has no serving semantics at all. This blocks B1 — you cannot gate serving
on a membership plane before deciding which membership plane.
Options. (a) v1 is the line for shipped serving, v2 folds in later; (b) v2 is canonical and serving waits on it; (c) settle the split explicitly and document the migration.
External review input. sol agrees: ship v1 for the first gated path (“a larger version
number is not a readiness argument”), but front it with a lineage-independent authorization
interface and one authoritative adapter, require a signed one-way v1→v2 transition that
binds the v2 genesis to a final v1 head, and never authorize from the union of both
lineages — and don’t admit Companion until its publish/consume rights are normative. —
sol · transport §3
Decides: Luke ▲. Source: transport review, questions 1 & 3.
B3 — Relay trust model
Gap. With no relays configured, iroh selects the number0 public relay set; group
documents can pin relay_endpoints under the authority signature, but peer-serve takes
relays from unsigned CLI arguments only. Relays see connection metadata (who talks to
whom, when, how much) and are a third-party availability dependency. No fallback policy is
stated anywhere.
Options. (a) Agent54-run relay(s) pinned in the signed head; (b) explicitly document
acceptance of n0’s relays, direct-preferred with relay fallback; (c) relay-optional
(hole-punch-only) for LAN-local fleets. Ships alongside scheduling the self-hosted
iroh-relay that d/014 requires before
production.
External review input. sol reinforces that “no central hub” holds only for device-state topology — catalog root, forge, release, and relays stay centralized operational dependencies — and that the relay’s authentication prerequisites belong in the protocol profile, not in the unsigned CLI args peer-serve reads relays from today. — sol · transport §4
Decides: Luke ▲ — infrastructure plus a trust boundary. Source: transport G3.
B4 — Ticket distribution channel
Gap. Publish prints only ticket_path=; the operator moves the bearer-secret file out
of band. Custody is meticulous at both endpoints and undefined in the middle — the
secret’s entire security rests on an unspecified transfer channel, which also leaves no
audit record.
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. B1(a) + a member-to-member path dissolve this gap entirely for members.
External review input. sol frames delivery as a first-class protocol question even after bearer secrets disappear: how route grants or non-secret discovery records are delivered, acknowledged, audited, refreshed, and withdrawn — the channel needs an audit record, not merely careful custody at the two endpoints. — sol · transport §6
Decides: Luke + Jan ◆ — UX + custody. Source: peer-serving G5.
B5 — Windows exposure-tripwire asymmetry
Gap. On Unix, opening an over-shared ticket refuses with a chmod 600 instruction;
on Windows the same condition is silently repaired (the ACL is narrowed and reading
proceeds). Refusal is a tripwire — “something widened access to your credential” is a signal
the operator should see; repair hides it.
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) — but it wants a taste call. Source: peer-serving G2.
Theme C — Permissions & consent
Where the phrase “your agent answers for you” is either made real or conceded.
C1 — The in-frame permission model
The headline gap of the shell layer.
Gap. Today only geolocation, only in the development driver, same-origin. The installed Controlled-Frame driver deliberately refuses rather than fake a result when it cannot mint a lease or journal the answer — honest, but it means the whole non-geolocation set (camera, microphone, clipboard, USB) is undesigned. Three open questions are each a sovereignty question: who prompts (the shell, or Chrome’s native dialog inside the partition), who remembers (inspectable, revocable per-origin persistence?), and the capability set itself.
Options. A — shell brokers everything via the agency ladder, shield-by-default, journaled: inspect what it sees + auditable both met, but real browser-driver engineering per capability. B — delegate to Chrome: nearly free, already works, but cedes “your agent answers for you” at exactly the surface where users feel it most.
External review input. sol endorses shell-brokering but lands on a hybrid with
browser-process enforcement: Prism owns policy, explanation, shielding, persistence, and
journaling; Chrome/OS owns final device selection and enforcement; the effective outcome is
the intersection, never the union. It also raises Option A’s cost — shield-by-default is
infeasible via injected JS, because the Controlled Frame permissionrequest API can only
allow/cancel, so an approximate location or virtual camera needs a Xenon browser-process
permission delegate or resource portal, not a JS patch. Journal every answer and
periodically sign and anchor the journal head. — sol · shell §1
Decides: Luke ▲ — litmus-touching; option A is the entire product differentiator. A design packet should precede any implementation. Source: shell G6.
C2 — The frame↔shell communication contract
Gap. The surface is deliberately asymmetric and minimal: app→shell is a four-verb,
nonce-authed channel (tab.open/close/reload, key); shell→app is a capability-leased
gateway. There is no contract for an app to set a window title, raise a
notification, request focus, or report liveness — a guest sets document.title and the
shell has no defined reaction.
Options. (a) Extend the nonce-authed verb set with a small app-declared surface (title / notify / badge) — ergonomic, but every verb is a capability whoever holds the port then holds; (b) keep apps fully unprivileged and derive what the shell needs by observation — tiny boundary, but apps cannot cooperate.
External review input. sol would split the surface into two specs — a cooperative
lifecycle/presentation RPC and a separately-audited privileged automation gateway
(readDom / request-rewrite / CDP are shell authority over a guest, not an app contract) —
and drop raw tab.open{url} (it conflicts with a descriptor-constrained launch model)
and key (too generic). The nonce is message validation, not app authentication: any
holder of the port has the capability, so bind the port to frame_id + document_epoch + app_id and re-key it on every committed navigation. — sol · shell §3
Decides: Fable ○ proposes the verb-set delta; Luke ▲ on anything touching user-visible identity (notifications). Source: shell G2.
Theme D — The agent layer
The agent layer is the mission and the least-built layer. The whole system exists so a resident agent can act for you, under your inspection, bounded by capabilities you can fork and revoke. That agent, as a bounded resident process, does not exist yet. What exists is a real capability-and-consent substrate and a real research-preview browser (darc) whose only live boundary is a human approving each tool call.
This is not a criticism to soften; it is the program’s central honest fact. Both gaps below are downstream of it.
D1 — The resident agent itself (darc → Reagent)
Gap. darc is real but unbounded: the agent sidebar and the untrusted page share one
IWA shell with no capability check between them, the CDP proxy forwards '*' so
Runtime.evaluate passes unconditionally, and daemon/main.js is 0 bytes. And darc’s model
IDs are hardcoded — the BYO-models litmus
violation the kickoff flagged, live in the tree. Open: where the agent physically lives (in
the shell now, or a real daemon/), what it may see and do, and how BYO-model wiring replaces
the hardcoded IDs. Until the lease-signing root (D-5) is answered, a “signed Lease” is
aspirational — the shell shares an address space with the forgeable iwa: IPC channel.
Options. Injection-boundary D-1: ship the boundary in the shell now (faster, keeps agent and page co-resident) vs. stand up the daemon first (the honest boundary, net-new). D-5: a separate signer process vs. the OS keystore vs. deferring signed leases. BYO model: a model-selection capability vs. a config file vs. status-quo hardcoding.
External review input. sol judges the shell an indefensible home for the boundary and
proposes a five-process split (agent daemon · policy/signing service · browser broker ·
shell-as-UI · page + model, both untrusted), with the lease-signing root and model credentials
out of the shell and the console.log('iwa:…') channel retired for an OS process boundary
with peer credentials. Human per-call approval is inspection, not an authority bound — it
fails under click-fatigue, injection, and TOCTOU, and does not constrain what the approved
primitive can do. Exclude raw CDP, execute.script, and persistent injection from v1;
start with a small brokered set (visible-text read, one-origin navigation, constrained element
activation). — sol · devices §3 · sol · shell §4
Decides: Luke + Jan ▲ — the injection-boundary D-1…D-7 burn-down. Source: agent-layer G-1.
D2 — Delegation model: per-action consent vs. standing grants
Gap. The substrate is genuinely UCAN-adjacent — xenon-cap capabilities, xenon-grant
authority-signed grant bundles with onward:false, shipped ConsentAttestation. But the
capabilities[] → plan table → minted grant pipeline is parsed, not wired; the agent
family (agent://act) is declared + audited only, not enforced; leases are designed but
not minted. You cannot yet fork or revoke the agent’s permissions because it holds none.
Options. Make the UCAN alignment an explicit product decision, not an implementation
detail: decide whether Reagent’s authority is the xenon-cap/xenon-grant substrate (it
should be — Grant = long-lived delegation, Lease = attenuated invocation, onward:false = a
no-proof leaf), and whether agent://act is enforced in v1 (honest, couples the agent
milestone to the grant-minting pipeline) or stays declared+audited (ships sooner, boundary
advisory). Also CM-1 (v1 grantable set) and CM-2 (outbound network: declared-only vs.
block-catalog).
External review input. sol finds a live contradiction in the model: onward:false
conflicts with minting a lease under a grant — a lease that hands authority to the agent
is the onward delegation the grant forbids. Resolve it by modelling the lease as an
audience-bound invocation, not a delegation (the install principal signs an exact request to
a named executor; model and page stay untrusted requesters holding no capability). Attenuation
must be proved by a deterministic subsumption algorithm (resource containment, an
ability-implication lattice, per-constraint partial orders, fail-closed on unknowns) with
conformance vectors — a signature proves issuance, not narrowing. Revocation has an unavoidable
availability boundary: publish a maximum stale-authority window, decide whether executors
fail closed past it, and requalify “verifiable offline” as verifiable against a named signed
snapshot under a declared staleness ceiling. Drop the “nonce is CID-equivalent” claim. —
sol · devices §1
Decides: Luke ▲ — CM-1…CM-7. Source: agent-layer G-2 · outline in Part V.
Theme E — Platform coverage
The device fabric is the most-built layer of its beat; its gaps are about crossing the gap between two devices and reaching platforms the container/wake model does not yet cover.
E1 — The d/104 launch seam — PROPOSED
Canonical entry — folds the shell review’s launch-seam item and the devices review’s G-7.
Gap. The launch descriptor — (app label → plane origin http://<app>.localhost:5454),
no per-app baked guest URLs, one route on every platform — is already the code on desktop
(Prism’s fleet catalog navigates Controlled Frames to arbitrary app origins), but blessing it
as the canonical cross-platform seam is a PROPOSED decision awaiting Luke. It surfaced when
an installed Android Prism could not repoint its baked guest to the plane origin. The same
proposal unblocks two things: Android’s two BLOCKED frame cells (in-frame render, in-frame
offline honesty) and the parked apps.list responder (P-188 — adopt_node never dispatches
LAUNCHER_APPS_ALPN; the responder exists with no caller). P-188 is parked on d/104 precisely
so it wires the ratified descriptor shape, not an improvised one.
Options. (a) Ratify the catalog descriptor as the seam — reversal is cheap, no code depends on it; (b) hold out for an explicit Prism/IWA install-time guest-manifest launch contract.
External review input. All three memos that touch d/104 agree it is “not ratifiable as
written.” Keep the descriptor as the seam but replace label → plane_origin — a mutable
label must not be the security principal. Bind launch to an immutable app_id +
release_digest + mutability_tier resolved by an authenticated plane router; hand Prism a
resolved, authenticated launch capability, not an arbitrary URL; and constrain
post-launch navigation (redirects, window.open, script navigation) via a Xenon
browser-process navigation guard (NavigationThrottle) bound to a verified LaunchGrant —
validating only the first URL leaves redirects outside the boundary. d/104 also embeds an
unresolved HTTP-vs-HTTPS Controlled Frame portability decision that ratification must
settle. — sol · mission §4 · sol · shell §2 ·
sol · devices §7
Decides: Luke ▲ — ratify d/104. Source: devices launch-seam · devices G-7 · shell desired-vs-current, launch seam · shape in Part V.
E2 — Authed-wake and the socket-activation crux
Canonical entry — folds devices G-4 (wake) and G-6 (per-OS push paths).
Gap. The peer transport that shipped is iroh (QUIC/UDP + relay), and iroh cannot be
OS-socket-activated the way launchd/systemd can hold a plain socket and start a process
on first connection. The transport a peer uses to reach a device is precisely the transport
that cannot wake a zero-resident device. So the only honest wake is a same-network,
zero-resident desktop over an OS-held socket; internet-only, Windows, and force-stopped
Android all honestly refuse today. The missing primitive behind the Android row is a
sovereign push path for a dead process (G-6).
Options. (a) Accept same-network-only for v1 and reserve a relay-mailbox design for
cross-internet wake later — honest, ships, defers the hard part; (b) design the relay mailbox
now — full reach, net-new machinery with subtle replay/amplification concerns; (c) per-platform
wake shims. For the Android push primitive:
UnifiedPush self-hosted (on-thesis, more to run) vs. FCM opt-in
(trivial reach, a Google dependency on the wake path) vs. both, user-selected. The crypto core
(≤24h WakeAdmissionV1, response-free WakeHintV1, at-most-once replay admission) is already
designed either way.
External review input. sol backs same-network v1 but sharpens the wording: it is process activation, not wake (a kernel-held socket starts a verifier only while the OS is awake), and a relay mailbox is not an activation mechanism — nothing polls it. The sovereignty-fitting shape is a user-controlled home broker doing authenticated Wake-on-LAN plus contentless push hints (self-hosted UnifiedPush / opt-in FCM carrying only “check your mailbox”). Split the Android row into “process killed / background-restricted” and “user force-stopped” — normal push does not override a force-stop. Reserve the mailbox format; don’t build it until a target-side wake consumer exists. — sol · devices §2
Decides: Luke ▲ — AW-1 (accept the crux), AW-2 (authorization source), and d/037 Q3 (the push primitive). Source: devices G-4 · devices G-6.
E3 — No backend exec bridge for Windows (WSL2)
Gap. The connector exec bridge is implemented for smolvm and colima only. WSL2 and ProvisionedVm have no bridge — Windows backend-carrying apps are unsolved (the serve and in-frame consume legs are proven; a backend behind the app is not). A genuine hole, not a rough edge.
Options. (a) Add a WSL exec bridge; (b) publish a Windows named-pipe socket contract mirroring d/094. Trade-off: every new engine widens the socket-custody surface the launcher must own.
External review input. sol treats this as a real three-way (WSL exec bridge / versioned
named-pipe agent / explicit deferral) and warns the named-pipe design must not expose
Docker’s admin pipe — a narrow, launcher-authenticated Xe engine-agent protocol resolving an
opaque backend handle. A catch that also bounds the shipped macOS/Linux bridges: the
sh+nc exec bridge is not a general unmodified-container contract — distroless/minimal
images ship neither, so “unmodified backend” is already untrue for a real class of images and
the compose profile/probe must say so. — sol · app-model §5
Decides: orchestrator ○ for the reversible architecture; Luke + Jan ◆ on whether Windows-with-backend is in scope now. Source: engines EB-2.
E4 — GPU / ML backend class collides with the isolation subset
Gap. The safe-compose subset forbids exactly what GPU passthrough needs — raw
devices: and privileged are both on the normative refusal list. Immich dropped its ML
service to install at all (the honest P-207 stop). So a GPU-bearing backend — including a
BYO local model backend, which ties this straight to the
BYO-models litmus — collides head-on with the
validator.
Options. (a) A narrow, separately-consented device-grant carve-out; (b) route ML backends to a dedicated GPU placement outside the container subset. Any device access punctures the compose-subset trust boundary.
External review input. sol is firm that raw devices:/privileged must not be
admitted just because the user clicks a stronger dialog — GPU access should be a typed
placement capability (hardware.gpu/compute, with device class, sharing mode, reason) mapped
by the placement layer, or a dedicated ML placement (GPU VM / broker) outside the compose
profile. And Immich’s dropped ML service must be represented as an explicit Xe
feature-delta, not silently presented as the complete upstream release. —
sol · app-model §5
Decides: Luke ▲ — litmus / security-touching. Source: engines EB-4. Cross-ref D1 BYO-model.
E5 — Multi-user machines / per-user plane
Gap. 5454 is a single global loopback port; data roots are already per-user, but the
port is fixed, so two OS users on one host collide on 5454. (This is multi-OS-user on one
machine — distinct from multi-person sharing, which has no decision at all and lives in the
agenda.)
Options. Per-user port derivation (hash of UID into a range), with the origin string carrying it — but the origin is the browser’s storage/service-worker key, flagged (SEQ-1) as the single most expensive thing to change later; or first-user-wins + an honest doctor for the second user.
External review input. Whatever the port story, sol says stabilize the canonical origin first — derive it from package + instance identity, never a mutable display name — because the origin string is the browser’s storage/service-worker key and (per SEQ-1) the most expensive thing to change once users have accumulated origin-scoped state. — sol · app-model §4
Decides: Luke + Jan ◆. Source: serving G4.
Theme F — Operational / versioning
The long tail: schema evolution, honesty-contract refinements, ceremony scheduling, and the reversible hygiene the reviews flagged. Most are orchestrator/Fable calls; two need Luke or Luke + Jan.
F1 — Route-table evolution & the d/103-B whole-origin route type — PROPOSED
Canonical entry — folds serving G1 + G5 and app-model AM-4.
Gap. The route-table type discriminator is frozen (P-148: static-digest | backend-proxy | peer). The whole-origin compose case (a container service whose entire UI is the app) ships
as a backend-proxy overload — empty digest + prefix:"/" (P-203). Whether that overload
is blessed as the permanent shape or a distinct additive container-origin type is added is
d/103-B, PROPOSED, pending Luke — the first real test of how the frozen table grows. There is
no explicit v0→v1 migration path today (additive-only + fail-closed is the de-facto rule). Gates
P-210 (the Immich compose UI).
Options. (a) Accept the backend-proxy overload as documented (schema-conservative, already
tested); (b) add container-origin as an additive type (cleaner invariant: backend-proxy stays
“static digest + proxied prefix”) — this is the schema change requiring ratification. On growth:
additive-only + fail-closed, or explicit schema-version negotiation + a migration journal (there is
already a migrations vector precedent in the tombstone file).
External review input. sol calls “frozen but considering a fourth type” not a workable
evolution strategy: keep xe/origin-routes/v0 permanently at three types and put
container-origin in xe/origin-routes/v1 with an explicit rollout protocol
(reader-capability advertisement, dual-read order, atomic generation+digest, last-known-good
rollback, and a table-wide 503 Plane upgrade required rather than a misleading per-route
404). Take the distinct type, not the overload — overloading backend-proxy hides the shift
from an /api request boundary to an active-content publication boundary the VM does not
contain — but note a distinct tag is cosmetic unless it also gets a distinct policy/validation
path. — sol · serving §4 · sol · mission §4
Decides: Luke ▲ for the frozen-type addition; orchestrator ○ for reversible field
additions. Source:
serving G1 ·
serving G5 ·
app-model AM-4.
F2 — The no-502 question
Gap. There is no 502 today: upstream backend/peer failures are synthesized as 503
(a plane-side “can’t reach”), and a genuine upstream 5xx passes through unchanged. Distinguishing
“plane can’t reach upstream” (503) from “upstream errored” (502) is a deliberate addition to the
honest-error contract, not a bug.
Options. (a) Add the 502 distinction; (b) keep the single synthesized 503 and document it.
External review input. sol calls the 503 collapse defensible but a telemetry hazard:
reserve 502 for a protocol-invalid or absent upstream response and 504 for upstream
timeouts, pass a genuine upstream 500 through unchanged, and — whatever the status policy —
add machine-readable Xe-Plane-Error + Xe-Request-Id headers with Cache-Control: no-store on every synthesized error, so automation need not parse mutable English bodies.
(sol also caught the contract’s self-contradiction: the 400/405 rows carry no next-action
copy.) — sol · serving §4
Decides: orchestrator ○ (contract refinement), surfaced to Luke if it changes user-visible copy. Source: serving no-502 question.
F3 — Production signing & app-identity ceremony
Canonical entry — folds app-model AM-3 and shell G1.
Gap. The trust chain is closed in code, but the production signing keys do not exist yet —
the orchestrator holds only dev/build-phase seeds (d/066), and every install literally reads
“(DEV-SIGNED)”. Separately, compose apps are always UnsignedDerived (identity = URL + subdir,
no publisher key), with no per-publisher key pinning across updates. The offline split-custody
ceremony (Luke + Jan) gates all public distribution.
Options. (a) A single offline-root ceremony at world-release (current plan) — DEV until then; (b) an intermediate cross-signed release tier before public. And: does compose’s identity get a publisher-key story, or stay URL-derived?
External review input. sol says scheduling the ceremony does not answer governance: move catalog trust to a TUF/Uptane model — offline threshold roots, delegated publisher roles, snapshot/timestamp metadata, rollback+freeze protection, key rotation, compromise recovery, and user-selectable roots — and give compose apps a real publisher-key story instead of leaving identity URL-derived. — sol · mission §3 · sol · app-model §3
Decides: Luke + Jan ◆ — key custody is a hard rule; d/066 is a bounded build-phase override. Source: app-model AM-3 · shell G1.
F4 — App updates have no user loop
Gap. crates/apps/src/update.rs is a complete engine — semver-gated discovery,
escalation-detecting diff, snapshot → stage → activate → rollback-on-failure, an Auto | Notify | Manual policy where only patch, non-escalating updates auto-apply. There is no CLI or
consent-surface wiring driving any of it; the update loop is unproven end to end.
Options. (a) Wire xe app update + consent-queue integration and prove one
update-with-rollback; (b) leave until an app needs it.
External review input. sol wants the loop built as an authorization + recovery protocol,
not a CLI wrapper: bind a canonical UpdateAuthorization/v1 (from/to release digests,
diff_digest + ruleset version, provenance-bundle digest, rollout/rollback policy, expiry,
nonce) into the gate and recompute + re-verify it immediately before activation; layer TUF
metadata over it; and ship two distinct rollbacks (failure-triggered atomic soak;
user-triggered with explicit data-loss disclosure). Auto needs a signed policy object plus a
full non-escalation checklist, and a data-schema/migration declaration — volume-only restore
is not a real rollback contract. — sol · app-model §3
Decides: orchestrator ○ (surface + one end-to-end proof). Source: engines EB-1.
F5 — Resource limits are declared, not enforced
Gap. The XAM ResourceEnvelope is validated at install (request ≥ required_minimum), but the
compose adapter sets ResourceEnvelope::default() for every compose component and does not map
deploy.resources.limits into the envelope or into container cgroups. The envelope is advisory, not
runtime-enforced.
Options. Map deploy limits → envelope → engine --memory/--cpus.
External review input. sol wants resource/isolation declarations made executable engine policy: map normalized CPU/memory/PID/disk/restart/network/volume limits into each backend, verify effective settings after creation, and fail when a backend cannot enforce a required minimum — an unprivileged container can still exhaust memory, PIDs, CPU, or disk and deny service to co-tenants of the same engine. — sol · app-model §7
Decides: orchestrator ○ + a small spec addition. Source: engines EB-5.
F6 — Compose-validator holes / P-174 sequencing
Gap. The normative refusal list fails closed, but P-174 (open) documents three pre-existing
holes: short-form host ports (ports: ["8080"]), non-UTF-8 corruption in interpolate_declared,
and network_mode: container:. These matter because P-174 gates running compose on
user-supplied manifests — the guestbook proofs used authored manifests; a third-party manifest
is a different trust posture.
Options. (a) Dispatch P-174 to close the holes before P-210 (Immich, a third-party compose); (b) accept the holes as known-and-bounded and sequence P-210 first. Recommendation: P-174 first.
External review input. sol warns that closing the three recorded holes is not enough:
an allow-by-default parser plus a denylist inherits every new Compose feature as implicitly
permitted, and it enumerates many more vectors (gpus:, device_cgroup_rules, volumes_from,
use_api_socket, extends/include/anchors, configs/secrets, alternate build contexts,
implicit .env, hidden profiles). Expand P-174 into a versioned safe-profile project — an
allowlisted deployment IR, parser-differential tests, adversarial fixtures, fuzzing, and a
post-creation inspection of the created objects before start — and treat Compose as an
import language, not the deployment authority. — sol · app-model §2
Decides: Fable ○ to dispatch; confirm sequencing with Luke. Source: engines EB-3.
F7 — Code & doc hygiene bundle
Canonical entry — folds app-model AM-1 + AM-2 and engines EB-6.
Gap. Three reversible cleanups the decisions already settled: (AM-1) RecipeOperation::MarkUi’s
comment still says “Controlled Frame UI” and compose_components maps ui → RuntimeDriver::Cf,
contradicting d/103-A’s plane-origin rendering; (AM-2) two dev-signed swbn example apps
(scratchpad, appdex) are residue of the earlier per-app-IWA direction and don’t model the d/101
default cleanly; (EB-6) docs/serving-and-placement-design.md is stale — it still calls
backend-proxy “dormant … no listener owns 5199,” which shipped P-203 contradicts at every clause.
Options. Reconcile MarkUi to plane rendering; migrate or relabel the swbn examples; refresh the stale doc or supersede it with this site as live truth.
External review input. sol adds a naming hazard to the pile: the XAM document family
conflates three incompatible formats under package.json / .xam.json, and a byte-for-byte
Compose file named guestbook.xam.json trains tools and authors to trust an extension with no
reliable meaning — rename it now, and split recipe / manifest / lock into distinct
discriminated, independently-versioned names. — sol · app-model §1
Decides: orchestrator / Fable ○ (the decisions are made; this is hygiene). Source: app-model AM-1 · app-model AM-2 · engines EB-6.
F8 — Chromium-fork maintenance cadence
Gap. agent54/android-iwa is deliberately a patch series, not a full fork — three patches on
a stock Chromium base, upstream-aligned (Controlled Frame and IWAs are real upstream features). But no
rebase cadence, patch-count budget, or upstreaming path is recorded.
Options. (a) Ride the desktop-android variant + origin-trial features and drive patches toward zero as upstream lands (lowest burden, upstream-timing dependent); (b) maintain a standing carry-patch set (control at the cost of rebase toil). Note d/098: upstreaming is a human-published contribution, never an agent-opened PR.
External review input. sol names the missing browser-fork sustainability plan as an open question — who tracks Chromium security releases, the rebase and emergency-patch SLA, the behavior if Controlled Frame or IWA changes upstream — and asks for a conformance + patch-budget gate (which permission/navigation/isolation/multi-pane tests must pass before a rebase ships). It also flags the deeper dependency: Controlled Frame is an incubating, Chromium-specific API treated as a universal seam, and wants a documented abstraction + fallback. — sol · mission §5 · sol · shell §8
Decides: Jan + Luke ◆ — fork strategy. Source: shell G5.
Theme G — The local trust boundary
Net-new from the external review: two gaps that no Part III layer review named. sol surfaced them by probing the host-local boundary directly — a public web page, and an untrusted process running as the same OS user — rather than the peer or the guest. Both are downstream of the ruling the mission memo demands first: is an untrusted same-host process inside the threat model? Until that is answered, neither can be closed, only scoped.
G1 — Browser request admission on the loopback plane
Gap. The refusal ladder authenticates routing, not the caller. The cross-origin arm only
runs when an Origin header is present, and the Fetch-Metadata floor only refuses cross-site
unsafe/WebSocket requests — so a cross-site safe request without Origin (an image, a
top-level navigation) can reach a backend prefix, including a whole-origin backend-proxy.
Worse, /xe/api/index and /xe/api/catalog answer before the hostname/Origin/Fetch-Metadata
rungs, so app inventory is readable by anything that reaches the loopback socket. Sibling
<app>.localhost origins are cross-origin but same-site, which SameSite cookies and
Sec-Fetch-Site do not isolate. Host and Fetch Metadata are routing/CSRF signals, not plane
authentication, and any local process can forge them.
Options. (a) Adopt sol’s normative Browser Request Admission clause — exact authority,
exact Origin on unsafe/upgrade requests, coherent Fetch Metadata, a narrow
top-level-navigation exception, cross-origin backend subresources denied (incl. GET/HEAD) absent
an explicit Prism bootstrap capability, internal x-xe-* headers stripped from browser ingress,
and the reserved service paths authenticated — plus an explicit PNA/LNA preflight policy
(Access-Control-Request-Private-Network honoured only under a live grant). (b) Accept that even
(a) cannot authenticate an arbitrary local process and add an unguessable browser/Prism session
capability (or app-level auth) for unsafe backend requests. (c) Document the ladder’s limits
honestly and scope the untrusted-local-process threat out of v1. Regular apps should also receive
a minimal frame-ancestors CSP (Prism + declared embedders only) so a public page cannot frame
them for confused interaction.
Decides: Luke ▲ — a threat-model / security-boundary call, litmus-adjacent (inspect what it sees); it cannot be settled without ruling on the same-UID-process question. Source: sol · serving §1 · sol · mission §5.
G2 — Connector & engine-API caller authentication
Gap. The engines chapter’s “no loopback TCP” invariant is false as stated. The backend
connector listens on 127.0.0.1:5199 and authenticates only the app selector (a
syntactically-validated x-xe-app header), which does not authenticate the caller — any
local process can act as a confused deputy, drive the connector, and select an installed app;
the browser’s grant rules do not protect direct loopback access. And on macOS the engine
socket is a raw tcp://127.0.0.1:2375 Docker API, far more privileged than any backend port.
The defensible invariant is narrower than “no loopback TCP”: no app-owned backend port is
published on the host, and no app or router receives the engine credential.
Options. (a) Move connector access to a peer-authenticated local transport — a Unix socket with peer credentials, a Windows named pipe with an ACL, or a per-boot authenticated channel — and give the macOS Docker TCP endpoint an explicit local-adversary analysis (or replace it). (b) Keep loopback TCP but require an unguessable per-boot credential the router holds and apps never see. (c) Restate the invariant to the defensible one and scope the local-process threat out of v1. Any choice should carry a post-creation check that the connector resolves an opaque launcher-held handle, never an arbitrary caller-supplied project/service selector.
Decides: Luke ▲ — the same threat-model call as G1; couples to the Windows-backend engine decision (E3) and the connector’s role in the compose IR (F6). Source: sol · app-model §2 & §5.
The runnable disposition of every open decision above — in the order Luke and Jan should take them — is the review agenda. Protocol and format candidates already firming up out of the built code are in Part V.
The Review Agenda
This is the running order for the Luke + Jan review sitting. It consolidates the ten-question agenda from the decisions ledger and every “session questions” list across the eight Part III reviews — 43 raw questions — de-duplicated into 34 items in the order they are best taken. Each line is one decision: who decides, why it matters in a sentence, and a link to the section that argues it. The design frontier is the reasoning behind these; this page is the driver’s seat.
Decider key: ▲ Luke (litmus / security / frozen-schema) · ◆ Luke + Jan (ceremony / placement / strategy) · ○ Fable / orchestrator (reversible — confirm intent only). Take the sections top to bottom: ratifications unblock dispatch, so they come first; then the big forks that set the spine; then per-layer decisions; then quick confirmations that need a yes/no, not a debate.
1 · Ratifications (unblock dispatch first)
Two PROPOSED decisions gate real work — nothing downstream dispatches until they are disposed.
1. Ratify d/104 — the launch seam (▲ Luke) — Bless the catalog descriptor
(label → http://<app>.localhost:5454) as the single Controlled-Frame launch seam on every
platform? It unblocks Android’s two BLOCKED frame cells and unparks the P-188 responder;
reversal is cheap, no code depends on it. →
devices: launch seam
2. Rule on d/103-B — the whole-origin route type (▲ Luke) — Ratify a distinct
container-origin route type, or bless the shipped backend-proxy empty-digest overload as
permanent? It is the first change to the frozen route-table schema and gates P-210 (the Immich
compose UI). →
serving G1
2 · The big forks (set the spine)
The four decisions that shape the most downstream work. Budget real interview time here.
3. Membership-vs-ticket serving (▲ Luke) — Gate the serve tunnel on the existing
MembershipRegistry (revocation cuts serving for free), or keep bearer tickets and add UCAN?
A removed device still serves and consumes while it holds a live ticket — enrollment and
authorization-to-serve are not the same graph today. (Sub-question: adopt a bounded-lifetime
exp ticket now to unblock durable secrets, or wait for full membership gating?) →
peer-serving: questions
4. https-vs-plain-http on loopback (▲ Luke) — Keep https-by-default with the
name-constrained CA (Chromium-build-dependent enforcement), or lean on the browser’s
localhost secure-context and drop the CA ceremony for regular apps? The code says
https-default and d/101 says plain-http-default — the site needs one current truth. →
serving G3
5. In-frame permission model (▲ Luke) — Does the shell broker all app permissions via the agency ladder (shield-by-default, journaled), or delegate to Chrome’s native per-origin prompts inside the partition? This decides whether “your agent answers for you” is real or marketing — the highest-leverage undesigned sovereignty surface in the whole system. → shell G6
6. v1-vs-v2 group document (▲ Luke) — Is crates/xe-sync (v2: Device/Companion kinds,
endpoint bindings) or crates/net/group (v1: what gossip and the membership gate compile
against) the canonical membership layer for shipped serving? This blocks item 3. →
transport: questions
3 · Per-layer decisions
7. Movable memory & exit-without-loss format (▲ Luke) — Commit a per-app export/import
format now, or stay single-home? Two litmus clauses are half-met: app data is single-home
(sync moves metadata, not the data inside apps) and xe export is intent, not a proven
whole-machine round-trip. →
ledger: review agenda
8. Production key ceremony (◆ Luke + Jan) — When does the offline split-custody signing
ceremony run (after Thailand?), and does compose’s always-UnsignedDerived identity get a
publisher-key story? It gates all public distribution — every install reads “(DEV-SIGNED)”
until then. →
app-model AM-3
9. Boot Service wake-channel mechanism (▲ Luke) — Loopback + per-install token, IWA Direct Sockets with an authenticated handshake, or OS socket activation? An unauthenticated loopback TCP socket is disqualifying by our own standard (d/053); this is the load-bearing piece of the local-trust boundary. → shell G4
10. Authed-wake crux (AW-1 / AW-2) (▲ Luke) — Accept “same-network zero-resident desktop
only; cross-internet asleep-wake is out of v1” (relay mailbox reserved), and membership-derived
authorization vs. a group-doc Statement wake-policy type? The transport that reaches a device
(iroh) is the one that cannot OS-socket-activate it. →
devices G-4
11. Port custody / framing trust (▲ Luke + Jan) — Accept same-UID loopback squat as v1-scoped, or make Prism verify the plane’s identity before framing it (pinned leaf / handshake secret)? A shell that frames a squatter has been quietly compromised with no transport broken. → serving G2
12. Multi-OS-user plane (◆ 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)? → serving G4
13. Relay trust + self-hosted iroh relay (▲ Luke) — Run Agent54 relays pinned in the
signed head, or accept number0’s public relays direct-preferred with relay-fallback documented?
Relays see connection metadata; d/014 requires a self-hosted iroh-relay (a human packet:
container + DNS + TLS) before any production peer traffic. →
transport G3
14. Multi-person groups / sharing scope (▲ Luke) — In near-term scope, or explicitly post-camp? Everything today is one user’s own device group under a single-writer group doc; there is no decision covering sharing an app or data with another person. The local-first audience will ask first. → ledger: review agenda
15. GPU / ML backend class (▲ Luke) — Design a narrow, separately-consented device-grant
carve-out, or keep ML backends off the container subset entirely? The isolation subset forbids
exactly what GPU passthrough needs (devices: + privileged); Immich dropped its ML service to
install. Ties the BYO-models litmus. →
engines EB-4
16. Windows backend engine (◆ Luke + Jan) — Is Windows-with-backend in scope now (a WSL exec bridge or a named-pipe socket contract), or explicitly deferred? Serve and in-frame consume are proven on Windows; a backend behind the app is not. → engines EB-2
17. P-174 sequencing (○ Fable — confirm) — Dispatch P-174 to close the three compose-validator holes before P-210 runs a third-party manifest? The proofs so far used authored manifests; a third-party manifest is a different trust posture. → engines EB-3
18. Device recovery ceremony (▲ Luke) — Lost phone / new laptop: admin-re-invite (the current implicit answer), or threshold / social recovery of the group authority key? A new device inherits no grants, and a lost authority device is currently unrecoverable. → devices G-3
19. Sovereign push for force-stopped Android (▲ Luke) — UnifiedPush self-hosted (on-thesis, more to run), FCM opt-in (a Google dependency on the wake path), or both user-selected? The missing primitive behind the Android wake row (d/037 Q3). → devices G-6
20. Agent delegation / the UCAN decision (▲ Luke) — Is Reagent’s authority the same
xenon-cap/xenon-grant substrate (Grant = long-lived delegation, Lease = attenuated
invocation), and is agent://act enforced in v1 or kept declared + audited? Make the UCAN
alignment an explicit product decision, not an implementation detail. →
agent-layer G-2
21. Agent boundary siting + lease-signing root (D-1 / D-5) (◆ Luke + Jan) — Does the
injection boundary ship in the shell now or wait for a real daemon, and what is the lease-signing
root — given the shell shares an address space with the forgeable iwa: IPC channel, which can
otherwise mint the very lease meant to bound it? →
agent-layer G-1
22. BYO model wiring (▲ Luke) — A model-selection capability, a config file, or status-quo hardcoding for v1? Hardcoded model IDs in darc are a live sovereignty-litmus violation the kickoff flagged. → agent-layer G-1
23. Journal unification (▲ Luke) — Fold the standalone grant/trust JSON-lines logs into the hash-chained audit journal so the Console’s Activity view can witness them, and converge the two duplicate ledger implementations onto one discipline? An inspectability-litmus gap. → agent-layer: one ledger
24. Composition scope (▲ Luke) — Promote composition onto the Controlled-Frame + catalog path so any two fleet apps compose (d/103’s intended generalization), or keep it a curated shell feature over the two fixtures it runs on today? → shell G3
25. Chromium-fork maintenance cadence (◆ Luke + Jan) — Drive android-iwa toward zero
patches as upstream Controlled Frame / IWA lands (accepting an upstream-timing dependency), or
maintain a standing carry-patch set? Rebase cadence and patch-count budget are unset. →
shell G5
26. Companion serving semantics (▲ Luke) — Should MemberKind::Companion (v2) be allowed
to consume but never publish? No serving semantics exist for it today. Ties to item 6. →
transport: questions
27. Route-table versioning under the freeze (○ orchestrator — confirm) — Confirm
additive-only + fail-closed as the evolution rule, and whether the first type addition
(item 2) should introduce explicit schema-version negotiation rather than relying on
404-on-unknown. →
serving G5
4 · Quick confirmations (yes / no, not a debate)
Reversible or already-owned calls that just need a nod to keep the site and the code aligned.
28. The no-502 question (○ orchestrator → Luke if copy changes) — Distinguish 503
(plane can’t reach upstream) from 502 (upstream errored) in the honest-error contract, or keep
the single synthesized 503? →
serving: no-502
29. Windows exposure-tripwire (○ Fable — taste call) — Refuse-by-default on a non-owner ACE
(Unix-symmetric) with --repair opt-in, or keep the silent repair plus a loud audit record? →
peer-serving G2
30. Example-set hygiene (○ Fable — confirm intent) — Migrate the dev-signed swbn examples
(scratchpad, appdex) to the plain-HTTP default tier so the set teaches d/101 cleanly, or
relabel them explicitly as super-tier demos? →
app-model AM-2
31. Doc hygiene (○ Fable) — Refresh the stale serving-and-placement-design.md (it still
calls backend-proxy “dormant,” contradicted by shipped P-203), or supersede it with this
documentation site as the live truth? →
engines EB-6
32. darc → Reagent rename timing (◆ Luke + Jan) — Does review-session material and near-term copy say “Reagent” now, or keep “darc” until the rename (d/053) is actually performed by Luke + Jan? → ledger: review agenda
33. Naming glossary fix (○ Fable — agreed?) — Confirm the glossary keeps the product
delegation model (capability-model + injection-boundary) distinct from the unrelated
delegation-interfaces.md broker, so no reader conflates them. →
agent-layer: questions
34. Xenon-is-Helium-based — phrasing check (◆ Luke + Jan) — The site’s shell primer states “Helium is its base (d/053),” and d/008 / d/061 record Helium as the confirmed Xenon base. The shell writer flagged this as a copy-phrasing point to confirm before it hardens — not a re-opening of the decision, just a nod that the phrasing is correct. → ledger: d/061
Reasoning and options for each item: The Design Frontier. Decision records: the decisions ledger, with PROPOSED items collected under Proposed / pending Luke.
External Review (sol)
This section is an independent critical design review of this site’s own chapters,
run by an external frontier model — gpt-5.6-sol at xhigh reasoning (via codex exec
on lane-182) — on 2026-07-18. It is not our voice. The layer reviews in
Part III are ours; the six memos below are someone
else’s adversarial read of the same material, reproduced verbatim.
What this is, and what it is not
Six memos, one per beat of the stack. Each was produced under a deliberately adversarial brief: the reviewer was told to be direct and critical, given no filesystem or repo access, and handed only the relevant chapters inlined verbatim into a single self-contained prompt (Part II architecture + Part III review + the matching Part V spec-candidate block). So sol is critiquing exactly what the site says — not the code behind it — which is the right test for a documentation program: if a claim cannot survive a careful reading of the prose, the prose is wrong, or the design is.
- These are critiques, not decisions. Nothing here supersedes a decision record. Where sol disagrees with one of our layer reviews, we have kept both voices rather than quietly adopting sol’s — the disagreement is the useful part.
- The positions are folded forward. Every place sol takes a substantive position, the design frontier now carries a short External review input line pointing back to the relevant memo, and the genuinely new gaps sol surfaced are first-class entries there (including a new Theme G — the local trust boundary).
- Each memo ends with a machine sentinel (
SOLREV-…-DONE). That is the reviewer’s own completion marker, kept so the record shows each run finished rather than truncated. - The header of each page is lightly normalized (title · date · reviewer · scope · method); everything below the header is sol’s text, unedited.
How to read it
Start with the memo for the layer you care about; each opens with a five-line orchestrator-facing summary, then the long-form argument, prior-art table, ranked proposals, and the questions sol says are missing from the review agenda. If you only read one thing, read the strongest criticism called out in each summary below.
Recurring cross-cutting asks
The same five demands surface in memo after memo, which is itself the signal worth heeding:
- Publish a threat model + trusted computing base + authority matrix before ratifying d/103-B or d/104 — every security claim currently rests on an unnamed boundary.
- Make the principal immutable. A mutable display label must never be the security
principal; bind identity, grants, storage, and audit to a stable
app_id(+ per-installinstance_id), with labels display-only — and stabilize the canonical origin before users accumulate origin-scoped state. - Separate the four things “signed” is doing — publisher identity, catalog authorization, payload integrity, local consent — and move catalog trust toward TUF/Uptane (offline threshold roots, snapshot/timestamp, rollback+freeze protection, user-selectable roots).
- Turn slogans into checkable invariants + conformance vectors. “Zero-trust”, “no cliffs”, “every rung undoable”, and the as-built spec candidates all need normative prose, canonical encodings, and positive/negative test vectors before anyone can implement them independently.
- Name and defend the local trust boundary. The loopback plane and the engine connector
are reachable by any local process;
Host/Fetch-Metadata and a syntactic header are not authentication.
The six memos
Mission & decisions spine
Reviews the big picture, the mission/principles/litmus, and the decisions ledger.
Strongest criticism: there is no explicit threat model or trusted computing base —
every security claim (origin isolation, VM containment, launch resolution, delegation,
audit) rests on an unnamed boundary, and both PROPOSED decisions each widen an authority
boundary no invariant governs. The concrete consequence: d/104 is “not ratifiable as
written” (a mutable display label must not become a security principal) and d/103-B should
take the distinct container-origin route type, because overloading backend-proxy
hides a shift from an /api request boundary to an active-content publication boundary.
Also names the litmus tension head-on: the “user-owned trust chain” is operationally
operator-owned today.
Transport & peer serving
Reviews transport/identity, peer serving, and their four spec candidates.
Strongest criticism: the spec candidates are not yet sufficient for an independent
interoperable implementation — the Peer Ticket carries no on-wire version/type
discriminator, the JSON + base64url parsing profiles are unspecified, and the single-byte
ACK conflates ~7 distinct failure classes. On the headline question it narrows our
review: group membership is an authenticated ACL, not a capability system, so the first
gated release should be membership AND a per-route, audience-bound grant (not membership
replacing the ticket), shipped on a new ALPN xenon/serve/tcp/2 — never by mutating
/1 in place. Ship group-doc v1, front it with a lineage-independent authorization
interface, and require a signed one-way v1→v2 transition.
The serving plane & router
Reviews the loopback plane, the workerd router, and its three spec candidates.
Strongest criticism: a cross-site safe request without Origin (an image, a
navigation) can reach a backend prefix — including a whole-origin backend-proxy —
because the cross-origin arm only runs when Origin is present and the Fetch-Metadata floor
only refuses cross-site unsafe/WebSocket requests; and /xe/api/index//catalog answer
before the hostname/Origin/Fetch-Metadata rungs, exposing app inventory to anything that
reaches the loopback socket. Host and Fetch Metadata are routing/CSRF signals, not plane
authentication. The fix is a normative Browser Request Admission clause plus an
explicit PNA/LNA preflight policy; on scheme, choose plain HTTP for regular apps (reject
the local CA) with a separate cryptographic plane-identity handshake for Prism.
App model & engines
Reviews the install spectrum, XAM v0.2, the compose safe-subset, the engine socket contract,
and the update engine.
Strongest criticism: the chapter’s “no loopback TCP” claim is false as stated —
127.0.0.1:5199 plus a syntactically-validated x-xe-app header does not authenticate
the caller, so any local process can drive the connector and select an installed app; and
on macOS the engine is a raw tcp://127.0.0.1:2375 Docker API, far more privileged than a
backend port. The defensible invariant is narrower: no app-owned backend port is published
on the host, and no app or router receives the engine credential. The through-line fix:
treat Compose as an import language, not the deployment authority — compile a pinned
dialect into a small allowlisted IR, execute only that, and inspect the created objects
before start. Two sharp catches: a fixed non-latest tag is not a pin, and the sh+nc
exec bridge is not a general unmodified-container contract (distroless images ship
neither).
Shell & rendering (Prism / Xenon)
Reviews the shell architecture, the G1–G6 gap register, and the Prism↔app contract.
Strongest criticism: a hash-linked journal is tamper-evident only relative to a
trusted head — without signed or externally anchored checkpoints the whole chain can be
truncated or replaced, so “exit without loss” is today an intention, not a guarantee;
and shield-by-default is infeasible via injected JS (the Controlled Frame
permissionrequest API can only allow/cancel — real shielding needs a Xenon browser-process
permission delegate). On G6, sol lands on a hybrid: Prism owns policy/shielding/
persistence/journaling, Chrome/OS owns final device selection and enforcement, and the
effective outcome is the intersection, never the union. The four-verb contract should
split into a cooperative lifecycle RPC and a separately-audited automation gateway (drop raw
tab.open{url} and key).
Devices & the agent layer
Reviews the device fabric, the resident-agent architecture, the grant/lease model, and the
audit hash-chain.
Strongest criticism: the security model is not yet coherent enough to implement —
the problems are unresolved semantics, not missing code. onward:false contradicts
minting a lease under a grant (a lease that transfers authority is the onward delegation the
grant forbids); the shell is not a defensible security boundary; and the
64-bit-truncated hash chain is corruption detection, not adversarial tamper evidence.
Fixes: model the lease as an audience-bound invocation, not a delegation, with a
deterministic attenuation-subsumption algorithm and a published max stale-authority window;
split the agent into five processes and exclude raw CDP / execute.script / persistent
injection from v1; and converge on one full-hash, signed-checkpoint ledger with
cross-device head gossip. Human approval is inspection, not an authority bound.
Sol design review — Mission & decisions spine
Part of the site’s external review; reproduced verbatim below the header. sol’s positions are folded into the design frontier as External review input lines.
- Date: 2026-07-18
- Reviewer: gpt-5.6-sol (via
codex exec,model_reasoning_effort=xhigh), run on lane-182 - Scope:
src/big-picture.md,src/part1/mission.md,src/part1/decisions-ledger.md— the whole-system big picture, the mission/principles/litmus, and the curated decisions ledger. - Method: All three chapters were inlined verbatim into a single self-contained prompt (no filesystem context given to the reviewer); the reviewer was briefed to be direct and critical, and to return five structured sections plus a terminal sentinel line.
Orchestrator-facing summary (5 lines)
- Verdict: the spine is directionally sound but oversells in three recurring ways — false absolutes (“a browser cannot install / own identity”; “native OS … but closed” ignoring Linux/BSD), slogans used as guarantees (“zero-trust”, “every rung is consented, receipted, and undoable”, “no cliffs”), and a trust chain described as user-owned that is operationally operator-owned (orchestrator holds the seeds; no user-selectable roots).
- Strongest single criticism: there is no explicit threat model or trusted computing base — every security claim (origin isolation, engine/VM containment, launch resolution, delegation, audit) rests on an unnamed boundary, and both proposed decisions each widen an authority boundary that no invariant governs.
- Contradicts our ledger where it bites: mission says regular apps need “no signing”, but d/079 + the Big Picture say install verifies a signature + content hash — sol demands we split publisher identity / catalog authorization / payload integrity / local consent into separate terms; and the “two independent axes” are not independent (a plain L1 app can be driven only because an L3 shell + browser-level CDP act on it; L3 is a genuine cliff, contradicting “no cliffs”). Also flags “unmodified, un-Xe-aware L1” as unsupported (re-origining breaks OAuth redirect URIs, WebAuthn RP-ID, cookies, service-worker scope) — one Excalidraw success is not a compatibility envelope.
- Top-5 actionable asks (ranked): (1) publish an executable threat model + authority matrix before implementing d/103-B or d/104; (2) define a versioned App Contract v1 with an immutable
app_id/instance_id(labels display-only) and a conformance suite; (3) replace blanket “undo” with a transactional action protocol (reversible / compensatable / irreversible classes); (4) specify and prove a portable-state format before shipping peer metadata schemas; (5) replace the single operator catalog root with a TUF-style, user-selectable trust system. - On the two PROPOSED decisions: take the distinct
container-originroute type (overloadingbackend-proxyhides an authority change from an/apirequest boundary to an active-content publication boundary; the VM boundary does not contain the browser-side blast radius); and d/104 is “not ratifiable as written” — a mutable display label must not become a security principal, so bind launch to an immutableapp_id+instance_idcapability (not an arbitrary URL) and constrain post-launch navigation, not just the first URL.
1. Internal inconsistencies and weak arguments
-
The foundational “missing layer” argument is built on false absolutes. The text says, “A browser can open an app but cannot let you truly own its install,” while browsers already support installation, offline execution, origin-bound identity, storage, background workers, and updates through PWAs. It also says native operating systems provide these capabilities “but [are] closed,” ignoring Linux and BSD. Xe may offer a different ownership and composition model, but the claimed absence of any existing computer-like web layer is not established.
-
“Universal app package manager” does not describe the presented system. Regular apps explicitly have “no bundle format, no manifest requirement,” while installation is actually a signed catalog record plus payload or proxy configuration. There is no described dependency model, version solver, update-channel policy, uninstall contract, transaction protocol, or rollback mechanism. This is a catalog-driven application router, shell, and backend supervisor—not yet a universal package manager.
-
The isolation argument is circular and changes between chapters. Chapter A says each
*.localhostorigin gets “exactly the isolation the web already provides” and therefore needs no packaging. d/101 instead says isolation “is already delivered by the Controlled Frame.” Those are different boundaries: same-origin policy separates origins; Controlled Frame partitions and mediates an embedded guest; an engine VM contains backend execution. The design never states which boundary protects which asset from which attacker. -
The anti-IWA argument conflates packaging, authenticity, and gatekeeping. d/101 argues that per-app IWA packaging “would just be a store-like gate.” A signed package format does not require a centralized store or publisher approval. Xe itself permits self-signed packages and depends on a signed catalog. Rejecting mandatory IWA packaging may be correct for compatibility, but the stated reason is a false equivalence.
-
The regular-app signing story contradicts itself. The mission says regular apps require “No signing,” while d/079 says they ship as “a signed catalog row + content-addressed static payload,” and the Big Picture says installation verifies “the signature and content hash before anything runs.” Either the catalog signature authenticates the executable mapping, in which case regular apps are indirectly signed, or it does not authenticate the publisher, in which case the claimed trust chain does not establish app identity. The document needs separate terms for publisher identity, catalog authorization, payload integrity, and local consent.
-
The “user-owned trust chain” is actually operator-owned. Distribution is controlled by an Ed25519 catalog, the orchestrator holds the signing seeds, public distribution waits for Luke, and the production ceremony is controlled by Luke and Jan. There is no design for user-selected roots, multiple catalogs, threshold recovery, root rotation, publisher delegation, compromise response, or catalog transparency. A user can inspect this trust chain but does not own or fork it in any operationally defined sense.
-
The two supposedly independent capability axes are not independent. The install spectrum mixes lifecycle state—“visited” and “installed”—with capability grants and a packaging/security class. L3 is an IWA-only tier with Controlled-Frame powers, which is a substantial cliff despite the claim that there are “no cliffs.” A plain L1 app can be “driven” only because an L3 shell and browser-level CDP authority act on it, so the achievable agency rung directly depends on another app’s install class.
-
The vocabulary collapses the same axes it claims to separate. “Super web app / super user agent” are declared interchangeable, while one is an app class on the install spectrum and the other is the terminal rung of the agency ladder. A shell capable of hosting Controlled Frames is not necessarily an agent, and an agent is not necessarily an installable app. Reusing the same term on both axes makes grants and audit records ambiguous.
-
“Every rung is consented, receipted, and undoable” is false. Sending correspondence, making a purchase, disclosing data, deleting remote data, or booking a non-refundable service cannot be made undoable by a journal. At most, Xe can support rollback for local state and compensating actions where an external service offers them. The mission’s “audit + undo” rule needs explicit irreversible and compensatable action classes.
-
The full-authority mission conflicts with the permission model. The mission defines the agent as acting “with full authority” across the user’s digital life, while d/027 requires attenuable delegation and the agency ladder requires per-action consent. User sovereignty can mean that the user retains ultimate authority; it must not mean that the resident agent receives root-equivalent ambient authority. The current language fails to distinguish those positions.
-
“Zero-trust” is being used as an uncheckable slogan. “The system assumes nothing and proves its own state” is impossible: Xe trusts the host OS, browser, browser update chain, workerd binary, engine, compiler, signing roots, device enrollment, and hardware. The design needs an explicit trusted computing base and compromise model. Calling the system zero-trust does not remove those dependencies.
-
The hard promise that L1 apps run “unmodified and un-Xe-aware” is unsupported. Re-origining an app to
<app>.localhostchanges OAuth redirect URIs, WebAuthn relying-party identity, cookie behavior, service-worker scope, storage identity, absolute URLs, CSP, framing policy, mixed-content behavior, backend discovery, and assumptions about TLS._headers, catalog rows, proxy routes, and launch descriptors are also integration metadata even if application source code is unchanged. The promise needs a defined compatibility envelope, not a single successful Excalidraw example. -
“Browsers treat
*.localhostas a secure origin” is imprecise and security-critical. Loopback origins may be treated as potentially trustworthy for many secure-context checks, but plain HTTP localhost is not equivalent to authenticated HTTPS for all cookies, APIs, redirects, certificates, or origin-bound credentials. The document uses “secure origin,” origin isolation, transport authentication, and trustworthy context as though they were interchangeable. -
The stable-origin promise has no stable identifier. d/077 derives origins from names and resolves collisions with suffixes; d/104 proposes “app label → plane origin.” Labels are mutable, localizable, attacker-influenced, and collision-prone. Installation order could therefore change the hostname across devices, breaking cookies, IndexedDB, service workers, grants, OAuth configuration, and audit identity.
-
“No cross-app CORS, ever” is incompatible with the composition mission unless the bypass is specified. Generated composition requires the agent to read panes A and B, and the driven rung uses CDP. That means an L3 component intentionally crosses the isolation boundary that “no cross-app CORS” supposedly protects. This can be a capability-mediated bypass, but the design must name it, bind it to grants, and constrain its outputs rather than presenting origin isolation as absolute.
-
The peer narrative overstates what is peer-to-peer. The catalog trust root, forge, release process, and relay infrastructure remain centralized operational dependencies. “There is no central hub” is true only of the intended device-state topology. It is false as a description of control, bootstrap, discovery fallback, and software distribution.
-
The state-sync premise is not supported by the selected scope. The mission promises users can “sync your own state,” but d/068 syncs only source pins, serial anchors, and receipts while app volumes remain single-home. Byte-identical transfer proves static artifact transport, not application-state synchronization, conflict handling, recovery, or continuity when the home device is unavailable.
-
The phone role is internally inconsistent. Chapter A says the phone is “not a sync peer”; d/042 calls iOS a “full group member”; d/062 includes a future Android runtime; and Chapter A already claims an Android on-device plane is proven. The design does not distinguish group membership, identity membership, runtime participation, serving, synchronization, and consent-only operation.
-
The zero-resident Boot Service claim evades the wake problem. A service that wakes a runtime needs an OS-managed activation mechanism or a resident listener. d/053 admits that the wake channel is undecided. “Zero resident processes” is therefore either delegated to launchd/systemd/Windows—making the OS service manager the resident mechanism—or technically unexplained.
-
The project is not universal while its core shell depends on Chromium-specific experimental surfaces. Xenon does not exist as a built artifact, stock browsers require flags, Controlled Frame is Chromium-specific, and iOS cannot host the shell. “Any of your devices” and “universal” are future aspirations contradicted by the current dependency structure.
-
The ledger’s “higher number wins” rule is not design governance. Superseding a decision requires compatibility impact, migration rules, affected artifacts, and conformance tests. d/094 still has a stale TCP implementation, d/103 contradicts its own code comment, and the serving scheme changed materially. Number ordering records intent; it does not reconcile deployed state or protect mixed-version peers.
-
The claimed proof boundary is too weak for the claims attached to it. Byte equality, successful rendering, and an offline notice prove narrow integration paths. They do not prove isolation, app authenticity, rollback resistance, permission attenuation, revocation, audit completeness, compromised-peer behavior, or recoverable updates. Calling the entire path “proven” without naming the property under test invites security claims to inherit evidence that only establishes functionality.
2. Prior art and standards alignment
| Prior art or standard | Required position |
|---|---|
| Isolated Web Apps | Align for the rare L3 class, including bundle identity, update semantics, key rotation, and reproducible builds. Differentiate only on making IWA packaging optional for regular apps; do not equate IWA adoption with centralized store control. |
| Controlled Frame | Treat it as a Chromium-specific implementation of a guest-isolation interface, not the universal architectural seam. Xe needs a documented abstraction and fallback because upstream API changes, platform gaps, or removal would otherwise invalidate Prism. |
| UCAN | Either implement a documented interoperable profile with canonical serialization, test vectors, proof-chain validation, expiry, audience binding, replay control, and revocation, or stop saying “UCAN-compatible.” Semantic resemblance alone does not produce interoperability. |
| Solid | Borrow its separation of application code from user-controlled data and its explicit data authorization model. Differentiate from Solid’s server/pod assumptions where Xe requires offline-first peer replication, but use it as a test of whether users can change apps without moving or surrendering data. |
| Automerge, Yjs, and Ink & Switch local-first work | Borrow their treatment of offline edits, actor identity, conflicts, causal history, schema migration, and partial replication. Do not claim CRDTs solve opaque app volumes; Xe needs per-data-type adapters plus snapshot/restore for non-CRDT state. |
| Sandstorm | Study it closely: per-app isolation, proxying existing web apps, capability-based sharing, grains, and the Powerbox are direct predecessors. Borrow its capability-request UX and app-instance model, and explicitly address the packaging and adoption costs that limited its application ecosystem. |
| WebTransport and WebRTC | Keep iroh behind a transport interface and test browser-native alternatives for cases that do not require the native worker. WebRTC provides browser peer connectivity and WebTransport provides QUIC-based browser transport, but neither supplies Xe’s identity, authorization, discovery, or relay policy by itself. |
| Tauri and Electron | Differentiate Xe’s shared runtime from their per-app native shell model, but borrow their mature lessons on native IPC allowlists, navigation restrictions, updater signing, protocol handlers, and web-to-native confused-deputy attacks. The Boot Service and engine connector recreate many of the same risks. |
| Capsicum, seL4, and Fuchsia capabilities | Borrow object-capability discipline: typed handles, explicit rights, no ambient authority, rights attenuation, and revocation through indirection. Do not borrow seL4’s assurance language unless Xe can define and verify a comparably bounded trusted computing base. |
| DIDs and Verifiable Credentials | Do not adopt them merely because Xe uses decentralized keys. Use ordinary device and publisher keys until multi-person or cross-domain identity requires an interoperable identifier and credential exchange; VCs are claims, not an authorization system. |
| Web App Manifest and the PWA install model | Align regular-app metadata with manifest concepts such as stable id, start_url, scope, icons, display mode, shortcuts, and protocol handlers. A Xe launch descriptor should extend or translate this model instead of inventing an unrelated label-to-URL identity system. |
| IPFS and Nix | Borrow IPFS-style content identifiers for retrieval and Nix-style immutable closures, atomic activation, reproducible derivations, and rollback. Content addressing proves bytes, not publisher authority, freshness, or safe execution, so it must remain separate from catalog trust. |
| The Update Framework and Uptane | Align the catalog with delegated roles, threshold offline roots, snapshot/timestamp metadata, expiry, rollback and freeze protection, key rotation, and compromise recovery. An Ed25519 signature plus monotonically increasing serial is an incomplete update-security design. |
| in-toto, SLSA, and Sigstore-style transparency | Borrow build provenance, artifact attestations, reproducibility evidence, and append-only publication records. A forge under operator control does not by itself prove that catalog bytes correspond to reviewed source. |
| OAuth/OIDC and WebAuthn | Position explicitly against their origin-bound identities and registered redirect URIs. Re-origining apps to localhost will break or alter these flows unless Xe provides a stable identity, brokered redirect contract, or a declared incompatibility. |
| OCI and WASI Component Model | Align engine packaging with OCI where full containers are required, and evaluate WASI capabilities for smaller backends that do not need a VM. A standardized engine contract would reduce coupling to smolvm, Colima, Docker, WSL2, and host-specific socket behavior. |
3. Top five highest-leverage improvement proposals
1. Publish an executable threat model and authority matrix before implementing d/103-B or d/104
Define the principals—ordinary web page, L1 app, L3 shell, resident agent, local process, engine workload, peer, catalog publisher, and compromised device—and list which assets each may read, mutate, launch, or delegate. State whether SOP, Controlled Frame, VM isolation, signed metadata, or a capability protects each edge, then add negative tests for DNS rebinding, hostile Host headers, CSRF, path confusion, redirect escape, replayed grants, malicious peers, compromised catalogs, prompt injection, and engine escape.
Touches: d/027, d/028, d/053, d/075, d/082, d/094, d/100–d/104, and the “zero-trust” claim.
Expected payoff: It replaces overlapping security slogans with reviewable invariants and prevents route, launch, wake, and agency mechanisms from independently widening the same boundary.
2. Define a versioned App Contract v1 with immutable principals, typed routes, and a conformance suite
Create an immutable app_id derived from a publisher key or content identity, a distinct per-install instance_id, and a launch descriptor containing the exact route kind, expected origin, version/digest, frame policy, engine endpoint capability, redirect policy, and offline behavior. Labels must be display-only. Use a tagged container-origin route rather than overloading backend-proxy, map compatible Web App Manifest fields into the contract, and atomically activate the descriptor and router table.
The conformance suite must cover OAuth redirects, WebAuthn, cookies, service workers, IndexedDB, CSP, COOP/COEP, WebSockets, streaming, large uploads, absolute URLs, backend wake, offline behavior, and update rollback across representative real applications.
Touches: d/067, d/076–d/081, d/092, d/100–d/104.
Expected payoff: It makes stable origin identity enforceable, resolves both proposed decisions on a coherent schema, and turns “unmodified web apps” from a promise into a measurable compatibility class.
3. Replace blanket undo claims with a transactional action protocol
Define action classes as reversible, compensatable, or irreversible. Require a prepare step that exposes the exact principal, resource, input hash, external effects, grant, expiry, and available compensation; bind execution to that prepared operation; use idempotency keys; record the result and verification state; and require explicit confirmation for irreversible actions. Unify grants, invocations, external results, compensation attempts, and revocations in one tamper-evident journal.
Touches: the Power Design “audit + undo” rule, d/027, d/064, d/082, d/091, and the sovereignty audit clause.
Expected payoff: It produces a defensible agent-safety model for purchases, messages, disclosures, and deletions instead of promising an impossible universal undo.
4. Specify and prove a minimum portable-state format before shipping peer metadata schemas
Define a versioned export container containing catalog pins, app identity, configuration, grants, journal references, encrypted volume snapshots, integrity metadata, and application-specific export hooks. Specify quiescing, incremental snapshots, key rotation, failed restore, schema migration, deletion propagation, and recovery from a corrupt or lost home device. Require a fresh-machine export/import round trip and an independent reader before calling exit-without-loss met.
CRDT synchronization should be an optional data-type contract layered on this format, not a universal solution for opaque volumes.
Touches: d/062, d/068, the peer-topology claim, and sovereignty clauses 4 and 6.
Expected payoff: It prevents metadata sync from becoming an incompatible spine while the actual user data remains immovable and unrecoverable.
5. Replace the single operator catalog root with a user-selectable, TUF-style trust system
Define offline threshold roots, delegated publisher roles, expiry, snapshot/timestamp metadata, rollback and freeze protection, emergency revocation, key rotation, compromised-root recovery, and multiple user-selectable catalogs. Add build provenance, SBOMs, and reproducibility evidence, and specify how personal/self-signed packages coexist with catalog packages without acquiring catalog authority.
Touches: d/007, d/013, d/041, d/064–d/067, and the claims that users own the trust chain and can fork or leave.
Expected payoff: It moves control of installation policy from Luke, Jan, and the orchestrator to an actual user-governed trust configuration while making compromise recovery possible.
4. Risks and hidden coupling in the proposed decisions
d/103-B: container-origin versus overloading backend-proxy
The design should use a distinct, versioned container-origin route type. Overloading backend-proxy hides a change in authority behind a reused schema name.
A prefix proxy mediating /api exposes a bounded request surface. A whole-origin proxy publishes active HTML and JavaScript, controls navigation, sets origin-scoped storage, may register service workers, handles cookies and redirects, opens WebSockets, processes uploads, and can exercise APIs available to a trustworthy localhost context. This moves the connector from a backend request boundary to an active-content publication boundary.
The VM boundary does not contain the browser-side consequences. A compromised container may remain unable to escape the VM while still serving hostile code into Prism, phishing the user, persisting through a service worker, abusing redirects, exhausting storage, or targeting shell mediation and agent automation.
Silent dependencies include:
- Route-table schema versioning and mixed-version peer behavior.
- Hostname allocation and app identity.
- Workerd path normalization, header rewriting, redirect handling, cache behavior, WebSocket upgrades, streaming, range requests, and upload limits.
- Cookie
Domain,SameSite, CSP, COOP/COEP, CORS, and frame policy. - Engine lifecycle, socket ownership, readiness, restart behavior, and per-app endpoint authorization.
- Controlled-Frame permissions and storage partitioning.
- Peer forwarding and whether dynamic responses can still be described as byte-identical or content-addressed.
- Service-worker and browser-cache invalidation during app updates.
- Offline notices, health pages, and whether platform-generated failures can be distinguished from application responses.
- Router privacy policy versus the diagnostics required for a live whole-origin proxy.
A distinct route kind is only the first step. It must have a separate policy implementation and validation path. If container-origin merely dispatches to the same permissive proxy with a different string tag, the distinction is cosmetic.
The wrong decision will be discovered late when a real application uses nontrivial redirects, OAuth callbacks, WebSockets, streaming, large uploads, range requests, service workers, or unusual cookie and CSP behavior. Security failures will appear later still, after path-encoding, redirect, header-smuggling, and hostile-container tests. By then the route schema may already be synchronized across devices and embedded in catalog records.
d/104: catalog-resolved Controlled-Frame guest launch
The proposal is not ratifiable as written. “App label → plane origin” makes a mutable display label a security principal and turns mutable catalog/runtime state into Prism’s navigation authority.
The existing baked URL is inflexible, but it binds guest selection to the installed IWA artifact. Runtime resolution moves that boundary into the catalog, installed-app database, router generator, sync layer, and resolver API. A compromise, rollback, collision, or inconsistent update in any of those components can redirect a privileged shell toward a different guest while retaining grants associated with the expected app.
The descriptor must use an immutable app_id and per-install instance_id, not a label. It must be versioned and bind at least:
- Catalog/publisher identity and artifact digest.
- Exact allowed scheme, host template, port policy, and route type.
- Expected frame and navigation policy.
- Allowed redirects, popups, downloads, and external-protocol launches.
- Required engine and wake behavior.
- Offline and source-unavailable behavior.
- Grant principal and audit identity.
- Descriptor expiry, revocation, rollback, and migration rules.
Prism must receive a resolved, authenticated launch capability—not an arbitrary URL—and grants must remain bound to the immutable app principal across hostname or port changes. Navigation after initial launch must also be constrained; validating only the first URL leaves redirects and guest-initiated navigation outside the boundary.
Silent dependencies include:
- Signed catalog schema and serial-anchor semantics.
- Unsigned personal-app installation policy.
- Catalog sync and stale-device behavior.
- Router-table atomicity: the descriptor and route must never disagree.
- IWA update caching and old Prism versions consuming new descriptors.
- App rename, uninstall, reinstall, catalog fork, and collision handling.
- d/081 generated wrappers, deep links, dock shortcuts, and protocol handlers.
- Permission caches, consent receipts, CDP targets, and journal identity.
- Android lifecycle behavior when the plane sleeps or the source peer disappears.
- Configurable ports, multiple Xe instances, and recovery after partial updates.
The wrong call will first appear as intermittent platform-specific launch failures, stale-cache behavior, or grants attached to the wrong guest after rename/reinstall. A malicious redirection or rollback would be found much later because the current success criterion—“Android renders the app in-frame”—does not test descriptor tampering, split-brain router state, downgraded catalogs, or navigation escape.
5. Questions missing from the Luke + Jan review agenda
-
What is the explicit threat model? Which attackers are in scope: hostile websites, malicious regular apps, compromised L3 apps, local unprivileged processes, hostile engine workloads, malicious peers, compromised catalogs, stolen devices, and compromised browser updates?
-
What is the trusted computing base for each security claim? Which exact components must be correct for origin isolation, engine containment, launch resolution, delegation, and audit integrity?
-
How is the entire
127.0.0.1:5454plane protected from hostile web pages and local processes? The wake channel is only one endpoint; Host-header validation, DNS rebinding, CSRF, browser Private Network Access, route enumeration, and unauthenticated management endpoints require a separate ruling. -
What is the blast radius of a compromised Prism or Xenon update? An L3 shell hosting Controlled Frames and driving CDP can bypass ordinary app boundaries; what authority remains unavailable to it?
-
How are prompt injection and confused-deputy attacks handled when an agent reads hostile app content and then acts in another app? Origin isolation does not protect an agent that intentionally consumes both origins.
-
Which actions are reversible, compensatable, or irreversible? What consent rule applies to purchases, messages, data disclosure, remote deletion, and actions whose consequences cannot be undone?
-
What is the canonical app principal? How do rename, collision, reinstall, publisher-key rotation, catalog forks, and cross-device installation affect the origin, storage, grants, and audit history?
-
Who may change a launch descriptor, and how is that change authenticated and made atomic with routing state? What happens during rollback, expiry, partial update, or disagreement between peers?
-
What is the catalog-governance model after the initial key ceremony? Scheduling a ceremony does not answer publisher delegation, user-selected roots, multiple catalogs, key compromise, emergency revocation, threshold recovery, or succession if Luke and Jan are unavailable.
-
How are device keys enrolled, rotated, recovered, and revoked? What happens when a phone is stolen, a disk is cloned, a removed peer returns with stale state, or two restored backups present the same device identity?
-
What is the actual L1 compatibility envelope? Which CSP, OAuth, WebAuthn, service-worker, cookie, WebSocket, TLS, backend, framing, and storage patterns are supported without source changes, and what representative application corpus gates the claim?
-
What is the browser-fork sustainability plan? Who tracks Chromium security releases, how quickly must Xenon rebase, what is the emergency patch SLA, and what happens if Controlled Frame or IWA behavior changes upstream?
-
What is the fallback if Controlled Frame remains unavailable on iOS or changes incompatibly on Chromium? The architecture currently treats a vendor-specific browser surface as a universal platform primitive.
-
What does the VM isolation class guarantee? What are the engine’s network-egress policy, resource quotas, filesystem mounts, socket credentials, host integration, patch policy, and response to a backend escape?
-
How does Xe recover from corrupt or malicious synchronized metadata? Serial anchors do not solve conflicting updates, tombstones, rollback from restored backups, journal divergence, poisoned route records, or lost encryption keys.
-
How are audit records protected from the audit system itself becoming a surveillance archive? The journal may contain prompts, correspondence, purchase details, credentials, and cross-app observations; encryption, retention, redaction, selective export, and access control are unspecified.
-
What are the atomic update and rollback rules across catalog, router, Prism, engine, and peer schemas? “Higher decision number wins” provides no operational answer for mixed versions or crashes between activation steps.
-
What happens when dependencies are unavailable for days rather than seconds? The design needs behavior for relay outage, catalog outage, home-device loss, expired metadata, exhausted disk, engine crash loops, port collision, and unavailable browser updates.
-
What evidence is required before a property is called proven? Functional rendering, byte equality, containment, authenticity, rollback resistance, offline recovery, and audit completeness need separate test claims and independently reproducible evidence.
-
What makes the project forkable beyond source availability? Licensing, protocol governance, conformance suites, trademark policy, catalog federation, reproducible builds, and migration away from the operator’s forge and keys are not defined.
-
What is the adoption path for application developers and ordinary users? The current path requires a local runtime, router, browser flags or an unbuilt browser fork, catalog metadata, and possibly an engine. The design does not state which existing app classes work immediately or why developers would target Xe-specific capabilities.
-
What operational metrics are permitted? Router privacy forbids per-request logging while debugging whole-origin proxying and agent actions requires evidence. The review must decide what can be observed, by whom, for how long, and with what user control.
SOLREV-MISSION-DONE
Sol design review — Transport & peer serving
Part of the site’s external review; reproduced verbatim below the header. sol’s positions are folded into the design frontier as External review input lines.
- Date: 2026-07-18
- Reviewer: gpt-5.6-sol (codex
model_reasoning_effort=xhigh; ran on lane-182 / 10.56.0.182; 104,702 tokens) - Scope: Part II
transport-identity.md+peer-serving.md; Part III reviews of both; the transport/peer spec-candidate block of Part Vemerging-specs.md(Peer Ticket v1, Fleet App-Index v1, peer-serve wire protocol v1, ALPN & signing-domain registry v1). Material inlined; reviewer had no repo access.
Orchestrator-facing summary (5 lines)
- Verdict on membership-gating: directionally right but overstated — group membership is an authenticated ACL, not a full capability system; first gated release should be membership AND a per-route, audience-bound signed grant, not membership replacing the ticket. “Reserve UCAN for non-members” is the wrong boundary; the real boundary is transitive delegation/attenuation. Ship gating on a new ALPN
xenon/serve/tcp/2, never mutate/1in place. - Verdict on the v1/v2 fork: ship v1 (
crates/net/group) as the first canonical serving-gate lineage — it is what join/gossip/revocation/gating already compile against; v2’s nicer data model has no evidence of a complete lifecycle (“a larger version number is not a readiness argument”). Front it with a lineage-independent authorization interface; require a signed one-way v1→v2 transition; never authorize from the union of both lineages. - Strongest criticism: the four spec candidates are not yet sufficient for an independent interoperable implementation — the Peer Ticket has no on-wire version/type discriminator, JSON + base64url parsing profiles are unspecified, and the single-byte ACK conflates ~7 distinct failure classes while the 1-byte “length” is not real extensibility.
- Second-order flags:
origin_id = sha256(json(node_addr))is encoding-unstable — derive identity from the endpoint public key; and single-key lifecycle coupling means one rotation changes transport, membership, and catalog identity at once (domain separation does not fix this). - Top-5 proposals: (1) ratify one membership authority + serving-policy profile; (2)
xenon/serve/tcp/2structured handshake; (3) replace session-wide bearer with sender-constrained grants; (4) publish normative schemas + parsing profiles + conformance vectors; (5) separate stable identity from routing and catalog freshness.
1. Protocol-design weaknesses in the spec candidates.
The candidates are useful as-built extractions, but they are not yet sufficient for independent interoperable implementations.
-
Peer Ticket v1 lacks an on-wire version. “v1” appears only in the document title; the JSON has no
schemaorversiondiscriminator and the token has no type prefix. Add a mandatory schema value, define whether future versions use a new prefix or schema, and specify whether unknown fields are ignored, preserved, or rejected. -
The JSON parsing profile is unspecified. All JSON formats need rules for UTF-8 validity, duplicate keys, trailing data, numbers, unknown fields, unknown enum values, and nesting/string/array limits. Duplicate keys should be rejected; otherwise different parsers can disagree about which value is authoritative. Base64url decoding also needs an exact alphabet, padding policy, whitespace policy, and decoded-size limit—not merely a file-size cap.
-
The ticket table contradicts itself.
index_sha256is marked optional but required by the consumer. That should be either mandatory in v1 or assigned to a named legacy profile with explicitly reduced behavior. The same issue affects optionalorigin_idanddevice_name: the spec does not say what validation is skipped when they are absent. -
node_addris not actually specified.{provider, address}does not define the types, bounds, ordering, or supported address variants insideaddress; it merely names an implementation-dependent iroh serialization. Pin an iroh-independent representation or a specific upstream schema version. Define behavior for unknown providers and unsupported transport-address types. -
origin_idis unstable and encoding-dependent. Hashingjson(node_addr)makes identity depend on JSON serialization, transport-hint ordering, and potentially changing direct or relay addresses. Stable identity should derive from the iroh endpoint public key alone; reachability hints should be separately replaceable metadata. If hashing JSON remains, a canonical JSON encoding and exact preimage must be normative. -
Ticket fields need stricter bounds and normalization. The exact secret-decoding result must be 32 bytes; accepting arbitrary decoded lengths is inconsistent with the credential type.
origin_id, provider strings, address collections, and relay URLs have no bounds.device_nameneeds a byte limit as well as a character limit, control-character policy, Unicode normalization decision, and exact comparison rule.origin_portneeds validity rules, including whether zero is invalid. -
The ticket has no lifecycle vocabulary. There is no ticket/grant ID, issuer, audience, scope, issuance time, not-before time, expiry, group identifier, authorization epoch, or replacement relationship. Consequently, replay is simply reusable presentation, rotation is global publisher restart, and revocation cannot identify one consumer, ticket, origin, or app. Key rotation is more disruptive still: because the same Ed25519 key is also the node address and apparent origin identity, rotation changes transport identity, membership identity, and catalog identity simultaneously.
-
Fleet App-Index v1 is only partially typed.
PublishedApp.version,sha256, names, URLs, timestamps, observations, and several durable-row fields have no complete type or bound. The referent of each app’ssha256is not stated. Maximum app count, unique-name requirements, total string sizes, and maximum peer count are absent. -
“Deterministic name order” is underspecified. Define the comparator over encoded bytes or Unicode scalar values, normalization, case sensitivity, and duplicate-name handling. The localhost-label grammar must be normative. Unknown
kindvalues currently have no evolution rule: rejecting them makes every new kind a schema break, while accepting them requires fallback semantics. -
Digest pinning does not replace canonicalization. Pinning exact bytes is sufficient to detect a byte mismatch, but publishers still need a deterministic serialization profile for cross-implementation generation and test vectors. Specify key ordering, whitespace, escaping, number encoding, final newline, and whether the digest covers compressed or uncompressed representation.
-
The index has no publication sequence or freshness model. “Immutable” appears to mean that each generated object is content-addressed, but there is no revision, previous digest, issuance time, expiry, or relation to a group head. A valid old index is therefore indistinguishable from the current index. Retaining last-good data is good availability behavior, but the catalog needs separate
cached,stale,withdrawn, andauthorization-invalidconcepts rather than treating all retained entries as merely offline. -
Disk state and public API are mixed into one spec.
FleetPeer,peers.json, the workerd mirror, and the merged HTTP view have different compatibility and migration requirements. They need separate schemas and versioning rules. The statedxe/peer-list/v1schema is also ambiguous about whether it labels a row or a top-level collection. -
Derived local identifiers need collision behavior. A connector based on 64 bits of a digest needs a deterministic collision-resolution rule, not just an assumption of uniqueness. URL construction needs escaping rules for
?xe-peer=<id>, and duplicatedorigin_id,device-name, and per-app fields need explicit equality checks against their enclosing index. -
Peer-serve v1 has no extensible handshake. ALPN provides a clean incompatible-version boundary, but the stream itself has no message type, feature bits, auth-method discriminator, request ID, or reserved extension area. The one-byte secret length is not meaningful extensibility: the publisher accepts lengths
1..=64, while the only defined credential is exactly 32 bytes. -
The QUIC connection/stream model is unclear. The spec does not state whether one QUIC connection carries one tunnel or many, whether every stream repeats authentication, or whether
MAX_TUNNELSapplies per process, publisher, peer, connection, or origin. Queueing behavior, refusal behavior at capacity, stream limits, idle timeouts, and connection reuse are absent. -
The ACK is too coarse and its timing is ambiguous.
REJECTconflates malformed preamble, unsupported auth method, invalid credential, non-member, revoked member, capacity exhaustion, and unavailable backend. It is also unclear whetherACCEPTmeans “credential accepted” or “backend connection established.” Define a small stable status frame, coarse peer-visible categories, detailed local diagnostics, QUIC application close codes, and retryability. -
Malformed-input and timeout behavior is missing. Specify handling for length zero, length over 64, early EOF, unknown ACK values, partial writes, preamble timeout scope, backend-connect timeout, and cancellation. Half-close propagation and whether one direction may continue after the other finishes also need definition.
-
Rotation and revocation semantics omit live connections. Publisher restart incidentally closes them, but future
exp, per-ticket revocation, membership changes, and secret rotation must state whether existing streams close immediately, receive a grace period, or remain authorized until disconnect. Race behavior at a group-head or epoch transition must be deterministic. -
The catalog’s literal HTTP probe is not sufficiently described. The spec needs accepted status codes, header and body limits,
Content-Lengthversus chunked versus EOF framing, redirect behavior, response timeout, connection-close behavior, and how the 1 MiB cap is enforced while streaming. Host syntax and normalization also belong in the protocol, not only in implementation citations. -
The ALPN registry records policy that can vary at runtime. “Membership if provider built with it” means the same ALPN can have materially different authorization semantics on two deployments. Authentication prerequisites should be fixed by the protocol profile; a mandatory-gated serving protocol should receive a new ALPN rather than silently changing
/1. -
The registry lacks normal registry metadata. Add exact octets, owner, stability status, reference, roles, framing, mandatory authorization profile, implementation versions, deprecation date, and replacement. Test-only identifiers should live in a clearly non-production namespace.
-
The signing-domain list is not a complete signing specification. Wildcards such as
xenon/interface-*and brace notation are unsuitable as allocations; enumerate every exact byte string. For each domain, define the signed structure, canonical encoding, concatenation or length-prefix rule, hash/prehash mode, Ed25519 verification profile, and test vectors. A trailing NUL is useful separation only when the exact construction is normative. -
Domain separation does not solve single-key lifecycle coupling. It prevents one signed message type from being accepted as another, but compromise, loss, and rotation still affect transport, membership, recovery statements, and every application signature at once. The registry needs a root/transport/signing-key lifecycle and algorithm-agility policy, even if v1 continues using one key.
2. The membership-blind-serving question.
The review’s verdict is directionally correct but stated too strongly. The existing membership plane should be reused before introducing a general delegation system, but group membership is an authenticated ACL, not “already a capability system” in the normal sense. It expresses coarse admission and revocation; it does not express an origin, app, port, action, attenuation chain, or per-consumer grant.
For the first gated release, I would use membership AND a route-specific grant, not membership as a replacement for the ticket:
- The authenticated QUIC remote key must be an active group member authorized for
consume. - The publisher must be an active member authorized for
publish. - A route grant should identify the publisher, consumer key, origin or service, validity interval, and grant ID. Initially this can be a simple publisher-signed structure; it does not need UCAN’s delegation machinery.
This distinction matters. Membership-only serving means every member who learns a publisher’s address may receive every exposed route unless another policy exists. Conversely, membership plus the current unbound bearer secret still leaves per-ticket replay and active-member sharing semantics unresolved. Group revocation fixes device removal, but it does not fully “collapse G1”: it cannot revoke one route while retaining the device, and a copied secret remains usable by another authorized member unless the grant is bound to the connection’s remote key.
“Reserve UCAN for a non-member” is also the wrong boundary. The relevant boundary is transitive delegation and attenuation, not membership status. A one-hop grant to a non-member does not require UCAN; a member who may delegate one app to another party might. Adopt UCAN only when Xe has a real requirement for proof chains, attenuation, or third-party delegation.
The integration is not a “small change.” Serving and group currently have separate endpoints and accept loops, so enforcement requires a defined source of membership truth, startup readiness, persistence, head freshness, active-stream registration, and behavior during partitions or chain-update failure. Both inbound and outbound checks need the same role semantics.
The v1/v2 fork must not remain implicit when gating lands. Otherwise:
- A v1 boolean member check may grant publish rights to a v2
Companion. - Endpoint-binding semantics may be enforced on one path and ignored on another.
- Revocation accepted by one lineage may not close connections authorized through the other.
- Persisted low-water marks and chain heads may describe different authority histories.
- Deployments may silently disagree about who is a member while sharing the same serving ALPN.
I would first ratify a lineage-independent authorization interface—group ID, canonical member key, member kind, publish/consume rights, status, head version/hash, and readiness state—then provide exactly one authoritative adapter. Ship mandatory gating in xenon/serve/tcp/2 with structured status messages. During migration, advertise /1 only under an explicit legacy policy, prefer /2, audit legacy use, and set a removal point; do not change /1’s contract in place.
3. The v1 (crates/net/group) vs v2 (crates/xe-sync) group-document fork.
I would apply these decision criteria:
- Lifecycle completeness: join, succession, gossip, revocation, active disconnect, persistence, and restart anti-rollback must operate against the same model.
- Normative clarity: canonical bytes, signing inputs, chain rules, group identity, member identity, role semantics, and test vectors.
- Serving-policy fit: explicit publish/consume permissions, companion behavior, and any necessary endpoint binding.
- Operational maturity: integration tests, restart behavior, corrupt-state handling, observability, and proven deployment paths.
- Migration safety: a signed transition from existing persisted chains, prevention of dual-authority acceptance, and low-water-mark continuity.
- Dependency ownership: one crate must own the canonical model without circular adapters or two independently evolving validators.
- Extensibility: additions should not require interpreting unknown statements as authorization; unknown authorization-affecting data should fail closed.
- Recovery compatibility: authority rotation and future recovery must preserve a single comprehensible chain of authority.
Given the material, these criteria point to v1 for the first shipped membership-gated serving path. V1 is the lineage used by join, gossip, revocation, provider gating, live-connection closure, canonical encoding, and persisted anti-rollback. V2 has attractive data-model additions, but the material supplies no equivalent evidence that its join, chain, gossip, persistence, and serving semantics are complete. A larger version number is not a readiness argument.
That should be a bounded decision, not permanent format entrenchment. Ship v1 through a stable authorization interface; do not admit a Companion concept until its rights are normative. Treat v2 as a proposed successor, and require a signed v1-to-v2 transition that binds the v2 genesis to a final v1 head, migrates low-water state, supplies cross-version test vectors, and establishes a point after which v1 documents are no longer accepted. Never authorize from the union of both lineages.
4. Prior art & standards alignment.
-
iroh: Using ALPN to route multiple protocols over an authenticated QUIC endpoint is directly aligned with iroh’s model. Iroh recommends a shared endpoint/router for multiple protocols, however, while Xe runs separate endpoints and accept loops under the same key; that divergence adds lifecycle and policy-consistency work without an articulated benefit. Current iroh guidance also treats the endpoint ID as stable and transport addresses as quickly changing hints, reinforcing that
origin_idshould derive from the endpoint key rather than serialized address metadata. Iroh endpoints -
iroh tickets: Combining dialing information with application data is an established iroh convention, and short-lived, manually transferred tickets fit the current publish session. Xe diverges by using unprefixed base64url JSON instead of iroh’s usual typed prefix plus compact encoding. Human-readable JSON is defensible, but omitting a type/version prefix is not; iroh also recommends endpoint IDs plus discovery rather than stale full tickets when an application already has coordination or gossip. Iroh ticket guidance
-
TLS/ALPN: A distinct, versioned ALPN per incompatible protocol is correct. ALPN selects a protocol; it does not define authorization, and RFC 7301 requires precise, non-empty protocol-identification octets and treats the selected protocol as definitive for that connection. The IANA ALPN registry uses Expert Review and requires an interoperable reference. Keeping unregistered names during a closed experiment is understandable, but ratified externally implemented Xe protocols should seek registration and publish exact octets. RFC 7301, IANA ALPN registry
-
OAuth bearer practice: The 32-byte secret has conventional bearer semantics: possession alone authorizes use, exactly the property formalized by RFC 6750. Strong file custody and encrypted transport align with bearer-token practice; missing expiry, scope, audience, token ID, standardized rejection categories, and refresh/rotation semantics do not. OAuth’s DPoP work demonstrates the established alternative of sender-constraining a token to a public key. Xe can obtain much of that benefit more simply by comparing a grant’s audience key to the already-authenticated iroh remote identity. RFC 6750, RFC 9449
-
UCAN: UCAN is a plausible fit for public-key-verifiable delegation, attenuation, time bounds, and offline proof chains. The draft’s “signed JWT-like token” description should not be ratified without pinning a UCAN generation: the current specification is divided into delegation, invocation, promise, and revocation sub-specifications, with current containers serialized as signed DAG-CBOR rather than simply an ad hoc JWT. Audience alone is not a complete proof-of-possession story; UCAN invocation signs the requested use, while Xe could alternatively bind a delegation to the authenticated QUIC peer. UCAN’s CID revocation also still requires monotonic distribution and local checking, so it does not make the revocation channel disappear. UCAN specification, UCAN revocation
-
Macaroons and Biscuit: Macaroons offer contextual caveats and attenuation but are fundamentally service-rooted and commonly bearer-oriented; their symmetric-root model is a poor match for Xe’s existing public device identities. Biscuit is closer—public-key verification, offline attenuation, structured policy, and revocation identifiers—but remains a bearer token unless the policy binds it to a proven peer, and revocation still needs external state. Both are excessive for “active fleet member may connect,” though Biscuit becomes relevant if Xe needs a rich policy language rather than a few fixed permissions. Macaroons paper, Biscuit specification
-
Willow/Meadowcap: Meadowcap is useful prior art for rigor: it explicitly defines receiver, namespace, access mode, granted area, recursive delegation, restrictions, canonical encodings, and concrete parameter bounds. Its actual resource model is Willow data—read/write access over namespaces, paths, subspaces, and timestamps—not a live TCP origin, so adopting it directly would be unjustified. Xe should copy its semantic discipline, not its data model. Meadowcap specification
-
did:key: Ed25519 keys can be represented as
did:keywithout network resolution, but it is not literally free: Xe must implement and test multicodec, multibase, canonical key validation, and DID comparison. More importantly,did:keydoes not support update or deactivation and its own specification discourages unqualified long-term use. It is justified when interoperating with UCAN or DID tooling; otherwise the raw iroh endpoint key is simpler and has fewer representations. did:key v0.9 -
Capability-system terminology: Xe’s group document is a strong signed membership ACL with prompt revocation. Calling it a capability system obscures the distinction between ambient group authority and an attenuable, transferable grant to a particular resource. Reusing membership for fleet admission is justified; claiming that it subsumes route capabilities is not.
-
One key across planes: Reusing one iroh identity across ALPN-routed protocols is consistent with iroh’s endpoint model, and signing-domain separation is good practice. Reusing the same long-lived signing authority for transport identity, group authority, recovery, and application statements creates correlated identity and coupled rotation. A better long-term design is a stable device root that certifies purpose-specific transport and signing keys, with explicit migration; retaining one key in v1 is defensible only if that lifecycle limitation is documented.
5. Top-5 concrete improvement proposals, ranked.
-
Ratify one membership authority and serving-policy profile. Ship v1 as the initial canonical engine, define group ID, head readiness, member-key identity, publish/consume rights, and fail behavior, and explicitly exclude unresolved companion semantics. Specify a signed, one-way migration before v2 can replace it.
-
Introduce
xenon/serve/tcp/2with a structured handshake. Authenticate the QUIC remote key, require canonical membership, carry a typed route grant, and return bounded status codes distinguishing retryable capacity/backend conditions from non-retryable authorization failures. Define connection reuse, per-stream authentication, limits, timeouts, half-close behavior, and live revocation. -
Replace session-wide bearer authority with sender-constrained grants. Give each grant a random ID, publisher key, audience endpoint key, group ID, origin/app scope,
iat, optionalnbf,exp, and authorization epoch; compare the audience to the authenticated iroh peer. Use a simple signed grant first and adopt UCAN only when transitive delegation or attenuation is an approved requirement. -
Publish normative schemas, parsing profiles, and conformance vectors. Define every type, byte bound, ordering rule, Unicode rule, duplicate-key rule, unknown-field rule, canonical encoding, digest preimage, and error category for tickets, indexes, durable state, and signing inputs. Provide positive, negative, cross-version, and canonical-byte test vectors.
-
Separate stable identity from routing and catalog freshness. Derive
origin_idfrom the endpoint public key, keep transport addresses replaceable, and add index revision, previous digest, issuance time, withdrawal/tombstone, and explicit stale status. Define how device-key rotation preserves catalog continuity through a signed old-key-to-new-key transition.
6. Questions missing from the session agenda.
-
What is the exact authorization matrix for
publish,consume, index fetch, app launch, and raw tunnel creation? Is membership fleet-wide authority or merely eligibility for a separate route grant? -
Will mandatory gating change
/1in place, or will Xe introduce a new ALPN with dual-advertisement and retirement rules? What prevents a mixed-version fleet from silently falling back to the membership-blind path? -
What membership state is sufficiently fresh to authorize serving? What happens at startup, during a partition, after low-water state is present but no current head is available, or when group persistence is damaged?
-
How are route grants or non-secret discovery records delivered, acknowledged, audited, refreshed, and withdrawn? G5’s ticket-distribution channel is a protocol and product question even if bearer secrets disappear.
-
What is the stable group identifier, and can the same device key participate in multiple groups? If so, which group and policy context does a serving connection select?
-
Is a served authority one configured TCP port, one origin, one app, or an arbitrary byte tunnel to the publisher’s local plane? Where are Host normalization, allowed targets, app scope, and backend readiness enforced?
-
What are the normative clock assumptions for
exp, membership challenges, and catalog freshness? What skew is allowed, and what is the behavior when a device clock is unreliable? -
How do grant expiry, group revocation, authority rotation, and publisher restart affect existing QUIC connections and streams? Are closure semantics immediate, graceful, or epoch-based?
-
How are device-key loss and rotation handled across iroh addressing, group membership, endpoint bindings, origin IDs, cached indexes, audit identity, and outstanding grants? The idle recovery vocabulary does not answer this operationally.
-
Should the two current endpoints be consolidated behind one iroh router so every ALPN shares one membership snapshot, relay policy, shutdown lifecycle, and connection identity? If they remain separate, what invariant keeps their policies synchronized?
-
What are the public error taxonomy and retry contract? Which failures are visible to the peer, which appear only in local audit logs, and how do operators distinguish authorization, stale membership, capacity, relay, and backend failures?
-
How are catalog updates, app removals, device removal, stale last-good indexes, and intentional peer forgetting represented? “Never prune” is an availability policy, not a complete deletion or fleet-exit lifecycle.
-
What interoperability scope is intended? If third-party implementations are expected, when will Xe freeze exact encodings, register stable ALPN identifiers, publish test vectors, and replace the historical
xenon/namespace if necessary?
SOLREV-TRANSPORT-DONE
Sol design review — Serving plane & router
Part of the site’s external review; reproduced verbatim below the header. sol’s positions are folded into the design frontier as External review input lines.
- Date: 2026-07-18
- Reviewer: gpt-5.6-sol (via
codex exec,model_reasoning_effort=xhigh), run on lane-182 - Scope:
src/part2/serving-plane.md,src/part3/serving-plane.md, and the serving-plane marker block ofsrc/part5/emerging-specs.md(Plane Origin Convention v1, Route Table schema v1, honest-error contract v1). - Method: All in-scope material was inlined verbatim into a single self-contained prompt (no filesystem context given to the reviewer); the reviewer was briefed on the one-loopback-origin / pinned-workerd-router / three-frozen-route-types architecture and asked seven structured questions (confused-deputy robustness, https-vs-http, port custody, freeze+versioning+no-502, prior art, top-5 proposals, missing agenda questions), ending with a terminal sentinel line.
Orchestrator-facing summary (5 lines)
- Verdict: the isolation core is real but the review “overstates the strength” —
Hostand Fetch Metadata are request-routing and browser-CSRF signals, not plane authentication; sol demands a normative Browser Request Admission clause (exact authority, exact Origin on unsafe/upgrade, coherent Fetch Metadata, defined navigation exception) and an explicit PNA/LNA preflight policy (Access-Control-Request-Private-Networkgated on a grant). - Strongest single criticism: the cross-origin arm runs only when
Originis present and the Fetch-Metadata floor refuses only cross-site unsafe/websocket requests — so a cross-site safe request withoutOrigin(image, navigation) can reach a backend prefix, including a whole-originbackend-proxy; relatedly,/xe/api/index//catalogare served before the hostname/Origin/Fetch-Metadata rungs, exposing app inventory to anything that reaches the loopback socket, and sibling.localhostapps are same-site, which SameSite cookies andSec-Fetch-Sitedo not isolate. - On G3 (https-vs-http): choose plain HTTP for regular apps, strictly limited to
*.localhost:5454, with a separate cryptographic plane-identity handshake for Prism — the local CA is rejected as unreliable (non-enforcing clients gain authority beyond.localhost; root-key custody; ACME inapplicable to.localhost); migration is not a config toggle because scheme is part of the origin: staged per-app export/import, service-worker unregistration,Secure-cookie + HSTS audit, CA removal only after all legacy origins retire. - On G2 (port custody): option (a) is “not an adequate security disposition” — framing a squatter is an integrity failure, not DoS; best shape = boot service generates a per-boot secret, hands it to Prism-native and the router via inherited descriptors / credential-checked OS IPC (never routes.json/env/URL/browser storage); Prism challenges
/.well-known/xe-plane-identity/v1with a fresh nonce and verifies an HMAC binding authority + full IPv4/IPv6 listener set + boot id + workerd digest + route-table generation before creating any guest frame — combined with supervisor-held socket custody of both stacks (socket-activation style, retained across workerd restarts). - On the freeze / no-502 / top-5: “frozen but considering a 4th type” is not a workable evolution strategy — keep v0 at three types forever and put
container-origininxe/origin-routes/v1with an explicit rollout protocol (dual-read, generation digest, last-known-good, table-wide503 Plane upgrade required); the 503 collapse is defensible but a telemetry hazard — addXe-Plane-Error+Xe-Request-Id, reserve502for protocol-invalid upstream responses and504for timeouts (and fix the honest-error contract’s self-contradiction on the 400/405 rows). Top-5 ranked: (1) normative Browser Request Admission clause; (2) Prism verifies a boot-bound plane attestation; (3) supervisor reserves both listener sockets; (4) freeze v0 + explicit v1 rollout protocol; (5) machine-identifiable, provenance-aware errors.
- ROBUSTNESS OF A LOOPBACK PLANE AGAINST CONFUSED-DEPUTY TRAFFIC
The strict .localhost Host grammar is useful, but the review overstates the strength of the “isolation core.” DNS rebinding to 127.0.0.1 would retain the attacker’s public hostname in Host, so the current guard should reject it; a browser cannot set a different Host. However, a public page can request http://photos.localhost:5454 directly, while an untrusted local process can forge Host, Origin, and every Sec-Fetch-* header. Host and Fetch Metadata are request-routing and browser-CSRF signals, not plane authentication.
There are concrete gaps in the refusal ladder:
-
/xe/api/indexand/xe/api/catalogare served before the ordinary hostname, Origin, and Fetch Metadata checks. The Plane Origin Convention must specify their exact numeric authority, admission policy, CORS policy, and authentication. App inventory must not be available merely because the request reached a loopback socket. -
The cross-origin arm runs only when an
Originis present, while the later Fetch Metadata check refuses only cross-site unsafe methods and WebSockets. A cross-site image, navigation, or other safe request withoutOrigincan therefore reach a backend prefix, including a whole-originbackend-proxy. “GET is safe” is not an admission control, andidempotent_getis described only as a cold-start wait-set. -
Sibling
<app>.localhostorigins are cross-origin but may be same-site. SameSite cookies and aSec-Fetch-Site: same-sitevalue do not isolate one Xe app from another. The policy must treat every non-exact origin as untrusted for backend access, not merely requests classified ascross-site. -
The internal
x-xe-peer-request-originheader is accepted at the browser-facing listener and merely checked against the requested hostname. Any local process can satisfy that check. Browser ingress must always discard it; only an authenticated peer connector on a distinct internal channel may supply equivalent metadata. -
Regular apps receive no
frame-ancestorspolicy. A public page can frame an app even if it cannot read the response, enabling confused user interaction. A minimal CSP allowing only Prism’s exact origin, plus any explicitly declared embedders, is appropriate for regular apps and does not require IWA packaging.
The specs need a normative admission sequence:
- Confirm that the accepted socket is one of the launcher-owned loopback listeners.
- Parse exactly one HTTP/1.1
Hostor HTTP/2:authority; require the configured scheme, ASCII label, and exact:5454. Reject duplicate authorities, absolute-form target mismatches, userinfo, trailing dots, and other ports. If TLS remains, require matching SNI as well. IgnoreForwardedand allX-Forwarded-*headers. - Authenticate reserved service paths and remove all internal
x-xe-*headers from browser ingress. - Load and validate the target route before answering a preflight.
- Parse exactly one non-
nullOrigin and compare its normalized scheme, host, and port. Require exact Origin for unsafe methods and WebSocket handshakes; treat missing or contradictory Origin/Fetch Metadata as untrusted, apart from a narrowly specified top-level-navigation bootstrap. - Refuse cross-origin backend traffic, including safe subresources, unless a separate browser-session capability is defined. Allow only explicit GET/HEAD document navigation for direct opening; Prism iframe navigation should carry a Prism bootstrap capability.
- Apply grant-scoped CORS last, echoing the exact allowed Origin and emitting the corresponding
Varyfields.
Private Network Access/Local Network Access must be explicit. An OPTIONS request containing Access-Control-Request-Private-Network: true should fail unless a live grant separately authorizes private-network access. Only then may the response contain Access-Control-Allow-Private-Network: true, alongside exact Access-Control-Allow-Origin, methods, headers, credentials, and Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network. Declared static CORS alone should not grant private-network access.
Even this does not authenticate against an arbitrary local client, because that client can forge browser headers. If untrusted local processes are an integrity threat rather than merely a denial-of-service threat, unsafe backend requests need an unguessable browser/Prism session capability or application-level authentication. The specs must say this plainly.
- THE HTTPS-WITH-LOCAL-CA vs PLAIN-HTTP TENSION (decision d/101 / gap G3)
I would make plain HTTP the default for regular apps, strictly limited to *.localhost:5454, and use a separate cryptographic plane-identity handshake for Prism. TLS adds little confidentiality on a correctly bound loopback socket, while a locally trusted CA adds key custody, trust-store installation, rotation, removal, and platform-compatibility obligations.
RFC 6761 gives .localhost its special resolution behavior, and Secure Contexts treats loopback/localhost origins as potentially trustworthy. Consequently, conforming browsers can provide secure-context APIs and service workers to http://<app>.localhost. That treatment does not authenticate the process holding port 5454, so it does not solve G2.
The local CA is not a reliable substitute. Name constraints are useful on enforcing clients, but the material already identifies clients that do not enforce them. On such clients, trusting the issuing CA can create authority beyond .localhost; short-lived leaves reduce leaf-key exposure but do not cure compromise of the CA signing key. Public ACME cannot issue certificates for reserved .localhost names, while private ACME would automate issuance without removing the local-root trust problem.
Plain HTTP still needs browser release gates. Prism’s IWA/controlled-frame implementation must be tested for HTTP-localhost embedding, WebSockets, mixed-content handling, secure-context APIs, cookies, and service-worker registration. Although localhost is potentially trustworthy, controlled frames or individual clients may impose stricter scheme rules. Any future non-loopback exposure must use a different HTTPS origin convention rather than weakening the loopback-only premise.
The migration is not merely a configuration toggle because scheme is part of the origin:
-
Amend Plane Origin Convention v1 to fix regular apps at
http://<label>.localhost:5454; change generated catalogs, Prism navigation, tests, and the launcher default together. Keep Prism itself under its IWA security model. -
If the current HTTPS origins have not shipped with durable user data, make the breaking change now. If they have, provide a staged, per-app export/import flow while the old HTTPS origin still runs; IndexedDB, Cache Storage, service workers, cookies, and other origin state cannot be moved generically by the router.
-
Before each cutover, app-owned migration code should export supported state and unregister its HTTPS service worker. After stopping TLS and starting HTTP on 5454, the app imports that state under the new origin. Apps without a defined migration must disclose that the cutover resets browser-local state.
-
Audit for
Securecookies and HSTS. Secure cookies will not transfer automatically; if HSTS was ever emitted, the legacy HTTPS endpoint must clear it before the switch. -
Remove generated CA keys and trust anchors only after all legacy origins have been retired, including separate browser/NSS and OS trust stores.
- PORT CUSTODY (gap G2)
Option (a) is not an adequate security disposition. Accepting same-UID denial of service may be a reasonable v1 scope, but framing a squatter is an integrity failure, not merely denial of service. Binding both address families and improving xe doctor narrows failure modes without giving Prism evidence about the process it is framing.
A pinned serving leaf can work if HTTPS is retained and Prism’s native embedding layer can enforce an SPKI pin. Trusting the local CA normally is insufficient: it proves only possession of some acceptable certificate, not that this is the launcher-supervised instance. Pin rotation and recovery would also need a defined boot-service trust path.
mTLS is a poor default for ordinary framed apps. It complicates browser certificate selection and app access, and authenticating Prism as a client does not by itself resolve how ordinary browser navigation should work.
A handshake secret or signed instance attestation is the best scheme-independent option, but it must not live in routes.json, an environment variable, a URL, browser storage, or the ordinary per-user data root. The trusted boot service should generate a per-boot secret and hand it separately to the router and Prism’s native process through inherited descriptors, a sealed pipe, or credential-checked OS IPC. Prism—not guest JavaScript—should challenge /.well-known/xe-plane-identity/v1 with a fresh nonce and verify an HMAC or signature before creating any guest frame.
The attestation should bind at least:
- Protocol version and fresh Prism nonce.
- Boot/session identifier and expiry.
- Scheme, port, and the complete IPv4/IPv6 listener set.
- Requested app authority.
- Pinned workerd binary digest.
- Route-table generation or digest.
This must be combined with socket custody. The boot service should acquire both 127.0.0.1:5454 and [::1]:5454, fail the plane if either cannot be acquired, and pass the open descriptors to workerd. It should retain custody across workerd restarts, preferably through systemd/launchd-style socket activation. That closes the verification-to-framing race and the IPv4/IPv6 split-bind hazard.
If “same UID” includes unrestricted filesystem inspection or process memory access, no user-space shared secret is a complete boundary. The threat model must distinguish “can bind/connect to loopback” from “can inspect or instrument Prism and the launcher”; the latter requires OS process isolation or a more privileged service.
- ROUTE-TABLE FREEZE + VERSIONING + THE NO-502 ERROR CONTRACT
Failing closed on an unknown route type is correct. Calling a three-value discriminator permanently frozen while simultaneously considering container-origin, however, is not a workable evolution strategy. The shipped empty-digest plus prefix:"/" overload is already evidence that the freeze is pushing new semantics into old shapes.
The clean rule is: xe/origin-routes/v0 remains permanently limited to its three existing types. A fourth type requires xe/origin-routes/v1, not an “additive” change to v0. The candidate itself is confusingly named “Route Table schema v1” while documenting a v0 wire identifier; that should be resolved before ratification.
The migration protocol should be explicit:
- Deploy a reader that accepts both v0 and v1.
- Have the supervisor advertise the reader versions/features it supports.
- Only then allow the generator to atomically publish v1 with a generation number and digest.
- Retain the ability to regenerate v0 during a rollback window.
- Reject an unsupported schema table-wide with
503 Plane upgrade required, rather than returning a misleading per-route 404.
Because this table is generated, regeneration from canonical state is preferable to an in-place mutation journal. Grants and peer-route state still need their own compatibility and atomic-write rules. The supervisor should validate a complete candidate before swapping it in and retain a last-known-good table rather than letting per-request reads observe a partial or incompatible generation.
The absence of 502 can be defensible, but the current contract makes it a telemetry hazard. 503 is reasonable for an intentionally unavailable peer, a cold backend, a missing connector, or an unavailable plane dependency. 502 should mean that the gateway reached an upstream path but received no valid HTTP response or a protocol-invalid response; a timeout is more precisely 504. A genuine, valid upstream 500 should continue to pass through unchanged, not be relabeled 502.
Whatever status policy is selected, add stable machine-readable headers such as Xe-Plane-Error: peer_offline|backend_unreachable|table_missing|upstream_protocol_error and Xe-Request-Id, plus structured local logs identifying whether the status was synthesized or passed through. Otherwise automation must parse mutable English bodies to distinguish several unrelated 503 conditions. All generated errors should be Cache-Control: no-store, not only the peer and index cases.
The honest-error candidate also contradicts itself: it says every body names a next action and ends with “then retry,” while its 400 and 405 rows show only 400 Bad Request and 405 Method Not Allowed. Either provide actionable copy for every row or narrow the invariant.
- PRIOR ART & STANDARDS ALIGNMENT
-
Private Network Access / Local Network Access: The design is behind the relevant browser work because its preflight clause does not mention private-network request headers or permission. Its default refusal is directionally safe, but legitimate grants may fail in supporting browsers, while a future generic CORS change could accidentally over-authorize them. PNA/LNA is defense in depth and cannot replace server authorization or protect against non-browser local processes.
-
Fetch Metadata: Using
Sec-Fetch-Siteas a CSRF floor aligns with established resource-isolation policy. The implementation is incomplete because it permits cross-site safe requests to reach backends and does not define handling for missing headers, sibling same-site apps, document navigation, orSec-Fetch-Mode/Sec-Fetch-Dest. Fetch Metadata is deliberately forgeable by non-browser clients and must not be described as authentication. -
RFC 6761 and Secure Contexts: Using separate
*.localhosthosts is a sound way to obtain distinct browser origins without DNS configuration, and it makes classic public-hostname DNS rebinding fail the Host check. Secure-context treatment supports the plain-HTTP recommendation. Neither standard proves that the process holding port 5454 is Xe, and separate origins do not imply separate sites for SameSite-cookie purposes. -
Local development proxies: Tools serving
*.localhostor locally resolved*.testcommonly validate Host, terminate a wildcard or generated local certificate, and explicitly install a development CA. Xe is ahead of ordinary development tooling in its fail-closed route table and per-chunk grant revocation, but it is claiming a stronger production trust boundary. That requires stricter port identity, cookie isolation, frame policy, and request admission than a developer proxy normally supplies. -
Tailscale Serve/Funnel: Tailscale Serve is relevant as a daemon-owned front door that proxies local services behind a named, authenticated identity; Funnel additionally makes exposure an explicit operator action. Neither is a direct browser-loopback CSRF solution. The useful precedent is that listener custody, service identity, and exposure policy are owned by the fronting daemon rather than inferred from a port number.
-
Cloudflare Workers/workerd: A pinned runtime, declarative bindings, and a small fail-closed ingress worker fit the Workers/workerd model well. The Xe route discriminator and grants are Xe-specific policy, not protections supplied by workerd. Mature deployment systems also carry explicit configuration versions, compatibility declarations, atomic rollout, and rollback; the unnegotiated v0 JSON table is behind that practice.
-
Local TLS and CAs: A name-constrained local CA is better than an unconstrained development root on clients that enforce constraints. mkcert-style practice demonstrates that local trust can be made usable, but also exposes the costs of root-key custody and separate browser/OS trust stores. Public ACME is not applicable to
.localhost; a private ACME service could automate leaves but would not solve root trust, client enforcement, or plane-instance identity.
The strongest parts relative to prior art are the content-addressed static store, unknown-type fail-closed behavior, static-only grants, live revocation during streaming, and honest user-facing refusals. The weak parts are the missing browser admission matrix, absent plane attestation, and undeclared configuration-upgrade protocol.
-
TOP-5 CONCRETE IMPROVEMENT PROPOSALS, RANKED
-
Add a normative Browser Request Admission clause. Require exact Host/authority, exact Origin on unsafe and upgrade requests, coherent Fetch Metadata, and a narrowly defined top-level-navigation exception. Deny all cross-origin backend subresources—including GET/HEAD—and require an explicit Prism bootstrap capability for iframe navigation.
-
Make Prism verify a boot-bound plane attestation. Have the boot service pass a per-boot secret to Prism native code and the router through inherited protected handles. Define a nonce-based HMAC handshake binding the authority, listener set, boot ID, workerd digest, and route-table generation; Prism must not frame before it verifies successfully.
-
Reserve both listener sockets in the supervisor. Acquire IPv4 and IPv6 together, fail closed if either is unavailable, and pass the sockets into workerd so restarts never relinquish custody. Add PID diagnostics for failures, but do not treat diagnostics as authentication.
-
Freeze v0 and introduce an explicit v1 rollout protocol. Keep the three v0 types forever and put
container-origin, if adopted, inxe/origin-routes/v1. Add reader capability advertisement, atomic generation/digest fields, dual-read deployment order, last-known-good rollback, and a table-wide upgrade-required error. -
Make errors machine-identifiable and provenance-aware. Add
Xe-Plane-Error,Xe-Request-Id,Cache-Control: no-store, and structured logs to every synthesized error. Retain 503 for expected unavailability, add 502 only for invalid upstream responses and 504 for upstream timeouts, and pass valid upstream 5xx responses unchanged. -
QUESTIONS MISSING FROM THE SESSION AGENDA
- Is an untrusted same-UID process assumed only to bind/connect to sockets, or may it read files, inspect process memory, or instrument Prism?
- What is the complete admission matrix for public cross-site pages, sibling Xe apps, direct top-level navigation, Prism frames, missing Fetch Metadata, and WebSockets?
- Will PNA/LNA access require a new
private_networkfield in grants, and which exact preflight headers will be emitted? - How does Prism authenticate itself for initial iframe navigation, and what exact
frame-ancestorspolicy should regular apps receive? - Are
/xe/api/indexand/xe/api/catalogpublic to every local browser origin, Prism-only, or native-client-only? - Are ports 5198 and 5199 authenticated against arbitrary local processes, and on which channel may internal
x-xe-*headers be trusted? - Must backend
Set-Cookieresponses be forced to host-only cookies so one app cannot setDomain=localhostcookies affecting sibling apps? - How are backend
Location, CORS, CSP, cookie, hop-by-hop, and WebSocket headers rewritten or rejected at the proxy boundary? - Who may write route and grant files, and what are the required permissions, atomicity, integrity checks, and last-known-good behavior?
- What happens to service workers, caches, IndexedDB, and cookies when an app is uninstalled, upgraded, tombstoned, or explicitly reclaimed?
- Which browsers and controlled-frame versions are release-gated for
.localhost, IPv6, Secure Contexts, PNA/LNA, Fetch Metadata, and HTTP framing? - What request-size, header-size, timeout, streaming, and malformed-upstream limits apply to backend and peer proxies?
- How are opaque origins such as
Origin: null, sandboxed frames, requested CORS headers, and credentialed grants handled? - What per-request audit data is retained, for how long, and how are app names, device names, and paths protected from unnecessary disclosure?
SOLREV-SERVING-DONE
Sol design review — App model & engines
Part of the site’s external review; reproduced verbatim below the header. sol’s positions are folded into the design frontier as External review input lines.
- Date: 2026-07-18
- Reviewer: gpt-5.6-sol (via
codex exec,model_reasoning_effort=xhigh), run on lane-182 - Scope:
src/part2/app-model.md,src/part2/engines-backends.md,src/part3/app-model.md,src/part3/engines-backends.md, and the app-model-engines spec-candidate block ofsrc/part5/emerging-specs.md— the install spectrum L0–L3, two serving tiers, XAM v0.2, the microVM engine socket contract, the compose safe-subset, the two-gate install-consent model, and the library-complete-but-loopless update engine. - Method: All five in-scope pieces were inlined verbatim into a single self-contained prompt (the reviewer had no filesystem access). The reviewer was briefed to be direct and critical and to answer an eight-part structured ask with fixed headings, ending on a sentinel line.
Orchestrator-facing summary (5 lines)
- Verdict: XAM v0.2 is an as-built serialization, not an interoperable spec (a Rust validator is implementation authority, not specification authority); the compose denylist is “a useful audit, not a sufficient boundary” for unauthored input. The through-line fix across the review is one idea: treat Compose as an import language, not the deployment authority — compile a pinned dialect into a small allowlisted internal IR, execute only that, and inspect the created objects before start.
- Strongest single criticism: the chapter’s “no loopback TCP” claim is false as stated and hides a confused-deputy hole —
127.0.0.1:5199+ a syntactically-validatedx-xe-appheader does not authenticate the caller, so any local process can drive the connector and select an installed app; the browser’s grant rules do not protect direct loopback access. The defensible invariant is narrower: no app-owned backend port is published on the host, and no app or router receives the engine credential. (Plus: on macOS the engine is a rawtcp://127.0.0.1:2375Docker API — far more privileged than a backend port.) - Update-loop position: design it as an authorization + recovery protocol, not a CLI wrapper. Bind a canonical
UpdateAuthorization/v1object (from/to release digests,diff_digest+diff_ruleset_version, provenance-bundle digest, rollout + rollback policy, expiry, nonce) into the gate, recomputed and re-verified immediately before activation. TUF alignment is warranted: root → the eventual offline Luke/Jan root; targets → locked-release descriptors; delegated targets → publisher/namespace authority; snapshot + timestamp → anti-freeze/rollback/mix-and-match. Two distinct rollback designs (failure-triggered atomic soak; user-triggered with explicit data-loss disclosure);Autoneeds a signed policy object + a full non-escalation checklist. A schema/migration declaration is required or “volume-restore” is not a real rollback contract. - L1-envelope position: “install unmodified and un-Xe-aware” is too broad — re-origining onto
http://<app>.localhost:5454breaks OAuth redirect URIs, WebAuthn RP-ID,Domaincookies/SameSite, service-worker scope, CSPframe-ancestors,postMessage/server origin checks, and absolute URLs. Replace the universal promise with three versioned profiles —L1-Origin-Portable/L1-Standard-Configurable/L1-Incompatible— specified and tested by a conformance-fixture suite + a black-box install probe on the pinned Chromium + a versioned report. Also: stabilize the canonical origin (derive from package + instance identity, never a mutable display name) before users accumulate origin-scoped state. - Top-5 (ranked) + sharpest catches: (1) allowlisted Compose→IR + authenticate the connector; (2) split the
package.json/.xam.jsondocument family + publish normative validation & canonical digest encoding (RFC 8785 / deterministic CBOR); (3) ship the update loop aroundUpdateAuthorization+ TUF; (4) L1 origin-portability profiles + conformance program; (5) make resource/isolation declarations executable engine policy. Two catches worth surfacing: fixed non-latesttags are not pins (must resolve to an OCI manifest digest before gate-2), and thesh+ncexec bridge is not a general unmodified-container contract (distroless/minimal images ship neither) — so “unmodified backend” is currently untrue for a real class of images.
1. XAM v0.2 as a spec.
XAM v0.2 is an as-built serialization format, not yet an interoperable specification. Its Rust validator defines one implementation’s behavior, but an independent producer cannot implement XAM reliably from the candidate text alone.
The “runtime validation is authoritative; JSON Schema is advisory” stance confuses implementation authority with specification authority. JSON Schema need not express every cross-field invariant, but those invariants still need normative prose, canonical algorithms, and conformance vectors. Otherwise:
- Different launcher versions can interpret the same
"0.2"document differently. - Publishers cannot validate without running the matching launcher binary.
- Consent subjects may differ if two implementations serialize the same plan differently.
- A generated schema can silently drift with Rust types while remaining nominally version 0.2.
The specification should consist of a structural schema plus normative semantic validation. At minimum it needs to define:
- Unknown-field behavior at every object level.
- Whether empty
componentsis valid. - Component-ID syntax and uniqueness.
- Dependency resolution, cycle detection, and optional-component semantics.
- Endpoint ownership, route normalization, overlap resolution, and valid port ranges.
- Units and defaults for resource fields.
- Per-
source.kindsyntax forlocatorandpinned. - Canonical URL normalization for
UnsignedDerived, including Unicode, case, trailing slashes, default ports, redirects, and repository aliases. - Publisher-key lookup, rotation, revocation, and package-namespace continuity.
- The exact capability vocabulary. Arbitrary
{resource, ability}strings are not a stable permission model. - The semantics of
reason: presentation-only, or part of authorization? A publisher must not be able to preserve authority while changing its explanation invisibly. - Canonical encoding for
sha256(plan), locked-release digests, and every consent-bound object. Use a specified deterministic encoding such as RFC 8785 JSON or deterministic CBOR, with domain separation for each digest class.
There is also a direct vocabulary inconsistency: SourceKind is listed as Compose | Git | Image | Web | Static | Swbn, while the prose repeatedly calls Oci an unimplemented source kind. In the model shown, Oci is a runtime driver, not a source kind. A specification cannot leave that distinction ambiguous.
The moving-target use of "0.2" is untenable once manifests leave the repository. If behavior changes while the version remains "0.2", the version field provides no compatibility information. Either designate every current document explicitly experimental and non-portable, or publish immutable revisions such as 0.2.0, 0.2.1, and so forth. Once third-party publication begins, use conventional compatibility rules:
- A major version changes interpretation or removes fields.
- A minor version adds optional behavior that older readers can safely reject or ignore.
- A required-feature list handles operational additions that cannot be silently ignored.
- Patch revisions clarify validation without changing accepted meaning.
- The launcher records which validator/profile revision accepted an installed release.
Extensions should be divided into two classes. Namespaced annotation extensions may be ignored. Operational extensions must name a required feature and must cause rejection when unsupported. Silently accepting an operational field is unsafe.
That applies particularly to handlers, interfaces, and notifications. “Parsed and stored, not acted on” creates false affordance: a publisher may believe a handler or notification obligation is active when it is not. Current runtimes should reject non-empty reserved families unless the manifest declares a supported feature version. Merely preserving them for a future runtime is appropriate only for inert annotations.
The format family should be separated as follows:
| Purpose | Recommended name | Required discriminator |
|---|---|---|
| Ordinary Node package metadata | package.json | npm semantics only |
| Xe compose authoring input | app.xe-recipe.yaml | document_kind: xe.compose-recipe, recipe_version |
| Docker Compose input | compose.yaml | Compose dialect/profile |
| Portable XAM app descriptor | app.xam.json | document_kind: xe.app-manifest, xam_version |
| Fully resolved install/update object | app.xam.lock.json | document_kind: xe.locked-release, independent lock-format version |
.xam.json must mean exactly one thing: a XAM document. A byte-for-byte Compose file named guestbook.xam.json should be renamed immediately. It is not harmless example residue; it trains tools and authors to trust an extension that currently has no reliable meaning.
Detection must use explicit discriminators, not key heuristics. Xe should never inspect an arbitrary package.json and guess whether it is npm, XAM, or a recipe. An Xe recipe can explicitly point to an npm manifest as a build input. Discovery should reject ambiguous directories rather than choose a precedence order silently.
The layering should also be explicit:
- A recipe expresses author intent and may reference mutable or unresolved inputs.
- The adapter resolves and compiles it into a normalized XAM release description.
- Materialization produces a locked release containing exact artifact, image, source, and provenance digests.
- Installation state and consent records refer to the locked release but are not part of the manifest.
Each layer needs its own version. schema_version: 1 for a recipe and xam_version: 0.2 are unrelated compatibility promises and should remain visibly unrelated.
Finally, source kinds and runtime drivers should be profiled against actual launcher capabilities. A core vocabulary may reserve future kinds, but an installable profile should list only implemented combinations. A manifest that is syntactically valid yet has no adapter is not usefully “valid”; it is valid against the vocabulary but unsupported by the selected execution profile. Those outcomes need distinct errors.
2. The compose refusal list.
The refusal list is a useful audit of known dangerous Compose features, but it is not a sufficient security boundary for unauthored input. Compose is a large, evolving language whose default assumption is that the author controls the host. Xe’s assumption is the opposite. An allow-by-default parser followed by a denylist inherits every new Compose feature as implicitly permitted until Xe notices it.
That creates several classes of exposure beyond the three P-174 holes.
First, there are alternate spellings and alternate mechanisms for the same authority:
gpus:anddeploy.resources.reservations.devicesmay grant accelerators without using the refuseddevices:spelling.device_cgroup_rules, runtime selection, or implementation-specific extensions may expose devices or broader runtimes.volumes_fromcan inherit mounts from another container.use_api_socket, if supported by the selected Compose implementation, is effectively a first-class engine-socket grant and must be refused.uts: host, cgroup modes,cgroup_parent, Windowscredential_spec, and other namespace or host-integration controls are not covered by the stated list.- Restricting only “unconfined”
security_optis too narrow; other security options can disable or replace the intended profile.
Second, Compose has indirection mechanisms that make source-level denial incomplete:
extends,include, fragments, YAML merge keys, and anchors can introduce fields from elsewhere.configsandsecretscan read files, external objects, or environment-derived values.build.additional_contexts, Dockerfile-specific contexts,COPY --from, remoteADD, cache imports/exports, and symlink traversal can escape the apparent build-input model.- Compose’s implicit
.envprocessing and inherited process environment can bypass a validator that only examines explicitenv_file. - Profiles can hide services from one validation path and enable them in another.
- New lifecycle or development/watch fields may introduce host synchronization or commands that did not exist when the validator was written.
Third, the parser itself is part of the boundary. It needs explicit rules for duplicate YAML keys, merge precedence, multiple documents, aliases, tag handling, scalar coercion, alias-expansion limits, document size, recursion depth, and UTF-8. Validation must operate on the same normalized semantic object that execution will use. Validating one parse and asking Docker Compose to perform a second parse creates parser-differential risk.
Fourth, image “pinning” is currently misstated. A fixed non-latest tag is not pinned; tags are mutable. An accepted image must be resolved to an OCI manifest digest before the locked release and gate-2 consent are created. Platform selection must also be locked, including the selected platform manifest and config/layer digests. Builds need the same treatment for base images and external stages.
Fifth, build execution is substantially more dangerous than the refusal list acknowledges. A Dockerfile is arbitrary code with potential network access. Plan Review may display network hosts, but the material does not establish that BuildKit is technically restricted to those hosts. A secure profile needs a disposable builder, a scrubbed environment, no inherited registry or SSH credentials, controlled DNS and egress, bounded resources, and exact input-root enforcement that survives symlinks and archive extraction.
Sixth, Compose naming can cross application boundaries even without “external” resources. Explicit container_name, volume name, network name, project-name overrides, labels, and implementation-specific resource lookup can cause collision or attachment to another app’s objects. Xe should generate engine names from an internal installation ID and should not accept user-selected global names.
Seventh, runtime egress is unspecified. The refusal list protects the host boundary more than it protects user data. A backend appears able to contact arbitrary internet endpoints unless another layer constrains it. That may be an intentional L1 default, but it must be explicit and visible in the install plan. Build-time network and runtime network are different grants.
Eighth, resources are not enforced. An unprivileged container can still exhaust memory, PIDs, CPU, disk, log storage, or connection slots. MicroVM containment prevents some host compromise, but not denial of service to other apps sharing that engine.
A positive allowlist is preferable. More precisely, Compose should be an import language, not the deployment authority. Xe should compile a pinned Compose dialect into a small internal deployment IR and execute only that IR. A first profile could allow:
- Services with either digest-resolved images or builds performed in the restricted builder.
- Literal or declared-and-typed environment values.
- Xe-scoped named volumes with generated engine names.
expose, notports.- Restricted
depends_onand health checks. - Explicit commands and entrypoints.
- Generated per-app networks.
read_only, controlledtmpfs, andcap_drop, preferably includingALLby default.- Enforced CPU, memory, PID, disk, and restart limits.
- A narrow set of labels owned by Xe.
Every other field, including unknown fields, should be rejected recursively. The accepted profile should name both its Compose-language revision and tested engine implementation. Xe should execute a generated Compose document or use the engine API directly; it should never pass the original unauthored document through unchanged.
Admission should also include a post-creation check before start: inspect the created containers, mounts, namespace modes, networks, devices, security options, and resource limits and compare them with the authorized IR. That protects against translation bugs and engine defaults.
There is a separate connector issue that becomes more important with third-party apps. 127.0.0.1:5199 plus a syntactically validated x-xe-app header does not authenticate the route worker. Any local process may be able to invoke the connector as a confused deputy and select an installed app. The browser’s grant rules do not protect direct loopback access. Use a Unix socket with peer credentials where available, a Windows named pipe with an ACL, or a per-boot authenticated channel. Likewise, the macOS tcp://127.0.0.1:2375 Docker endpoint needs an explicit local-adversary analysis; a raw Docker API is far more privileged than a backend port.
The phrase “no loopback TCP” should therefore be corrected. The implementation has a loopback connector, an ephemeral loopback bridge, and on macOS a loopback Docker API. The defensible invariant is narrower: no app-owned backend port is published on the host, and no app or router receives the engine credential.
P-174 should be expanded into a versioned safe-profile project, with parser-differential tests, adversarial fixtures, fuzzing, generated-IR inspection, and connector authentication. Closing only its three currently recorded holes would not make unauthored Compose safe.
3. The update engine.
The missing loop should be treated as an authorization and recovery protocol, not merely a CLI wrapper around apply_update.
A suitable user-facing state machine is:
Available → Reviewed/PolicyAuthorized → Downloaded → Staged → ActivationPending → HealthVerifying → Committed
with transitions to Rejected, Paused, Failed, and RolledBack. Discovery and download must not imply activation authority.
The surface should provide at least:
xe app updates— list candidates, channels, provenance status, and escalation class.xe app update <app> --show— display the normalized diff without changing state.xe app update <app>— review, stage, activate, and verify.xe app rollback <app> [--to <release>]— user-initiated rollback.xe app history <app>— installed releases, snapshots, decisions, health outcomes, and provenance.
The update diff shown to the user should include:
- Current and candidate versions and release digests.
- Publisher identity and any signer, delegation, or source change.
- Source commit, recipe digest, OCI image/platform digests, and build provenance.
- Added, removed, and changed components.
- Capability changes, including changed reasons.
- Endpoint, ingress, runtime-egress, and discovery changes.
- Configuration and secret-schema changes.
- Resource and placement changes.
- New build effects and build-network access.
- Data-format changes, migration hooks, and rollback compatibility.
- Changes to update channel or minimum launcher/profile version.
- Whether the candidate is reproducibly built or only publisher-attested.
The current rule that an escalation requires fresh gate-2 consent is correct but insufficiently bound. A candidate locked-release digest does not by itself bind the installed predecessor, the displayed diff, or the applicable rollout policy. The update gate should authorize a canonical object resembling:
UpdateAuthorization/v1
action
package_id
instance_id
from_release_digest
to_release_digest
locked_manifest_digest
artifact_set_digest
plan_digest, if materialization is required
diff_ruleset_version
diff_digest
provenance_bundle_digest
trusted_metadata_versions
snapshot_and_rollback_policy
activation_policy
expiry
nonce
Its subject should be domain-separated and hashed using the manifest’s normative canonical encoding. The consent UI must render from this exact object. The runtime must recompute the diff from the installed and candidate releases and verify the object immediately before activation. This prevents a reviewed diff from being reused for another predecessor or another interpretation of “escalation.”
Gate 1 remains necessary when an update performs a build or fetch with changed effects. Gate 2 authorizes the resulting locked release and its permission/provenance transition. A patch update is not inherently safe merely because SemVer calls it a patch.
For Auto, the stored policy itself should be a signed or locally authenticated authorization object. Auto-activation should require all of the following:
- Publisher/delegation continuity.
- Verified update metadata and freshness policy.
- No capability, endpoint, secret, resource, placement, or build-effect escalation.
- No new component class or device request.
- No irreversible data migration.
- An available rollback snapshot.
- Passing staged health checks.
- A channel and rollout policy previously selected by the user.
Every automatic decision should produce the same audit record as a human decision, naming the policy that authorized it.
Rollback needs two distinct designs:
-
Failure-triggered rollback. Keep the old artifacts and routes available, take a pre-activation snapshot, activate atomically, run declared and generic health probes, and commit only after a soak interval. A failed candidate restores the previous release and its state.
-
User-triggered rollback. Retain a bounded history of releases and snapshots. A user may select a previously consented digest, but restoring data is potentially destructive because it discards writes made after the update. The gate must show the snapshot timestamp, affected volumes/browser storage, and expected data loss. Offer export-before-restore. “Code-only rollback” should be allowed only when the app declares backward-compatible data formats; otherwise restore code and state together.
An app migration can make rollback impossible even when volume restoration is mechanically correct. XAM or the locked-release format therefore needs a data-schema version and migration declaration, including whether rollback is supported, whether migration runs before or after route switch, and which state classes it touches. Browser storage, secrets, generated configuration, and external state must be accounted for; volume-only snapshots are not a complete rollback contract.
“Staged rollout” needs a defined cohort axis. On one machine it may mean download now, activate later, canary one instance, and soak before activating other instances. Across multiple Xe peers it can mean deterministic device cohorts. The rollout controller should support pause, resume, explicit promotion, and emergency withdrawal. Escalating releases must never be introduced to a cohort without each affected user’s consent. If Xe does not currently have a fleet concept, percentage rollout should not be implied; staging plus one-instance canary is enough.
TUF alignment is warranted. Xe already has signed catalogs and key rotation, but TUF contributes the missing defenses against rollback, freeze, stale metadata, and mix-and-match attacks:
- Root metadata: offline threshold keys, role assignments, key rotation, and recovery. This maps naturally to the eventual Luke/Jan offline root, not the current online catalog-signing key.
- Targets metadata: hashes and lengths of locked-release descriptors. Signed custom metadata can include package ID, version, channel, XAM/profile version, required launcher version, artifact descriptors, and rollout constraints.
- Delegated targets: publisher or package-namespace authority. Delegations should constrain which package IDs or target paths a publisher may sign.
- Snapshot metadata: a consistent versioned view of targets and delegated metadata, preventing the client from receiving an inconsistent catalog.
- Timestamp metadata: short-lived evidence of the latest snapshot, preventing freeze attacks.
The target should preferably be a small locked-release envelope that references OCI artifacts by digest, rather than listing every layer directly in TUF metadata. The launcher must persist trusted root and metadata versions to reject rollback.
TUF does not replace publisher provenance, reproducible builds, user consent, or state rollback. It proves that repository metadata is authorized and sufficiently fresh. In-toto attestations, SBOMs, builder identities, and reproducibility results belong in a provenance bundle referenced by the locked release. The update gate should display their verification status but should not reduce it to a single undifferentiated “signed” badge.
Offline sovereignty needs an explicit TUF expiry policy. A user should be able to keep running installed apps indefinitely. Installing metadata that is expired should normally fail, with an auditable expert override if Xe chooses to support one. Root recovery, clock failure, and repository unavailability must not brick existing installations.
4. Install spectrum coherence — the L1 compatibility envelope.
The unconditional statement “apps install unmodified and un-Xe-aware” is too broad. The honest promise is:
An app can be L1 without Xe-specific code if its unchanged distributable satisfies a versioned origin-portability and framing profile.
Re-origining is behaviorally significant even when bytes remain unchanged.
- OAuth: Redirect URIs and JavaScript origins are commonly registered exactly. A provider may reject
http://<app>.localhost:5454, may permit loopback only for native-client patterns, or may require HTTPS. Existing registrations do not follow the app to its Xe origin. - WebAuthn: Credentials are bound to an RP ID related to the effective domain. Credentials created at the original domain do not migrate. Apps that hard-code or server-select their original RP ID will fail. Exact
.localhostbehavior should be tested against Xe’s pinned Chromium build rather than assumed. - Cookies:
Domainattributes tied to the original site fail.Secure,SameSite, public-suffix handling, iframe context, and storage partitioning interact with.localhostand controlled frames in browser-specific ways. Host-only relative cookies are the portable case. - Service workers: Registration, cache storage, and scope are origin-bound. Existing offline state does not transfer. Absolute scopes or imports tied to the original origin fail.
- CSP and framing:
frame-ancestors,X-Frame-Options, fixedconnect-src, fixedform-action, and fixed navigation targets can prevent Prism embedding or API access. Controlled Frame behavior must be tested in the actual browser; it should not be presumed to bypass ordinary application policy. - Storage: localStorage, IndexedDB, Cache Storage, and origin-scoped credentials start empty at the new origin. Re-origining is not user-data migration.
postMessage: Exact origin checks against a production domain fail, as do parent-origin assumptions.- Absolute URLs: Canonical redirects, API URLs, WebSocket URLs, asset roots, callback URLs, and server-generated links can escape the plane or point back to the upstream service.
- Server origin checks: Host allowlists, CSRF
Origin/Referervalidation, trusted-proxy behavior, and generated scheme/host values require a definedHost/Forwardedcontract.
Three compatibility classes are more honest than a universal L1 claim:
| Profile | Meaning | Examples |
|---|---|---|
L1-Origin-Portable | Unchanged artifact works at an assigned framed origin with no app-specific setup | Relative-URL static SPAs; self-contained apps using host-only cookies and same-origin APIs |
L1-Standard-Configurable | Unchanged upstream code/artifact works using documented standard configuration | Apps with BASE_URL, OAuth callback, trusted-proxy, or cookie settings |
L1-Incompatible | Origin or top-level context is intrinsic to the app | Fixed-domain SaaS clients, fixed WebAuthn RP-ID apps, unconfigurable framing denial, hard-coded origin trust |
Xe must decide whether standard configuration counts as “unmodified.” It reasonably can: an unchanged image configured through declared variables remains unmodified and un-Xe-aware. Rewriting HTML, stripping CSP, patching JavaScript, changing source, or silently dropping services does not.
The assigned origin also needs stability. If <app> derives from a mutable display name, rename or collision resolution changes the origin and destroys access to origin-scoped state. Use a stable identifier derived from package identity plus instance identity, with display-name aliases treated as redirects only if they do not change the canonical origin.
The use of plain HTTP narrows the envelope. Browsers grant some secure-context exceptions to localhost, but external OAuth policy, cookie behavior, and WebAuthn acceptance are not governed solely by the browser’s secure-context flag. Xe should either specify the resulting limitations or evaluate a stable local HTTPS scheme. Local HTTPS will not, by itself, make third-party OAuth providers recognize the callback.
The compatibility program should have four parts:
-
A declared profile. XAM records the profile claimed by the publisher, required transport, framing requirements, base-URL configuration, top-level-only behavior, OAuth callbacks, and WebAuthn RP-ID expectations.
-
A conformance fixture suite. Synthetic apps exercise each failure class: secure cookies,
SameSite, OAuth redirects, WebAuthn with a virtual authenticator, CSP framing, absolute URLs, service-worker scope, IndexedDB, popup flows,postMessage, WebSockets, and deep-link SPA reloads. -
A black-box install probe. Launch the real app at its assigned origin and collect navigation failures, blocked requests, console errors, service-worker registration, cookie behavior, deep-link reloads, offline behavior, and backend health. Interactive authentication paths may require publisher-supplied test accounts.
-
A versioned compatibility report. Record the browser build, platform, Xe profile, exercised behaviors, and untested paths. A probe is evidence, not a proof of compatibility or security.
Static analysis can flag absolute URLs and restrictive headers, but it cannot establish OAuth, WebAuthn, or login correctness. One Excalidraw success establishes that Excalidraw’s tested paths fit one profile. It says almost nothing about the broader web application population.
5. Windows backend strategy + the GPU/isolation collision.
Windows backends
There are three coherent choices.
| Option | Strength | Main cost/risk | Appropriate when |
|---|---|---|---|
| WSL exec bridge | Closest to the existing smolvm/Colima path; fastest route to parity | Process startup, quoting, WSL distribution custody, poor per-connection overhead | Windows backend support is near-term and a bounded implementation is acceptable |
| Versioned Windows named-pipe contract | Better performance, ACL-based custody, engine-neutral long-term boundary | New resident agent/protocol and larger implementation surface | Windows is a productized backend platform |
| Explicit deferral | Avoids an undersecured half-platform | Windows supports only static/origin apps | Backend demand is not yet sufficient to justify the TCB |
A WSL bridge should invoke wsl.exe with an explicit distribution, user, and argv—never a shell-composed command. It needs an exact-one distribution/engine identity rule comparable to socket discovery, pinned engine components, sanitized environment, Windows/WSL path validation, deadlines, stream cancellation, lifecycle integration, and concurrency tests. Measurements are important: adding WSL process startup to the already expensive docker compose exec per connection may be unacceptable for WebSockets and request bursts.
A productized named-pipe design should not expose Docker’s administrative named pipe. It should expose a narrow Xe engine-agent protocol over a pipe such as \\.\pipe\xe-engine-<instance>, ACLed to the launcher’s user SID and SYSTEM as required. The launcher should authenticate the agent with a per-install or per-boot credential. Requests should carry an opaque launcher-resolved backend handle, not an arbitrary project/service selector supplied by HTTP. The protocol needs version negotiation, half-close semantics, backpressure, cancellation, health events, and reconnect behavior.
The named-pipe option mirrors the invariants of d/094, not its literal transport:
- The launcher owns the credential.
- The router and app never receive it.
- No app backend port is published on Windows.
- Route/service resolution comes from launcher state.
- Ambiguous engine discovery is refused.
Decision criteria should include:
- Whether Windows backend apps are a release requirement.
- Windows Home/Pro and enterprise environments where WSL is disabled.
- The local-adversary threat model and multi-user Windows hosts.
- Cold-start and per-stream latency.
- WebSocket and long-lived-stream behavior.
- Engine upgrade and rollback ownership.
- Diagnostic parity with macOS/Linux.
- Ability to test the entire supported Windows matrix continuously.
- Whether one implementation can later support non-WSL provisioned VMs.
If Windows backend support is required soon, a WSL exec bridge is a reasonable compatibility milestone. It should not automatically become the permanent public contract. If Windows is a first-class long-term platform, the narrow named-pipe agent is the cleaner design.
GPU and ML placement
Raw Compose devices: or privileged should not be admitted merely because the user clicks a stronger consent dialog. Those fields grant implementation-shaped authority that is too broad to explain, audit, or preserve across engines. GPU access should be a typed placement capability, not an exception that passes arbitrary Compose syntax.
The three choices are:
-
Narrow device grant. Add a capability such as
hardware.gpu/compute, with device class, vendor constraints, memory expectations, sharing mode, and reason. The placement layer maps it to audited platform-specific mechanisms such as CDI/device requests, cgroup device rules, or a dedicated GPU VM. It must not implyprivileged. Gate-2 consent binds the selected device class, driver exposure, and placement. -
Dedicated ML placement. Run ML in a dedicated GPU VM or Xe-owned broker outside the ordinary Compose profile. Apps receive a narrow inference or job API. If arbitrary publisher model code must run, use a dedicated VM rather than pretending a broker API makes arbitrary code safe.
-
Out of scope. Reject the component with a stable diagnostic and declare the application partially incompatible. Do not silently remove ML and continue under the upstream product identity.
Decision criteria include:
- Whether the use case needs arbitrary CUDA/ROCm code or only a bounded inference API.
- Platform coverage: Linux passthrough, Windows GPU virtualization, and macOS acceleration differ substantially.
- IOMMU/vGPU availability and the host-driver attack surface.
- Whether the microVM is per app or shared.
- GPU memory isolation, reset behavior, scheduling, and denial of service.
- Confidentiality between models and workloads.
- Reproducibility and driver/toolkit version locking.
- CPU fallback quality.
- User-visible data flow to the ML placement.
- Whether the app remains functional and truthful without the optional component.
The dedicated placement is the safer first design for a small number of known ML functions. A narrow device grant is justified only after a concrete workload and supported platform demonstrate that it can avoid general privilege. General-purpose privileged Compose should remain outside the profile.
Immich’s removed ML service must be represented as an Xe adaptation with an explicit feature delta, not silently presented as the complete upstream release. Its package identity, provenance, update path, and Plan Review should make that distinction visible.
6. Prior art.
-
TUF: Transfer root/targets/snapshot/timestamp roles, delegated publisher namespaces, threshold rotation, expiry, and rollback/freeze protection. TUF does not transfer user consent, app permission semantics, build provenance, rollout policy, or state rollback. Xe’s exact plan/release gates and state snapshots solve problems TUF deliberately does not address.
-
OCI image, artifact, and distribution specifications: Transfer descriptors with media type, digest, and size; platform indexes; content-addressed blobs; registry distribution; and artifact referrers for SBOMs and attestations. Mutable tags must not be treated as pins. OCI does not define app identity, permissions, safe Compose, or lifecycle consent. Xe is already stronger on user authorization and stateful rollback, but should stop inventing weaker alternatives to OCI descriptor semantics.
-
Dev Container specification: Transfer explicit host requirements, feature/version metadata, prebuild concepts, and the separation between a development description and resolved environment. Its Compose integration is not a security model: host mounts, sockets, lifecycle commands, and developer trust are normal. Xe’s microVM boundary and consent gates are materially stricter. The useful lesson is metadata structure, not its permissive execution posture.
-
Sandstorm: Transfer the grain as an isolated per-instance unit, capability-mediated inter-app access, package identity, backup/restore, and a proxy-controlled web boundary. Sandstorm’s Powerbox is especially relevant to future
interfacesandhandlers: capabilities should be typed handles, not only string permissions. Sandstorm often required app adaptation, so its packaging does not establish Xe’s L1 promise. Xe has a stronger aspiration for unmodified web apps and a more explicit two-stage build/result gate; Sandstorm remains the stronger prior art for instance isolation and capability composition. -
F-Droid: Transfer strict separation of upstream metadata, build recipe, produced APK, signing identity, and repository metadata; reproducible-build verification; independent rebuilders; and visible “anti-feature” declarations. Android signing and a centrally curated repository do not transfer directly. Xe is stronger for local multi-service apps and state rollback, but currently weaker on reproducibility and on distinguishing upstream software from Xe-modified recipes.
-
WASI and the WebAssembly component model: Transfer typed imports/exports, interface versioning, compositional components, and deny-by-default host capabilities. This is a strong model for future XAM
interfaces, tasks, plugins, and bounded ML functions. It does not replace OCI/Compose for arbitrary existing services, and GPU/network support is not a drop-in answer. Xe’s model covers full web apps and stateful services that WASI currently cannot transparently absorb. -
systemd portable services: Transfer immutable image identity, release metadata, host policy applied at attachment time, resource accounting, and an explicit distinction between image contents and activation policy. It shares the host kernel and is Linux-specific, so it does not replace Xe’s microVM isolation. Xe is stronger against host-kernel exposure; systemd is more mature in resource enforcement and lifecycle supervision.
-
Flatpak and Snap: Transfer versioned permission vocabularies, portals/interfaces for narrow host access, permission-diff UX, channels, rollback, and explicit classic/unconfined escape hatches. Their desktop/store assumptions and broad static permissions do not transfer wholesale. Xe’s plan digest plus locked-release digest is more precise than a generic install warning, and its exit/export goals are stronger. Flatpak’s portal model is the relevant pattern for GPU, files, cameras, and future handlers.
-
Nix/Guix: Transfer the separation among source recipe, derivation, realized closure, and profile generation; content-addressed inputs; atomic activation; rollback; and garbage collection. Pure builds remain difficult for networked Dockerfiles and mutable registries, and Nix does not solve mutable application volumes. Xe’s snapshot-aware rollback and human consent are stronger for stateful apps; its current recipe/manifest/lock conflation is weaker.
-
Sigstore: Transfer short-lived build identities, transparency-log inclusion, verification bundles, and optional keyless publisher workflows. It should augment, not replace, TUF roots and offline publisher keys. Mandatory online identity or transparency access may conflict with sovereignty and offline operation. Xe’s local consent remains necessary even for transparently signed software.
-
in-toto and SLSA provenance: Transfer signed statements over materials, build steps, builder identity, and products. These map directly to source commit, recipe, base images, produced OCI descriptors, and independent rebuild results. They do not provide repository freshness, user permission consent, or runtime isolation. Xe’s gate should consume their result rather than invent a custom “trusted build” boolean.
-
CNAB: Transfer the idea of a bundle with install, upgrade, uninstall, parameters, credentials, and outputs. CNAB invocation images are often highly privileged and are not a safe execution baseline. Xe already has the better authorization and isolation shape; CNAB is useful mainly as a warning that lifecycle descriptions and security confinement are separate concerns.
-
Kubernetes Pod Security Standards and admission control: Transfer the use of a named, versioned, positive security profile, normalized admission, capability dropping, namespace restrictions, seccomp policy, and enforcement after defaulting. Kubernetes itself is excessive for Xe. Xe’s scope is simpler and its microVM provides a stronger outer boundary, but its Compose denylist is less robust than a restricted-profile admission model.
-
PWA/Web App Manifest and browser installability: Transfer start URL, scope, icon, display, service-worker, and origin continuity concepts. Browser-installed PWAs retain their original origin; they do not validate Xe’s re-origining promise. Xe is stronger in offline package custody and backend support, but materially less compatible when it changes origin. PWA conformance cases should inform the L1 test suite.
7. Top-5 concrete improvement proposals, ranked.
-
Replace pass-through Compose with a versioned allowlisted deployment IR and authenticate the connector.
Problem: The current denylist inherits unknown Compose behavior, while the loopback connector lacks a stated caller-authentication boundary.
Change: Parse a pinned dialect strictly, reject unknowns, compile permitted fields into an internal IR, execute generated engine objects, inspect them before start, and move connector access to a peer-authenticated local transport.
Rank: First because unauthored backend execution is the highest-consequence boundary and is already waiting behind P-174. -
Split the XAM document family and publish normative validation plus canonical digest rules.
Problem:package.jsonand.xam.jsonidentify three incompatible formats, while"0.2"and the Rust validator do not provide stable interoperability.
Change: Reservepackage.jsonfor npm, introduce explicit recipe/manifest/lock names and discriminators, version each independently, reject active unsupported fields, and publish schemas, semantic rules, and conformance vectors.
Rank: Second because it is relatively inexpensive and prevents ambiguity from becoming an ecosystem compatibility contract. -
Ship the update loop around a consent-bound
UpdateAuthorizationand TUF metadata.
Problem: A correct library with no surfaced discovery, review, activation, or user rollback path provides no practical update safety.
Change: Add CLI/tray flows, bind predecessor/candidate/diff/provenance into the gate, introduce root/targets/snapshot/timestamp metadata, retain snapshots, and prove one escalating update plus both rollback paths end to end.
Rank: Third because most mechanism already exists; the remaining work unlocks large reliability and supply-chain value. -
Replace the universal L1 promise with versioned origin-portability profiles and a conformance program.
Problem: Re-origining breaks important classes of unchanged web application, making the current compatibility claim untestable.
Change: Define portable/configurable/incompatible profiles, stabilize canonical origins, declare transport and framing requirements, and run synthetic plus real-app probes on the pinned browser.
Rank: Fourth because it corrects a foundational product promise and directs app selection without requiring a new runtime. -
Make resource and isolation declarations executable engine policy.
Problem: Resource envelopes and Compose limits are currently advisory, allowing denial of service and making consent displays inaccurate.
Change: Map normalized CPU, memory, PID, disk, restart, network, and volume policy into each backend; generate app-scoped names; verify effective settings after creation; fail when a backend cannot enforce a required minimum.
Rank: Fifth because it closes an important operational-security gap with bounded implementation work, while Windows and GPU investment remain scope-dependent.
8. Missing session-agenda questions.
-
What is the isolation unit: one microVM per app, per user, or per machine?
Luke should decide the user-facing security promise; Jan should determine feasible placement. This matters now because a shared Docker daemon makes container escape, resource exhaustion, networks, and named volumes cross-app concerns even when the host is protected. -
Is an untrusted local process inside the host threat model?
Luke and Jan should decide jointly. If yes, the loopback connector and macOS Docker TCP endpoint need authentication or replacement before they can be described as custody boundaries. -
Is Compose a stable third-party authoring API or only a best-effort import format?
Luke should decide the ecosystem promise; Jan should own the supported profile. A stable API requires versioning, compatibility commitments, and conformance fixtures. An import format can reject aggressively and evolve faster. -
What does “unmodified” permit?
Luke should decide whether standard environment configuration, a normal upstream build, reverse-proxy headers, or adapter-generated assets remain L1. The answer determines whetherL1-Standard-Configurableis part of the hard promise or a separate tier. -
What is the canonical installed origin, and can it ever change?
Luke should decide naming behavior; Jan should specify package/instance derivation, aliases, HTTPS, and migration. This must be fixed before users accumulate cookies, WebAuthn credentials, service workers, and IndexedDB under unstable display-name origins. -
What is the default runtime-egress policy for backend apps?
Luke should decide the privacy/sovereignty posture; Jan should define enforcement. Build-network hosts are reviewed, but the material does not say whether installed services may contact arbitrary runtime destinations. -
Where do builds run, and what credentials and network access can they inherit?
Jan should decide the builder isolation contract, with Luke approving the consent semantics. A microVM that safely runs the result is not automatically a safe place to run adversarial Dockerfiles with registry credentials. -
What constitutes package continuity across recipes, forks, and removed components?
Luke should decide product identity; Jan should define digest and namespace rules. An Immich recipe without ML is an adaptation, and update metadata must not let it masquerade as an unmodified upstream release. -
What is the rollback data contract?
Luke should decide acceptable user-visible data-loss behavior; Jan should specify schema versions, migration reversibility, browser-storage coverage, snapshot retention, and code-only versus full-state rollback. This is required beforeAutocan be safely enabled. -
What are the TUF offline, expiry, and recovery policies?
Luke and Jan must jointly decide threshold custody, root recovery, clock failure, expired-metadata overrides, publisher revocation, and compromised-release rollback. The current signing-timing question does not resolve these operational trust decisions. -
May unsupported and reserved XAM fields be ignored, preserved, or rejected?
Jan should own the compatibility rule, with Luke deciding whether misleading partial installs are ever acceptable. This must be settled before external publishers depend on current permissiveness. -
Is
shplusncinside every service an explicit backend compatibility requirement?
Jan should decide. Distroless and minimal images may contain neither, so the current exec bridge is not a general unmodified-container contract. If it remains required, the compose profile and probe must say so; otherwise the engine needs a tool-independent network-namespace bridge.
SOLREV-APPMODEL-DONE
Sol design review — Shell & rendering (Prism / Xenon)
Part of the site’s external review; reproduced verbatim below the header. sol’s positions are folded into the design frontier as External review input lines.
- Date: 2026-07-18
- Reviewer: gpt-5.6-sol (via
codex exec,model_reasoning_effort=xhigh), run on lane-182 - Scope:
src/part2/shell-rendering.md(architecture),src/part3/shell-rendering.md(design review + gap register G1–G6), and the shell-rendering marker block ofsrc/part5/emerging-specs.md(Prism↔app contract v1, super-app packaging profile v1)
Orchestrator summary (5 lines):
- Sol judges the layer’s honesty real but several headline claims overreach: the CSP is not the whole outbound trust surface (guests still make their own network requests), a Controlled Frame protects the shell from the guest but not the guest from the shell (the host can observe cookies, passwords, uploads), and the hash-linked journal is truncatable without signed/anchored checkpoints — so “exit without loss” is today an intention, not a guarantee.
- On G6 (headline gap) sol lands on a hybrid: Prism owns policy, shielding, persistence, and journaling; Chrome/OS keeps final device selection and enforcement; the effective outcome is the intersection, never the union. Shield-by-default is infeasible via injected JS because the Controlled Frame
permissionrequestAPI can only allow/cancel — real shielding needs a Xenon browser-process permission delegate or a resource portal. - On the launch seam (d/104) sol says keep the descriptor as the canonical seam but do not ratify the current
label → launch_urlshape (it conflates presentation, identity, and routing); bind launch to a key-derivedapp_id+release_digest+mutability_tierresolved by an authenticated plane router, enforced by a Xenon browser-process navigation guard (NavigationThrottle) bound to a verifiedLaunchGrant. d/104 also embeds an unresolved HTTP-vs-HTTPS Controlled Frame portability decision. - The agency ladder should become typed attenuated leases (one guarded dispatcher, complete mediation, fail-closed receipts) and is better modeled as a capability lattice than an ordinal ladder; the four-verb contract should split into a cooperative lifecycle RPC and a separately-audited automation gateway (drop raw
tab.open{url}andkey). - Top-5 ranked: (i) hybrid permission-broker MVP, (ii) signed
LaunchDescriptor+LaunchGrant, (iii) typed-lease agency ladder, (iv) contract v1.1 as lifecycle RPC, (v) onePaneHostabstraction gating composition migration on parity. Twelve added agenda questions — foremost the identity/update model for ordinary HTTP apps and how guest identity is shown (anti-impersonation) without an address bar.
The documents are unusually candid about implementation gaps, but several conclusions are stronger than the evidence supports:
- The CSP is a useful shell boundary, not “the whole outbound trust surface.” It limits Prism’s own fetches and framed top-level origins; guest applications can still make their own network requests.
- A Controlled Frame protects the shell from a guest process, but does not protect the guest from the shell. Prism can inject code, intercept requests, observe input, and potentially access session data. The Controlled Frame specification explicitly recognizes those risks.
- A hash-linked journal is tamper-evident only relative to a trusted head. Without signed or externally anchored checkpoints, the whole chain can be truncated or replaced.
- “Exit without loss” is currently a product intention. The launch, storage, composition, and journal-export contracts needed to make it a guarantee remain incomplete.
(1) G6 — the in-frame permission model
Recommended architecture: hybrid broker, browser enforcement
Prism should own policy, explanation, shielding, persistence, and journaling. Xenon/Chromium and the operating system should remain the final enforcement and device-selection layer.
That distinction matters. The current Controlled Frame permissionrequest shape exposes only coarse permission types—media, geolocation, pointerLock, download, filesystem, fullscreen, and hid—and its controller can only allow or cancel. It cannot itself return an approximate location, a restricted clipboard, or a virtual camera. Those require a Xenon browser-process permission delegate or a true resource portal, not an injected JavaScript patch. The current Controlled Frame draft confirms this limitation.
The broker should therefore produce one of four internal outcomes:
DENY
ALLOW_REAL
ALLOW_PORTAL(resource_handle)
ALLOW_VIRTUAL(shield_profile)
Unsupported ALLOW_PORTAL or ALLOW_VIRTUAL results must fail closed to DENY.
Permission taxonomy
| Risk group | Capabilities | Default | Enforcement |
|---|---|---|---|
| Interaction and attention | Fullscreen, pointer lock, focus, popups, external navigation, clipboard write, downloads, wake lock | Allow once only with a fresh user gesture and visible pane; otherwise deny | Shell gesture check plus browser guard |
| Shieldable personal values | Location, clipboard read, contacts, simple sensors | Shield once by default; prompt for real data | Shell value provider or scoped portal |
| User-selected resources | File open/save, directory access, photo selection, display/window capture | Always invoke a shell/native chooser; never grant an ambient directory or screen capability | Opaque resource handles |
| Live capture | Camera and microphone | Virtual feed if configured; otherwise deny by default; real access requires explicit prompt | Browser/OS device controls and visible indicators |
| Background or durable attention | Notifications, background sync, periodic work, persistent storage | Deny until enabled through an in-context prompt or app settings | Shell notification/lifecycle service |
| Physical or system authority | USB, HID, serial, Bluetooth, MIDI SysEx, local-network discovery, protocol handlers | Deny by default; explicit device- and session-scoped native chooser | Browser/OS; no shell-synthesized substitute |
| Credential and payment authority | Passkeys, payment credentials, secure authentication | Delegate entirely to browser/OS after shell eligibility check | Native trusted UI |
“Shield by default” should not be treated as universally meaningful:
- Location can be fixed, rounded, delayed, or bounded to a region.
- Clipboard read can expose only an explicitly selected clipboard capsule.
- File access can expose only user-selected resources through opaque handles.
- Camera and microphone can use an unmistakably labelled virtual feed or silence.
- USB, HID, passkeys, and payment credentials cannot usefully be “shielded.” Their safe default is denial.
Prompt UX
The shell should prompt only when all of the following are true:
- The request comes from the current, verified frame and document epoch.
- The top-level pane is visible and foregrounded.
- The descriptor declares the capability as optional or required.
- Any required user activation is fresh and browser-attested.
- There is no applicable stored rule or active task lease.
- The request has not been suppressed by rate limiting.
Requests from hidden panes, cross-origin subframes not declared by the app, stale documents, or prompt loops should be auto-denied and journaled without interrupting the user.
The prompt should appear in non-guest-controlled shell chrome and show:
- Verified app name and immutable app ID.
- Current top-level origin.
- The actual requesting origin if it is a subframe.
- The capability and requested scope: exact/coarse location, camera versus microphone, file versus directory, named hardware device.
- Whether the request followed a user action.
- An app-supplied explanation clearly marked as untrusted app text.
- The agent policy or prior rule that would otherwise answer.
- Duration choices.
The choices should be capability-specific. A useful baseline is:
Use shield once — default where shielding exists
Use real data once
Allow while this app is open
Always allow — omitted for raw capture and hardware initially
Deny once
Always deny
Camera, microphone, screen capture, and hardware access require a second native device/resource chooser where applicable. Avoid two semantically identical prompts: Prism decides whether the request is eligible; the native UI selects or confirms the actual device.
Continuous access needs persistent shell indicators. Clicking the camera, microphone, location, or screen indicator must revoke the active stream immediately.
Auto-answer policy
Prism may auto-answer when:
- A matching user-authored rule exists.
- A still-valid, attenuated task lease covers this capability and scope.
- A low-risk request is backed by a fresh gesture, such as same-pane fullscreen.
- A global browser or OS prohibition makes denial inevitable.
- Shielding is the stored default for that app and capability.
An agent should not independently grant real camera, microphone, display, directory, or physical-device access merely because the pane has reached a higher agency rung. Those need explicit prior user policy or contemporaneous user confirmation.
Persistence, inspection, and revocation
“Per-origin” memory is necessary but insufficient. Rules should be keyed by:
user_profile
shell_identity
immutable_app_id
top_level_origin
requesting_origin
capability
scope
They should also record the descriptor/publisher identity under which they were created. A mutable label or .localhost hostname is not enough.
Durations should include:
- One operation.
- Until navigation.
- Until pane close.
- Until Prism exit.
- Time bounded.
- Persistent.
For unsigned or mutable HTTP application code, persistent real-device grants should initially be disallowed or downgraded to session-only. A stable publisher/app key can allow permissions to survive routine content updates, but a publisher-key change, app-ID change, or capability-manifest expansion must invalidate or re-prompt.
Prism needs an inspectable permission center organized by app and origin, showing:
- Current grants, denials, shields, and expirations.
- Last requested and last used times.
- Active streams or handles.
- The policy or user action responsible.
- The requesting suborigin.
- A one-click revoke control and global emergency stop.
Revocation must remove the Prism rule, invalidate matching leases and resource handles, terminate live streams, and clear any corresponding Chromium content setting. Chrome should receive only ephemeral or tightly scoped grants where possible so Prism is not maintaining a contradictory second permission database.
Chrome already provides one-time, visit-scoped, persistent, revocable site permissions and automatically removes permissions for inactive sites. Chrome’s site-permission controls are the minimum parity target.
Journal binding
Every answer should create a permission.answer record containing:
request_id
wall_time and monotonic sequence
frame_id and document_epoch
launch_instance_id
immutable_app_id
descriptor_digest and release_digest
top_level_origin and requesting_origin
capability and normalized scope
visibility and user_activation state
requested outcome
effective outcome
decision source: user | stored-rule | task-lease | system | agent
policy_rule_id or lease_id
shield_profile_id and profile digest
duration, expiry, and use limit
prompt UI version
browser/OS result and denial reason
previous permission decision reference
Do not journal raw locations, clipboard contents, filenames, captured media, device serial numbers, or notification bodies. Record opaque handles, sizes, types, and keyed fingerprints where correlation is needed.
Long-running access also needs permission.use.start, permission.use.stop, and permission.revoke records. The journal should append an intent before enabling the capability and a completion or failure afterward. If the journal cannot append, high-risk operations should fail closed.
Finally, periodically sign and anchor the journal head using the device key or a user-selected external store. A local hash chain alone does not prove that an earlier suffix was not discarded.
Honest comparison with Chrome-native prompts
Prism is genuinely better where it can provide:
- Shielded values rather than binary allow/block.
- App identity that is more meaningful than
foo.localhost. - Purpose- and task-scoped agent leases.
- One policy view across browser, files, composition, and cross-app actions.
- Receipts tied to the action that caused the request.
- Origin-aware mediation for embedded subframes.
- Cross-app resource handles instead of broad ambient access.
Prism is mostly reimplementing Chrome where it provides:
- Allow once, while visiting, always allow, and block.
- Site settings and revocation.
- User-gesture checks.
- Prompt-abuse suppression and quiet UI.
- Accessible and localized device dialogs.
- Camera/microphone indicators.
- OS privacy integration and device enumeration.
- Browser-level handling of navigation races and compromised renderers.
Chrome has years of hardening around these cases. Replacing its trusted UI wholesale would enlarge Xenon’s patch burden and likely produce a less accessible and less consistent experience.
The right hybrid is:
- Prism brokers: location shielding, clipboard capsules, file portals, notification identity and rate limits, focus, downloads, external intents, agent/task leases, cross-app transfers, persistence policy, and journal records.
- Chrome/OS delegates: raw camera/microphone access, display capture, USB/HID/serial/Bluetooth, passkeys, payments, credential selection, and physical device controls.
- Both participate: Prism performs eligibility and policy; Chrome/OS performs final resource selection and enforcement. The effective outcome is the intersection, never the union, of their decisions.
(2) The launch seam — d/104
The descriptor should remain the canonical launch seam, but the present descriptor shape should not be ratified.
label -> launch_url confuses presentation, identity, and routing:
- A label is mutable and non-unique.
- A URL is a locator, not an app identity.
- The catalog comes from an unauthenticated loopback endpoint.
- The wildcard CSP admits every matching
.localhostorigin. - Assigning
activeFrame.srcconstrains only the initial navigation. - Redirects, links,
window.open, script navigation, and later channel verbs can leave the intended origin. - Reusing a hostname for different code can inherit that origin’s stored data and permissions.
The safer identity is a stable key-derived app_id, with a release digest identifying the exact version. The URL should be resolved from that identity by an authenticated plane router.
A launch descriptor should contain at least:
schema_version
app_id # hash/public-key-derived stable identity
publisher_id
display_name and icons # presentation only
release_version
release_digest # exact immutable release/snapshot
mutability_tier # sealed | signed-update | mutable
canonical_origin
start_path
allowed_top_level_origins
allowed_path_prefixes
allowed_redirect_origins
allowed_subframe_origins
external_navigation_policy
partition_id
declared_capabilities
contract_versions
device_id and service_instance_id
issued_at, expires_at, sequence
descriptor_signature
revocation/reference status
For an inherently dynamic server that cannot supply a meaningful content digest, the descriptor must say mutable. Such an app should not inherit durable high-risk grants as though its code were sealed.
The catalog label can change freely. The app_id, publisher key, partition assignment, and permission identity cannot.
Navigation constraints
The default policy should be:
- Initial navigation must equal the resolved canonical origin plus an allowed start path.
- Same-origin top-level navigation is permitted within allowed path prefixes.
- Cross-origin top-level redirects are blocked unless explicitly listed.
- Cross-origin links open outside the privileged pane through an
external.openpolicy path and require a user gesture. javascript:,file:,data:, arbitraryisolated-app:, and unknown schemes are rejected.blob:top-level navigation is allowed only if its creator origin matches the current allowed origin and the browser can verify that binding.window.openis denied by default. Approved new windows receive a fresh frame, partition decision, launch grant, channel, and leases.- The app channel is closed and re-keyed on every committed top-level document.
- All leases bind to a document epoch and become invalid on navigation.
- Permission requests from subframes are attributed to both the app and actual subframe origin.
This policy concerns top-level pane navigation. Guest subresource requests need a separate network policy; silently restricting all APIs and CDNs to the top-level origin would break ordinary web apps.
Enforcement point
The durable enforcement point should be a Xenon browser-process navigation guard—conceptually a NavigationThrottle—bound to a verified LaunchGrant for that Controlled Frame. Shell JavaScript is not a sufficient sole guard because it may observe a redirect or commit too late.
An interim implementation can use blocking Controlled Frame request interception plus loadredirect, newwindow, and loadcommit checks, but any unexpected commit must destroy and recreate the frame, not navigate it “back.”
There is also an unresolved standards mismatch: the current Controlled Frame draft states that Controlled Frames work with https: content, while Prism’s architecture uses http://*.localhost:5454. The local exception may work in the current Xenon patch set, but it is not the documented portable contract. Ratifying d/104 must explicitly resolve whether this is:
- An upstream-supported trustworthy-localhost exception.
- A permanent Xenon divergence.
- Or a migration to authenticated HTTPS plane origins.
Until then, d/104 is not merely a product decision; it embeds a browser-portability decision.
(3) Prism↔app contract v1 — verbs and gateway
The asymmetry is correct, but the current division is not.
There should be two specifications:
- A cooperative app protocol for lifecycle, presentation, and intents.
- A privileged automation gateway for observing and changing uncooperative content.
readDom, request rewriting, mutation, and CDP control are not a normal app contract. They are shell authority over a guest and should be specified and audited separately.
The four current verbs are also narrow in the wrong direction. They omit basic lifecycle cooperation while exposing raw navigation and a generic key actuator. tab.open{url} directly conflicts with a descriptor-constrained launch model. key{key} is too generic to justify as an app-to-shell primitive and should be removed.
The nonce should not be described as app authentication. The port is already a bearer capability; sending a nonce over the same port adds message validation but not an independent identity factor. Any holder has both. Close and recreate the port on navigation, bind it to frame_id + document_epoch + app_id, and describe it as a nonce-gated bearer channel.
A real protocol also needs version negotiation, request IDs, responses, typed errors, cancellation, quotas, sequence/replay handling, and channel-close semantics.
Proposed additions
| Addition | Path | Capability cost and constraints |
|---|---|---|
| Window title, subtitle, badge | Nonce channel: ui.setMetadata | Low. Sanitize length and characters, rate-limit, and always render the immutable shell-owned app identity beside it. App text must not replace identity. |
| Notification request | Nonce channel: notification.request; notification action returns through a single-use gateway delivery | Medium/high attention capability. Requires a separate notification permission, quotas, origin-labelled chrome, no arbitrary HTML, and body redaction in the journal. Possessing the port grants the ability to request, not to display. |
| Focus/attention | Nonce channel: ui.requestAttention or ui.requestFocus | Medium. Require browser-attested user activation or explicit shell policy. Never allow hidden scripts to steal focus. |
| Liveness | Controlled Frame load/crash events for process liveness; nonce channel app.ready/app.health for cooperative readiness | Low, but untrusted. A heartbeat proves only that the app script is responding. Rate-limit it and do not use it as a security assertion. |
| Resize | Nonce channel for advisory ui.requestSize; normal viewport/resize events from shell to app | Low/medium. Shell retains layout control, clamps dimensions, and prevents resize loops. No privileged gateway is needed merely to tell a page its viewport changed. |
| Clipboard | Use the permission broker and native Clipboard API for normal apps; use a leased gateway only for an agent placing a specific clipboard capsule into a specific field | High for reads, medium for writes. A general clipboard.read verb would let any port holder exfiltrate ambient clipboard data. |
| File open/save | Browser/shell resource portal, not a generic channel verb; inbound selected-file delivery through a leased gateway using an opaque handle | High. Never deliver arbitrary host paths. Scope handle by app, mode, resource, TTL, and document epoch. |
| Inbound file handling | Leased gateway: intent.deliverFile | High. Single app, typed content, selected file, explicit read/write mode. Revocation must invalidate the handle. |
| Deep link or cross-app intent | Outbound request on nonce channel: intent.open; inbound delivery on leased gateway: intent.deliver | High cross-app authority. Target by immutable app ID and declared intent type, not URL. Require consent for sensitive payloads and pass opaque references where possible. |
| Notification click/action | Leased gateway bound to notification ID and action ID | Medium. Single use, short TTL, and no ambient ability to focus or drive the app. |
The key contract rule should become: the channel conveys requests; operation-specific policy grants authority. Merely adding a verb must not implicitly grant the capability to every script holding the port.
Gateway leases also need stronger attenuation. A five-minute single-use lease should additionally bind:
actor
app_id
frame_id
document_epoch
operation
argument constraints
selector or resource
purpose
issued_at and expiry
use count
undo strategy
Read, mutation, input, network interception, composition, and CDP leases should be distinct types. A generic Stage-0 lease does not enforce the agency ladder.
(4) The agency ladder — vocabulary to enforcement
The ladder should not be an ordinal authorization check. “Composed” is not necessarily more powerful than “driven,” and a one-record cross-app transfer can be less invasive than full DOM observation. Represent the ladder as named capability sets; derive the user-facing rung from active capabilities.
| Rung | Minimal enforcement point |
|---|---|
| Nothing | Frame session starts with no injected bridge, automation attachment, or active lease. The gateway dispatcher default-denies every operation lacking an explicit typed lease. |
| Allowed / denied / shielded | Browser-process permission delegate accepts only a valid PermissionLease or stored rule and appends the answer before applying it. |
| Observed | ReadLease guard immediately before executeScript, DOM extraction, field reads, or request observation. DOM contents and network bodies should be separate scopes. |
| Enhanced | EnhancementLease guard before insertCSS or isolated-world annotation injection, restricted to shell-bundled assets and bounded selectors. |
| Mutated | RequestRewriteLease at the blocking network interceptor, constrained by URL patterns, methods, headers, and permitted transformations. |
| Controlled | InteractionLease at the input/navigation dispatcher, bound to a frame, document epoch, action types, selectors, and navigation policy. |
| Driven | DevToolsLease at CDP attachment and again at every CDP command dispatch, with a command allowlist, short TTL, visible indicator, and emergency revocation. |
| Composed / generated / delegated | TransferLease at the composition broker, binding source read, destination write, schema, payload fingerprint, consent record, use count, and undo operation. |
| Super-user-agent | A non-exportable ShellRoot authority minted only after Xenon verifies the installed Prism IWA identity and manifest policies. Every permission, gateway, CDP, and composition service requires this root to mint narrower leases. Guests never receive it. |
The smallest adequate enforcement kernel is:
- Static ceiling: signed IWA identity, CSP, Permissions Policy, and Controlled Frame availability.
- Verified frame context: immutable app identity, descriptor digest, partition, navigation policy, and document epoch.
- Typed attenuated leases: resource, verb, actor, argument constraints, TTL, use count, and delegation chain.
- Complete mediation: one guarded dispatcher for every privileged Controlled Frame, permission, request, input, CDP, and composition operation.
- Fail-closed receipts: append intent before effect; append result and undo reference afterward.
A journal record alone is not an enforcement boundary. It becomes one only if the operation cannot proceed when authorization or append fails.
(5) Iframes versus Controlled Frames in composition
Migration should not be treated as an automatic upgrade. Controlled Frames give Prism more power over guests; they are not simply “better-isolated iframes.” The current specification recommends ordinary iframes where they suffice and explicitly notes that an IWA host may observe activity, session cookies, passwords, uploads, and network behavior in a Controlled Frame. Controlled Frame security and privacy considerations.
Migration criteria
Composition should move to Controlled Frames only when all of these are true:
- d/103 plane origins have stable app IDs and authenticated routing.
- The replacement for
launch_urland its navigation guard is implemented. - The HTTP-versus-HTTPS Controlled Frame discrepancy is resolved on every supported platform.
- G6 handles permission requests in multiple simultaneous panes.
- Each pane receives a deliberate partition assignment; per-app, per-instance, and shared-session behavior is specified.
- The cooperative channel is bound and re-keyed by document epoch.
- Cross-app payloads use typed
TransferLeaserecords with consent, fingerprints, and real undo behavior. - Focus, keyboard, IME, screen-reader, resize, drag/drop, file picker, clipboard, and popup behavior have parity tests.
- Crash, freeze, visibility, and memory-pressure behavior is defined for multiple Controlled Frame processes, particularly on Android.
- Apps that rely on
window.parent, ordinary iframepostMessage, embedding headers, or shared browsing-context assumptions have migration adapters. - The shell visibly identifies the origin and agency state of each pane.
What breaks if migration is premature
- Local HTTP origins may simply fail in an upstream-conforming Controlled Frame.
- Existing iframe
postMessageand parent/child assumptions may stop working. - Authentication and service-worker state may move into a different partition.
- Two instances of the same app may unexpectedly share or fail to share state.
- Permission requests can be attributed or persisted incorrectly.
- Popups and cross-origin redirects can escape the composition policy.
- Mobile memory use increases because each guest is a top-level-like isolated context.
- Accessibility, focus, IME, and resize behavior may regress.
- Moving to Controlled Frames silently grants Prism observation and mutation powers that the iframe composition demonstration did not require.
Should the surfaces unify?
Unify the identity, descriptor, PaneHost abstraction, lifecycle, permission broker, journal, and composition lease model. Do not require one embedding primitive.
Use:
<iframe>for cooperative plane apps where normal web isolation and embedding consent are sufficient.<controlledframe>when Prism needs to host content that refuses iframe embedding, enforce navigation, provide automation, or intercept requests.
If the product promise remains “any two catalog apps,” Controlled Frames will be necessary for some composition cases. They should be selected by declared capability need, not used as a visual-compliance milestone.
(6) Prior art
Browser extensions
MV3’s split between required permissions, optional runtime permissions, host permissions, and gesture-derived activeTab authority transfers directly. Prism should have:
- Descriptor-declared possible capabilities.
- Optional runtime grants.
- Exact host/origin scopes.
- An
activePane-style, short-lived gesture capability for observation or interaction.
Chrome explicitly recommends optional permissions so users grant functionality in context, and activeTab provides temporary host authority following a user action. Chrome extension permission model.
What does not transfer is the extension principal model. Extensions are installed, signed code with a stable extension identity and browser-defined APIs. Most Prism guests are mutable HTTP applications, and a MessagePort holder is not equivalent to an extension service worker.
Wayland and xdg-desktop-portal
Portals are the strongest direct model for files, screen capture, devices, and notifications:
- The sandbox asks an external trusted service.
- Trusted UI identifies the requesting app.
- The user selects a specific resource.
- The app receives an opaque handle rather than ambient host access.
- A permission store supports inspection and revocation.
The XDG permission store is explicitly keyed by application and resource, and document portals expose only granted resources to sandboxed applications. XDG Permission Store, Document Portal.
What does not transfer is principal authentication. Flatpak portals receive a sandbox-established application ID. Prism must first establish that an HTTP origin really corresponds to the catalog app ID. Portals also do not provide DOM/CDP automation or shielded synthetic values.
Mobile entitlements and runtime permissions
Transfer:
- Static manifest declarations as a maximum capability ceiling.
- In-context runtime requests.
- One-time and while-in-use grants.
- Automatic expiry or reset.
- Persistent privacy indicators.
- Graceful behavior after denial.
- Resource pickers and scoped storage instead of broad filesystem permission.
Android’s guidance explicitly recommends asking only when the user invokes the relevant task and degrading gracefully after denial. Its Storage Access Framework grants access to user-selected files through scoped URIs rather than broad storage authority. Android runtime permissions, Storage Access Framework. Apple’s App Sandbox similarly treats entitlements as narrowly restoring specific capabilities to an otherwise contained application. Apple App Sandbox.
What does not transfer is OS-level attribution. Android and iOS usually see Xenon or Prism as the app principal, not each HTTP guest. Prism therefore needs its own subordinate authority model. Mobile systems also rarely offer synthetic shielding beyond coarse location or selected resources.
IWA, Controlled Frame, and Web Bundles
Transfer:
- Key-derived identity for Prism itself.
- Signed, offline-verifiable shell code.
- Default-none IWA Permissions Policy.
- Separate Controlled Frame storage partitions.
- Browser-provided permission, navigation, popup, lifecycle, scripting, and request hooks.
- Conformance tests tied to the draft APIs rather than private abstractions.
IWA identity is tied to the bundle signing key, while permissions remain dynamic and independently auditable. IWA architecture.
What does not transfer is identity for ordinary guests. Prism’s signed bundle authenticates Prism, not app.localhost. Controlled Frames are also an incubating API, not a W3C standard, and their current draft states HTTPS-only navigation. They deliberately allow the host to bypass ordinary embedding restrictions and control the guest; that is an authority requiring governance, not a free security improvement.
Capability systems: OCap, UCAN, and macaroons
Transfer:
- Possession conveys authority.
- Capabilities should name a resource and operation.
- Delegation must only attenuate authority.
- Tokens should bind audience, scope, time, use count, argument constraints, and purpose.
- Invocation and receipt are separate from delegation.
- Short-lived, single-use capabilities reduce leakage impact.
UCAN specifies verifiable attenuated delegation and separates delegation from invocation; macaroons add contextual caveats constraining when, where, and for what a bearer credential works. UCAN delegation, Macaroons paper.
What does not transfer is an automatic solution to port leakage. UCANs and macaroons are still credentials; a script that steals a usable bearer token can exercise it. In Prism’s single-device broker, cryptographic tokens may also be unnecessary internally—a non-forgeable in-memory lease handle with the same attenuation can be simpler. Use cryptography when leases cross processes, devices, or journal/export boundaries.
Electron and embedded-browser shells
Electron’s hard-earned checklist transfers almost verbatim: explicitly handle permissions, limit navigation and window creation, validate IPC senders, keep remote content sandboxed, and never pass untrusted URLs directly to external-open APIs. Electron security guidance.
What does not transfer is Electron’s Node/native integration model. Prism should not expose a general native bridge or reproduce Electron’s broad renderer-to-host API surface.
(7) Top-5 concrete improvement proposals, ranked
-
Ship a hybrid permission broker MVP. Add a browser-process permission delegate, typed
PermissionRequestmodel, per-app/origin rule store, shell prompt, live indicators, revocation, and fail-closed journal records; initially support location, media, files, clipboard, notifications, and downloads. Closes G6. Cost: Large—roughly 2–3 engineer-months for a credible MVP, excluding virtual camera/microphone work. -
Replace
launch_urlwith signedLaunchDescriptor + LaunchGrant. Introduce key-derived app IDs, release digests, authenticated plane resolution, per-app partitions, exact navigation rules, channel re-keying, and a browser-process navigation guard. Closes d/104’s unsafe shape, navigation escape, and part of G4. Cost: Large—approximately 1–2 engineer-months plus a Xenon integration patch. -
Implement the agency ladder as typed leases. Split read, enhancement, rewrite, interaction, CDP, permission, and transfer authority; bind each lease to app, frame, document epoch, constraints, TTL, uses, purpose, and receipt. Closes the vocabulary-to-enforcement gap. Cost: Medium/large—approximately 4–8 engineer-weeks and gateway refactoring.
-
Publish contract v1.1 as lifecycle RPC, not automation RPC. Remove raw
tab.open{url}andkey; add versioning, request/response IDs, title, readiness, attention, notification requests, constrained intents, quotas, navigation epochs, and typed errors. Keep the automation gateway as a separate specification. Closes G2 and the port-capability ambiguity. Cost: Medium—approximately 3–5 engineer-weeks plus app-adapter updates. -
Create one
PaneHostabstraction and gate composition migration on parity. Support iframe and Controlled Frame adapters behind the same identity, permission, lifecycle, and journal interfaces; prove one arbitrary catalog-app transfer with focus, accessibility, storage, crash, and undo tests before general migration. Closes G3 without making Controlled Frames mandatory where they add unnecessary host authority. Cost: Large—approximately 6–10 engineer-weeks after proposals 1–4.
(8) Missing session-agenda questions
The Luke + Jan agenda should also ask:
-
What is the identity and update model for ordinary HTTP apps? Who signs an app descriptor, how is an
app_idcreated, and what happens to data and permissions when publisher key, release digest, or routing changes? -
What exact navigation policy applies after launch? Are redirects, OAuth origins, external links, popups, downloads, and deep links allowed, and where is the policy enforced?
-
How is guest identity shown without an address bar? What shell-owned chrome prevents an app from impersonating Prism or another app, especially in prompts and notifications?
-
Is the upstream HTTPS-only Controlled Frame constraint acceptable? Is local HTTP a temporary Xenon patch, a standards proposal, or an architectural dependency that must be removed?
-
What is the partition unit? Per shell, app, app instance, user, composition session, or device? How are state clearing, export, revocation, service workers, and concurrent instances handled?
-
Who may mint agency leases? Which operations require current user presence, which can a resident agent exercise unattended, and what is the global emergency-stop path?
-
Is the agency model intentionally a ladder or actually a capability lattice? How should simultaneous capabilities be displayed, and can composed authority exist without observation or driving?
-
What is the channel’s actual threat model? Is XSS inside a guest considered equivalent to app compromise? Does the port authenticate an app, an origin, a document, or only possession?
-
What is the journal adversary model? How are chain heads anchored, rollback and truncation detected, sensitive metadata protected, retention limited, and exports authenticated?
-
What constitutes a valid undo? For network requests, notifications, file writes, messages, purchases, and cross-app carries, which actions are reversible and which must be labelled irreversible before consent?
-
What is the fallback on browsers without production-strength Controlled Frames? Does Prism refuse to run, degrade to cooperative iframes, or expose a visibly lower assurance tier?
-
What is the conformance and patch-budget gate? Which permission, navigation, isolation, accessibility, and multi-pane tests must pass before a Xenon rebase or Controlled Frame API update ships?
SOLREV-SHELL-DONE
Sol design review — Devices & the agent layer
Part of the site’s external review; reproduced verbatim below the header. sol’s positions are folded into the design frontier as External review input lines.
- Date: 2026-07-18
- Reviewer: gpt-5.6-sol (
codex exec, model_reasoning_effort=xhigh, codex-cli 0.144.1, lane-182) - Scope:
src/part2/devices.md·src/part2/agent-layer.md·src/part3/devices.md·src/part3/agent-layer.md· the devices-agent-layer marker block ofsrc/part5/emerging-specs.md(full text inlined in the prompt; sol had no filesystem)
Orchestrator summary (5 lines):
Sol’s verdict: the device fabric is materially real, but the security model is not yet coherent enough to implement — the biggest problems are unresolved semantics, not missing code.
Grant/lease: onward:false contradicts lease-minting under a grant; sol recommends “lease = audience-bound invocation, not delegation,” a deterministic attenuation-subsumption algorithm with conformance vectors, a published max stale-authority window for revocation, and requalifying “verifiable offline” as “verifiable against a named signed snapshot under a declared staleness ceiling.”
Authed-wake: same-network v1 is right but is “process activation, not wake”; a relay mailbox is not an activation mechanism (nothing polls it); the sovereignty-fitting pattern is a user-controlled home broker doing authenticated WoL plus contentless push hints (self-hosted UnifiedPush / opt-in FCM), and “force-stopped” must be split from “process killed.”
Agent: the shell is not a defensible boundary — sol proposes a five-part split (agent daemon / policy+signing service / browser broker / shell-as-UI / page+model both untrusted), excludes raw CDP, execute.script, and persistent injection from v1, and names human approval as inspection, not an authority bound; audit: the 64-bit-truncated chain is corruption detection, not adversarial tamper evidence — converge on one full-hash, signed-checkpoint ledger with cross-device head gossip.
Movable memory: ship replicated-home collections (single-writer, signed epoch transitions, durable-replica acks) before any generic CRDT; top follow-ups: wire the apps.list responder under a constrained launch descriptor, freeze a grant/lease v1 verifier profile, and land one out-of-shell capability-enforced agent action end to end.
The device fabric is materially real; the security model around it is not yet coherent enough to implement. The largest problems are not missing code but unresolved semantics: onward:false conflicts with lease minting, “offline revocation” is overstated, a relay mailbox cannot by itself wake a dead process, and the audit chain provides much less adversarial tamper evidence than the text implies. The resident agent should still be described as a research preview until an out-of-shell enforcement boundary exists.
(1) The grant/lease delegation model
The UCAN analogy is directionally useful, but the current mapping is not yet internally consistent.
onward:false conflicts with “lease minted under a grant”
A grant is described as a delegation to an install principal with onward:false. The same principal then supposedly mints a lease carrying narrower authority to an agent or surface. If that lease transfers authority to another principal, it is precisely the onward delegation the grant forbids.
There are two coherent ways to resolve this:
- Treat the lease as an invocation, not a delegation. The install principal signs an exact request to a specific executor, and the executor checks it against the parent grant. The agent never receives transferable authority.
- Permit one explicit delegation step: the grant permits attenuation to an executor-bound, non-delegable lease. In that case the grant cannot truthfully be
onward:false; it needs a delegation-depth or audience restriction.
The first option is cleaner. The grant audience should be a local policy service/install principal. That principal invokes an executor using an audience-bound, short-lived authorization. The model and page remain untrusted requesters, not capability holders.
Attenuation is not proved merely by linking the lease to a grant
Signatures prove who issued tokens. They do not prove that the child capability is narrower. A verifier needs a deterministic subsumption algorithm:
- Every lease capability must be covered by at least one capability in the parent grant.
- Resource containment must be precisely defined. URI prefix comparison is unsafe without canonical schemes, hosts, ports, paths, percent-encoding, origin rules, and app identifiers.
- Ability implication needs an explicit lattice. Does
read.domcontainread.text? Doesexecute.scriptsubsume every read and mutation ability? Canmutate.formnavigate or submit network requests? - Every constraint type needs a partial ordering. Shorter expiry is narrower; fewer allowed fields may be narrower; arbitrary JSON constraints are not automatically comparable.
- Unknown abilities or constraints must fail closed.
- Token encoding, hashing, and signature inputs must be canonical and domain-separated.
Aggregate constraints are especially difficult. If a grant allows ten calls, minting ten leases that each allow ten calls is not attenuation. Budgets, call counts, and rate limits require shared accounting at the minting service or executor. Independent offline verifiers cannot enforce a global counter.
The parent grant identifier or digest must appear in the lease proof. The verifier should check the complete chain, issuer and audience bindings, capability subsumption, time intersection, revocation status, executor binding, and replay state.
The statement that a nonce is “CID-equivalent” should be removed. A CID identifies content; a nonce supplies uniqueness. Neither makes an invocation single-use. Single-use requires a durable consumed-nonce database at the relevant enforcement point.
Revocation has an unavoidable availability boundary
“Revoke a grant to end all leases” is valid only as an admission rule:
- Every lease must name exactly one parent grant or grant-family generation.
- Revocation must identify that grant, family, or install principal in a signed, monotonic revocation record.
- Every executor must check revocation before beginning the action.
- Already-completed side effects cannot be revoked.
- The design must state what happens to an action admitted before revocation but still running: finish, cancel at the next boundary, or require continuous authorization.
Revocation cannot be immediate on a disconnected device. Gossip on the next accepted connection provides eventual convergence, not instantaneous revocation. The product must publish a maximum stale-authority window and decide whether an executor fails closed when its revocation checkpoint is older than that window.
Short leases alone do not solve this. If a compromised install principal can mint fresh leases while offline, it can continue indefinitely until it learns of the grant revocation. Bounding this requires at least one of:
- short-lived grants that require periodic renewal;
- a fresh signed revocation checkpoint required when minting or executing;
- an online authority for sensitive actions;
- an explicit acceptance that offline local authority cannot be remotely revoked.
Device membership revocation and grant revocation must also be related explicitly. Does revoking a device invalidate every grant whose audience is a principal on that device? A verifier should not have to infer this.
Expiry and renewal need exact boundary rules
A lease must be valid only within the intersection of all parent validity windows:
effective interval = grant interval ∩ lease interval ∩ revocation-freshness interval
At minimum, v1 must specify:
- whether boundaries are inclusive or exclusive;
- permitted clock skew;
- whether
lease.notBefore >= grant.notBefore; - whether
lease.notAfter <= grant.expiry; - that parent expiry invalidates the lease even if its own
notAfteris later; - whether validity is checked only at admission or throughout execution;
- maximum action duration;
- behavior when clocks move backward or a device reboots.
Renewal should issue a new grant identifier or generation. It must not silently extend leases under the expired grant. Otherwise historical proofs become ambiguous and grant revocation can accidentally affect renewed authority.
Offline time is itself a trust assumption. Wall-clock validity is weak if the holder controls the clock. Short TTLs can use a monotonic clock during one boot, but cross-reboot offline verification needs an authenticated time source or an explicit weaker guarantee.
Offline verification is possible, but only with qualified claims
An offline device can verify:
- signatures;
- proof-chain linkage;
- issuer/audience identities;
- structural attenuation;
- expiration according to its local clock;
- revocation as of its latest signed checkpoint;
- locally observed replay.
It cannot know:
- whether a newer revocation exists elsewhere;
- whether the same invocation was consumed on another disconnected verifier;
- whether a fleet-wide budget has been exhausted;
- whether its clock has been rolled back.
Therefore “verifiable offline” must mean “verifiable against a named signed state snapshot, subject to a declared staleness ceiling,” not “currently authorized everywhere.”
What v1 must pin down
Before implementation, v1 needs:
- the principal graph: authority, device, install, policy service, agent, and executor;
- whether a lease is an invocation or delegation;
- canonical token encoding and signature domains;
- grant and lease identifiers and proof linkage;
- the resource grammar and ability implication table;
- comparison rules for every constraint;
- executor and surface binding;
- replay-store scope and durability;
- revocation record format, distribution, and freshness policy;
- device-revocation cascade rules;
- temporal and in-flight-action semantics;
- key custody and rotation;
- conformance vectors covering valid attenuation and every rejection case.
If “one Ed25519 key” means one key reused for transport identity, membership, grants, and audit, that should also be changed. Separate role keys—or at least separately certified subkeys—are needed to limit compromise and support independent rotation.
(2) The authed-wake crux
The design needs to distinguish four states it currently blurs:
- Runtime process stopped while the OS is awake.
- OS awake with no resident Xe process.
- OS suspended.
- Application force-stopped or device powered off.
launchd/systemd socket activation addresses the first two. It does not generally wake a suspended machine. Waking suspended hardware requires platform power-management support, Wake-on-LAN, or a local broker. Powered-off behavior is separately hardware-dependent.
Same-network-only is the correct v1 scope—with narrower wording
A same-LAN, kernel-held TCP socket can start a small verifier when the OS is awake. That is a tractable first version, but it is “process activation,” not generic “wake.”
Even there, authentication occurs after the socket causes a process to start. A network scanner can therefore create some amplification before cryptographic verification. The activated component must be tiny, fixed-cost, rate-limited, and unable to start the full runtime until it has verified a bounded request. Firewall scope and per-source activation budgets matter.
For actual suspend wake, an authenticated user-controlled router or always-on home device can receive a request and emit a local Wake-on-LAN packet. The WoL packet is normally not itself strong authorization; the broker supplies the security boundary.
A relay mailbox is not an activation mechanism
A mailbox solves store-and-forward reachability. It does not cause a zero-resident process to poll it.
For desktop WAN wake, something still has to be resident or platform-integrated:
- a small background service;
- a scheduled OS task;
- a vendor wake facility;
- an always-on local broker that can perform WoL.
On mobile, mature systems use an OS-integrated push provider to deliver a contentless wake hint, after which the app fetches encrypted data over its normal authenticated channel. Apple effectively requires APNs for this class of background reachability. Android commonly uses FCM; UnifiedPush replaces the central provider with a user-selected distributor, but that distributor must itself remain reachable and survive Android background restrictions.
A true Android force-stop is stronger than an ordinary process kill. Normal push delivery should not be claimed to override it; the package generally remains stopped until the user launches it. The design should replace “force-stopped needs notifications” with separate rows for “process killed/background restricted” and “user force-stopped.”
Sovereignty does not require pretending the relay is absent
Asynchronous internet reachability requires some component that remains available. A sovereignty-first design can make that component:
- replaceable and user-selectable;
- self-hostable;
- unable to mint authority;
- blind to payload contents;
- unnecessary on the LAN;
- optional when the user accepts reduced reachability.
A self-hosted UnifiedPush distributor or user-controlled relay fits that definition better than an obligatory vendor service. FCM can remain an explicit opt-in reachability provider. Neither should carry an action: the push should say only “check your authenticated mailbox.”
A user-controlled home broker is the strongest fit for desktop wake because it can authenticate a remote request, then perform local WoL without trusting a central server. An untrusted iroh-adjacent mailbox may deliver the encrypted request to that broker.
Relay-mailbox replay and amplification controls
A safe wake envelope needs:
- a target-issued, narrowly scoped admission;
- target identity and mailbox binding;
- fixed wake class such as
ensure_running; - expiry and issuance generation;
- a random invocation nonce;
- sender proof;
- strict maximum size;
- encryption to the target;
- no arbitrary reason string or executable payload.
The relay should authenticate admission before accepting storage, enforce target and sender quotas, deduplicate envelope identifiers, expire entries quickly, and avoid responses to attacker-selected addresses.
The target needs a durable consumed-nonce store. Reboot, database rollback, or reinstall must not reopen old admissions; an admission epoch or monotonic generation is needed. Relay deduplication is useful but cannot be trusted as the security boundary.
At-most-once per nonce is insufficient against a peer generating unlimited fresh nonces. The target-issued admission must also constrain total uses or rate. Wake should cause at most one fixed-cost state transition and no reflected payload response. Otherwise a small request can produce a process-spawn storm, repeated model startup, network fan-out, or a reflection channel.
For v1, I would ship LAN process activation on the platforms that support it, accurately label suspend and WAN cases unsupported, and reserve the mailbox format without building it until there is a real target-side wake consumer.
(3) The resident agent architecture
The shell is not a defensible home for the security boundary. It can remain the user interface and browser integration surface, but it should not hold the agent’s durable memory, model credentials, grants, or signing material.
Physical architecture
A workable split is:
- Agent daemon: task queue, memory, model adapter, planning, and provider credentials.
- Policy/signing service: validates grants and consent, accounts for budgets, and issues executor-bound invocations or opaque handles.
- Browser actuator/broker: exposes a small typed operation set against the Controlled Frame and enforces the invocation at the final moment.
- IWA shell: displays state and collects user intent; it is not an authority source.
- Page and model: both treated as untrusted inputs.
The browser actuator may need to remain close to the Controlled Frame, but it should receive typed operations such as “read visible text from this navigation epoch” or “click this identified element.” It should not expose raw CDP.
The console.log('iwa:…') channel should be retired as an authority-bearing IPC path. Typed messaging improves accidental safety, but authentication inside the same compromised shell is not sufficient. The meaningful boundary is an OS process boundary with peer credentials, explicit protocol versions, message sequencing, and fail-closed behavior.
Lease-signing root
The runtime authority root should not be continuously available to the shell or model process. It should sign install grants through a narrow consent/admin path, preferably using an OS keystore or hardware-backed key where available.
The per-install principal key used to sign invocations should be held by the policy service, not the IWA. A non-extractable OS key helps against key theft, but it does not prevent a compromised authorized process from asking the keystore to sign. Process isolation and request validation remain necessary.
Calling this the “lease-signing root” risks collapsing two roles:
- the authority root certifies the standing grant;
- the install/policy principal signs a particular invocation under that grant.
For same-device execution, an opaque single-use handle maintained by the policy service may be safer and simpler than a transferable signed lease. Signed leases become valuable when a separate device or independently implemented executor must verify them.
Human approval is not an authority bound
Per-call approval provides inspection, but it fails under click fatigue, misleading previews, prompt injection, UI spoofing, and time-of-check/time-of-use changes. It also does not constrain what the approved primitive can do. Approving Runtime.evaluate effectively approves arbitrary page-context computation and potentially broad data extraction.
The executor needs enforceable bounds independent of the model and approval UI:
- exact device, app, top-level origin, frame, tab, and browser partition;
- navigation epoch so authority expires when the page changes;
- separate read, mutation, navigation, persistence, and network abilities;
- selectors or stable element references where practical;
- allowed output fields and maximum returned data;
- destination allowlists and byte/call/spend budgets;
- invocation count, duration, and concurrency limits;
- task/session binding and cancellation;
- step-up consent for irreversible effects;
- an emergency revocation path enforced outside the shell.
execute.script, persistent injection, and unrestricted CDP should not be grantable in the first bounded version. They collapse the proposed ability lattice into ambient authority. Begin with a small brokered set such as visible-text read, one-origin navigation, and constrained element activation.
The human should approve policy or high-risk effects, not serve as the only reference monitor. Audit records are evidence after the event; they are not enforcement.
(4) The companion audit hash-chain
The current chain is useful as a corruption-detection and diagnostic format. It is not strong adversarial tamper evidence.
What it resists
Assuming a verifier retained a trusted prior head, the chain detects:
- accidental changes to an earlier line;
- unintentional line reordering, insertion, or deletion;
- malformed manual edits that do not recompute subsequent hashes;
- field injection through newlines or separators, due to sanitization.
The off versus kill distinction is worth preserving because it captures useful lifecycle semantics.
What it does not resist
An attacker able to modify the file can:
- rewrite any event and recompute the remainder;
- replace the entire chain with a new one;
- truncate the tail;
- roll back to an older valid copy;
- delete the ledger;
- create two valid forks;
- omit an action before it is logged;
- make the logger record a false event;
- manipulate timestamps;
- exploit concurrent append or partial-write behavior, which is unspecified.
There is no secret or signature in the chain and no trusted head outside it. A public hash therefore does not authenticate the history.
The 64-bit truncation is unnecessary and materially weakens collision resistance: generic collision work is on the order of (2^{32}), while full-history rewriting is already trivial for a file attacker. Keeping the full 256-bit result costs little. The main defect, however, is the lack of anchoring, not the truncation alone.
seq = lines.size + 1 also needs hardening. It is vulnerable to truncation, malformed trailing records, and concurrent writers unless there is a single append service with locking and atomic durability.
One fleet-wide discipline
The companion and agent should use one versioned event schema and one implementation discipline:
- full SHA-256 chain values;
- stable event and device identifiers;
- device-local monotonic sequence;
- wall time plus boot/session identity;
- actor, cause, grant, lease, invocation, and result identifiers;
- hashes of sensitive before/after values rather than indiscriminate plaintext;
- explicit schema version and canonical encoding;
- atomic append, locking, recovery, rotation, and linked segments;
- signed periodic checkpoints using an audit-specific device key.
Fleet unification should preserve per-device chains rather than pretend there is a reliable global order. Devices can gossip signed segment heads and immutable segments over iroh. The group document or its anchor mechanism can periodically commit checkpoint digests without carrying the full log. Once another device has seen a head, later truncation or forking becomes detectable.
Historical verification must retain the membership/key history needed to validate an event from a device that was later revoked. Revocation prevents future trust; it should not invalidate authentic historical entries.
Audit sync also needs privacy and lifecycle rules: encryption at rest and in transit, retention, export, redaction, quotas, and who may inspect which records. Agent prompts, origins, and action results can be more sensitive than the actions themselves.
(5) Movable memory / cross-device sync
The minimum honest v1 is not generic multi-writer synchronization. It is a replicated-home collection with explicit limitations.
For each opted-in app data set, define:
- a stable collection ID, app owner, schema version, and encryption key;
- one authoritative home device and home epoch;
- immutable versioned snapshots or an append-only change log;
- at least one durable replica on another selected device;
- content-addressed chunks and a signed collection-root manifest;
- explicit quota, retention, and replication status.
Only the home accepts writes in v1. Other devices can read their last complete replica while offline, clearly marked with its generation and freshness. They do not accept offline mutations. That is weaker than local-first multi-master operation, but it is implementable, avoids silent conflict loss, and makes device loss survivable once another device has acknowledged the latest root.
Moving the home must be a signed epoch transition. A clean move requires the old home to flush and participate. A disaster promotion must say that an unreplicated tail may be lost. If the old home later returns with divergent changes, preserve its branch for explicit recovery; do not silently choose by wall clock.
A generic last-writer-wins rule is not acceptable for opaque app data. It silently discards work and depends on clocks the design does not otherwise trust. Multi-writer support should be opt-in by data type:
- CRDTs for structures with a defined merge;
- revision trees with preserved conflicts for documents/blobs;
- application-provided merges for domain-specific state.
CRDT adoption brings its own requirements: stable actor IDs, causal metadata, tombstone retention, compaction, schema migration, revoked-device handling, and bounded history.
The existing authenticated iroh session can carry a dedicated sync protocol:
- Exchange collection manifests: ID, home epoch, generation, root digest, schema, and chunk inventory.
- Authorize the collection separately from group membership.
- Transfer missing content-addressed chunks with resumption and backpressure.
- Verify the root before atomically installing a new snapshot.
- Return a durable replica acknowledgement.
- Audit transfer, promotion, conflict, and deletion events.
Membership authentication alone is too broad; not every group member necessarily receives every app collection. Revoking a device can stop future sync and trigger key rotation, but it cannot erase data the device already copied.
Agent memory and audit data should ride the same transfer machinery but use different semantic profiles. Agent memory may be a replicated-home or CRDT collection. Audit is immutable signed-segment replication. Calling all three “peer sync” must not imply they share one conflict algorithm.
(6) Prior art
UCAN
Borrow:
- issuer/audience capability tokens;
- proof-chain linkage;
- explicit delegation versus invocation;
- canonical capability grammar;
- monotonic attenuation;
- expiry and not-before checks;
- interoperable conformance vectors.
Avoid assuming UCAN supplies global revocation, single-use enforcement, budget accounting, trusted time, or offline freshness. Those require a product-specific profile and stateful verifier. Do not describe a nonce as a content identifier or treat proof-chain validity as proof of current authorization.
Matrix device verification and cross-signing
Borrow:
- explicit per-device keys;
- QR or short-authentication-string verification;
- a hierarchy separating recovery/user authority from device keys;
- signed device lists and visible trust state;
- clear UI when a device key changes or is revoked.
Avoid importing homeserver dependence, room-state complexity, or federation machinery into a one-person fleet. Also avoid treating “cross-signed” as synonymous with “currently safe”; compromised but not-yet-revoked devices remain a problem. Matrix’s recovery UX is more relevant here than its messaging data model.
Signal sessions, Double Ratchet, and sealed sender
Borrow Double Ratchet-style forward secrecy and post-compromise recovery only for asynchronous stored messages where iroh’s live QUIC session protection is insufficient. For a mailbox, encrypt to the target and minimize retained message history.
Borrow the sealed-sender principle—minimize what a relay learns about the sender—but preserve abuse controls. A relay may still need an opaque target token, rate-limited admission, and message size information.
Avoid adding a ratchet to every live iroh stream merely because Signal uses one; QUIC already supplies an encrypted session. Also avoid using messaging-session keys as authorization capabilities. Confidentiality and authority are different layers.
Push-notification relay patterns
Borrow:
- contentless or opaque wake hints;
- short TTLs and collapse/deduplication identifiers;
- “wake, then fetch over the authenticated channel”;
- provider-specific reachability treated as advisory;
- user-selected vendor or self-hosted delivery.
Avoid carrying commands, app data, or authority in a notification payload. Avoid claiming guaranteed delivery or force-stop recovery. Vendor push providers remain availability and metadata dependencies even when they cannot decrypt content.
For desktop wake, borrow the authenticated-broker-plus-WoL pattern used by managed home networks. The broker authorizes; WoL is only the local actuator.
SPKI/SDSI
Borrow:
- signed authorization certificates;
- issuer-local naming;
- linked authorization chains;
- explicit validity intervals;
- threshold authority where recovery eventually requires it.
Avoid recreating a global naming PKI. The relevant lesson is decentralized authorization, not X.509-style identity administration.
Macaroons
Borrow:
- monotonic caveat attenuation;
- verifier-enforced first-party caveats;
- contextual restrictions such as time, resource, and operation;
- optional third-party discharge concepts for explicit consent.
Avoid adopting bearer macaroons without proof-of-possession or executor binding. Copying and replay remain possible, and symmetric root secrets complicate public offline verification and multi-device delegation. Macaroons may fit local opaque lease handles better than the fleet-wide grant chain.
Object-capability and sandboxed process systems
Borrow the central rule of no ambient authority: give the executor a narrow handle to one surface and operation rather than general access to a browser, filesystem, or network. Capability-secure IPC, pre-opened handles, and browser broker/renderer separation are directly applicable.
Avoid treating a signed JSON token as a complete object-capability boundary. If the holder still has raw CDP, unrestricted network access, or shared signing memory, the token is advisory decoration.
CRDT and sync systems
From Automerge/Yjs-style systems, borrow causal changes, deterministic merges, local operation, and hash-linked change graphs for data types that genuinely support multi-writer editing. Avoid assuming every app’s state is a document CRDT or accepting unbounded histories.
From Syncthing-like replication, borrow device-to-device block hashing, resumable transfer, version vectors, and preserved conflict copies. Avoid inheriting filesystem-specific semantics or an always-running-daemon assumption.
From CouchDB-style replication, borrow revision trees and explicit conflict preservation for opaque documents. Avoid silent last-writer-wins resolution.
Transparency logs and TUF-style metadata
For audit and revocation, borrow signed checkpoints, monotonic versions, expiry, rollback detection, and gossip between witnesses. A small private fleet does not need a public global transparency service; devices can witness one another’s heads.
Avoid copying the full operational complexity of Certificate Transparency or TUF. The applicable pieces are signed state, freshness, and rollback protection.
(7) Top-5 concrete improvement proposals, ranked
-
Ratify one constrained launch descriptor and wire the member-gated
apps.listresponder.
Why: this converts the most-built path from an honest error screen into a working product with very little code. It also resolves the current mismatch between d/104’s.localhostplane origin and the protocol’s acceptance of arbitrary HTTP(S) andisolated-app://targets.
Smallest first step: freezeapp_id, label, exact plane origin, tier, serving-device identity, and catalog signature rules; then register and dispatch the existing ALPN responder. -
Freeze a Grant/Lease v1 verifier profile before minting any leases.
Why: the current outline cannot consistently enforce attenuation, replay, or revocation, andonward:falsecontradicts lease delegation.
Smallest first step: choose “lease as audience-bound invocation,” define three grantable abilities, and publish table-driven positive and negative conformance vectors. -
Implement one out-of-shell, capability-enforced agent action end to end.
Why: one narrow real boundary is more valuable than expanding the unbounded preview. It tests the mission’s core claim.
Smallest first step: placeread.visible-textbehind a local policy service and browser broker, bound to one tab/origin/navigation epoch, with the shell and model holding no signing key. -
Replace both audit variants with one full-hash, signed-checkpoint ledger.
Why: this is modest engineering work, removes duplicate semantics, and turns rollback/fork detection into a fleet property rather than a local-file claim.
Smallest first step: define a canonical v1 event envelope and have two devices gossip and persist each other’s signed chain heads. -
Ship replicated-home collections before attempting generic CRDT sync.
Why: it gives app data a second durable home and a credible recovery path without taking on universal multi-writer conflict resolution.
Smallest first step: replicate one versioned app collection to one peer over iroh, install it atomically by root hash, and surface whether the latest generation has a durable remote acknowledgement.
(8) Missing session-agenda questions
-
What is the v1 threat model? Are the page, model output, IWA shell, other group devices, relay, and local OS administrator considered malicious, compromised, or merely buggy?
-
What is the exact key and principal graph? Are transport, group membership, grant signing, install invocation, and audit using one Ed25519 key or separate keys? Who rotates, backs up, and revokes each one?
-
Is a lease an invocation or a delegation? If the latter, how is it compatible with
onward:false? If the former, which executor is its audience and who consumes its nonce? -
What revocation guarantee is actually promised? What is the maximum stale-authority window, what happens offline, and do sensitive actions fail closed without a fresh checkpoint?
-
What are the first enforceable agent abilities? Are raw CDP,
Runtime.evaluate, persistent injection, arbitrary network access, and cross-origin reads explicitly excluded from v1? -
What exactly does a consent approval authorize? One operation, one displayed plan, a task, a session, or a standing grant? What page or navigation change invalidates it, and which effects always require step-up confirmation?
-
Who is the final reference monitor? Is authorization checked in the shell, policy daemon, browser broker, hosting device, or all relevant layers? Which component owns budget and replay state?
-
How is the launch catalog trusted? Can any member supply arbitrary labels and
isolated-app://targets, or must each descriptor be bound to a serving device, stable app ID, signed catalog entry, and exact.localhostorigin? -
What does “wake” mean per OS state? The agenda should separately decide process activation, suspend wake, ordinary Android process death, Android force-stop, and power-off behavior. A relay mailbox should not be approved until its target-side consumer is named.
-
What audit adversary is in scope? Is the ledger for diagnostics, user accountability, or evidence against a compromised device? What are the anchoring, retention, privacy, export, and deletion rules?
-
What is the minimum app-data consistency contract? Is v1 replicated single-writer, last-writer-wins, revision-tree, or CRDT? Are offline writes permitted, and how is home transfer fenced during a partition?
-
What happens after a forced home-device promotion? How are a returning old home’s divergent writes preserved, and who decides whether they are merged or discarded?
-
How do upgrades fail? If shell, daemon, executor, grant schema, or revocation format versions diverge, does the system fail closed, retain read-only access, or silently fall back to human approval?
-
What authority governs model-provider egress? BYO model selection does not answer which page data may leave the device, where provider credentials live, or whether prompts and responses enter the audit/memory sync system.
-
What does device revocation not accomplish? The session should explicitly acknowledge that revocation cannot retract previously synchronized app data, agent memory, or secrets, and decide when collection keys are rotated.
SOLREV-DEVICES-DONE
Emerging Specs & Protocols
This is where formats and protocols firm up into candidates. A spec candidate here is
not a committee draft written ahead of code — it is the opposite: an as-built extraction
from the working tree at a named commit, written down so the shape is legible, allocatable,
and reviewable before it hardens. Every candidate below is honest about its maturity — most
are spec candidate (as-built, not ratified; field semantics may still move with an open
decision), a couple are marked normative where the code already fails closed against them,
and one (the launch descriptor) is PROPOSED, pending Luke. Where a candidate intersects an
open decision, it says so and links the design-frontier gap.
The rule these encode, worth making normative in itself: a new wire string, schema, or signing domain lands in the relevant table in the same change that introduces it — specs are allocated, not improvised.
External review (sol). An independent critical review by gpt-5.6-sol (2026-07-18) assessed
several of these candidates directly — the Peer Ticket / Fleet App-Index / peer-serve wire /
ALPN registry, the Plane Origin Convention / Route Table /
honest-error contract, the Prism↔app contract and super-app
profile, the XAM / compose-subset / engine-socket /
install-consent set, and the companion protocol / audit chain /
launch descriptor / grant–lease outline. Its blunt verdict:
these as-built extractions are honest but not yet sufficient for an independent, interoperable
implementation — they lack on-wire version/type discriminators, JSON and base64url parsing
profiles, canonical digest encodings, complete type bounds, and positive/negative conformance
vectors, and a few carry outright contradictions (the ticket’s “optional but required”
index_sha256; the honest-error contract’s actionless 400/405 rows; a v1-titled candidate
documenting a v0 wire identifier). Read each candidate’s Open design / Status note against
the matching memo in the external review before it hardens.
| Candidate | Layer | Source beat | Scope, in one line |
|---|---|---|---|
| Xe Peer Ticket v1 | Transport | transport / peers | The credential + address card a publisher hands a consumer. |
| Xe Fleet App-Index v1 | Peer serving | transport / peers | The published index, durable peer row, and merged catalog view. |
| Peer-serve wire protocol v1 | Transport | transport / peers | The authenticated QUIC byte-tunnel that carries a served app. |
| ALPN & signing-domain registry v1 | Transport | transport / peers | The allocation table for every wire ALPN and signing domain. |
| The Plane Origin Convention v1 | Serving plane | serving-plane | One loopback plane, one origin per app, deterministic conflicts. |
| Route Table schema v1 | Serving plane | serving-plane | The frozen route-table shape the workerd router is generated into. |
| The honest-error contract v1 | Serving plane | serving-plane | Every plane refusal: status, body shape, and its next-action rule. |
| Prism ↔ app contract v1 | Shell | shell-rendering | The asymmetric frame↔shell channel and leased gateway. |
| Super-app packaging profile v1 | Shell | shell-rendering | What makes an app Prism-class (the rare, opt-in super tier). |
Companion channel protocol v1 — apps.list | Devices | devices / agent | The read-only “what apps are installed?” protocol over iroh. |
| Companion audit hash-chain v1 | Devices | devices / agent | The tamper-evident per-device session ledger. |
| Launch descriptor (d/104) (PROPOSED) | Devices / shell | devices / agent | One label → plane origin shape serving every platform’s launch seam. |
| Grant / Lease delegation model | Agent layer | devices / agent | The UCAN-aligned capability → grant → lease → audit outline. |
| Xe App Manifest (XAM) v0.2 | App model | app-model / engines | The manifest carrying identity, source, and components. |
| Compose-subset security profile (normative) | Engines | app-model / engines | The install-time refusal list that fails closed on unsafe compose. |
| Engine socket contract v1 (d/094) | Engines | app-model / engines | Reaching a backend through the engine’s own unix socket, no sidecar. |
| Install-consent grant model | App model | app-model / engines | The two non-interchangeable consent gates and their custody. |
Each block below is contributed by the layer’s own writer and reproduced verbatim.
Xe Peer Ticket v1
Status: spec candidate — an as-built extraction from
crates/launcher/src/peer_serve.rs:86-105atxenon-launcher main @56e94575. Not ratified; field semantics may change with the G1/G4 decisions (review).
The ticket is the credential + address card a publisher hands a consumer.
Wire form: base64url-nopad( JSON(struct) ) — one shell-safe token, written
to a 0600/owner-only-DACL file. The file round-trips through a trailing
newline (:989-1003) and is capped at 16 KiB (MAX_TICKET_FILE_BYTES).
| Field | JSON key | Type | Req? | Meaning / source |
|---|---|---|---|---|
| node_addr | node_addr | {provider, address} | yes | Publisher iroh EndpointAddr (JSON); provider = "iroh-1" (lib.rs:41) |
| secret | secret | string | yes | base64url of 32 random bytes — the bearer credential |
| index_sha256 | index_sha256 | string (64 hex) | opt* | SHA-256 of the immutable app-index bytes; required by the consumer (peer_serve.rs:609) |
| origin_id | origin_id | string | opt | "iroh-" + sha256(node_addr); copied into each index row |
| device_name | device_name | string (1–128) | opt | Calm publisher name; consumer enforces a match (:593-604) |
| origin_port | origin_port | u16 | opt | Publisher HTTP authority port for Host rewrite; absent ⇒ legacy byte-pipe (:101-104) |
Open design intersecting this format: exp / lifetime (G1 option a),
audience-binding or UCAN envelope (G1 option d), membership gating making the
secret field secondary (G4).
Xe Fleet App-Index v1
Status: spec candidate — as-built from
crates/launcher/src/fleet_catalog.rs@56e94575. Three related shapes: the published index, the durable peer row, and the merged catalog view.
PublishedAppIndex — schema xe/app-index/v1, served at /xe/api/index
(:26, :72-85); deterministic name order; ≤ 1 MiB (MAX_INDEX_BYTES):
| Field | JSON key | Type | Meaning |
|---|---|---|---|
| schema | schema | string | "xe/app-index/v1" |
| device_name | device-name | string | publisher device name |
| origin_id | origin-id | string | public iroh publisher id |
| apps | apps | [PublishedApp] | deterministic name order |
PublishedApp (:52-70): name (localhost label), kind
(static | built | live-backend | multi-service), version, origin-id,
sha256 (lowercase hex), device-name, origin
(http://<name>.localhost:5454/).
FleetPeer durable row — schema xe/peer-list/v1, in peers.json + the
workerd mirror xe/peers.json (:88-120): id (= origin_id, row key),
device_name, origin_id, port (consumer forward), connector
(peer_ + first 16 hex of sha256(origin_id)), index_sha256,
index_file (xe/indexes/{peer_key}.json), liveness_file,
liveness_origin, publisher_origin_port?.
Merged catalog — /xe/api/catalog (assets/route-worker.js:102-130):
per peer {device_name, origin_id, status(live|offline), last_seen_unix_ms, observation, index_sha256, app_count} plus per-app entries whose launch URL
carries ?xe-peer=<id>.
Invariants worth keeping if this hardens: digest mismatch never overwrites
the last-good cached index; offline peers are retained with last_seen,
never silently pruned; register_peer refuses port 0 and the plane port
5454.
Peer-serve wire protocol v1
Status: spec candidate — as-built from
peer_serve.rs@56e94575. ALPN:xenon/serve/tcp/1.
Consumer Publisher (ALPN xenon/serve/tcp/1)
dial_alpn(node_addr, SERVE_ALPN) ── QUIC bi-stream open ──►
u8 len (=32) ‖ secret[len] ──────────────────────► read u8 len (1..=64)
read len bytes
constant-time compare vs 32-byte secret
◄──────────── u8 ACK: ACCEPT(1) | REJECT(0) ─────── (≤10 s preamble deadline)
if ACCEPT: copy_bidirectional(local TCP ⇄ iroh stream) ⇄ connect 127.0.0.1:target_port
- Framing: 1-byte length-prefixed secret preamble, then a raw untyped byte pipe (carries whatever HTTP the browser plane sends).
- Constants:
ACCEPT = 1,REJECT = 0,MAX_SECRET_LEN = 64,MAX_TUNNELS = 64, preamble deadline 10 s. - The catalog probe reuses the same authenticated stream but writes a literal
GET /xe/api/index HTTP/1.1with a rewrittenHost:(:788-839) — HTTP-over-tunnel for the catalog, opaque pipe for apps. - Revocation: none in-protocol; publisher restart voids all secrets (G1 — the primary open design on this wire).
ALPN & signing-domain registry v1
Status: spec candidate — a registry extracted from the codebase @56e94575 so that new protocol strings are allocated, not improvised. Rule worth adopting: every new ALPN or signing domain lands in this table in the same change that introduces it.
ALPNs (wire protocols):
| ALPN | Purpose | Gate |
|---|---|---|
xenon/net/echo/1 | echo experiment / dial default | membership if provider built with it |
xenon/serve/tcp/1 | peer-serve byte tunnel | bearer secret (today) |
xenon/net/group-sync/1 | membership head gossip | membership |
xenon/net/group-join/1 | join ceremony | single-use invite + proof-of-possession |
xenon/substrate/1 | remote substrate multi-lane session | membership (checked at dial) |
xenon/consent/test/1 | test-only extra-ALPN gate proof | membership |
Signing domains (what gets signed, never wire ALPNs):
xenon/group-document/v1\0 · xenon/group-envelope/v1\0 ·
xenon/group-join-proof/v1\0 ·
xenon/recovery/{successor,takeover,counter,freeze,rotation,rotation-accept}/v1\0 ·
xenon/wireguard-key/v1\0 · xenon/interface-* · xenon/mod-manifest/v1\0 ·
xenon/profile-* · (xe-sync v2:) xenon/group-document/v2\0 ·
xenon/group-envelope/v2\0 · xenon/member-endpoint-binding/v1\0.
Convention already followed in-tree, worth making normative: version suffix
on every string; signing domains end in \0; xenon/ prefix pending any
product-naming decision (the prefix is wire-internal and never user-facing).
The Plane Origin Convention v1
Status: spec candidate — an as-built extraction from
agent54/xenon-launcheratmain @56e9457(crates/launcher/src/origin.rs,assets/route-worker.js,fleet_catalog.rs). Ratifies d/100 (port) and d/101 (scheme). The scheme row is the one live tension — see review G3.
The addressing rule for every app on an Xe Computer: one loopback plane, one origin per app, deterministic conflict handling.
| Element | Rule | Source |
|---|---|---|
| Plane authority | 127.0.0.1:5454 + [::1]:5454, loopback only | d/100; fleet_catalog.rs:24 |
| App origin form | http(s)://<label>.localhost:5454 | d/100; route-worker.js:1 |
| Label grammar | 1–63 chars, [a-z0-9-], no leading/trailing - | valid_label origin.rs:1048-1055; regex route-worker.js:1 |
| Reserved labels | xe, api, catalog, moby, localhost | origin.rs:309 |
| Reserved service paths (loopback host) | /xe/api/index, /xe/api/catalog | route-worker.js:11-12,384-385 |
| Conflict / qualified form | <pretty-prefix>-<first-12-hex-of-sha256(package_id)>; A1 tiebreak — the lexicographically smaller identity hash keeps the pretty name | qualified_name origin.rs:1072-1082; conflict_winner :401-409 |
| Claim lifecycle | claim → tombstone (revoke) → explicit xe origin reclaim ceremony; tombstoned names never silently reused | origin.rs:286-396 |
| Scheme | https default (name-constrained CA leaf); http only under the dev-http flag | d/101; origin.rs:819-830, app_command.rs:844-850 |
| Port override | XE_ORIGIN_PORT (installers / isolated smoke tests only) | origin.rs:39-45 |
Invariants worth ratifying. Origins are stable identity: the origin string is the browser’s storage and service-worker key, so a tombstoned name is never silently reused and conflicts resolve by a deterministic hash tiebreak, never by timestamp. Open at ratification: the scheme row — https-default (code) vs plain-http-default (d/101) — must be settled before this hardens.
Route Table schema v1
Status: spec candidate — as-built from
origin.rs@56e9457. Thetypediscriminator is frozen by P-148 (PR#36); this table documents the frozen shape. Growth rule and the whole-origin overload are open — review G1 / G5.
RouteTable — schema xe/origin-routes/v0 (origin.rs:99-103):
| Field | Type | Notes |
|---|---|---|
schema | string | xe/origin-routes/v0 |
port | u16 | rendered plane port |
routes | [OriginRoute] | sorted by name |
OriginRoute (origin.rs:105-134):
| Field | Type | Present when | Notes |
|---|---|---|---|
name | string | always | .localhost label |
package_id | string | always | provenance |
type | enum static-digest | backend-proxy | peer | always | frozen (P-148); unknown → fail-closed 404 |
digest | string | non-empty for static; omitted otherwise | sha256 of extracted payload; skip_serializing_if keeps pure-static bytes identical |
static_serving | StaticServingPolicy | opt | headers / redirects / spa_fallback, parsed at install |
backend | BackendArm | backend-proxy only | omitted for pure static |
peer | PeerArm | peer only | |
iwa | bool | opt-in only | drives COOP/COEP/CSP + dotfile manifest store (d/101) |
BackendArm (origin.rs:137-152): prefix (single proxied path prefix,
segment-matched) · idempotent_get [string] (cold-start wait-set) ·
connector (workerd binding name; always "backend" today).
PeerArm (origin.rs:155-165): device (calm display name) · connector
(binding; "peer" or fleet connector) · origin_port ?u16 (publisher
authority port; absent = legacy portless ticket).
PeerRouteState — schema xe/peer-route/v0 (origin.rs:168-177): app ·
name · device · port (must == 5198, d/102) · origin_port?.
Grant — schema xe/origin-grants/v0 (grants.rs:69-88): id ·
source_origin · target_app · path_prefix · methods[] · credentials
(omit | same-origin | include) · byte_ceiling · issued_at · expires_at
(TTL cap 3600 s). Grants cover static reads only and are re-read per request
and per streamed chunk.
Growth rule (proposed, G5). Additive-only + fail-closed is the de-facto rule
today. The whole-origin compose case ships as a backend-proxy overload (empty
digest + prefix:"/", P-203); a distinct container-origin type would be the
first real schema change and is pending Luke (d/103-B).
The honest-error contract v1
Status: spec candidate — as-built from
route-worker.js@56e9457. Every refusal in the refusal ladder conforms to this. The502distinction is an open question — review.
Every plane-generated error is text/plain; charset=utf-8 with
X-Content-Type-Options: nosniff, and every body names the next action.
| Status | Trigger(s) | Body shape | Extra headers | Source |
|---|---|---|---|---|
400 | CONNECT/TRACE; bad Host; hostname ≠ authority; bad %-encoding; path traversal | 400 Bad Request | nosniff | :381,386,388,390,391 |
403 | cross-origin refused / preflight / credentials / backend cross-origin / cross-site CSRF | action-ending copy, e.g. “Ask the app publisher to declare access or request a grant, then retry.” | grant CORS echoed when a grant applies | :305,412,421,429,431 |
404 | not installed; unsupported route type; not found; peer-under-grant | “This app is not installed. Run xe app install PACKAGE…” / “…Run xe doctor, then retry.” | — | :298,318,342,424,425,437,438 |
405 | unsafe method on static route | 405 Method Not Allowed | Allow: GET, HEAD | :449 |
503 | plane table missing; backend connector unavailable; backend unreachable; index unpublished; peer offline | “…Run xe doctor, then retry.” / peer: “This app runs on <device>, which is offline. Retry, or open it on that device.” | backend: Retry-After: 2; peer/index: Cache-Control: no-store; peer adds Vary: *, xe-peer-liveness: offline | :280,286,92,99,423,436; peerOffline :39-44 |
Contract invariants worth ratifying. Every body ends by naming a next action
(, then retry.); every error carries nosniff; backend transients carry
Retry-After; liveness/catalog responses carry Cache-Control: no-store.
The one deliberate gap. 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. Distinguishing “plane can’t reach
upstream” (503) from “upstream errored” (502) is a deliberate addition, not a
bug — flagged for ratification.
Prism ↔ app contract v1
Status: spec candidate — an as-built extraction from
agent54/prismatmain @98acaa3(docs/channel.md,src/channel/,src/gateway/). The surface is deliberately minimal and asymmetric today; field names and the verb set may change with the frame↔shell decision (G2 in the review). The document’s own note is normative: a future Controlled-Frame adapter must keep the same port → nonce → schema order and the rejection-journal contract.
The contract has two directions, and they are not symmetric.
App → shell — the authenticated verb channel
Transport: one MessageChannel per frame; one port is transferred to the guest
with targetOrigin pinned to the guest origin; a 128-bit nonce is sent only
over that transferred port. The shell accepts a message only after three checks
in this order — port identity → nonce equality → exact {nonce, verb, args}
schema (src/channel/message.ts:41-63).
| Verb | Args | Meaning | Journals as |
|---|---|---|---|
tab.open | {url} | Navigate the pane to a URL | navigate.tab |
tab.close | {} | Close the pane | navigate.tab |
tab.reload | {} | Reload the pane | navigate.tab |
key | {key} (non-empty string) | Deliver a keystroke to the pane | navigate.tab |
Every accepted verb is journaled; every rejection is journaled as
channel.reject with the origin and the failing check, before/after null
(src/channel/frame-channel.ts:18-35). Explicit non-goals stated in
docs/channel.md: the channel does not establish user intent, does not
authorize anything beyond gateway behaviour, and does not distinguish
same-frame scripts — “any script that obtains the transferred port and nonce
holds the capability.”
Shell → app — the leased gateway
The shell reaches into the pane through a capability-leased gateway
(src/gateway/gateway.ts:18-44). Each operation mints a single-use Stage-0
lease (5-minute TTL) and records an AuditEntry; cross-origin cooperation is
possible only if the app opts in by exposing a prismRpc bridge
(web/dev-origins/site.html).
| Operation | Purpose |
|---|---|
readText / readDom / readField | Observe the pane’s content |
mutateNode / filterContent / restoreContent | Enhance / mutate the pane |
answerPermission | Answer a permission request (allow / deny / shield) |
navigate / navigateTab | Drive navigation |
Reserved but not a contract yet: window title, notifications, focus, and app liveness — the app-declared surface a future revision may add (G2).
Super-app packaging profile v1
Status: spec candidate — what makes an app Prism-class (super-app tier, d/101), extracted from
packaging/bundle/+ the two manifests atagent54/prism main @98acaa3. This profile is opt-in and rare — per d/101 the default app tier is plain http(s) with none of the below. This is the exception the shell itself takes.
An app qualifies for the super-app tier — the tier permitted to host Controlled Frames and run cross-origin isolated — when all of the following hold:
| Requirement | Value / source | Why |
|---|---|---|
| Packaging | Signed web bundle (.swbn) via wbn / wbn-sign; origin key-derived (isolated-app://…) | Sealed, key-identified app the OS installs natively |
Manifest display | standalone (web/manifest.webmanifest) | No address bar; app-window presentation |
Manifest permissions_policy | { "controlled-frame": ["self"], "cross-origin-isolated": ["self"] } | The two capabilities that define a super-app: may host Controlled Frames; runs cross-origin isolated |
| Response headers | COOP: same-origin + COEP: require-corp + CORP: same-origin (packaging/bundle/headers.json) | Yields crossOriginIsolated: true, a precondition for hosting a Controlled Frame |
CSP frame-src | admits the guest plane origins (http://*.localhost:5454) | Lets the shell frame app panes; nothing else |
| Install | as an IWA with an OS app shim (d/053) | Dock / taskbar identity; wake via Boot Service |
A note on the manifest split as-built: the root manifest declares
start_url: "/" while .well-known/manifest.webmanifest declares
start_url: "/iwa-controlledframe.html" (the fleet-catalog home). Both carry
the same permissions_policy. Whether the catalog descriptor is the canonical
launch seam across platforms is the open decision d/104
(review).
Companion channel protocol v1 — apps.list
Status: spec candidate — an as-built extraction from
crates/companion-core/src/launcher_apps.rsatxenon-runtime main @b0cfc90. Read-only in v1; field semantics may change with d/104 (the launch descriptor, devices review). Note: the responder side is not yet wired on the phone-as-host path (P-188); this table specifies the protocol the dialer already speaks.
The companion asks a desktop “what apps are installed on you?” over a single authenticated iroh stream. One method, read-only, no install/start/mutate verb.
- ALPN:
xenon/companion/apps/1· version:1· method:apps.list(the only v1 method). - Framing: one newline-delimited JSON request → one newline-delimited JSON response.
- Bounds:
MAX_MESSAGE_BYTES = 256 KiB,MAX_APPS = 1024. - Auth: rides the already-authenticated, membership-checked group session — the lane opens only after membership passes at dial time. There is no in-protocol credential; authorization is the session.
Request ListAppsRequest (serde(deny_unknown_fields)):
| Field | JSON key | Type | Value |
|---|---|---|---|
| version | v | u64 | 1 |
| id | id | string | correlation id (e.g. android-list-1) |
| method | method | string | apps.list |
| params | params | object | {} (empty in v1) |
Response ListAppsResponse → result: LauncherAppsView or
error: {code, message}:
LauncherAppsView | Type | Notes |
|---|---|---|
computer_name | string | ≤128, non-control |
apps[] | LauncherAppEntry | ≤1024 |
LauncherAppEntry | Type | Validation |
|---|---|---|
name | string | ≤256 |
serving_origin | string | ≤2048, must start http:// | https:// |
isolated_app_url | string? | ≤2048, must start isolated-app:// if present |
Error codes: unsupported_request (wrong version/method) ·
invalid_request (parse failure).
Companion ALPNs (the phone’s three, for the wire registry — companion-core @b0cfc90; distinct from the launcher-side registry in the transport beat):
| ALPN | Purpose | Phone direction |
|---|---|---|
xenon/net/consent/1 | consent request/attest | inbound + registered (only dispatched ALPN) |
xenon/companion/apps/1 | this apps.list protocol | outbound dial (responder unwired, P-188) |
xenon/substrate/1 | on-device plane route_info + open | outbound dial |
LauncherAppEntry is the launch descriptor minus
the tier flag — the two should converge if d/104 ratifies.
Companion audit hash-chain v1
Status: spec candidate — as-built from
companion-android/.../SessionStore.kt:95-112@b0cfc90. Tamper-evident (each line commits to the prior), not append-only or externally anchored.
record := seq "|" ts "|" action "|" detail "|" hash "\n"
seq := monotone line index (lines + 1)
ts := ISO-8601 "yyyy-MM-dd'T'HH:mm:ss.SSSZ" (millis + tz)
prevHash := prior record's hash field, or literal "genesis" at chain head
payload := seq "|" ts "|" action "|" detail
hash := hex(SHA-256(prevHash + "|" + payload))[0..16] // 64-bit truncated
sanitize := detail/action → [\r \n |]+ collapsed to '-', field capped 2048 bytes
Actions observed in-tree: wake, kill, off, cross, list, launch, resume.
Invariants worth keeping if this hardens: off is written only on an explicit
clean STOP; every other teardown (crash, failed bind, onDestroy without STOP)
is kill — the ledger records how a session ended, not just that it did.
Convergence note: the agent-layer AuditEntry.prevHash
(injection-boundary-design.md §3.4) is the same scheme; the product should
land one ledger discipline, not two subtly different “audit logs”
(agent-layer review).
Launch descriptor (d/104)
Status: spec candidate — PROPOSED shape from
decisions/104-cf-guest-launch-seam.md(Luke disposes; nothing dispatches until ratified). Grounded in d/100 (plane origin) + d/101 (tiers) + d/103 (compose UIs are plane origins).
LaunchDescriptor {
label: string // user-facing app name (from the host xe catalog)
plane_origin: URL // http://<app>.localhost:5454 (d/100)
tier: regular | super // d/101 — super = IWA / isolated-app://
}
One descriptor shape serves every platform and both surfaces — the desktop
fleet catalog (P-197) and the parked companion apps.list responder (P-188)
would feed from it, replacing per-app baked guest URLs with a single route.
Android’s LauncherAppEntry is
already this descriptor minus the tier flag.
Grant / Lease delegation model (UCAN-aligned)
Status: spec candidate — an outline built from the shipped primitives
xenon-cap/src/lib.rs:30-84andxenon-grant/src/lib.rs:23-53,233-291@b0cfc90, plusinjection-boundary-design.md §3(paper). Thecapabilities[] → grantminting pipeline and theagent://actenforcement are not yet wired — this specifies the model those would target. See the agent-layer chapter.
Capability = (resource-URI, ability-verb, constraints{ paths, ttl, max_calls,
fields, budget })
Grant = runtime-authority-signed Delegation → install-principal,
audience + expiry, grouped in a named/versioned Bundle,
onward:false // no re-delegation
Lease = short-lived attenuation minted UNDER a grant:
nonce + useBinding(single-use | count | session)
+ notBefore/notAfter + signer + audit-ref
AuditEntry = ledger twin per lease-use:
{ leaseId, nonce, before, after, cause, prevHash }
UCAN mapping (the recommended external anchor — a product decision, not an implementation detail):
| This model | UCAN analog |
|---|---|
| Grant | a UCAN delegation — issuer = runtime authority, audience = install principal, capabilities, exp |
| Lease | an attenuated UCAN invocation used once — the nonce is the CID-equivalent replay guard |
onward:false | a leaf carrying no further delegation proofs |
| agency-ladder rung (d/082, d/091) | the ability-verb axis under agent://act |
Open decisions intersecting this shape: CM-1 (the v1 grantable set);
CM-2 (outbound network — declared-only vs. block-catalog); whether
agent://act is enforced in v1 or stays declared + audited; and D-5 (the
lease-signing root, given the shell shares an address space with the forgeable
iwa: IPC channel). All decider = Luke.
Xe App Manifest (XAM) v0.2
Status: spec candidate — an as-built extraction from
crates/apps/src/model.rsatxenon-launcher main @56e94575.validate()(:564) is authoritative; the emitted JSON Schema (:674) is advisory by its own admission. Field names may change with the tiers/rendering decisions (app-model review).
Required: xam_version = "0.2", identity, source, components. Optional:
endpoints, capabilities, config_schema, secret_bindings, instances, serving.
Reserved (parsed + stored, not acted on): handlers, interfaces, notifications.
| Group | Field | Type | Notes |
|---|---|---|---|
| identity | package_id | string | non-empty; xenon/* system-reserved |
| identity | publisher | Signed{key_id} | UnsignedDerived{canonical_url,subdir} | compose ⇒ always UnsignedDerived |
| identity | derived id | — | sha256("xenon.unsigned-package.v1\0" + url + "\0" + subdir) (:736) |
| source | kind | compose | git | image | web | static | swbn | image/web/oci have no adapter yet |
| source | pinned | string | sha256 digest or git commit |
| component | type | ui | service | task | task-only fields on a non-task ⇒ reject |
| component.runtime | driver | cf | iwa | origin | oci | compose | driver decides how it renders/runs |
| endpoint | class | http{port,routes} | tcp | udp | lan_discovery | non-http ⇒ separate consent |
| endpoint.route | flags | websocket, sse, range, no_buffer, large_body, long_timeout | per HttpRoute (:377) |
| capability | {resource, ability, reason} | strings | sorted + unique, else reject |
| resources | {request, required_minimum, breach} | envelope | request ≥ required_minimum enforced; container-limit enforcement is a TODO (EB-5) |
Adapter stages (crates/apps/src/adapter.rs:242), the contract a source
kind is compiled through: probe (pure) → resolve (may fetch) → analyze
(pure → Plan with unsupported_features) → materialize (executes, only
under a matching BuildGrant) → compile (pure → PackageRelease) →
validate (against LauncherFeatures). A non-empty unsupported_features
prevents materialization.
Compose-subset security profile — NORMATIVE refusal list
Status: spec candidate (normative) — as-built from
compatibility_diagnostics,crates/apps/src/compose.rs:907@56e94575. Install fails closed if any item appears; each yields a stable code +source:line:col.
privileged · raw_devices (devices:) · host_networking
(network_mode: host) · network_namespace_join
(network_mode: container:/service:) · pid_sharing (pid:) · ipc_sharing
(ipc:) · ambient_env_file (env_file:) · added_capabilities (cap_add:)
· host_user_namespace (userns_mode: host) · unconfined_security_opt ·
unpinned_image (must be @sha256: or a fixed non-latest tag; build-only
exempt) · host_path (bind mounts) · docker_socket
(/var/run/docker.sock) · out_of_tree_build_context · ssh_forwarding
(build.ssh) · build_entitlements · host_port_publication (ports: — use
expose) · external_network · external_volume.
Interpolation profile (interpolate_declared, compose.rs:856):
${VAR:-} / - / ? / + operators are forbidden; only keys present in the
declared config_schema may interpolate; non-scalars are rejected.
⚠️ Known holes (P-174, open): short-form host ports (ports: ["8080"]),
non-UTF-8 corruption in interpolate_declared, and network_mode: container:.
P-174 gates running compose on user-supplied manifests — see
EB-3.
Engine socket contract v1 (d/094)
Status: spec candidate — as-built from
crates/launcher/src/backend.rs,origin.rs, andcrates/substrate/src/backends/smolvm.rs@56e94575. This is the “third path” of d/094: no sidecar, no loopback publication of backend ports.
| Concern | Rule | Anchor |
|---|---|---|
| Discovery | exactly-one docker.sock under <data-root>/engine/smolvm/ (depth ≤ 8); zero or many ⇒ refuse | smolvm.rs:276 |
| Custody | launcher sets DOCKER_HOST=unix://… (Linux) / tcp://127.0.0.1:2375 (mac); the router never holds the socket | backend.rs:854 |
| Reach container | docker compose exec -T <svc> sh -c 'nc 127.0.0.1 <port>' — never host-published, never a sidecar | backend.rs:827 |
| Connector | one launcher process on 127.0.0.1:5199, started iff the app has_backend | origin.rs:708, manifest.rs:333 |
| Lifecycle | singleflight start · WaitClass wait-set · 503 (slow) / 502 (failed) split · invalidate / stop | backend.rs:188 |
| Tamper check | pinned launcher + binary sha256 verified before exec | smolvm.rs:301 |
Install-consent grant model
Status: spec candidate — as-built from
crates/apps/src/lifecycle.rs,adapter.rs,runtime.rs, andcrates/launcher/src/consent.rs@56e94575.
Two non-interchangeable gates:
| Gate | Subject digest | Grants | Anchor |
|---|---|---|---|
| Plan Review | sha256(plan) | BuildGrant (unforgeable: minted only from a matching gate-1 consent) | adapter.rs:93 |
| Grant Consent | locked release digest | authorizes the runtime to build a runnable project | lifecycle.rs:109, runtime.rs:138 |
Custody invariants: a consent request is an inert JSON record
(pending/<id>.json) minting no authority (consent.rs:75); deciding claims it
by atomic rename pending → claimed → decided (single executor);
MAX_PENDING=8, REQUEST_TTL_SECS=15 min. The Console / web bridge has no
approval endpoint by construction — only a launcher-owned surface (xe consent / macOS tray) mints authority (consent.rs:1-28). This install
consent is distinct from the cross-origin Grant table (grants.rs,
d/086; scoped, request-level, ≤ 3600 s CORS grants) — do not conflate “install
consent” with “cross-origin grant.”
Glossary & vocab
The shared vocabulary of the site, in one place. The product uses a small, canonical set of names precisely — the calm-vocabulary note explains why the words are load-bearing. Terms are grouped for reading; each entry is one to three lines with an external primer where one helps. Where a name is still settling, the entry says so rather than implying it is fixed.
The product surface
- Xe Computer — the cross-platform runtime (home:
xe.computer). The whole “missing layer between the browser and the OS.” Not “the launcher.” - Xe Computer Boot Service — the on-demand wake mechanism that stands the runtime up and lets it sleep; zero resident processes when idle. Not a “daemon,” not an “agent.”
- Xe Computer console — the management UI; the witness for receipts and the update ledger
(d/064).
xe— the command-line interface. - app engine — the substrate layer inside Xe Computer that runs app backends. The concrete engine names (colima, lima, docker, smolvm, WSL2) never appear in user copy.
- plane — the local, loopback-only serving surface bound to
127.0.0.1:5454, where every installed or peer-served app appears at a stablehttp://<app>.localhost:5454origin (d/100). - origin router — the small, pinned web server (workerd) behind the plane whose routing table is generated from installed-app records, never hand-edited.
- console / catalog / forge — see catalog and forge under Distribution.
Browsers & shells
- Prism — Agent54’s super-app shell: itself an Isolated Web App that frames each app in a Controlled Frame, composes apps, and journals every agent action. The canonical super web app.
- Xenon — Agent54’s Isolated-Web-App-enabled Chromium fork (the browser, not the
runtime). Its differentiator is that IWAs work without developer flags. Phrasing note: the
decisions of record (d/008, d/061) name Helium as the confirmed Xenon base and the site’s
shell primer states this; that specific “Helium-based” wording is pending a quick human
confirmation before it hardens (agenda item 34) — the base decision itself is not re-opened.
Repo names like
xenon-*are historical. - Helium — a privacy-forward, de-Googled Chromium build that ships Isolated-Web-App support; the base Xenon forks. imputnet/helium.
- darc → Reagent — the resident agent layer that acts through Prism. darc is the current research-preview name; Reagent is the decided rename (d/053), executed later by Luke + Jan. New copy writes darc → Reagent until the rename is performed.
- super web app / super user agent — an app granted fabric-level capabilities (the L3 tier); interchangeable terms, and never framed as “privileged.” Prism is the example.
The two capability axes
- install spectrum (L0–L3) — a property of the app (d/076): L0 visited · L1 installed (has a home, icon, stable origin; installed unmodified and un-Xe-aware as a hard promise) · L2 capability-enhanced · L3 super (Controlled-Frame-hosting, agent-grade). One substance (a web app), one axis, no cliffs. See the two axes.
- agency ladder — a property of the agent’s action (d/082, revised d/091): nothing → allowed / denied / shielded → observed → enhanced → mutated → controlled → driven → composed / generated / delegated → super user agent. Every rung is consented, receipted, undoable.
- shielded — an agency-ladder state where the agent actively guards a surface; permission denied is a first-class outcome, not an error.
- driven — the rung where the agent operates a live surface over the Chrome DevTools Protocol (CDP), the wire protocol browser automation uses.
Web-platform primitives
- IWA (Isolated Web App) — a Chromium mechanism for a web app packaged and signed as a self-contained bundle, running in stronger isolation than an ordinary tab and permitted powerful capabilities. Explainer.
- swbn (Signed Web Bundle) — the packaged, Ed25519-signed
.swbnfile an IWA installs from; the origin is key-derived (isolated-app://…). Reserved for the super tier (d/079, d/101). - Controlled Frame — a strong, fully partitioned embedding element available inside an IWA —
a far stronger cousin of the
<iframe>— that lets a host app (Prism) frame and mediate another app. Explainer · spec. - COOP / COEP / CORP — the response headers (
Cross-Origin-Opener/Embedder/Resource-Policy) that together yieldcrossOriginIsolated: true, a precondition for hosting a Controlled Frame. - CSP (Content-Security-Policy) — an HTTP header declaring exactly which origins a page may load, connect to, or frame; a tight CSP is a declared trust boundary. MDN.
- secure context /
localhost— browsers treat*.localhostas a potentially-trustworthy (secure) origin, which is why plane origins get the web’s isolation without per-app packaging.
Transport, identity & trust
- iroh — a peer-to-peer transport that dials a device directly by its public key, using relays only to help peers find each other and as a fallback when a direct connection is not possible. iroh.computer.
- ALPN — Application-Layer Protocol Negotiation: the short string (e.g.
xenon/serve/tcp/1) that names which protocol a QUIC/TLS connection speaks; the registry lives in Part V. - relay — a third-party (default: number0) or self-hosted server that helps two iroh peers
connect; sees connection metadata but not payload. A self-hosted
iroh-relayis required before production (d/014). - membership plane / group document — the signed, hash-chained document that says which devices
are in your group; supports enrollment, immediate revocation, and offline verification. Two
lineages exist (v1
crates/net/group, v2crates/xe-sync); which is canonical is an open decision. - ticket — the bearer credential + address card a publisher hands a consumer to serve an app
peer-to-peer; a 32-byte secret in a
0600file today. See Xe Peer Ticket v1. - companion — a phone as a remote client and consent surface, not a sync peer (d/042).
- Ed25519 — a widely used public-key signature scheme (RFC 8032); the device identity is its Ed25519 key.
- serial anchor — a monotonically increasing number tying each catalog release to an order, so rollbacks are detectable (d/067).
- UCAN — User-Controlled Authorization Networks: a signed, attenuable, chainable capability token (issuer/audience are keys as did:key) that needs no central server to verify. Xe carries UCAN-compatible semantics with the wire format deferred (d/027).
- capability / grant / lease — the delegation vocabulary: a capability is
(resource × ability × constraints); a grant is a long-lived, authority-signed delegation to an install principal (onward:false); a lease is a short-lived attenuation minted under a grant per action. Outlined in Part V. - consent gates (Plan Review / Grant Consent) — the two non-interchangeable install-time gates, distinct from a cross-origin grant (a request-level, ≤3600 s CORS allowance). Do not conflate “install consent” with “cross-origin grant.”
Packaging, engines & distribution
- XAM (Xe App Manifest) — the manifest carrying an app’s
identity,source, andcomponents; the driver (not the packaging) decides how a component renders. See XAM v0.2. - XAA — the Xe export archive format that backs update snapshots and per-app export.
- microVM / smolvm — a lightweight virtual-machine isolation boundary for untrusted backends; smolvm is the default app engine that publishes a unix socket the connector reaches through (d/094). The standalone-firecracker/microVM lane is retired onto this track (d/028 amendment).
- compose subset — the fail-closed, install-time-validated safe subset of
docker-composean app may declare; anything that would puncture microVM isolation is refused with a stable code. See the normative refusal list. - catalog — the signed, static, Ed25519-signed, serial-anchored index that publishing produces; install verifies signature + content hash before anything runs (d/067).
- forge — the git host Agent54 owns (Forgejo on the NAS), the origin of record; “the forge is home” (d/007, d/025). Pushes go to the forge only (d/013).
- Folknet — the community compute fabric that backs the fleet.
- fleet — your set of peer devices, each running its own Xe Computer instance, syncing to each other with no central hub.
- FROST — a threshold-signature scheme reserved (not built) for future quorum-based key recovery of the group authority.
Naming discipline (things this glossary keeps apart)
- capability-model / injection-boundary (the product delegation model) vs.
delegation-interfaces.md(an unrelated broker) — distinct; do not conflate (agenda item 33). - install consent (the two gates) vs. cross-origin grant (a scoped CORS allowance) — see above.
- Xe Computer (runtime) vs. Xenon (browser) vs. Prism (shell) vs. darc → Reagent
(agent) — four different things; the
xenon-*repo names are historical.
Repo & link directory
Where each piece of the system actually lives, and every external reference the site leans on — each with a one-line “why we care.” Our repos are on the Agent54 forge (Forgejo, the origin of record, d/007); distribution stays cluster-internal until Luke flips it public (d/065), so forge URLs are reachable from inside the fabric. External links are public.
Our forge repositories
Forge web base: forge.discus-mora.ts.net:3000/agent54/<repo> (cluster-internal). Code citations
throughout the reviews pin commits on these repos.
| Repo | What it is | Why we care |
|---|---|---|
xenon-launcher | The runtime/launcher monorepo (crates/launcher, crates/apps, crates/net/group, crates/substrate, crates/xe-sync, package-index). | The core: the plane, origin router, app model, engine socket contract, peer serving, and the membership plane all live here. Most Part III citations are @56e94575; post-P-201w main is @2671bea9. |
prism | The super-app shell (an IWA): Controlled-Frame hosting, the frame↔shell channel, composition, the hash-linked journal. | The rendering layer and the agency-ladder host; Part III shell citations are @98acaa3. |
xenon-runtime | The device-fabric runtime and the phone companion (companion-core, companion-android, the on-device plane). | Enrollment, membership over iroh, the companion apps.list protocol, the session audit chain; devices/agent citations are @b0cfc90. |
android-iwa | A patch series (three patches + gn args + validation scripts) on a stock Chromium base, not a full fork. | The Xenon-on-Android track; the fork-maintenance cadence is an open gap. |
xe-example-apps | The example-app monorepo (Excalidraw, guestbook, compose-board, and the dev-signed swbn samples). | Proof material for every app kind; kept out of any super-app’s repo by d/079. Citations @5752a3e. |
xe-darc | The agent-layer research preview (darc) and its design docs (injection-boundary-design, capability-model-design). | Where darc → Reagent and the delegation model are being worked out; the least-built, highest-mission layer. |
iroh (fork) | agent54/iroh — upstream iroh plus one commit exposing a network-change hook; explicitly temporary. | The transport; the fork is meant to be upstreamed and repinned vanilla (d/046). |
xe-docs | This site (mdBook). | The successor review surface to the retired docs/map.html (d/099). |
The trust chain & key names
- catalog — the signed static index publishing produces (Ed25519, serial-anchored, d/067); install verifies signature + content hash first.
- console — the witness surface for receipts and the update ledger (d/064).
- Production signing keys do not exist yet — the orchestrator holds only build-phase seeds (d/066); the ceremony is an open scheduling item.
External references — web platform
- Isolated Web Apps — explainer — the IWA / Signed-Web-Bundle mechanism the super tier is built on.
- Controlled Frame — WICG explainer · spec — the partitioned embedding element Prism frames guests in.
- workerd — Cloudflare’s runtime; our pinned, launcher-managed origin router.
- Chrome DevTools Protocol — the wire protocol the agency ladder’s driven rung uses.
- Content-Security-Policy — MDN — the declared trust boundary the shell’s CSP is.
- Helium · Chromium — the browser base Xenon forks (phrasing pending confirmation; see glossary).
External references — transport, identity & capabilities
- iroh — the peer-to-peer transport (dial by public key; relays for discovery + fallback).
- UCAN — working group & spec · delegation · invocation — the capability-token model our grant/lease semantics align to (d/027).
- did:key — expressing a public key as a DID, free for us since device keys are already Ed25519.
- Ed25519 — RFC 8032 — the signature scheme for device identity and the catalog.
- UnifiedPush — the sovereign, self-hostable push option weighed against FCM for the Android wake path.
External references — engines, apps & tooling
- smolvm — the default microVM app engine; gets the socket-connector capability first as an upstream contribution (d/094).
- mdBook — the static-site tool this book is built with; the Mermaid diagrams use mdbook-mermaid.
- Excalidraw — the real production app proven end-to-end through the peer pipeline (P-211).
- Immich — the compose / whole-origin target gated on d/103-B (it dropped its ML service to install; see GPU/ML gap).
- Patchwork — the first real-app target named by d/076.
External references — the audience & context
- Internet Archive — the first audience; Brewster Kahle has seen the work and the backing is “warm” (d/041).
- the decentralized web (DWeb) · User & Agents — the local-first / agent community that will check the sovereignty litmus first.
The proving matrix
The receipts. This is the full ●/◇ table the Big Picture
summarises — what has been built-and-run versus what has a design of record but is not yet
demonstrated end to end. Source material: the build-and-run matrix in
status/2026-07-18-night.md and its morning addendum, which closed the Windows column when
P-201w merged. Legend: ● proven (verified in the 2026-07-18 matrix) · ◇ designed-only /
blocked (design of record, not yet demonstrated) · — (not applicable / not this leg).
Every ● below is backed by CI-green + a second-mind verdict quoted in the night record; every ◇ names what gates it.
Platform legs — serve, consume, in-frame render
| Platform | Serve an app | Consume an app | Render in Prism’s frame | Evidence / gate |
|---|---|---|---|---|
| Linux | ● | ● | ● | Night matrix, all legs on the smolvm engine socket path. |
| macOS | ● | ● | ● | P-208 (serve) + P-201m (macOS consume-in-frame). |
| Windows | ● | ● | ● | Serve leg P-206 (native VM 200, launcher PR #62); in-frame consume P-201w (launcher PR #64), merged in the morning addendum — column fully closed. |
| Android | — | ● (on-device plane) | ◇ blocked | Consume proven on the on-device plane (P-202); the two frame cells (in-frame render, in-frame offline honesty) wait on PROPOSED d/104. |
The two-device story holds end to end: Device B publishes over iroh and the payload arrives byte-identical (matching sha256); a source drop shows an honest offline notice and recovers on return (P-184).
App kinds — all four proven
| App kind | Status | Evidence |
|---|---|---|
| Static | ● | static-v0 / Origin driver; Excalidraw is the exemplar. |
| Live-backend (single service) | ● | guestbook through install → start → serve → recover (P-141 + P-203). |
| Single-page app (vite / built) | ● | vite-SPA kind exercised in the fleet catalog. |
| Compose (multi-service) | ● | compose-board through the full lifecycle (P-203 auto-wired end to end). |
The fleet catalog that lists and launches all of these is merged on prism main (@98acaa3,
PR #18), with CI runs down to 24–31s (previously red with ~51-min stalls, fixed by PR #19).
Real applications & end-to-end pipelines
| Capability | Status | Evidence / gate |
|---|---|---|
| Real production app inside Prism’s frame | ● | Excalidraw runs the whole peer pipeline (P-211; xe-example-apps @5752a3e, PR #2). |
| Two-device serve → consume, byte-identical | ● | P-184; matching sha256, honest offline notice, recovery on return. |
| Android companion (membership + consent + audit) | ● | v1 phone-as-client: consume, launcher, membership/consent, tamper-evident session ledger. |
| Whole-origin compose UI (e.g. an Immich UI) | ◇ gated | P-210 gated on PROPOSED d/103-B (route type) + the connector socket-custody reconcile; Immich sits at L0 today. |
| Peer-sync v1 (two desktops, metadata only) | ◇ designed | Design of record landed (d/068); not yet wired — volumes stay single-home. |
| Xenon fork artifact | ◇ designed | Not yet built; today = stock Chromium + a flag recipe (d/057). |
| Console reads Prism’s journal (same device) | ◇ open seam | No wiring yet; named in the system spine. |
| App update loop (discover → consent → apply / rollback) | ◇ built, idle | The engine is library-complete but has no user loop (EB-1). |
| Authed-wake (peer wakes a sleeping device) | ◇ designed | Honest disabled stub only; the socket-activation crux is unresolved. |
The night’s merged packets (final main per repo)
For traceability, the merges behind the ● rows above:
xenon-launcher→@2671bea9: P-197 (#56), P-209 hardening (#58), P-202 path (#59), P-203 backend auto-wire (#60), P-204 origin correctness (#61), P-206 Windows peers (#62), P-201w Windows consume-in-frame (#64, morning addendum).xenon-runtime→@ed028a7: P-212 (#9), P-214 first-run (#10), P-213 fronting (#11), P-202 on-device plane (#12).prism→@98acaa3: fleet catalog (#18), CI hardening (#19).xe-example-apps→@5752a3e: P-200 (#1), P-211 Excalidraw (#2).
P-201w’s second-mind verdict was MERGE-READY: gates independently reproduced (cargo test 740
passed / 0 failed, clippy clean -D warnings), clean rebase, workerd-windows 1.20250718.0
hash-verified. The 10s dial deadline is a consciously-ratified honest-offline behaviour on all
platforms.