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

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