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

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