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

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.