App model & packaging
What is an app on the Xe Computer? This chapter answers that from the top down: first the spectrum an app can occupy (from a page you merely visit to a shell that hosts the whole computer), then the two serving tiers, then the on-disk package format (XAM) and the pipeline that turns a package into a running, routed app. The design review asks the harder question — desired versus built versus missing.
Everything here cites code at xenon-launcher main @56e94575 and the example
apps at xe-example-apps @5752a3e. Where a claim is a decision rather than
code, it cites d/NNN in the decisions ledger.
Primer: the words this chapter assumes
You do not need to know browser or container internals to read this. A few terms recur:
- Web app — an app whose interface is HTML/JS/CSS served over HTTP, the same substance a browser renders. On the Xe Computer, everything rendered is a web app (d/076); the differences are about capability, not technology.
- Origin — a web app’s address-and-trust boundary, e.g.
http://notes.localhost:5454. The browser isolates one origin from another. - Manifest — a small declarative file describing an app: its identity, where its code comes from, what it needs. Xe’s manifest format is XAM.
- Controlled Frame / Prism — Prism is Xe’s shell (an app itself). It loads other apps into controlled frames: partitioned, unprivileged panes, the way a browser isolates a tab. See shell & rendering.
- IWA / swbn — an Isolated Web App is a browser packaging format with stronger isolation and install-grade guarantees; swbn (Signed Web Bundle) is its signed on-disk bundle. Xe reserves this tier for super apps; ordinary apps do not need it (d/101).
- docker compose — a text file (
compose.yaml) describing one or more containers that make up a backend service. Some Xe apps carry one. See engines & backends for how they run. - Digest — a SHA-256 hash of some bytes, used as their tamper-evident name (e.g. a locked release, a pinned image).
External references, used on first mention: the compose specification, the OCI image spec, and Isolated Web Apps.
Zoom 1 — what an app is
The spectrum: one substance, one axis (d/076)
The Xe Computer does not have “web pages” on one side and “native apps” on the other. It has one substance — the web app — and one axis — granted capability (d/076). Every rendered thing sits somewhere on a ladder, and moving up the ladder is a matter of grants, never a rewrite:
flowchart TB
L0["<b>L0 — Visited</b><br/>loaded from origin, untouched<br/>(what browsers do today)"]
L1["<b>L1 — Installed</b><br/>unmodified, NOT Xe-aware<br/>gains identity · pinning · offline · membership"]
L2["<b>L2 — Enhanced</b><br/>declared capabilities granted at the gates<br/>(read Xe data, stats) · journaled · revocable"]
L3["<b>L3 — Super</b><br/>fabric-level grants: frames.host + shell (Prism)<br/>apps.manage (package manager) · carry.broker"]
L0 --> L1 --> L2 --> L3
caption["No cliffs: L0 to L3 are grants, not rewrites.<br/>The same consent/trust machinery at every rung."]
L3 -.- caption
Three consequences bind the design (d/076):
- No cliffs on the ladder. The same consent and trust machinery applies at every rung. An app does not get “ported” to move up; it gets granted.
- Placement and capability are one decision seen from two sides. An app’s level determines both where it runs (a tab, a controlled-frame partition, an IWA profile) and what it may touch. They are designed together.
- L1 is a hard promise. Apps must be installable unmodified and un-Xe-aware. Any design that requires an app to “integrate with Xe” just to be installed violates the model. The first real-app target named in d/076 is Patchwork (Ink & Switch), L1 first.
The level is a property of the app’s grants, not of its packaging. That distinction is the whole point of the next section.
The two serving tiers (d/101)
If capability is the axis, packaging is a separate question — and here Xe made the default deliberately boring. There are two tiers, and the default is the plain one (d/101):
| Tier | Who | How it is served | Requirements |
|---|---|---|---|
| Regular apps (default, ~all apps) | ordinary web apps | plain HTTP on the plane origin http://<app>.localhost:5454, framed and isolated by Prism’s controlled frame | none beyond what the app itself wants — no signing, no bundle format, no manifest floor |
| Super web apps (rare, opt-in) | shells that need IWA-class powers themselves (controlled-frame hosting, agent-grade device surface) | the swbn/IWA tier | signed bundle; Prism is the canonical example |
The reasoning (d/101, from Luke: “most apps should be regular apps that are just http/s”): requiring per-app swbn packaging adds signing and bundling friction and quietly rebuilds a store-like gate — against the sovereignty litmus (exit without loss, no gatekeeper). Isolation is already delivered by the controlled frame; app-side IWA adds nothing for the common case. An ordinary app never needs the super tier to be published, listed, or loaded.
Honest note on residue. The super-tier machinery (
swbn/Iwa) is fully built and tested in the tree, and two example apps still ship as dev-signed swbn. Under d/101 those are the deprioritized super tier, not the default. The review tracks this as a live cleanup item.
Zoom 2 — the package format (XAM v0.2)
The manifest, as built
An Xe app is described by an XAM manifest — XenonAppManifest in
crates/apps/src/model.rs:127. Two things are worth knowing before the field
tables:
- Runtime validation is authoritative, not the JSON Schema.
validate()(model.rs:564) is the source of truth; the emitted JSON Schema (xam_json_schema,model.rs:674) is explicitly advisory — the code itself says cross-field invariants “are not honestly expressible as a small structural schema.” xam_versionis"0.2". This is a moving target; treat the field tables as an as-built snapshot, not a frozen spec.
Top-level shape (model.rs:127-165):
| Field | Required | Meaning |
|---|---|---|
xam_version | yes | "0.2" |
identity | yes | who this app is (below) |
source | yes | where its code comes from (below) |
components | yes | the parts that run (below) |
endpoints | no | declared network surfaces |
capabilities | no | requested grants, with reasons |
config_schema | no | declared config keys (also gates compose interpolation) |
secret_bindings | no | named secret slots |
instances | no | instance shaping |
serving | no | only a narrow SPA-fallback subset is recognized today |
handlers, interfaces, notifications | no | reserved — parsed and stored, not yet acted on |
Identity (model.rs:168): package_id, release.version, publisher,
display_name, optional icon. Publisher is a tagged enum (model.rs:191):
Signed { key_id }— a keyed publisher.UnsignedDerived { canonical_url, subdir }— identity is derived from the source URL. Deterministically:derived_package_id = sha256("xenon.unsigned-package.v1\0" + url + "\0" + subdir)(model.rs:736). Every compose app is currently unsigned-derived — see the review’s signing gap.
Source (model.rs:210): kind, locator, pinned, optional size,
adapter, detection, optional compose_path. SourceKind (model.rs:230)
is Compose | Git | Image | Web | Static | Swbn — with the explicit caveat in
code that these are “supported source kinds in the format, not necessarily
implemented adapters.”
Component (model.rs:249): id, type (Ui | Service | Task),
runtime { driver, entrypoint }, optional, depends_on, run_policy,
resources, optional task, optional frontend. The RuntimeDriver
(model.rs:299) is Cf | Iwa | Origin | Oci | Compose — the driver decides
how a component is rendered or run.
Endpoints (model.rs:357): Http { port, routes[] } | Tcp { port } |
Udp { port } | LanDiscovery { port }. An HttpRoute (model.rs:377)
carries the flags websocket, sse, range, no_buffer, large_body, long_timeout.
Non-HTTP classes “require separate consent.”
Capabilities (model.rs:396): each is { resource, ability, reason },
and the list must be sorted and unique or validation rejects it.
Resources (model.rs:407): a ResourceEnvelope of request,
required_minimum, and a breach { memory_pids, disk, cpu }. Validation only
checks that request ≥ required_minimum (model.rs:699) — it does not
yet push these numbers into container limits (see the
review).
Two on-disk package shapes — a conflation hazard, honestly
This is the one thing most likely to mislead a reader of the example apps: a
file named package.json (or *.xam.json) on disk can be one of three
different things. The recon found all three in xe-example-apps @5752a3e:
| App(s) | On-disk file | What it actually is |
|---|---|---|
excalidraw | package.json | a full XAM v0.2 (kind=static, adapter=static-v0, driver=origin, serving.spa_fallback=true) |
scratchpad, appdex | *.xam.json | a full XAM v0.2 (kind=swbn, driver=iwa, publisher signed, key_id=agent54-dev) |
guestbook, compose-board | package.json | a build-input descriptor (schema_version:1: package, version, display_name, compose, data_classes, gates) — the compose adapter compiles this into a XAM |
xe-notes, vitehello | package.json | ordinary npm manifests (Vite build inputs) — not Xe metadata at all |
So “the package.json” is not a single format. When you read one, check its
keys: a xam_version means it is a real manifest; a schema_version:1 with a
compose key means it is a recipe the adapter turns into a manifest; an npm
dependencies/scripts block means it is not Xe’s at all.
A naming artifact worth flagging.
guestbook/guestbook.xam.jsonis a byte-for-byte copy of the app’scompose.yaml, not a compiled XAM. It looks like a manifest because of its name; it is not one. Do not treat the file extension as proof of format.
Also note: the seven example apps that exist at 5752a3e are scratchpad,
appdex, guestbook, vitehello, xe-notes, compose-board, and
excalidraw. There is no paperlog, and Immich is not in this repo — it
exists only as a launcher live-test (crates/apps/tests/live_immich_manager.rs)
and on the p-207-immich branch.
The adapter / driver matrix
A source kind is turned into an installable release by an adapter, and the resulting components are rendered or run by a driver. Only three adapters are implemented today; the matrix below is honest about what is proven versus merely expressible:
source.kind | adapter | driver | Concrete instance | Proven? |
|---|---|---|---|---|
static | static-v0 | Origin | excalidraw (v0.18.1) | Yes — P-211 merged, live render, byte-identical on both peers |
swbn | swbn | Iwa | scratchpad, appdex (dev-signed); catalog console/hello-xe | swbn adapter tested; shells pinned (swbn.rs:434) — the d/101 super tier |
compose (frontend) | compose | Cf | guestbook frontend | Yes — frontend maps to Cf (compose.rs:1225) |
compose (service) | compose | Compose | guestbook backend, compose-board | Yes, live — P-141 lifecycle + P-203 full auto-wired end-to-end |
compose (whole-origin UI) | compose | Compose | Immich | No — package sound (P-207), render blocked → d/103 + P-210 (gated) |
git | via compose | Compose | git-sourced compose | path exists; not separately proven in examples |
image / web / oci | — | — | — | No standalone adapter |
Git is handled inside the compose adapter (GitComposeSource +
stage_source_tree, compose.rs:458, 685), not as its own adapter. The
Image, Web, and Oci source kinds are expressible in the format but have
no adapter behind them yet — a gap the review records.
Zoom 3 — from package to running app
The install pipeline, step by step
Installing an app is a small state machine with two human consent gates.
InstallState (crates/apps/src/lifecycle.rs:23; transitions at
lifecycle.rs:137):
Probing → Resolved → AwaitingPlanReview →[gate 1]→ Fetching → Building
→ AwaitingGrantConsent →[gate 2]→ Staging → Installed (any → Failed)
- Analyze. The adapter’s
analyzestep produces aPlan. If itsunsupported_featureslist is non-empty, materialization is prevented (adapter.rs:45, enforcedcompose.rs:1498). The install fails closed before anything is fetched or run. - Gate 1 — Plan Review (
approve_plan,lifecycle.rs:83). The user reviews the declared build effects: network hosts, context roots, resource summary, emulation (adapter.rs:69). The consent’s subject issha256(plan)— it approves exactly this plan, nothing else. - Materialize (fetch + build), gated by a
BuildGrantthat can only be minted from a matching gate-1 consent. - Gate 2 — Grant Consent (
approve_grants,lifecycle.rs:109). The subject is the exact locked release digest. The two gates are deliberately non-interchangeable — approving the plan does not approve the result. - Route registration.
origin::generatederives the route table from registry rows (crates/launcher/src/origin.rs:495). An install with a payload digest gets aStaticDigestroute; a backend-carrying install gets aBackendProxyroute. Either way the app is reachable athttp://<name>.localhost:5454.
The consent machinery — how a request is filed, claimed, and decided, and why the Console web bridge cannot approve — is a cross-cutting mechanism shared with backends, so it lives in engines & backends → consent gates. The install sequence diagram is there too.
Where the app model does not reach yet
- Half the source kinds have no adapter (
image,web,oci). They are expressible but not installable. - The reserved manifest families (
handlers,interfaces,notifications) are parsed and stored but not acted on. - Compose apps have no publisher key — they are always
UnsignedDerived, identity from URL + subdir. The real signing ceremony is deferred. - Resource envelopes are declared, not enforced into container limits.
Each of these is picked up with a decider and options in the app-model review.
Links
- App-model review · Engines & backends · Emerging specs: XAM v0.2
- d/076 (app spectrum) · d/101 (two tiers) · d/103 (compose UI rendering) — decisions ledger
- compose spec · OCI image spec · Isolated Web Apps
- Forge: xenon-launcher · xe-example-apps