Skip to content

App execution model: no app JS - components drive a curated DOM surface, the UI frame runs only framework code #16

Description

@lannbot

Provisional plan — recorded 2026-08-16 from the founding sandbox discussion. Adopted as the working app-execution model; converted to a ruling only by the checkpoints at the end. This amends the sketch in NOTES.md — App-frame sandboxing: the UI frame no longer executes an app-supplied HTML/JS bundle — it executes only framework shim code. Related: #5, #6, #7, #13; shape rules for the surface in #15.

The decision

Applications ship no JS, HTML, or CSS that executes or parses as page content anywhere in the system. The component is the entire app:

  • App logic runs as a wasm component (deltic, framework side), per the existing components bet.
  • App UI is expressed through a typed, DOM-shaped host surface (WIT): element creation from validated vocabulary, attributes checked per (tag, attribute), URL-typed values only as resource asset handles resolved from the manifest, events delivered as records (stream<event>), styling via parsed-and-laundered stylesheet assets or property-level calls.
  • The UI frame (sandboxed, dedicated origin, header CSP per NOTES) runs only trusted framework shim code, applying validated operations to real DOM. App-authored strings are never parsed as HTML there; app-authored script never executes there.
  • JS is userland, not platform: apps wanting JS compile it into their component (componentize-js / StarlingMonkey; QuickJS), optionally against a guest-side DOM shim so DOM-expecting frameworks (React-class) largely work. The sandbox is indifferent — the interpreter sits inside the capability boundary.

Why

  1. Any realm executing app JS keeps a residual-channel long tail. The App-frame sandboxing: a ruling table per sandbox flag and CSP directive; residual channels; anti-spoofing chrome #5 ruling table (WebRTC data channels with webrtc 'block' support caveats, speculation rules, fresh same-origin realms resurrecting censored globals, UA-initiated fetches…) is load-bearing forever if app script runs in the frame, and it degrades on exactly our worst browser (Safari). With no app JS, that table demotes to defense-in-depth backstop; the primary boundary becomes the component import set — enumerable, diffable in CI, fuzzable per-function. There is no realm to escape and no global to censor.
  2. String-HTML never crosses a trust boundary, killing the mutation-XSS / parser-differential class outright rather than sanitizing at it. CSS is treated as code (it is one): parsed with the engine's own parser (constructable stylesheets), URL-bearing properties rewritten to asset handles, @import inlined at install.
  3. No live platform objects are reachable from app code. Handles are opaque component resources; traversal escapes (ownerDocument → window → network) exist only if the surface exports them. Validator TCB shrinks from "HTML/CSS/JS sanitizer" to typed per-op checks.
  4. Capability story becomes uniform. The UI surface is just another set of imports under the Permission model: linking as enforcement - deny-by-unlink, prompt-on-first-use, revocation semantics, durable grants #6 linker model — same deny/prompt/revoke semantics as network and data, one consent vocabulary. The honest exfil claim from Network capabilities: per-destination grants, the app manifest, proxied fetch and the audit log, exfiltration stance #7 is unchanged: this bounds unconsented channels and shrinks the enforcement surface; it does not make granted-channel encoding impossible.

What it costs (stated plainly — the Sandstorm lesson)

  • No as-is porting of existing web apps. UI must target the surface (directly or via the SDK shim). This is the existential DX risk; Developer experience: the componentize-js app SDK, fetch/storage shims, the porting story #13 carries it.
  • Sync layout reads become async or shim-cached (getBoundingClientRect-shaped code changes form). Two-hop RPC latency budget per NOTES stands.
  • Accessibility must be first-class in the surface vocabulary from day one — semantic elements and ARIA in v1, not retrofitted. (This is also why canvas-only rendering was rejected despite its smaller surface.)
  • Text input, IME, focus, selection are the classically thorny parts of mediated DOM; they need explicit design, not emergence.

Alternatives considered

  • App HTML/JS bundle in the sandboxed+CSP frame (the prior NOTES sketch): rejected as primary — keeps the ruling table as the real boundary forever, plus sanitizer burden, plus realm problems. Any future "legacy tier" running such bundles is a separate decision with its own threat entry, not a default.
  • Hardened JS (SES/Compartment) in a worker: viable but adds lockdown/engine-compat risk and keeps JS-realm reasoning; NOTES prior-art already names SES/Endo the road not taken — this extends that choice to the UI layer.
  • Platform-imposed interpreter-in-wasm: unnecessary given components; apps may do it themselves (see JS-as-userland above).
  • Canvas-only UI: total mediation, unacceptable a11y/text/i18n.

Performance posture

App logic runs at native wasm speed. The DOM surface is per-call and chatty by design (future-compatible per #15); the shim batches host-side per animation frame, and the postMessage hop — not the component boundary — dominates the UI path. Mozilla's Dodrio experiment (45% of DOM-change application time was glue overhead) both motivates host-side batching now and prices the native-binding upside later.

Conversion checkpoints (provisional → ruling)

Open sub-questions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions