Emerging Specs & Protocols
This is where formats and protocols firm up into candidates. A spec candidate here is
not a committee draft written ahead of code — it is the opposite: an as-built extraction
from the working tree at a named commit, written down so the shape is legible, allocatable,
and reviewable before it hardens. Every candidate below is honest about its maturity — most
are spec candidate (as-built, not ratified; field semantics may still move with an open
decision), a couple are marked normative where the code already fails closed against them,
and one (the launch descriptor) is PROPOSED, pending Luke. Where a candidate intersects an
open decision, it says so and links the design-frontier gap.
The rule these encode, worth making normative in itself: a new wire string, schema, or signing domain lands in the relevant table in the same change that introduces it — specs are allocated, not improvised.
External review (sol). An independent critical review by gpt-5.6-sol (2026-07-18) assessed
several of these candidates directly — the Peer Ticket / Fleet App-Index / peer-serve wire /
ALPN registry, the Plane Origin Convention / Route Table /
honest-error contract, the Prism↔app contract and super-app
profile, the XAM / compose-subset / engine-socket /
install-consent set, and the companion protocol / audit chain /
launch descriptor / grant–lease outline. Its blunt verdict:
these as-built extractions are honest but not yet sufficient for an independent, interoperable
implementation — they lack on-wire version/type discriminators, JSON and base64url parsing
profiles, canonical digest encodings, complete type bounds, and positive/negative conformance
vectors, and a few carry outright contradictions (the ticket’s “optional but required”
index_sha256; the honest-error contract’s actionless 400/405 rows; a v1-titled candidate
documenting a v0 wire identifier). Read each candidate’s Open design / Status note against
the matching memo in the external review before it hardens.
| Candidate | Layer | Source beat | Scope, in one line |
|---|---|---|---|
| Xe Peer Ticket v1 | Transport | transport / peers | The credential + address card a publisher hands a consumer. |
| Xe Fleet App-Index v1 | Peer serving | transport / peers | The published index, durable peer row, and merged catalog view. |
| Peer-serve wire protocol v1 | Transport | transport / peers | The authenticated QUIC byte-tunnel that carries a served app. |
| ALPN & signing-domain registry v1 | Transport | transport / peers | The allocation table for every wire ALPN and signing domain. |
| The Plane Origin Convention v1 | Serving plane | serving-plane | One loopback plane, one origin per app, deterministic conflicts. |
| Route Table schema v1 | Serving plane | serving-plane | The frozen route-table shape the workerd router is generated into. |
| The honest-error contract v1 | Serving plane | serving-plane | Every plane refusal: status, body shape, and its next-action rule. |
| Prism ↔ app contract v1 | Shell | shell-rendering | The asymmetric frame↔shell channel and leased gateway. |
| Super-app packaging profile v1 | Shell | shell-rendering | What makes an app Prism-class (the rare, opt-in super tier). |
Companion channel protocol v1 — apps.list | Devices | devices / agent | The read-only “what apps are installed?” protocol over iroh. |
| Companion audit hash-chain v1 | Devices | devices / agent | The tamper-evident per-device session ledger. |
| Launch descriptor (d/104) (PROPOSED) | Devices / shell | devices / agent | One label → plane origin shape serving every platform’s launch seam. |
| Grant / Lease delegation model | Agent layer | devices / agent | The UCAN-aligned capability → grant → lease → audit outline. |
| Xe App Manifest (XAM) v0.2 | App model | app-model / engines | The manifest carrying identity, source, and components. |
| Compose-subset security profile (normative) | Engines | app-model / engines | The install-time refusal list that fails closed on unsafe compose. |
| Engine socket contract v1 (d/094) | Engines | app-model / engines | Reaching a backend through the engine’s own unix socket, no sidecar. |
| Install-consent grant model | App model | app-model / engines | The two non-interchangeable consent gates and their custody. |
Each block below is contributed by the layer’s own writer and reproduced verbatim.
Xe Peer Ticket v1
Status: spec candidate — an as-built extraction from
crates/launcher/src/peer_serve.rs:86-105atxenon-launcher main @56e94575. Not ratified; field semantics may change with the G1/G4 decisions (review).
The ticket is the credential + address card a publisher hands a consumer.
Wire form: base64url-nopad( JSON(struct) ) — one shell-safe token, written
to a 0600/owner-only-DACL file. The file round-trips through a trailing
newline (:989-1003) and is capped at 16 KiB (MAX_TICKET_FILE_BYTES).
| Field | JSON key | Type | Req? | Meaning / source |
|---|---|---|---|---|
| node_addr | node_addr | {provider, address} | yes | Publisher iroh EndpointAddr (JSON); provider = "iroh-1" (lib.rs:41) |
| secret | secret | string | yes | base64url of 32 random bytes — the bearer credential |
| index_sha256 | index_sha256 | string (64 hex) | opt* | SHA-256 of the immutable app-index bytes; required by the consumer (peer_serve.rs:609) |
| origin_id | origin_id | string | opt | "iroh-" + sha256(node_addr); copied into each index row |
| device_name | device_name | string (1–128) | opt | Calm publisher name; consumer enforces a match (:593-604) |
| origin_port | origin_port | u16 | opt | Publisher HTTP authority port for Host rewrite; absent ⇒ legacy byte-pipe (:101-104) |
Open design intersecting this format: exp / lifetime (G1 option a),
audience-binding or UCAN envelope (G1 option d), membership gating making the
secret field secondary (G4).
Xe Fleet App-Index v1
Status: spec candidate — as-built from
crates/launcher/src/fleet_catalog.rs@56e94575. Three related shapes: the published index, the durable peer row, and the merged catalog view.
PublishedAppIndex — schema xe/app-index/v1, served at /xe/api/index
(:26, :72-85); deterministic name order; ≤ 1 MiB (MAX_INDEX_BYTES):
| Field | JSON key | Type | Meaning |
|---|---|---|---|
| schema | schema | string | "xe/app-index/v1" |
| device_name | device-name | string | publisher device name |
| origin_id | origin-id | string | public iroh publisher id |
| apps | apps | [PublishedApp] | deterministic name order |
PublishedApp (:52-70): name (localhost label), kind
(static | built | live-backend | multi-service), version, origin-id,
sha256 (lowercase hex), device-name, origin
(http://<name>.localhost:5454/).
FleetPeer durable row — schema xe/peer-list/v1, in peers.json + the
workerd mirror xe/peers.json (:88-120): id (= origin_id, row key),
device_name, origin_id, port (consumer forward), connector
(peer_ + first 16 hex of sha256(origin_id)), index_sha256,
index_file (xe/indexes/{peer_key}.json), liveness_file,
liveness_origin, publisher_origin_port?.
Merged catalog — /xe/api/catalog (assets/route-worker.js:102-130):
per peer {device_name, origin_id, status(live|offline), last_seen_unix_ms, observation, index_sha256, app_count} plus per-app entries whose launch URL
carries ?xe-peer=<id>.
Invariants worth keeping if this hardens: digest mismatch never overwrites
the last-good cached index; offline peers are retained with last_seen,
never silently pruned; register_peer refuses port 0 and the plane port
5454.
Peer-serve wire protocol v1
Status: spec candidate — as-built from
peer_serve.rs@56e94575. ALPN:xenon/serve/tcp/1.
Consumer Publisher (ALPN xenon/serve/tcp/1)
dial_alpn(node_addr, SERVE_ALPN) ── QUIC bi-stream open ──►
u8 len (=32) ‖ secret[len] ──────────────────────► read u8 len (1..=64)
read len bytes
constant-time compare vs 32-byte secret
◄──────────── u8 ACK: ACCEPT(1) | REJECT(0) ─────── (≤10 s preamble deadline)
if ACCEPT: copy_bidirectional(local TCP ⇄ iroh stream) ⇄ connect 127.0.0.1:target_port
- Framing: 1-byte length-prefixed secret preamble, then a raw untyped byte pipe (carries whatever HTTP the browser plane sends).
- Constants:
ACCEPT = 1,REJECT = 0,MAX_SECRET_LEN = 64,MAX_TUNNELS = 64, preamble deadline 10 s. - The catalog probe reuses the same authenticated stream but writes a literal
GET /xe/api/index HTTP/1.1with a rewrittenHost:(:788-839) — HTTP-over-tunnel for the catalog, opaque pipe for apps. - Revocation: none in-protocol; publisher restart voids all secrets (G1 — the primary open design on this wire).
ALPN & signing-domain registry v1
Status: spec candidate — a registry extracted from the codebase @56e94575 so that new protocol strings are allocated, not improvised. Rule worth adopting: every new ALPN or signing domain lands in this table in the same change that introduces it.
ALPNs (wire protocols):
| ALPN | Purpose | Gate |
|---|---|---|
xenon/net/echo/1 | echo experiment / dial default | membership if provider built with it |
xenon/serve/tcp/1 | peer-serve byte tunnel | bearer secret (today) |
xenon/net/group-sync/1 | membership head gossip | membership |
xenon/net/group-join/1 | join ceremony | single-use invite + proof-of-possession |
xenon/substrate/1 | remote substrate multi-lane session | membership (checked at dial) |
xenon/consent/test/1 | test-only extra-ALPN gate proof | membership |
Signing domains (what gets signed, never wire ALPNs):
xenon/group-document/v1\0 · xenon/group-envelope/v1\0 ·
xenon/group-join-proof/v1\0 ·
xenon/recovery/{successor,takeover,counter,freeze,rotation,rotation-accept}/v1\0 ·
xenon/wireguard-key/v1\0 · xenon/interface-* · xenon/mod-manifest/v1\0 ·
xenon/profile-* · (xe-sync v2:) xenon/group-document/v2\0 ·
xenon/group-envelope/v2\0 · xenon/member-endpoint-binding/v1\0.
Convention already followed in-tree, worth making normative: version suffix
on every string; signing domains end in \0; xenon/ prefix pending any
product-naming decision (the prefix is wire-internal and never user-facing).
The Plane Origin Convention v1
Status: spec candidate — an as-built extraction from
agent54/xenon-launcheratmain @56e9457(crates/launcher/src/origin.rs,assets/route-worker.js,fleet_catalog.rs). Ratifies d/100 (port) and d/101 (scheme). The scheme row is the one live tension — see review G3.
The addressing rule for every app on an Xe Computer: one loopback plane, one origin per app, deterministic conflict handling.
| Element | Rule | Source |
|---|---|---|
| Plane authority | 127.0.0.1:5454 + [::1]:5454, loopback only | d/100; fleet_catalog.rs:24 |
| App origin form | http(s)://<label>.localhost:5454 | d/100; route-worker.js:1 |
| Label grammar | 1–63 chars, [a-z0-9-], no leading/trailing - | valid_label origin.rs:1048-1055; regex route-worker.js:1 |
| Reserved labels | xe, api, catalog, moby, localhost | origin.rs:309 |
| Reserved service paths (loopback host) | /xe/api/index, /xe/api/catalog | route-worker.js:11-12,384-385 |
| Conflict / qualified form | <pretty-prefix>-<first-12-hex-of-sha256(package_id)>; A1 tiebreak — the lexicographically smaller identity hash keeps the pretty name | qualified_name origin.rs:1072-1082; conflict_winner :401-409 |
| Claim lifecycle | claim → tombstone (revoke) → explicit xe origin reclaim ceremony; tombstoned names never silently reused | origin.rs:286-396 |
| Scheme | https default (name-constrained CA leaf); http only under the dev-http flag | d/101; origin.rs:819-830, app_command.rs:844-850 |
| Port override | XE_ORIGIN_PORT (installers / isolated smoke tests only) | origin.rs:39-45 |
Invariants worth ratifying. Origins are stable identity: the origin string is the browser’s storage and service-worker key, so a tombstoned name is never silently reused and conflicts resolve by a deterministic hash tiebreak, never by timestamp. Open at ratification: the scheme row — https-default (code) vs plain-http-default (d/101) — must be settled before this hardens.
Route Table schema v1
Status: spec candidate — as-built from
origin.rs@56e9457. Thetypediscriminator is frozen by P-148 (PR#36); this table documents the frozen shape. Growth rule and the whole-origin overload are open — review G1 / G5.
RouteTable — schema xe/origin-routes/v0 (origin.rs:99-103):
| Field | Type | Notes |
|---|---|---|
schema | string | xe/origin-routes/v0 |
port | u16 | rendered plane port |
routes | [OriginRoute] | sorted by name |
OriginRoute (origin.rs:105-134):
| Field | Type | Present when | Notes |
|---|---|---|---|
name | string | always | .localhost label |
package_id | string | always | provenance |
type | enum static-digest | backend-proxy | peer | always | frozen (P-148); unknown → fail-closed 404 |
digest | string | non-empty for static; omitted otherwise | sha256 of extracted payload; skip_serializing_if keeps pure-static bytes identical |
static_serving | StaticServingPolicy | opt | headers / redirects / spa_fallback, parsed at install |
backend | BackendArm | backend-proxy only | omitted for pure static |
peer | PeerArm | peer only | |
iwa | bool | opt-in only | drives COOP/COEP/CSP + dotfile manifest store (d/101) |
BackendArm (origin.rs:137-152): prefix (single proxied path prefix,
segment-matched) · idempotent_get [string] (cold-start wait-set) ·
connector (workerd binding name; always "backend" today).
PeerArm (origin.rs:155-165): device (calm display name) · connector
(binding; "peer" or fleet connector) · origin_port ?u16 (publisher
authority port; absent = legacy portless ticket).
PeerRouteState — schema xe/peer-route/v0 (origin.rs:168-177): app ·
name · device · port (must == 5198, d/102) · origin_port?.
Grant — schema xe/origin-grants/v0 (grants.rs:69-88): id ·
source_origin · target_app · path_prefix · methods[] · credentials
(omit | same-origin | include) · byte_ceiling · issued_at · expires_at
(TTL cap 3600 s). Grants cover static reads only and are re-read per request
and per streamed chunk.
Growth rule (proposed, G5). Additive-only + fail-closed is the de-facto rule
today. The whole-origin compose case ships as a backend-proxy overload (empty
digest + prefix:"/", P-203); a distinct container-origin type would be the
first real schema change and is pending Luke (d/103-B).
The honest-error contract v1
Status: spec candidate — as-built from
route-worker.js@56e9457. Every refusal in the refusal ladder conforms to this. The502distinction is an open question — review.
Every plane-generated error is text/plain; charset=utf-8 with
X-Content-Type-Options: nosniff, and every body names the next action.
| Status | Trigger(s) | Body shape | Extra headers | Source |
|---|---|---|---|---|
400 | CONNECT/TRACE; bad Host; hostname ≠ authority; bad %-encoding; path traversal | 400 Bad Request | nosniff | :381,386,388,390,391 |
403 | cross-origin refused / preflight / credentials / backend cross-origin / cross-site CSRF | action-ending copy, e.g. “Ask the app publisher to declare access or request a grant, then retry.” | grant CORS echoed when a grant applies | :305,412,421,429,431 |
404 | not installed; unsupported route type; not found; peer-under-grant | “This app is not installed. Run xe app install PACKAGE…” / “…Run xe doctor, then retry.” | — | :298,318,342,424,425,437,438 |
405 | unsafe method on static route | 405 Method Not Allowed | Allow: GET, HEAD | :449 |
503 | plane table missing; backend connector unavailable; backend unreachable; index unpublished; peer offline | “…Run xe doctor, then retry.” / peer: “This app runs on <device>, which is offline. Retry, or open it on that device.” | backend: Retry-After: 2; peer/index: Cache-Control: no-store; peer adds Vary: *, xe-peer-liveness: offline | :280,286,92,99,423,436; peerOffline :39-44 |
Contract invariants worth ratifying. Every body ends by naming a next action
(, then retry.); every error carries nosniff; backend transients carry
Retry-After; liveness/catalog responses carry Cache-Control: no-store.
The one deliberate gap. There is no 502 today: upstream backend/peer
failures are synthesized as 503 (a plane-side “can’t reach” condition), and a
genuine upstream 5xx passes through unchanged. Distinguishing “plane can’t reach
upstream” (503) from “upstream errored” (502) is a deliberate addition, not a
bug — flagged for ratification.
Prism ↔ app contract v1
Status: spec candidate — an as-built extraction from
agent54/prismatmain @98acaa3(docs/channel.md,src/channel/,src/gateway/). The surface is deliberately minimal and asymmetric today; field names and the verb set may change with the frame↔shell decision (G2 in the review). The document’s own note is normative: a future Controlled-Frame adapter must keep the same port → nonce → schema order and the rejection-journal contract.
The contract has two directions, and they are not symmetric.
App → shell — the authenticated verb channel
Transport: one MessageChannel per frame; one port is transferred to the guest
with targetOrigin pinned to the guest origin; a 128-bit nonce is sent only
over that transferred port. The shell accepts a message only after three checks
in this order — port identity → nonce equality → exact {nonce, verb, args}
schema (src/channel/message.ts:41-63).
| Verb | Args | Meaning | Journals as |
|---|---|---|---|
tab.open | {url} | Navigate the pane to a URL | navigate.tab |
tab.close | {} | Close the pane | navigate.tab |
tab.reload | {} | Reload the pane | navigate.tab |
key | {key} (non-empty string) | Deliver a keystroke to the pane | navigate.tab |
Every accepted verb is journaled; every rejection is journaled as
channel.reject with the origin and the failing check, before/after null
(src/channel/frame-channel.ts:18-35). Explicit non-goals stated in
docs/channel.md: the channel does not establish user intent, does not
authorize anything beyond gateway behaviour, and does not distinguish
same-frame scripts — “any script that obtains the transferred port and nonce
holds the capability.”
Shell → app — the leased gateway
The shell reaches into the pane through a capability-leased gateway
(src/gateway/gateway.ts:18-44). Each operation mints a single-use Stage-0
lease (5-minute TTL) and records an AuditEntry; cross-origin cooperation is
possible only if the app opts in by exposing a prismRpc bridge
(web/dev-origins/site.html).
| Operation | Purpose |
|---|---|
readText / readDom / readField | Observe the pane’s content |
mutateNode / filterContent / restoreContent | Enhance / mutate the pane |
answerPermission | Answer a permission request (allow / deny / shield) |
navigate / navigateTab | Drive navigation |
Reserved but not a contract yet: window title, notifications, focus, and app liveness — the app-declared surface a future revision may add (G2).
Super-app packaging profile v1
Status: spec candidate — what makes an app Prism-class (super-app tier, d/101), extracted from
packaging/bundle/+ the two manifests atagent54/prism main @98acaa3. This profile is opt-in and rare — per d/101 the default app tier is plain http(s) with none of the below. This is the exception the shell itself takes.
An app qualifies for the super-app tier — the tier permitted to host Controlled Frames and run cross-origin isolated — when all of the following hold:
| Requirement | Value / source | Why |
|---|---|---|
| Packaging | Signed web bundle (.swbn) via wbn / wbn-sign; origin key-derived (isolated-app://…) | Sealed, key-identified app the OS installs natively |
Manifest display | standalone (web/manifest.webmanifest) | No address bar; app-window presentation |
Manifest permissions_policy | { "controlled-frame": ["self"], "cross-origin-isolated": ["self"] } | The two capabilities that define a super-app: may host Controlled Frames; runs cross-origin isolated |
| Response headers | COOP: same-origin + COEP: require-corp + CORP: same-origin (packaging/bundle/headers.json) | Yields crossOriginIsolated: true, a precondition for hosting a Controlled Frame |
CSP frame-src | admits the guest plane origins (http://*.localhost:5454) | Lets the shell frame app panes; nothing else |
| Install | as an IWA with an OS app shim (d/053) | Dock / taskbar identity; wake via Boot Service |
A note on the manifest split as-built: the root manifest declares
start_url: "/" while .well-known/manifest.webmanifest declares
start_url: "/iwa-controlledframe.html" (the fleet-catalog home). Both carry
the same permissions_policy. Whether the catalog descriptor is the canonical
launch seam across platforms is the open decision d/104
(review).
Companion channel protocol v1 — apps.list
Status: spec candidate — an as-built extraction from
crates/companion-core/src/launcher_apps.rsatxenon-runtime main @b0cfc90. Read-only in v1; field semantics may change with d/104 (the launch descriptor, devices review). Note: the responder side is not yet wired on the phone-as-host path (P-188); this table specifies the protocol the dialer already speaks.
The companion asks a desktop “what apps are installed on you?” over a single authenticated iroh stream. One method, read-only, no install/start/mutate verb.
- ALPN:
xenon/companion/apps/1· version:1· method:apps.list(the only v1 method). - Framing: one newline-delimited JSON request → one newline-delimited JSON response.
- Bounds:
MAX_MESSAGE_BYTES = 256 KiB,MAX_APPS = 1024. - Auth: rides the already-authenticated, membership-checked group session — the lane opens only after membership passes at dial time. There is no in-protocol credential; authorization is the session.
Request ListAppsRequest (serde(deny_unknown_fields)):
| Field | JSON key | Type | Value |
|---|---|---|---|
| version | v | u64 | 1 |
| id | id | string | correlation id (e.g. android-list-1) |
| method | method | string | apps.list |
| params | params | object | {} (empty in v1) |
Response ListAppsResponse → result: LauncherAppsView or
error: {code, message}:
LauncherAppsView | Type | Notes |
|---|---|---|
computer_name | string | ≤128, non-control |
apps[] | LauncherAppEntry | ≤1024 |
LauncherAppEntry | Type | Validation |
|---|---|---|
name | string | ≤256 |
serving_origin | string | ≤2048, must start http:// | https:// |
isolated_app_url | string? | ≤2048, must start isolated-app:// if present |
Error codes: unsupported_request (wrong version/method) ·
invalid_request (parse failure).
Companion ALPNs (the phone’s three, for the wire registry — companion-core @b0cfc90; distinct from the launcher-side registry in the transport beat):
| ALPN | Purpose | Phone direction |
|---|---|---|
xenon/net/consent/1 | consent request/attest | inbound + registered (only dispatched ALPN) |
xenon/companion/apps/1 | this apps.list protocol | outbound dial (responder unwired, P-188) |
xenon/substrate/1 | on-device plane route_info + open | outbound dial |
LauncherAppEntry is the launch descriptor minus
the tier flag — the two should converge if d/104 ratifies.
Companion audit hash-chain v1
Status: spec candidate — as-built from
companion-android/.../SessionStore.kt:95-112@b0cfc90. Tamper-evident (each line commits to the prior), not append-only or externally anchored.
record := seq "|" ts "|" action "|" detail "|" hash "\n"
seq := monotone line index (lines + 1)
ts := ISO-8601 "yyyy-MM-dd'T'HH:mm:ss.SSSZ" (millis + tz)
prevHash := prior record's hash field, or literal "genesis" at chain head
payload := seq "|" ts "|" action "|" detail
hash := hex(SHA-256(prevHash + "|" + payload))[0..16] // 64-bit truncated
sanitize := detail/action → [\r \n |]+ collapsed to '-', field capped 2048 bytes
Actions observed in-tree: wake, kill, off, cross, list, launch, resume.
Invariants worth keeping if this hardens: off is written only on an explicit
clean STOP; every other teardown (crash, failed bind, onDestroy without STOP)
is kill — the ledger records how a session ended, not just that it did.
Convergence note: the agent-layer AuditEntry.prevHash
(injection-boundary-design.md §3.4) is the same scheme; the product should
land one ledger discipline, not two subtly different “audit logs”
(agent-layer review).
Launch descriptor (d/104)
Status: spec candidate — PROPOSED shape from
decisions/104-cf-guest-launch-seam.md(Luke disposes; nothing dispatches until ratified). Grounded in d/100 (plane origin) + d/101 (tiers) + d/103 (compose UIs are plane origins).
LaunchDescriptor {
label: string // user-facing app name (from the host xe catalog)
plane_origin: URL // http://<app>.localhost:5454 (d/100)
tier: regular | super // d/101 — super = IWA / isolated-app://
}
One descriptor shape serves every platform and both surfaces — the desktop
fleet catalog (P-197) and the parked companion apps.list responder (P-188)
would feed from it, replacing per-app baked guest URLs with a single route.
Android’s LauncherAppEntry is
already this descriptor minus the tier flag.
Grant / Lease delegation model (UCAN-aligned)
Status: spec candidate — an outline built from the shipped primitives
xenon-cap/src/lib.rs:30-84andxenon-grant/src/lib.rs:23-53,233-291@b0cfc90, plusinjection-boundary-design.md §3(paper). Thecapabilities[] → grantminting pipeline and theagent://actenforcement are not yet wired — this specifies the model those would target. See the agent-layer chapter.
Capability = (resource-URI, ability-verb, constraints{ paths, ttl, max_calls,
fields, budget })
Grant = runtime-authority-signed Delegation → install-principal,
audience + expiry, grouped in a named/versioned Bundle,
onward:false // no re-delegation
Lease = short-lived attenuation minted UNDER a grant:
nonce + useBinding(single-use | count | session)
+ notBefore/notAfter + signer + audit-ref
AuditEntry = ledger twin per lease-use:
{ leaseId, nonce, before, after, cause, prevHash }
UCAN mapping (the recommended external anchor — a product decision, not an implementation detail):
| This model | UCAN analog |
|---|---|
| Grant | a UCAN delegation — issuer = runtime authority, audience = install principal, capabilities, exp |
| Lease | an attenuated UCAN invocation used once — the nonce is the CID-equivalent replay guard |
onward:false | a leaf carrying no further delegation proofs |
| agency-ladder rung (d/082, d/091) | the ability-verb axis under agent://act |
Open decisions intersecting this shape: CM-1 (the v1 grantable set);
CM-2 (outbound network — declared-only vs. block-catalog); whether
agent://act is enforced in v1 or stays declared + audited; and D-5 (the
lease-signing root, given the shell shares an address space with the forgeable
iwa: IPC channel). All decider = Luke.
Xe App Manifest (XAM) v0.2
Status: spec candidate — an as-built extraction from
crates/apps/src/model.rsatxenon-launcher main @56e94575.validate()(:564) is authoritative; the emitted JSON Schema (:674) is advisory by its own admission. Field names may change with the tiers/rendering decisions (app-model review).
Required: xam_version = "0.2", identity, source, components. Optional:
endpoints, capabilities, config_schema, secret_bindings, instances, serving.
Reserved (parsed + stored, not acted on): handlers, interfaces, notifications.
| Group | Field | Type | Notes |
|---|---|---|---|
| identity | package_id | string | non-empty; xenon/* system-reserved |
| identity | publisher | Signed{key_id} | UnsignedDerived{canonical_url,subdir} | compose ⇒ always UnsignedDerived |
| identity | derived id | — | sha256("xenon.unsigned-package.v1\0" + url + "\0" + subdir) (:736) |
| source | kind | compose | git | image | web | static | swbn | image/web/oci have no adapter yet |
| source | pinned | string | sha256 digest or git commit |
| component | type | ui | service | task | task-only fields on a non-task ⇒ reject |
| component.runtime | driver | cf | iwa | origin | oci | compose | driver decides how it renders/runs |
| endpoint | class | http{port,routes} | tcp | udp | lan_discovery | non-http ⇒ separate consent |
| endpoint.route | flags | websocket, sse, range, no_buffer, large_body, long_timeout | per HttpRoute (:377) |
| capability | {resource, ability, reason} | strings | sorted + unique, else reject |
| resources | {request, required_minimum, breach} | envelope | request ≥ required_minimum enforced; container-limit enforcement is a TODO (EB-5) |
Adapter stages (crates/apps/src/adapter.rs:242), the contract a source
kind is compiled through: probe (pure) → resolve (may fetch) → analyze
(pure → Plan with unsupported_features) → materialize (executes, only
under a matching BuildGrant) → compile (pure → PackageRelease) →
validate (against LauncherFeatures). A non-empty unsupported_features
prevents materialization.
Compose-subset security profile — NORMATIVE refusal list
Status: spec candidate (normative) — as-built from
compatibility_diagnostics,crates/apps/src/compose.rs:907@56e94575. Install fails closed if any item appears; each yields a stable code +source:line:col.
privileged · raw_devices (devices:) · host_networking
(network_mode: host) · network_namespace_join
(network_mode: container:/service:) · pid_sharing (pid:) · ipc_sharing
(ipc:) · ambient_env_file (env_file:) · added_capabilities (cap_add:)
· host_user_namespace (userns_mode: host) · unconfined_security_opt ·
unpinned_image (must be @sha256: or a fixed non-latest tag; build-only
exempt) · host_path (bind mounts) · docker_socket
(/var/run/docker.sock) · out_of_tree_build_context · ssh_forwarding
(build.ssh) · build_entitlements · host_port_publication (ports: — use
expose) · external_network · external_volume.
Interpolation profile (interpolate_declared, compose.rs:856):
${VAR:-} / - / ? / + operators are forbidden; only keys present in the
declared config_schema may interpolate; non-scalars are rejected.
⚠️ Known holes (P-174, open): short-form host ports (ports: ["8080"]),
non-UTF-8 corruption in interpolate_declared, and network_mode: container:.
P-174 gates running compose on user-supplied manifests — see
EB-3.
Engine socket contract v1 (d/094)
Status: spec candidate — as-built from
crates/launcher/src/backend.rs,origin.rs, andcrates/substrate/src/backends/smolvm.rs@56e94575. This is the “third path” of d/094: no sidecar, no loopback publication of backend ports.
| Concern | Rule | Anchor |
|---|---|---|
| Discovery | exactly-one docker.sock under <data-root>/engine/smolvm/ (depth ≤ 8); zero or many ⇒ refuse | smolvm.rs:276 |
| Custody | launcher sets DOCKER_HOST=unix://… (Linux) / tcp://127.0.0.1:2375 (mac); the router never holds the socket | backend.rs:854 |
| Reach container | docker compose exec -T <svc> sh -c 'nc 127.0.0.1 <port>' — never host-published, never a sidecar | backend.rs:827 |
| Connector | one launcher process on 127.0.0.1:5199, started iff the app has_backend | origin.rs:708, manifest.rs:333 |
| Lifecycle | singleflight start · WaitClass wait-set · 503 (slow) / 502 (failed) split · invalidate / stop | backend.rs:188 |
| Tamper check | pinned launcher + binary sha256 verified before exec | smolvm.rs:301 |
Install-consent grant model
Status: spec candidate — as-built from
crates/apps/src/lifecycle.rs,adapter.rs,runtime.rs, andcrates/launcher/src/consent.rs@56e94575.
Two non-interchangeable gates:
| Gate | Subject digest | Grants | Anchor |
|---|---|---|---|
| Plan Review | sha256(plan) | BuildGrant (unforgeable: minted only from a matching gate-1 consent) | adapter.rs:93 |
| Grant Consent | locked release digest | authorizes the runtime to build a runnable project | lifecycle.rs:109, runtime.rs:138 |
Custody invariants: a consent request is an inert JSON record
(pending/<id>.json) minting no authority (consent.rs:75); deciding claims it
by atomic rename pending → claimed → decided (single executor);
MAX_PENDING=8, REQUEST_TTL_SECS=15 min. The Console / web bridge has no
approval endpoint by construction — only a launcher-owned surface (xe consent / macOS tray) mints authority (consent.rs:1-28). This install
consent is distinct from the cross-origin Grant table (grants.rs,
d/086; scoped, request-level, ≤ 3600 s CORS grants) — do not conflate “install
consent” with “cross-origin grant.”