Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 like http://<app>.localhost:5454. Because browsers treat *.localhost as 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://&lt;app&gt;.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.

CapabilityStatusEvidence / what gates it
Serve and consume an app on Linux, macOS, and Windows● provenNight matrix, all three OSes (Windows in a native VM)
All four app kinds: static, live-backend, single-page app, compose● provenFleet catalog merged and exercised
Real production app inside Prism’s frame● provenExcalidraw runs the whole peer pipeline
Two-device story end to end● provenDevice 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)● provenApp renders on-device; cross-origin isolation verified on the emulator
Android in-frame render + in-frame offline honesty◇ blockedPending the PROPOSED d/104 launch-seam decision
Peer-sync v1 (two desktops, metadata only)◇ designedDesign of record landed; not yet wired
Compose whole-origin route (e.g. an Immich UI)◇ gatedPending the PROPOSED d/103-B route-type decision
Xenon fork artifact◇ designedNot yet built; today = stock browser + flag recipe
Console reads Prism’s journal (same device)◇ open seamNo 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.

  1. The Big Picture (you are here) — the whole system on one screen.
  2. 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.
  3. 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.
  4. Part III — Design Reviews — a critical read of each layer against the litmus: what is strong, what is thin, where the seams are.
  5. 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.
  6. Part V — Emerging Specs — protocol and format candidates as they firm up.
  7. 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:

  1. Inspect what it sees — you can look at the context, inputs, and state the agent acts on.
  2. Fork its permission model — you can change the rules of what it may do, not just toggle vendor-provided switches.
  3. 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.)
  4. Move its memory — your state and data are portable, not held hostage.
  5. Audit its actions — every action leaves an inspectable trail.
  6. 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.
  • Xenonour 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:

LevelMeaning
L0Visited — opened, not installed.
L1Installed — 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.
L2Capability-enhanced — granted extra platform capabilities on request.
L3Super — 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 origin http://<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>.localhost naming 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 :5196 is 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:5196 via a launcher-managed, name-constrained local certificate authority (DNS .localhost only). 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 _headers file (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-scope Set-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 binds 5454 (Agent54 → 54:54); guest origins are http://<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 — 5454 is the user-facing plane; the internal peer forward stays 5198. Decided: the plane origin is 5454; the internal workerd → iroh plumbing keeps PEER_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-origin route type or overloads the shipped backend-proxy type. 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_class field). 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-relay is 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/iroh fork 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 by docs/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-origin route type, or overload the shipped backend-proxy? Security-critical: it widens the connector boundary from a single /api prefix 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 from 5196 to 5454. d/080’s name-constrained-CA machinery is still valid, but its “HTTPS is the product default” ruling is narrowed, and d/078’s :5196 ingress 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 MarkUi recipe 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.)

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

  2. Rule on d/103-B. A distinct container-origin route type versus overloading the already-shipped backend-proxy type. 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 /api prefix.

  3. 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 export exists 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.

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

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

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

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

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

  9. Schedule the self-hosted iroh relay. d/014 requires a self-hosted iroh-relay before 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.

  10. 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-launcher at main @56e94575 (“P-215: deterministic router conformance tests”). The xenon-* 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:

  1. 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.
  2. 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.
  3. 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_hash chain, 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);
  • revocations is append-only;
  • opaque statements give 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:

ALPNConstant / sitePurposeMembership-gated?
xenon/net/echo/1ALPN, crates/net/src/lib.rs:25Original echo experiment; NetworkProvider::dial defaultOnly if built with_membership
xenon/serve/tcp/1SERVE_ALPN, crates/net/src/lib.rs:31Peer-serve bearer-ticket byte tunnelNo — bearer secret only
xenon/net/group-sync/1GROUP_SYNC_ALPN, crates/net/src/group/protocol.rs:31Membership-gated head gossipYes
xenon/net/group-join/1GROUP_JOIN_ALPN, crates/net/src/group/protocol.rs:33Invite-gated join ceremonyPre-membership (invite + proof-of-possession)
xenon/substrate/1ALPN, crates/substrate-proto/src/wire.rs:14Remote substrate multi-lane session (dial_session, group/node.rs:414)Yes (member-checked at dial)
xenon/consent/test/1EXTRA_ALPN, crates/net/tests/extra_alpn.rs:25Test-only — proves extra ALPNs inherit the membership gateYes

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

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-launcher at main @56e9457. The router source lives at crates/launcher/assets/route-worker.js (request logic) and crates/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 name localhost — and, by RFC 6761, any name ending in .localhost such as photos.localhost — always resolves to loopback, with no DNS lookup. That is why every Xe app gets its own <app>.localhost origin 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:

  1. 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).
  2. 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.
  3. Fail closed, explain in plain language. Anything the router cannot safely serve becomes a small text/plain response that names the next action (“Run xe 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)

PortRoleSource
5454The Xe local plane — the single browser-facing origin http://<app>.localhost:5454fleet_catalog.rs:24 LOCAL_PLANE_PORT; origin.rs:34
5199Launcher-owned backend connector (workerd → container)origin.rs:708 BACKEND_CONNECTOR_ADDR
5198Peer-forward (workerd → iroh loopback forward), present but dormant until a peer route existsorigin.rs:728 PEER_FORWARD_PORT
per-peerFleet-catalog consumer forwards, one binding eachfleet_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: nosniff on 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 honest 503 naming 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):

  1. Entry & peer-origin unwrap (:453). An internal x-xe-peer-request-origin header 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 .localhost or it is ignored (:462-467). Authorization for a peer tunnel is the iroh bearer-ticket handshake, not this header (:458-459).
  2. Method floorCONNECT/TRACE400 (:381).
  3. Reserved service paths — on the loopback host (127.0.0.1[:port]), /xe/api/index serves the published app index and /xe/api/catalog the merged fleet catalog (:384-385). These are the only non-.localhost host the router answers.
  4. Hostname guardHost must match the strict grammar ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.localhost(?::PORT)?$, and url.hostname must equal label + ".localhost" (:386-388). This closes host/URL-authority mismatch. IPv4 and IPv6 are separate binds.
  5. Path safetydecodeURIComponent in a try/catch (400 on malformed percent-encoding), then reject \, NUL, or any .. segment (:390-391).
  6. Fetch-Metadata — derive sameOrigin, crossSite, safe (GET/HEAD), and upgrade (websocket) from Origin and Sec-Fetch-Site (:392-399).
  7. OPTIONS preflight — a matching live grant → 204; else a declared static CORS allowance for a safe GET/HEAD off a non-backend prefix → synthetic 204; else 403 No capability grant authorizes this cross-origin preflight (:400-413).
  8. Route lookuprouteFor() 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.
  9. Cross-origin arm — with an Origin that is not same-origin: a matching grant runs a credential check (cookies/Authorization refused unless the grant says credentials:"include") then serves static reads only with a live byte-ceiling; no grant means allow only a declared-static safe path, else 403 Cross-origin request refused (:417-429).
  10. Same-origin CSRF floor — a same-origin-looking but cross-site unsafe or websocket request → 403 Cross-site request refused (Fetch Metadata) (:430-432).
  11. Dispatch — honest 503/404 for missing/not-installed/unsupported; peerservePeer; a backend-prefix path → serveBackend; a non-safe method on a static route → 405 with Allow: GET, HEAD; otherwise serveRouteStatic (: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), and guardStream re-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 installed404 “This app is not installed. Run xe app install PACKAGE…”.
  • Backend cold or unreachable503 with Retry-After: 2 (“backend connector unavailable” / “backend not reachable”).
  • Peer offline503 “This app runs on <device>, which is offline. Retry, or open it on that device.” plus Cache-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-launcher at main @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-secret parameter 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_file refuses any ticket whose mode has group/other bits set (mode & 0o077 != 0) and tells the operator to chmod 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), opens create_new with share_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):

PortConstantRole
5454LOCAL_PLANE_PORT (fleet_catalog.rs:24)The browser-facing local plane — what URLs carry
5198PEER_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), writes xe/published-index.json plus a .sha256 sidecar, and appends a hash-chained audit record peer_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) and connector = "peer_" + peer_key[..16] (:254-260).
  • register_peer (:234-294) rejects port == 0 and port == 5454 (never squat the plane port), rejects a port already owned by a different peer, upserts by id, sorts, and writes durable peers.json and the workerd mirror xe/peers.json atomically.
  • cache_verified_index (:298-343) bounds the index at 1 MiB (MAX_INDEX_BYTES), validates schema/identity/label/order (validate_index, :410-440), audits peer_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. binaryPeerStatus collapses live|degraded → "live", else "offline" (:74-76). Offline peers are retained with last_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-secret is 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.

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):

  1. 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.
  2. 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.
  3. 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):

TierWhoHow it is servedRequirements
Regular apps (default, ~all apps)ordinary web appsplain HTTP on the plane origin http://<app>.localhost:5454, framed and isolated by Prism’s controlled framenone 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 tiersigned 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_version is "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):

FieldRequiredMeaning
xam_versionyes"0.2"
identityyeswho this app is (below)
sourceyeswhere its code comes from (below)
componentsyesthe parts that run (below)
endpointsnodeclared network surfaces
capabilitiesnorequested grants, with reasons
config_schemanodeclared config keys (also gates compose interpolation)
secret_bindingsnonamed secret slots
instancesnoinstance shaping
servingnoonly a narrow SPA-fallback subset is recognized today
handlers, interfaces, notificationsnoreserved — 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 fileWhat it actually is
excalidrawpackage.jsona full XAM v0.2 (kind=static, adapter=static-v0, driver=origin, serving.spa_fallback=true)
scratchpad, appdex*.xam.jsona full XAM v0.2 (kind=swbn, driver=iwa, publisher signed, key_id=agent54-dev)
guestbook, compose-boardpackage.jsona build-input descriptor (schema_version:1: package, version, display_name, compose, data_classes, gates) — the compose adapter compiles this into a XAM
xe-notes, vitehellopackage.jsonordinary 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.json is a byte-for-byte copy of the app’s compose.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.kindadapterdriverConcrete instanceProven?
staticstatic-v0Originexcalidraw (v0.18.1)Yes — P-211 merged, live render, byte-identical on both peers
swbnswbnIwascratchpad, appdex (dev-signed); catalog console/hello-xeswbn adapter tested; shells pinned (swbn.rs:434) — the d/101 super tier
compose (frontend)composeCfguestbook frontendYes — frontend maps to Cf (compose.rs:1225)
compose (service)composeComposeguestbook backend, compose-boardYes, live — P-141 lifecycle + P-203 full auto-wired end-to-end
compose (whole-origin UI)composeComposeImmichNo — package sound (P-207), render blocked → d/103 + P-210 (gated)
gitvia composeComposegit-sourced composepath exists; not separately proven in examples
image / web / ociNo 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)
  1. Analyze. The adapter’s analyze step produces a Plan. If its unsupported_features list is non-empty, materialization is prevented (adapter.rs:45, enforced compose.rs:1498). The install fails closed before anything is fetched or run.
  2. 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 is sha256(plan) — it approves exactly this plan, nothing else.
  3. Materialize (fetch + build), gated by a BuildGrant that can only be minted from a matching gate-1 consent.
  4. 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.
  5. Route registration. origin::generate derives the route table from registry rows (crates/launcher/src/origin.rs:495). An install with a payload digest gets a StaticDigest route; a backend-carrying install gets a BackendProxy route. Either way the app is reachable at http://<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.

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.yaml file 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:

  1. The browser (Prism) makes an ordinary HTTP request to the app’s plane origin, http://<app>.localhost:5454.
  2. The serving plane’s route worker recognizes a backend route and, rather than serving a file, forwards the request to a launcher-owned connector.
  3. The connector reaches the app’s container through the engine’s unix socket, using docker exec to touch the container’s port — never a published port, never a sidecar.
  4. 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:

ConcernRuleAnchor
Discoveryexactly-one docker.sock under <data-root>/engine/smolvm/ (depth ≤ 8); zero or many ⇒ refusesmolvm.rs:276
Custodylauncher sets DOCKER_HOST=unix://… (Linux) or tcp://127.0.0.1:2375 (mac); the router never holds the socketbackend.rs:854
Reach containerdocker compose exec -T <svc> sh -c 'nc 127.0.0.1 <port>' — never host-published, never a sidecarbackend.rs:827, 846
Connectorone launcher process on 127.0.0.1:5199, started only if the app has a backendorigin.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. ComposeBackendController resolves the project, route, and service entirely from launcher-written registry state — never from the HTTP request (backend.rs:416). The x-xe-app header is validated to [a-z0-9-]{1,63} (backend.rs:1091), and the route worker’s grants never cover the backend prefix (a 403, 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. SmolvmBackend refuses 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 one start (backend.rs:1573).
  • Wait class. A HEAD request, or a manifest-declared idempotent-GET prefix (matched on segment boundaries), is safe to hold while the backend warms → Wait. Everything else is Immediate fail-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 jittered Retry-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 names xe doctor as the next step (backend.rs:1110).
  • Self-healing edges. invalidate() drops a stale Ready on 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, and network_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.

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 a BuildGrant that is unforgeable by construction: BuildGrant::from_consent requires a gate-1 consent whose subject_digest == sha256(plan) (adapter.rs:93), and every adapter re-checks it at materialize (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), then decided/. 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 consent or 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::authorized refuses 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:1 states “the smolvm substrate is an explicit macOS experiment.” The productized default is AppEngineBackend — 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. SmolvmBackend now carries a Linux x86_64 path (smolvm.rs:66, pin 1.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_diff detects escalation across permissions, endpoints, config, and source.
  • apply_update takes a pre-update snapshot, stages, activates, and rolls back on failure; rollback_update restores artifacts and digest-verified volumes.
  • UpdatePolicy is Auto | Notify | Manual; auto_update_action only 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: + privileged a GPU needs; Immich dropped its ML service for this reason (a Luke ruling, per the review).
  • Resource limits are declared, not enforceddeploy.resources.limits in a compose file is not mapped to container cgroups.
  • The update engine has no user loop (above).

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/prism at main @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) and agent54/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 .swbn file 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:

  1. 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.
  2. 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.
  3. 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 partition persist:prism-fleet-catalog, :59, is used only as a transport to read the catalog JSON when Prism runs as an installed IWA and cannot fetch() 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-iwa is, 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-app baseline 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-known manifest 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)

HeaderValueEffect
content-security-policydefault-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-policysame-originIsolates the browsing-context group
cross-origin-embedder-policyrequire-corpEvery subresource must opt in
cross-origin-resource-policysame-originBlocks 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)

VerbArgsJournals 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

PartitionPurpose
persist:prism-fleet-appsThe visible guest pane; each app’s storage lives here
persist:prism-fleet-catalogHidden; 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.

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-runtime at main @b0cfc90 (companion-android + the companion-core Rust crate). Desktop proof is drawn from packet reports (packets/done/, lane REPORT.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:5454 that a device runs while its browser is open. Requests to <app>.localhost:5454 are 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 connectedDevice FGS — chosen because Android 15 time-limits the dataSync type, 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:

DeviceRole todayProof
LinuxReference 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).
macOSFull 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.”
WindowsFull 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).
AndroidThe 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.
iOSClient-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:

  1. Runtime enrollment (companion-core/src/lib.rs:120-171): the device redeems an authority-signed Invite (version, runtime identity, authority, token, expiry, signature), builds an enroll request in slot launcher with role Device, and verifies the returned EnrollmentAttestation.
  2. Group join (group.rs:244): a base64url JoinCode{invite, endpoint} is redeemed over iroh via GroupNode::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:5454 is bridged byte-for-byte over an open lane 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.dial grant on every open, 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 does refresh_routes (a real RouteInfo RPC that doubles as a liveness probe), then start_plane, then returns the <app>.localhost:5454 URL. 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): a connectedDevice foreground service started START_NOT_STICKY — the OS is told not to resurrect it, so “off means off” (:26,68). It binds a loopback readiness endpoint 127.0.0.1:47615 that answers READY <device> (:105-111). The connectedDevice FGS type is chosen over dataSync because Android 15 time-limits the latter (AndroidManifest.xml:37-40); Android 14+ requires this FGS to hold CHANGE_NETWORK_STATE (:6-9).
  • Honest kill vs. off. onDestroy without an explicit STOP writes a kill audit line (:161-170); a failed loopback bind tears down as kill (:118-141); a clean STOP writes off (: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 a status probe can answer without waking the device) plus the hash-chained audit log. deviceName sanitizes 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=false answers 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:

ScreenFunctionStates it renders
Apps (launcher) — primary surfacerenderLauncher (: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.
JoinshowJoin (:504)Paste join code + device name → “Join over iroh”. QR scan disabled (“coming next”).
Device rosterrender (:524)members / self / revoked · notice banner · Sync now · “Open apps ▸”.
Consent requestshowConsent (: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):

  1. Validate the target is an isolated-app:// URL — else audit cross outcome=refused-invalid-origin.
  2. Build the Intent: ACTION_VIEW (navigate) or setData(target) (restore, no navigate).
  3. setClassName("org.chromium.chrome", "com.google.android.apps.chrome.Main") — an explicit component to Xenon, never an app-chooser.
  4. 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 standalone WebappActivity) instead of coalescing into a tabbed-browser task — the P-160 fronting-over-an-existing-tab fix. There is no MULTIPLE_TASK, so an existing app task is re-fronted, never duplicated.
  5. 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 stringConst (source)Direction on the phoneWired?
xenon/net/consent/1CONSENT_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/1LAUNCHER_APPS_ALPN (launcher_apps.rs:23)Phone dials it outbound (launcher_apps_listdial_session(&addr, LAUNCHER_APPS_ALPN) group.rs:605). Phone-as-host responder is not wired (below).⚠️ dial-only
xenon/substrate/1SUBSTRATE_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_node never dispatches LAUNCHER_APPS_ALPN. Its stream loop is while 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/join register only vec![CONSENT_ALPN.to_vec()] (group.rs:233,272).
  • The responder exists but is never called: serve_list_apps_extra (launcher_apps.rs:271) checks incoming.alpn == LAUNCHER_APPS_ALPN and serves — but nothing in group.rs invokes it.
  • What serves today is only the debug TCP adapter bin/launcher-app-host.rs (used by the emulator rig) and the desktop xe host’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 reviewed CHANNEL_UNAVAILABLE copy (: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.list responder is unwired on the phone-as-host and the desktop-serve path (P-188) — the reviewed CHANNEL_UNAVAILABLE copy 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).

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-runtime at main @b0cfc90; darc citations are from agent54/xe-darc design docs (docs/darc-cf-alpha.md, docs/injection-boundary-design.md); the capability substrate is xenon-cap / xenon-grant at 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

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 by verify_chain / authorize_invocation (xenon-cap/src/lib.rs:30-84). This is the atom of permission.
  • xenon-grant issues runtime-authority-signed Delegation bundles to a per-install principal, grouped in a named, versioned bundle, with onward: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 / ConsentStatement are shipped and used by the companion: the Android consent screen (devices §3.2) signs a real ConsentAttestation on 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 '*', so Runtime.evaluate passes unconditionally (§1.1).
  • daemon/main.js is 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 not docs/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 is xenon-grant today.
  • 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 modelUCAN analog
Granta UCAN delegation — issuer = runtime authority, audience = install principal, capabilities, expiry
Leasean attenuated UCAN invoked once — the nonce is the replay-guard (CID-equivalent)
onward:falsea leaf with no further delegation proofs
agency-ladder rungthe 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://act is audited only; the capabilities[] → grant pipeline 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.

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 honest Unknown — with a test-only force_relay to 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

CriterionVerdict for this layer
Inspect what it seesMostly holds. Path receipts are honest; frames are small strict JSON. Relay metadata exposure (G3) is real but not yet documented to the user.
Fork permissionsBlunt. AuthorityMode::Single means one authority key governs succession; members cannot attenuate or delegate. Recovery/quorum (FROST) is reserved, not built.
BYO models + keysHolds 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 memoryNot this layer’s surface (app data single-homing is a site-wide finding; see Part IV).
Auditable actionsHolds in-plane. Hash-chained document, append-only revocations, canonical encoding. (Site-wide: grant/trust journals outside the hash chain — Part IV.)
Exit without lossPartial. 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

  1. v1 vs v2 group document — is crates/xe-sync (v2: Device/Companion kinds, endpoint bindings) the intended canonical membership layer for shipped serving, or is crates/net/group (v1: what gossip and the membership gate compile against) the line? This blocks G4.
  2. Relay trust — run Agent54 relays pinned in the signed head, or accept number0’s public relays with direct-preferred + relay-fallback documented? (G3)
  3. 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-launcher at main @56e9457 (crates/launcher/assets/route-worker.js, crates/launcher/src/origin.rs, boot_service.rs), plus the round-2 review doc agent54-hq/docs/serving-design-review-round2.md.

Desired vs current vs gap — the shape of it

DimensionDesiredCurrent (as-built)Gap
App reachone memorable origin per apphttp://<app>.localhost:5454, three route typesnone — shipped, d/100
Isolationbrowser-enforced same-origin, no ambient CORSsame-origin floor + per-request/per-chunk grantsnone — shipped
Transport schemeplain http(s) for regular apps (d/101)https-by-default with name-constrained CAlive tension — G3
Port custodyPrism can trust what it framesadvisory flock, first-come bindG2
Whole-origin (compose) appsUI is a plane origin like any appbackend-proxy empty-digest overload (P-203)route-type ratification — G1 / d/103-B
Multi-user hostworks for more than one OS usersingle global 5454G4
Table evolutionsafe, versioned schema growthadditive + fail-closed, no explicit migrationG5
Upstream errorsdistinguish plane-can’t-reach vs upstream-erroredone synthesized 503, no 502open question

The good news up top: the isolation core is strong and shipped. Same-origin by default, zero ambient cross-origin access, a Fetch-Metadata CSRF floor, grants that are re-read per request and per streamed chunk, a backend a grant can never reach, and a route discriminator that fails closed on anything unknown. The open items below are the edges around that core — trust of the port, the scheme on loopback, and how the table grows — not the core itself.

The gap register

G1 — Whole-origin route type ratification (d/103-B)

What exists. P-207 proved a sound Immich compose package but hit a real limit: the route model could not express “the whole UI is a container service.” d/103 splits this. Item A — the rendering model — is DECIDED: a compose app’s UI is a whole-origin proxy route on the plane (http://<app>.localhost:5454), framed by Prism exactly like every other app — no per-app controlled-frame special-casing, no swbn/COI/CSP. This follows directly from d/100 + d/101. Item B — the route type — is PROPOSED, pending Luke.

Why it needs design. Two fair shapes, and they trade schema-conservatism against semantic clarity:

backend-proxy overload (shipped by P-203)container-origin new type (proposed)
Shapeempty digest + prefix:"/" on a backend-proxy routeadditive 4th OriginRouteType variant
Frozen schema (P-148)untouchedtype discriminator unchangedis the schema change requiring ratification
Semanticsoverloads “empty digest means whole-origin”clean invariant: backend-proxy stays static-digest + proxied-prefix
Statusmerged as-is (schema-conservative, tested, receipt-backed)optional refinement for Luke to ratify or reject

The ruling in place. d/103’s amendment (2026-07-18, after P-203 review): P-203’s overload merges as the documented shape; container-origin stays an optional refinement. If Luke rejects it, the overload is the shape and P-210 builds on it. P-210 is gated — it dispatches only after P-203 merges, Luke ratifies (or rejects) item B, and the connector socket custody is reconciled (the stale BackendProbe::Ready(SocketAddr) TCP assumption must move to the d/094 unix-socket shape). It is an opus-lane, review-required packet with a reviewer-run live Immich end-to-end.

PROPOSED — pending Luke. Ratify the container-origin type, or bless the backend-proxy empty-digest overload as the permanent documented shape. Decides: Luke (frozen-schema addition).

G2 — Port custody / local-trust boundary

What exists. The plane binds 5454 first-come. The only mutual-exclusion is an advisory flock under the data root (boot_service.rs:938-951, used by up.rs:178). The round-2 review is explicit (NEW-6): this “protects against a second cooperating Xe, not against a hostile or buggy local process that binds first.” If the plane is down, another same-UID process can bind 5454 and be framed by Prism as if it were the plane. There is also an IPv4/IPv6 split hazard — 127.0.0.1 and ::1 are separate binds, so one can be held while the other is free.

Why it needs design. This touches the sovereignty litmus directly (inspect what it sees, auditable actions) and Prism’s trust in what it frames. A shell that frames a squatter has been quietly compromised without any transport being broken.

Options.

  • (a) Accept same-UID loopback DoS as v1-scoped. The review’s stated position: a same-UID actor “can already DoS loopback.” Harden by binding both IP stacks atomically and failing closed if either is held, plus a xe doctor path that names the PID holding the port. Cheap; does not close framing-a-squatter.
  • (b) Prism verifies the plane’s identity before framing it. A pinned serving leaf / mTLS / a handshake secret handed to Prism at launch. This is the strongest control — it closes framing-a-squatter outright. Open sub-question: where does the secret handoff live (boot service?).
  • (c) OS-assisted reservation of the port. Platform-specific, heaviest.

Decides: Luke + Jan (security / litmus → propose, Luke disposes).

G3 — HTTPS-on-loopback

The live tension with d/101.

What exists. The launcher defaults to https with a short-lived leaf from a name-constrained local CA (origin.rs:585); plain http is gated behind XE_SERVING_HTTP_DEV=1 (origin.rs:819-830). Meanwhile d/101 frames regular apps as “just http/s,” and every proof that worked to date ran plain-HTTP guests inside the frame. This is a genuine, unresolved tension in the tree — the code says https-default, the decision says plain-http-default — and it must be stated as such, not smoothed over.

Why it needs design. Round-2 NEW-7: name-constraint enforcement holds only on enforcing Chromium builds; a non-browser local client (e.g. curl trusting the OS store) is unprotected by a leaked leaf. And RFC 6761 + Secure Contexts already treat localhost as potentially-trustworthy — so is TLS strictly needed on loopback?

Options.

  • Keep TLS. Secure-context parity for any code that does not special-case localhost; a real cert chain. Backstop the enforcement gap with a per-OS release gate + short leaf lifetimes. Cost: CA ceremony for every regular app, cutting against d/101’s “no packaging friction.”
  • Plain http + rely on the browser’s localhost secure-context treatment. Removes the CA ceremony for regular apps. Cost: loses protection for non-localhost-aware clients and any future non-loopback bind. The dev-signed leaf already exists, so either direction is reachable from here.

Decides: Luke (litmus / security). This is the top session question on this beat.

G4 — Multi-user machines / per-user plane

What exists. 5454 is a single global loopback port; round-2 §8 scoped v1 to “one Xe per user.” Data roots are already per-user, but the port is fixed, so two OS users on one host collide on 5454.

Why it needs design. Shared workstations and fast-user-switching.

Options. Per-user port derivation (hash of UID into a range), with the origin string carrying it — but the origin is the storage/service-worker key, and SEQ-1 flags it as the single most expensive thing to change later; or first-user-wins + an honest doctor for the second user.

Decides: Luke + Jan.

G5 — Route-table versioning / migration under the freeze

What exists. Schema tags xe/origin-routes/v0, xe/peer-route/v0, xe/origin-grants/v0; unknown route types fail closed; no explicit v0→v1 migration path.

Why it needs design. The first real type addition (container-origin) or field change needs a defined upgrade story — an old plane reading a new table, and vice-versa.

Options. Additive-only + fail-closed (the current de-facto rule), or explicit schema-version negotiation + a migration journal — there is already a migrations vector precedent in the tombstone file (origin.rs:67).

Decides: orchestrator for reversible field additions; Luke for any type addition (the P-148 freeze).

The no-502 question

Not a numbered gap, but a deliberate design choice worth ratifying. There is no 502 today. Upstream backend/peer failures are synthesized as 503 (a plane-side “can’t reach” condition), and a genuine upstream 5xx passes through unchanged. If the honest-error contract wants to distinguish “the plane could not reach the upstream” (503) from “the upstream itself errored” (502), that is a deliberate addition to the contract — not a bug to be patched silently. Decides: orchestrator (contract refinement), surfaced to Luke if it changes user-visible copy.

Sovereignty litmus — the six criteria

The serving plane against the litmus (from CLAUDE.md): honest scoring, gaps named.

CriterionStatusEvidence / caveat
Inspect what it seesStrongEvery route is a plaintext row in routes.json; every refusal is a text/plain body naming the cause; /xe/api/catalog exposes the merged view. Caveat: G2 — a squatter on 5454 could be inspected-as-the-plane.
Fork permissionsStrongCross-origin access is only ever a user-consented grant, re-read per request and per chunk, revocable live (guardStream); no ambient CORS to fork around.
BYO models + keysN/A hereThe plane serves origins; model/key custody lives in the agent layer and engines.
Movable memoryPartialApp bytes are content-addressed (digest) and portable; but the origin string is the storage/service-worker key (SEQ-1) — moving to a per-user port (G4) would move the memory key, which is expensive.
Auditable actionsStrongSupervisor emits serving_plane_failed; workerd binary is size+SHA-256 verified before run; route generation is deterministic and diff-able. Caveat: no per-request audit log surfaced yet.
Exit without lossStrongStatic apps are just verified byte bundles on disk; no proprietary bundle format is required for regular apps (d/101). Nothing gatekeeps export.

The plane earns strong marks on the criteria it owns. Its one litmus-relevant weakness is G2 (trust of the port), and its one movability caveat is G4 / SEQ-1 (origin-as-key).

Session questions for Luke + Jan

  1. G3 / TLS-on-loopback (Luke): keep https-by-default with the name-constrained CA (accepting Chromium-build-dependent enforcement, NEW-7), or lean on the browser’s localhost secure-context treatment and drop the CA ceremony for regular apps? The code and d/101 currently disagree — which wins?
  2. G2 / port custody + framing (Luke + Jan): accept same-UID loopback squat/DoS as v1-scoped, or make Prism verify the plane’s identity before framing (pinned leaf / handshake secret)? If the latter, where does the secret handoff live — the boot service?
  3. G1 / d/103-B (Luke): ratify the container-origin type, or bless the backend-proxy empty-digest overload as the permanent documented shape? P-210 is gated on this.
  4. G4 / multi-user (Luke + Jan): is one-Xe-per-user-per-host an acceptable v1 boundary, or do we need per-user port derivation now — knowing the origin string is the storage/service-worker key and the most expensive thing to change later (SEQ-1)?
  5. G5 / versioning (Luke): confirm additive-only + fail-closed as the route-table evolution rule under the freeze, and whether the first type addition should introduce explicit schema-version negotiation rather than relying on 404-on-unknown.
  6. The no-502 question (orchestrator → Luke if copy changes): should the contract distinguish 503 (plane can’t reach upstream) from 502 (upstream errored)?

Port drift note. serving-design-review-round2.md predates d/100 and reasons about :5196. Every finding it raises — NEW-6 squatting, NEW-7 CA enforcement, §8 custody — transfers directly to :5454. Only the number changed.


Candidate specs distilled from this layer — the Plane Origin Convention, Route Table v1, and the Honest-error contract v1 — are collected in Part V.

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 lossCatalog + index publication hash-chain audited; liveness inspectable; tickets 0600/DACLRoute 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

CriterionVerdict for this layer
Inspect what it seesHolds. 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 permissionsWeakest point. One secret per publisher session — no per-consumer grant, attenuation, or revoke. The only permission change is restarting the world. (G1)
BYO models + keysPartial. Keys are local; relays configurable but unsigned on this plane (G3, transport review).
Movable memoryPartial. 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 actionsHolds for the catalog (hash-chained publish/verify records). Fails for handoff: ticket distribution is out-of-band and leaves no record (G5).
Exit without lossConsumer: 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

  1. Unify serving with membership, or keep bearer + add UCAN? (G1+G4+G5) Gating the serve ALPN on the existing MembershipRegistry reuses built code and yields real revocation for free; UCAN is heavier and partly duplicates the group plane. Which spine?
  2. 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.)
  3. Windows tripwire — refuse-by-default on a non-owner ACE (Unix-symmetric) with --repair opt-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 BuildGrant can only be minted from a gate-1 consent whose digest equals sha256(plan) (adapter.rs:93), and every adapter re-checks it at materialize. 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_features list prevents materialization before anything is fetched or built (adapter.rs:45, compose.rs:1498). The default is “no,” not “try and see.”
  • static-v0 is proven end to end. excalidraw installs via static-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. UninstallTransaction forces a data disposition before delete — VerifiedExportThenDelete | RetainDetached | DestructiveDeleteConfirmed (lifecycle.rs:299) — and export_xaa XAA archives already back update snapshots. Named volumes survive a stop.

Built, but idle

  • Half the source kinds have no adapter. Image, Web, and Oci are 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, and notifications are 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

LitmusApp-model verdict
Inspect what it seesStrong. XAM is human-readable; Plan Review shows declared build effects (network hosts, context roots, resources, emulation); capabilities carry required reason strings.
Fork permissionsHolds. Capabilities are declared, gated, journaled, and revocable (L2, d/076); the two consent gates bind to exact digests.
BYO models + keysPartial. Publisher signing is ORCH-held today (d/066); compose apps are unsigned-derived with no BYO-publisher-key story yet (AM-3).
Movable memoryPartial. XAA export archives + mandatory uninstall disposition exist; no first-class per-app export UX (AM-5).
Auditable actionsHolds. Install transitions write a signed audit; consent requests move pending → claimed → decided as inspectable records.
Exit without lossPartial. 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

  1. d/103 item B ratification (AM-4). Accept P-203’s shipped backend-proxy empty-digest overload as the documented whole-origin shape, or ratify a distinct container-origin route type? P-210 is blocked on this. (Luke)
  2. 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-UnsignedDerived identity get a publisher-key story, or stay URL-derived? (Luke + Jan)
  3. 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); the x-xe-app header is validated to [a-z0-9-]{1,63}; and the route worker’s grants never cover the backend prefix (a 403, route-worker.js:304). A cross-origin grant cannot be turned into backend reach.
  • The engine is refused if ambiguous. SmolvmBackend refuses zero or more-than-one docker.sock under 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, naming xe 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.rs is a complete engine — semver-gated discovery, escalation-detecting diff, snapshot → stage → activate → rollback-on-failure, digest-verified volume restore, and an Auto | Notify | Manual policy 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, and microsandbox.rs backends exist alongside smolvm and the Colima AppEngineBackend — 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

LitmusEngines/backends verdict
Inspect what it seesStrong. Compose diagnostics name source:line:col; the connector resolves everything from inspectable registry state; failure modes map to honest HTTP status.
Fork permissionsHolds. The safe-compose subset is a declared, enforced boundary; device/privilege escalations are refused with stable codes.
BYO models + keysPartial / 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 memoryMostly. Named volumes survive stop; update snapshots are digest-verified XAA archives; rollback restores volumes.
Auditable actionsHolds. Consent requests are inert-until-decided records; the runtime choke point (runtime.rs:138) leaves no ungated path.
Exit without lossPartial. 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

  1. 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)
  2. 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)
  3. 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)
  4. Doc hygiene (EB-6). serving-and-placement-design.md is stale (calls backend-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:5454 for the catalog and *.localhost:5454 for 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 a MessageChannel gated 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 HTMLControlledFrameElement on 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

AreaDesiredCurrentGap
App tier (d/101)Most apps plain http(s); IWA reserved for the shellExactly this — only Prism is bundled; every proven guest ran plain http— (holds)
Launch seam (d/104)One catalog descriptor is the launch path, all platformsCode already sets frame src = launch_url; Android can’t repoint installed guestRatify d/104 (G-seam)
In-frame permissionsShell brokers every permission, shield-by-default, journaledOnly geolocation, only in the dev driver, same-originG6 — undesigned
Composition (d/103)Any two fleet apps compose via CF + plane originsTwo fixture apps compose via iframesG3
Frame↔shell contractApp can title / notify / report liveness safelyFour tab verbs, one-way; no title/notifyG2
Production identityReal key ceremony, independent reviewDev-signed; name reads “(DEV-SIGNED)”G1
Local trustAuthenticated wake / bind of the planeCSP trusts whatever binds the portsG4
Fork burdenMinimal patches, upstream-aligned3-patch series, no rebase cadence setG5

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 everythingOption B — delegate to Chrome
Who promptsThe user’s agent, via the agency ladderChrome’s native per-origin prompt
Default answerShield by default (user-chosen value)Whatever Chrome offers (allow / block)
RecordJournaled, inspectable, revocableChrome’s site settings
Litmus fitInspect what it sees + auditable both metCedes “your agent answers for you”
CostReal browser-driver engineering per capabilityNearly 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

CriterionVerdict for this layer
Inspect what it seesHolds 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 permissionsPartial — 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 + keysHolds for shape, pending ceremony (G1). Signing keys are local and never escrowed; the dev key is honestly labelled; the production ceremony is ahead.
Movable memoryStructurally 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 actionsHolds. Hash-linked journal, separately verifiable; channel rejections recorded with cause; leased reads receipted.
Exit without lossHolds 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

  1. 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.
  2. 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.)
  3. 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.
  4. Fork strategy (G5). Commit to minimizing 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.
  5. 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?
  6. 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 beatOn 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 proofsHolds. 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 ratifiedd/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 designedNo code; the zero-resident-vs-reachability crux is unresolved (G-4).
Litmus: movable memoryGroup doc syncs; app state + agent memory are per-device, localNo 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 stateWakeable?
Desktop, same network, zero-residentYes (best-effort, OS socket activation)
Desktop, internet-onlyNo (relay cannot socket-activate)
Android, session activeMoot (already up)
Android, force-stoppedNo (needs the unbuilt notifications transport — G-6)
WindowsNo (no socket-activation primitive)
Powered-offNo (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

CriterionVerdict for the device fabric
Inspect what it seesHolds. 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 permissionsPartial. 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 + keysHolds for keys — device keys are local, group authority is a local signed document, no server. Models are out of the device beat.
Movable memoryWeakest point. Group doc moves; app data + agent memory are single-homed per device; the phone can’t hold app state offline (G-5).
Auditable actionsHolds 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 lossPartial. 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

  1. 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.list responder). Reversal is cheap — no code depends on it.
  2. 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 Statement wake-policy type? These are the only AW items needing real interview time.
  3. 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.)
  4. 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-cap capabilities, xenon-grant authority-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[] → grant pipeline; 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 boundsdarc research preview (real, unbounded); daemon/main.js is 0 bytesThe 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-shapedagent://act is declared + audited only, not enforced; capabilities[] → grant unwired. (G-2)
BYO models + keys (sovereignty litmus)Keys local; darc model IDs hardcodedSovereign 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 paperZero rungs enforced in code.
Auditable agent actionsHash-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.

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

CriterionVerdict for the agent layer
Inspect what it seesPartial. 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 permissionsWeakest 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 + keysFails for models. Model IDs are hardcoded in darc — the sovereignty debt the kickoff named. Keys are local. (G-1)
Movable memoryNot yet. Agent memory is local per device; no cross-device grant or memory sync (capability-model §9).
Auditable actionsReady, unwired. The hash-chain discipline exists and the AuditEntry shape is specified; nothing mints lease-use records because no bounded agent acts.
Exit without lossN/A yet. No resident agent state to carry; becomes real the moment the daemon holds memory.

Questions for the Luke + Jan session

  1. G-2 / the UCAN decision — is Reagent’s authority the same xenon-cap/xenon-grant substrate (Grant = long-lived UCAN delegation, Lease = attenuated invocation), and does agent://act get enforced in v1 or stay declared + audited? This is where UCAN alignment should be a product decision, not an implementation detail.
  2. 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?
  3. BYO model — a model-selection capability, a config file, or status-quo hardcoding for v1? (Litmus: BYO models + keys.)
  4. Naming hygiene — the site must not let a reader conflate the product delegation model (capability-model-design.md + injection-boundary-design.md) with docs/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 &amp; 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.


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.

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:

  1. 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.
  2. 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-install instance_id), with labels display-only — and stabilize the canonical origin before users accumulate origin-scoped state.
  3. 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).
  4. 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.
  5. 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 stated127.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)

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. On the two PROPOSED decisions: take the distinct container-origin route type (overloading backend-proxy hides an authority change from an /api request 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 immutable app_id + instance_id capability (not an arbitrary URL) and constrain post-launch navigation, not just the first URL.

1. Internal inconsistencies and weak arguments

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

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

  3. The isolation argument is circular and changes between chapters. Chapter A says each *.localhost origin 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.

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

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

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

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

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

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

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

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

  12. The hard promise that L1 apps run “unmodified and un-Xe-aware” is unsupported. Re-origining an app to <app>.localhost changes 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.

  13. “Browsers treat *.localhost as 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.

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

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

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

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

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

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

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

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

  22. 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 standardRequired position
Isolated Web AppsAlign 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 FrameTreat 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.
UCANEither 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.
SolidBorrow 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 workBorrow 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.
SandstormStudy 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 WebRTCKeep 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 ElectronDifferentiate 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 capabilitiesBorrow 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 CredentialsDo 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 modelAlign 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 NixBorrow 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 UptaneAlign 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 transparencyBorrow 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 WebAuthnPosition 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 ModelAlign 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

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

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

  3. How is the entire 127.0.0.1:5454 plane 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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  22. 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 V emerging-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)

  1. 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 /1 in place.
  2. 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.
  3. 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.
  4. 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).
  5. Top-5 proposals: (1) ratify one membership authority + serving-policy profile; (2) xenon/serve/tcp/2 structured 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 schema or version discriminator 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_sha256 is 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 optional origin_id and device_name: the spec does not say what validation is skipped when they are absent.

  • node_addr is not actually specified. {provider, address} does not define the types, bounds, ordering, or supported address variants inside address; 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_id is unstable and encoding-dependent. Hashing json(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_name needs a byte limit as well as a character limit, control-character policy, Unicode normalization decision, and exact comparison rule. origin_port needs 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’s sha256 is 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 kind values 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, and authorization-invalid concepts 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 stated xe/peer-list/v1 schema 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 duplicated origin_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_TUNNELS applies 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. REJECT conflates malformed preamble, unsupported auth method, invalid credential, non-member, revoked member, capacity exhaustion, and unavailable backend. It is also unclear whether ACCEPT means “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-Length versus 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_id should 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:key without network resolution, but it is not literally free: Xe must implement and test multicodec, multibase, canonical key validation, and DID comparison. More importantly, did:key does 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.

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

  2. Introduce xenon/serve/tcp/2 with 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.

  3. 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, optional nbf, 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.

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

  5. Separate stable identity from routing and catalog freshness. Derive origin_id from 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 /1 in 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 of src/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)

  1. Verdict: the isolation core is real but the review “overstates the strength” — Host and 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-Network gated on a grant).
  2. Strongest single criticism: the cross-origin arm runs only when Origin is present and the Fetch-Metadata floor refuses only cross-site unsafe/websocket requests — so a cross-site safe request without Origin (image, navigation) can reach a backend prefix, including a whole-origin backend-proxy; relatedly, /xe/api/index//catalog are served before the hostname/Origin/Fetch-Metadata rungs, exposing app inventory to anything that reaches the loopback socket, and sibling .localhost apps are same-site, which SameSite cookies and Sec-Fetch-Site do not isolate.
  3. 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.
  4. 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/v1 with 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).
  5. 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-origin in xe/origin-routes/v1 with an explicit rollout protocol (dual-read, generation digest, last-known-good, table-wide 503 Plane upgrade required); the 503 collapse is defensible but a telemetry hazard — add Xe-Plane-Error + Xe-Request-Id, reserve 502 for protocol-invalid upstream responses and 504 for 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.

  1. 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/index and /xe/api/catalog are 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 Origin is present, while the later Fetch Metadata check refuses only cross-site unsafe methods and WebSockets. A cross-site image, navigation, or other safe request without Origin can therefore reach a backend prefix, including a whole-origin backend-proxy. “GET is safe” is not an admission control, and idempotent_get is described only as a cold-start wait-set.

  • Sibling <app>.localhost origins are cross-origin but may be same-site. SameSite cookies and a Sec-Fetch-Site: same-site value 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 as cross-site.

  • The internal x-xe-peer-request-origin header 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-ancestors policy. 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:

  1. Confirm that the accepted socket is one of the launcher-owned loopback listeners.
  2. Parse exactly one HTTP/1.1 Host or 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. Ignore Forwarded and all X-Forwarded-* headers.
  3. Authenticate reserved service paths and remove all internal x-xe-* headers from browser ingress.
  4. Load and validate the target route before answering a preflight.
  5. Parse exactly one non-null Origin 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.
  6. 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.
  7. Apply grant-scoped CORS last, echoing the exact allowed Origin and emitting the corresponding Vary fields.

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.

  1. 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 Secure cookies 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.

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

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

  1. Deploy a reader that accepts both v0 and v1.
  2. Have the supervisor advertise the reader versions/features it supports.
  3. Only then allow the generator to atomically publish v1 with a generation number and digest.
  4. Retain the ability to regenerate v0 during a rollback window.
  5. 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.

  1. 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-Site as 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, or Sec-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 *.localhost hosts 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 *.localhost or locally resolved *.test commonly 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.

  1. TOP-5 CONCRETE IMPROVEMENT PROPOSALS, RANKED

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

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

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

  5. Freeze v0 and introduce an explicit v1 rollout protocol. Keep the three v0 types forever and put container-origin, if adopted, in xe/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.

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

  7. 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_network field in grants, and which exact preflight headers will be emitted?
  • How does Prism authenticate itself for initial iframe navigation, and what exact frame-ancestors policy should regular apps receive?
  • Are /xe/api/index and /xe/api/catalog public 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-Cookie responses be forced to host-only cookies so one app cannot set Domain=localhost cookies 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 of src/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)

  1. 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.
  2. 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-validated x-xe-app header 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 raw tcp://127.0.0.1:2375 Docker API — far more privileged than a backend port.)
  3. Update-loop position: design it as an authorization + recovery protocol, not a CLI wrapper. Bind a canonical UpdateAuthorization/v1 object (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); Auto needs a signed policy object + a full non-escalation checklist. A schema/migration declaration is required or “volume-restore” is not a real rollback contract.
  4. L1-envelope position: “install unmodified and un-Xe-aware” is too broad — re-origining onto http://<app>.localhost:5454 breaks OAuth redirect URIs, WebAuthn RP-ID, Domain cookies/SameSite, service-worker scope, CSP frame-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.
  5. Top-5 (ranked) + sharpest catches: (1) allowlisted Compose→IR + authenticate the connector; (2) split the package.json/.xam.json document family + publish normative validation & canonical digest encoding (RFC 8785 / deterministic CBOR); (3) ship the update loop around UpdateAuthorization + TUF; (4) L1 origin-portability profiles + conformance program; (5) make resource/isolation declarations executable engine policy. Two catches worth surfacing: fixed non-latest tags are not pins (must resolve to an OCI manifest digest before gate-2), and the sh+nc exec 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 components is 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.kind syntax for locator and pinned.
  • 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:

PurposeRecommended nameRequired discriminator
Ordinary Node package metadatapackage.jsonnpm semantics only
Xe compose authoring inputapp.xe-recipe.yamldocument_kind: xe.compose-recipe, recipe_version
Docker Compose inputcompose.yamlCompose dialect/profile
Portable XAM app descriptorapp.xam.jsondocument_kind: xe.app-manifest, xam_version
Fully resolved install/update objectapp.xam.lock.jsondocument_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:

  1. A recipe expresses author intent and may reference mutable or unresolved inputs.
  2. The adapter resolves and compiles it into a normalized XAM release description.
  3. Materialization produces a locked release containing exact artifact, image, source, and provenance digests.
  4. 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: and deploy.resources.reservations.devices may grant accelerators without using the refused devices: spelling.
  • device_cgroup_rules, runtime selection, or implementation-specific extensions may expose devices or broader runtimes.
  • volumes_from can 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, Windows credential_spec, and other namespace or host-integration controls are not covered by the stated list.
  • Restricting only “unconfined” security_opt is 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.
  • configs and secrets can read files, external objects, or environment-derived values.
  • build.additional_contexts, Dockerfile-specific contexts, COPY --from, remote ADD, cache imports/exports, and symlink traversal can escape the apparent build-input model.
  • Compose’s implicit .env processing and inherited process environment can bypass a validator that only examines explicit env_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, not ports.
  • Restricted depends_on and health checks.
  • Explicit commands and entrypoints.
  • Generated per-app networks.
  • read_only, controlled tmpfs, and cap_drop, preferably including ALL by 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:

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

  2. 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 .localhost behavior should be tested against Xe’s pinned Chromium build rather than assumed.
  • Cookies: Domain attributes tied to the original site fail. Secure, SameSite, public-suffix handling, iframe context, and storage partitioning interact with .localhost and 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, fixed connect-src, fixed form-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/Referer validation, trusted-proxy behavior, and generated scheme/host values require a defined Host/Forwarded contract.

Three compatibility classes are more honest than a universal L1 claim:

ProfileMeaningExamples
L1-Origin-PortableUnchanged artifact works at an assigned framed origin with no app-specific setupRelative-URL static SPAs; self-contained apps using host-only cookies and same-origin APIs
L1-Standard-ConfigurableUnchanged upstream code/artifact works using documented standard configurationApps with BASE_URL, OAuth callback, trusted-proxy, or cookie settings
L1-IncompatibleOrigin or top-level context is intrinsic to the appFixed-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:

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

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

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

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

OptionStrengthMain cost/riskAppropriate when
WSL exec bridgeClosest to the existing smolvm/Colima path; fastest route to parityProcess startup, quoting, WSL distribution custody, poor per-connection overheadWindows backend support is near-term and a bounded implementation is acceptable
Versioned Windows named-pipe contractBetter performance, ACL-based custody, engine-neutral long-term boundaryNew resident agent/protocol and larger implementation surfaceWindows is a productized backend platform
Explicit deferralAvoids an undersecured half-platformWindows supports only static/origin appsBackend 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:

  1. 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 imply privileged. Gate-2 consent binds the selected device class, driver exposure, and placement.

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

  3. 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 interfaces and handlers: 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.

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

  2. Split the XAM document family and publish normative validation plus canonical digest rules.
    Problem: package.json and .xam.json identify three incompatible formats, while "0.2" and the Rust validator do not provide stable interoperability.
    Change: Reserve package.json for 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.

  3. Ship the update loop around a consent-bound UpdateAuthorization and 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.

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

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

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

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

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

  4. 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 whether L1-Standard-Configurable is part of the hard promise or a separate tier.

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

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

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

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

  9. 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 before Auto can be safely enabled.

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

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

  12. Is sh plus nc inside 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 of src/part5/emerging-specs.md (Prism↔app contract v1, super-app packaging profile v1)

Orchestrator summary (5 lines):

  1. 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.
  2. 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 permissionrequest API can only allow/cancel — real shielding needs a Xenon browser-process permission delegate or a resource portal.
  3. On the launch seam (d/104) sol says keep the descriptor as the canonical seam but do not ratify the current label → launch_url shape (it conflates presentation, identity, and routing); bind launch to a key-derived app_id + release_digest + mutability_tier resolved by an authenticated plane router, enforced by a Xenon browser-process navigation guard (NavigationThrottle) bound to a verified LaunchGrant. d/104 also embeds an unresolved HTTP-vs-HTTPS Controlled Frame portability decision.
  4. 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} and key).
  5. 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) one PaneHost abstraction 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

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 groupCapabilitiesDefaultEnforcement
Interaction and attentionFullscreen, pointer lock, focus, popups, external navigation, clipboard write, downloads, wake lockAllow once only with a fresh user gesture and visible pane; otherwise denyShell gesture check plus browser guard
Shieldable personal valuesLocation, clipboard read, contacts, simple sensorsShield once by default; prompt for real dataShell value provider or scoped portal
User-selected resourcesFile open/save, directory access, photo selection, display/window captureAlways invoke a shell/native chooser; never grant an ambient directory or screen capabilityOpaque resource handles
Live captureCamera and microphoneVirtual feed if configured; otherwise deny by default; real access requires explicit promptBrowser/OS device controls and visible indicators
Background or durable attentionNotifications, background sync, periodic work, persistent storageDeny until enabled through an in-context prompt or app settingsShell notification/lifecycle service
Physical or system authorityUSB, HID, serial, Bluetooth, MIDI SysEx, local-network discovery, protocol handlersDeny by default; explicit device- and session-scoped native chooserBrowser/OS; no shell-synthesized substitute
Credential and payment authorityPasskeys, payment credentials, secure authenticationDelegate entirely to browser/OS after shell eligibility checkNative 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:

  1. The request comes from the current, verified frame and document epoch.
  2. The top-level pane is visible and foregrounded.
  3. The descriptor declares the capability as optional or required.
  4. Any required user activation is fresh and browser-attested.
  5. There is no applicable stored rule or active task lease.
  6. 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 .localhost origin.
  • Assigning activeFrame.src constrains 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.

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.open policy path and require a user gesture.
  • javascript:, file:, data:, arbitrary isolated-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.open is 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:

  1. A cooperative app protocol for lifecycle, presentation, and intents.
  2. 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

AdditionPathCapability cost and constraints
Window title, subtitle, badgeNonce channel: ui.setMetadataLow. Sanitize length and characters, rate-limit, and always render the immutable shell-owned app identity beside it. App text must not replace identity.
Notification requestNonce channel: notification.request; notification action returns through a single-use gateway deliveryMedium/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/attentionNonce channel: ui.requestAttention or ui.requestFocusMedium. Require browser-attested user activation or explicit shell policy. Never allow hidden scripts to steal focus.
LivenessControlled Frame load/crash events for process liveness; nonce channel app.ready/app.health for cooperative readinessLow, but untrusted. A heartbeat proves only that the app script is responding. Rate-limit it and do not use it as a security assertion.
ResizeNonce channel for advisory ui.requestSize; normal viewport/resize events from shell to appLow/medium. Shell retains layout control, clamps dimensions, and prevents resize loops. No privileged gateway is needed merely to tell a page its viewport changed.
ClipboardUse 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 fieldHigh for reads, medium for writes. A general clipboard.read verb would let any port holder exfiltrate ambient clipboard data.
File open/saveBrowser/shell resource portal, not a generic channel verb; inbound selected-file delivery through a leased gateway using an opaque handleHigh. Never deliver arbitrary host paths. Scope handle by app, mode, resource, TTL, and document epoch.
Inbound file handlingLeased gateway: intent.deliverFileHigh. Single app, typed content, selected file, explicit read/write mode. Revocation must invalidate the handle.
Deep link or cross-app intentOutbound request on nonce channel: intent.open; inbound delivery on leased gateway: intent.deliverHigh 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/actionLeased gateway bound to notification ID and action IDMedium. 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.

RungMinimal enforcement point
NothingFrame 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 / shieldedBrowser-process permission delegate accepts only a valid PermissionLease or stored rule and appends the answer before applying it.
ObservedReadLease guard immediately before executeScript, DOM extraction, field reads, or request observation. DOM contents and network bodies should be separate scopes.
EnhancedEnhancementLease guard before insertCSS or isolated-world annotation injection, restricted to shell-bundled assets and bounded selectors.
MutatedRequestRewriteLease at the blocking network interceptor, constrained by URL patterns, methods, headers, and permitted transformations.
ControlledInteractionLease at the input/navigation dispatcher, bound to a frame, document epoch, action types, selectors, and navigation policy.
DrivenDevToolsLease at CDP attachment and again at every CDP command dispatch, with a command allowlist, short TTL, visible indicator, and emergency revocation.
Composed / generated / delegatedTransferLease at the composition broker, binding source read, destination write, schema, payload fingerprint, consent record, use count, and undo operation.
Super-user-agentA 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:

  1. Static ceiling: signed IWA identity, CSP, Permissions Policy, and Controlled Frame availability.
  2. Verified frame context: immutable app identity, descriptor digest, partition, navigation policy, and document epoch.
  3. Typed attenuated leases: resource, verb, actor, argument constraints, TTL, use count, and delegation chain.
  4. Complete mediation: one guarded dispatcher for every privileged Controlled Frame, permission, request, input, CDP, and composition operation.
  5. 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_url and 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 TransferLease records 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 iframe postMessage, 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 postMessage and 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

  1. Ship a hybrid permission broker MVP. Add a browser-process permission delegate, typed PermissionRequest model, 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.

  2. Replace launch_url with signed LaunchDescriptor + 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.

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

  4. Publish contract v1.1 as lifecycle RPC, not automation RPC. Remove raw tab.open{url} and key; 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.

  5. Create one PaneHost abstraction 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:

  1. What is the identity and update model for ordinary HTTP apps? Who signs an app descriptor, how is an app_id created, and what happens to data and permissions when publisher key, release digest, or routing changes?

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

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

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

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

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

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

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

  9. What is the journal adversary model? How are chain heads anchored, rollback and truncation detected, sensitive metadata protected, retention limited, and exports authenticated?

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

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

  12. 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 of src/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:

  1. 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.
  2. 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.dom contain read.text? Does execute.script subsume every read and mutation ability? Can mutate.form navigate 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 notAfter is 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:

  1. Runtime process stopped while the OS is awake.
  2. OS awake with no resident Xe process.
  3. OS suspended.
  4. 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:

  1. Exchange collection manifests: ID, home epoch, generation, root digest, schema, and chunk inventory.
  2. Authorize the collection separately from group membership.
  3. Transfer missing content-addressed chunks with resumption and backpressure.
  4. Verify the root before atomically installing a new snapshot.
  5. Return a durable replica acknowledgement.
  6. 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

  1. Ratify one constrained launch descriptor and wire the member-gated apps.list responder.
    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 .localhost plane origin and the protocol’s acceptance of arbitrary HTTP(S) and isolated-app:// targets.
    Smallest first step: freeze app_id, label, exact plane origin, tier, serving-device identity, and catalog signature rules; then register and dispatch the existing ALPN responder.

  2. Freeze a Grant/Lease v1 verifier profile before minting any leases.
    Why: the current outline cannot consistently enforce attenuation, replay, or revocation, and onward:false contradicts lease delegation.
    Smallest first step: choose “lease as audience-bound invocation,” define three grantable abilities, and publish table-driven positive and negative conformance vectors.

  3. 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: place read.visible-text behind a local policy service and browser broker, bound to one tab/origin/navigation epoch, with the shell and model holding no signing key.

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

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

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

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

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

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

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

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

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

  8. 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 .localhost origin?

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

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

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

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

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

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

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

CandidateLayerSource beatScope, in one line
Xe Peer Ticket v1Transporttransport / peersThe credential + address card a publisher hands a consumer.
Xe Fleet App-Index v1Peer servingtransport / peersThe published index, durable peer row, and merged catalog view.
Peer-serve wire protocol v1Transporttransport / peersThe authenticated QUIC byte-tunnel that carries a served app.
ALPN & signing-domain registry v1Transporttransport / peersThe allocation table for every wire ALPN and signing domain.
The Plane Origin Convention v1Serving planeserving-planeOne loopback plane, one origin per app, deterministic conflicts.
Route Table schema v1Serving planeserving-planeThe frozen route-table shape the workerd router is generated into.
The honest-error contract v1Serving planeserving-planeEvery plane refusal: status, body shape, and its next-action rule.
Prism ↔ app contract v1Shellshell-renderingThe asymmetric frame↔shell channel and leased gateway.
Super-app packaging profile v1Shellshell-renderingWhat makes an app Prism-class (the rare, opt-in super tier).
Companion channel protocol v1 — apps.listDevicesdevices / agentThe read-only “what apps are installed?” protocol over iroh.
Companion audit hash-chain v1Devicesdevices / agentThe tamper-evident per-device session ledger.
Launch descriptor (d/104) (PROPOSED)Devices / shelldevices / agentOne label → plane origin shape serving every platform’s launch seam.
Grant / Lease delegation modelAgent layerdevices / agentThe UCAN-aligned capability → grant → lease → audit outline.
Xe App Manifest (XAM) v0.2App modelapp-model / enginesThe manifest carrying identity, source, and components.
Compose-subset security profile (normative)Enginesapp-model / enginesThe install-time refusal list that fails closed on unsafe compose.
Engine socket contract v1 (d/094)Enginesapp-model / enginesReaching a backend through the engine’s own unix socket, no sidecar.
Install-consent grant modelApp modelapp-model / enginesThe 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-105 at xenon-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).

FieldJSON keyTypeReq?Meaning / source
node_addrnode_addr{provider, address}yesPublisher iroh EndpointAddr (JSON); provider = "iroh-1" (lib.rs:41)
secretsecretstringyesbase64url of 32 random bytes — the bearer credential
index_sha256index_sha256string (64 hex)opt*SHA-256 of the immutable app-index bytes; required by the consumer (peer_serve.rs:609)
origin_idorigin_idstringopt"iroh-" + sha256(node_addr); copied into each index row
device_namedevice_namestring (1–128)optCalm publisher name; consumer enforces a match (:593-604)
origin_portorigin_portu16optPublisher 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):

FieldJSON keyTypeMeaning
schemaschemastring"xe/app-index/v1"
device_namedevice-namestringpublisher device name
origin_idorigin-idstringpublic iroh publisher id
appsapps[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.1 with a rewritten Host: (: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):

ALPNPurposeGate
xenon/net/echo/1echo experiment / dial defaultmembership if provider built with it
xenon/serve/tcp/1peer-serve byte tunnelbearer secret (today)
xenon/net/group-sync/1membership head gossipmembership
xenon/net/group-join/1join ceremonysingle-use invite + proof-of-possession
xenon/substrate/1remote substrate multi-lane sessionmembership (checked at dial)
xenon/consent/test/1test-only extra-ALPN gate proofmembership

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-launcher at main @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.

ElementRuleSource
Plane authority127.0.0.1:5454 + [::1]:5454, loopback onlyd/100; fleet_catalog.rs:24
App origin formhttp(s)://<label>.localhost:5454d/100; route-worker.js:1
Label grammar1–63 chars, [a-z0-9-], no leading/trailing -valid_label origin.rs:1048-1055; regex route-worker.js:1
Reserved labelsxe, api, catalog, moby, localhostorigin.rs:309
Reserved service paths (loopback host)/xe/api/index, /xe/api/catalogroute-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 namequalified_name origin.rs:1072-1082; conflict_winner :401-409
Claim lifecycleclaim → tombstone (revoke) → explicit xe origin reclaim ceremony; tombstoned names never silently reusedorigin.rs:286-396
Schemehttps default (name-constrained CA leaf); http only under the dev-http flagd/101; origin.rs:819-830, app_command.rs:844-850
Port overrideXE_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. The type discriminator 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):

FieldTypeNotes
schemastringxe/origin-routes/v0
portu16rendered plane port
routes[OriginRoute]sorted by name

OriginRoute (origin.rs:105-134):

FieldTypePresent whenNotes
namestringalways.localhost label
package_idstringalwaysprovenance
typeenum static-digest | backend-proxy | peeralwaysfrozen (P-148); unknown → fail-closed 404
digeststringnon-empty for static; omitted otherwisesha256 of extracted payload; skip_serializing_if keeps pure-static bytes identical
static_servingStaticServingPolicyoptheaders / redirects / spa_fallback, parsed at install
backendBackendArmbackend-proxy onlyomitted for pure static
peerPeerArmpeer only
iwaboolopt-in onlydrives 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. The 502 distinction 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.

StatusTrigger(s)Body shapeExtra headersSource
400CONNECT/TRACE; bad Host; hostname ≠ authority; bad %-encoding; path traversal400 Bad Requestnosniff:381,386,388,390,391
403cross-origin refused / preflight / credentials / backend cross-origin / cross-site CSRFaction-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
404not 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
405unsafe method on static route405 Method Not AllowedAllow: GET, HEAD:449
503plane 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/prism at main @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).

VerbArgsMeaningJournals as
tab.open{url}Navigate the pane to a URLnavigate.tab
tab.close{}Close the panenavigate.tab
tab.reload{}Reload the panenavigate.tab
key{key} (non-empty string)Deliver a keystroke to the panenavigate.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).

OperationPurpose
readText / readDom / readFieldObserve the pane’s content
mutateNode / filterContent / restoreContentEnhance / mutate the pane
answerPermissionAnswer a permission request (allow / deny / shield)
navigate / navigateTabDrive 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 at agent54/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:

RequirementValue / sourceWhy
PackagingSigned web bundle (.swbn) via wbn / wbn-sign; origin key-derived (isolated-app://…)Sealed, key-identified app the OS installs natively
Manifest displaystandalone (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 headersCOOP: same-origin + COEP: require-corp + CORP: same-origin (packaging/bundle/headers.json)Yields crossOriginIsolated: true, a precondition for hosting a Controlled Frame
CSP frame-srcadmits the guest plane origins (http://*.localhost:5454)Lets the shell frame app panes; nothing else
Installas 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.rs at xenon-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)):

FieldJSON keyTypeValue
versionvu641
ididstringcorrelation id (e.g. android-list-1)
methodmethodstringapps.list
paramsparamsobject{} (empty in v1)

Response ListAppsResponseresult: LauncherAppsView or error: {code, message}:

LauncherAppsViewTypeNotes
computer_namestring≤128, non-control
apps[]LauncherAppEntry≤1024
LauncherAppEntryTypeValidation
namestring≤256
serving_originstring≤2048, must start http:// | https://
isolated_app_urlstring?≤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):

ALPNPurposePhone direction
xenon/net/consent/1consent request/attestinbound + registered (only dispatched ALPN)
xenon/companion/apps/1this apps.list protocoloutbound dial (responder unwired, P-188)
xenon/substrate/1on-device plane route_info + openoutbound 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-84 and xenon-grant/src/lib.rs:23-53,233-291 @b0cfc90, plus injection-boundary-design.md §3 (paper). The capabilities[] → grant minting pipeline and the agent://act enforcement 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 modelUCAN analog
Granta UCAN delegation — issuer = runtime authority, audience = install principal, capabilities, exp
Leasean attenuated UCAN invocation used once — the nonce is the CID-equivalent replay guard
onward:falsea 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.rs at xenon-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.

GroupFieldTypeNotes
identitypackage_idstringnon-empty; xenon/* system-reserved
identitypublisherSigned{key_id} | UnsignedDerived{canonical_url,subdir}compose ⇒ always UnsignedDerived
identityderived idsha256("xenon.unsigned-package.v1\0" + url + "\0" + subdir) (:736)
sourcekindcompose | git | image | web | static | swbnimage/web/oci have no adapter yet
sourcepinnedstringsha256 digest or git commit
componenttypeui | service | tasktask-only fields on a non-task ⇒ reject
component.runtimedrivercf | iwa | origin | oci | composedriver decides how it renders/runs
endpointclasshttp{port,routes} | tcp | udp | lan_discoverynon-http ⇒ separate consent
endpoint.routeflagswebsocket, sse, range, no_buffer, large_body, long_timeoutper HttpRoute (:377)
capability{resource, ability, reason}stringssorted + unique, else reject
resources{request, required_minimum, breach}enveloperequest ≥ 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, and crates/substrate/src/backends/smolvm.rs @56e94575. This is the “third path” of d/094: no sidecar, no loopback publication of backend ports.

ConcernRuleAnchor
Discoveryexactly-one docker.sock under <data-root>/engine/smolvm/ (depth ≤ 8); zero or many ⇒ refusesmolvm.rs:276
Custodylauncher sets DOCKER_HOST=unix://… (Linux) / tcp://127.0.0.1:2375 (mac); the router never holds the socketbackend.rs:854
Reach containerdocker compose exec -T <svc> sh -c 'nc 127.0.0.1 <port>' — never host-published, never a sidecarbackend.rs:827
Connectorone launcher process on 127.0.0.1:5199, started iff the app has_backendorigin.rs:708, manifest.rs:333
Lifecyclesingleflight start · WaitClass wait-set · 503 (slow) / 502 (failed) split · invalidate / stopbackend.rs:188
Tamper checkpinned launcher + binary sha256 verified before execsmolvm.rs:301

Status: spec candidate — as-built from crates/apps/src/lifecycle.rs, adapter.rs, runtime.rs, and crates/launcher/src/consent.rs @56e94575.

Two non-interchangeable gates:

GateSubject digestGrantsAnchor
Plan Reviewsha256(plan)BuildGrant (unforgeable: minted only from a matching gate-1 consent)adapter.rs:93
Grant Consentlocked release digestauthorizes the runtime to build a runnable projectlifecycle.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 stable http://<app>.localhost:5454 origin (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 .swbn file 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 yield crossOriginIsolated: 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 *.localhost as 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-relay is 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, v2 crates/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 0600 file 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).
  • UCANUser-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, and components; 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-compose an 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.

RepoWhat it isWhy we care
xenon-launcherThe 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.
prismThe 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-runtimeThe 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-iwaA 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-appsThe 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-darcThe 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-docsThis 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

External references — transport, identity & capabilities

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

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

PlatformServe an appConsume an appRender in Prism’s frameEvidence / gate
LinuxNight matrix, all legs on the smolvm engine socket path.
macOSP-208 (serve) + P-201m (macOS consume-in-frame).
WindowsServe 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)◇ blockedConsume 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 kindStatusEvidence
Staticstatic-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

CapabilityStatusEvidence / gate
Real production app inside Prism’s frameExcalidraw runs the whole peer pipeline (P-211; xe-example-apps @5752a3e, PR #2).
Two-device serve → consume, byte-identicalP-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)◇ gatedP-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)◇ designedDesign of record landed (d/068); not yet wired — volumes stay single-home.
Xenon fork artifact◇ designedNot yet built; today = stock Chromium + a flag recipe (d/057).
Console reads Prism’s journal (same device)◇ open seamNo wiring yet; named in the system spine.
App update loop (discover → consent → apply / rollback)◇ built, idleThe engine is library-complete but has no user loop (EB-1).
Authed-wake (peer wakes a sleeping device)◇ designedHonest 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.