Skip to content

[DO NOT MERGE] execution runtime architecture - sandboxing prototype - #5740

Draft
habdelra wants to merge 93 commits into
mainfrom
codex/boxel-execution-runtime-architecture
Draft

[DO NOT MERGE] execution runtime architecture - sandboxing prototype#5740
habdelra wants to merge 93 commits into
mainfrom
codex/boxel-execution-runtime-architecture

Conversation

@habdelra

Copy link
Copy Markdown
Contributor

No description provided.

christse and others added 30 commits August 4, 2026 20:50
Two planning documents for completing Capsule/Sandbox parity: a root-cause
diagnosis with ordered work slices, and the protocol-first replan that
derives the smallest rendering contract from main and treats the three
trust tiers as conforming adapters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
evaluateModuleInCurrentRealm again sticks the AMD registration onto the
define function itself, with the original comment explaining why: Rollup
removes locals that are only visible to eval, so a closure variable is not
safe in a production build.

Deletes zero-referenced runtime code: TrustedBaseFormat, LocalSurfaceClient
(the surface modifiers are the Direct/Capsule dispatch path), the
evaluator's format-only import facade and test shim escape hatches, the
classifier's unconsumed format-only-import analysis, and the loader's
write-only fetched-shim bookkeeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The normative rendering contract extracted from main: component entry,
formats and the cascade table, field/model/argument semantics, computed
values (function-form only), configuration, routing rules R1-R5, the
five-state relationship model, instance identity, edit/save (full-document
PATCH, clientRequestId echo suppression, no revision token), the context
plane with exact degraded behaviors, scoped CSS, prerender constraints,
protocol records/operations, tier obligations, conformance machinery, and
the deferred/excluded lists. Statements carry RP-x.y ids for the
statement-to-test bijection; DRAFT until the Direct equivalence suite is
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The addon's package root is its Node-only Ember CLI build hook; Vite
bundles it into the test build, and evaluating window.require crashes
bundle load before any test runs. Alias the bare import to the
addon-test-support browser module and supply the broccoli-generated env
module from a local stub, following the existing classic-addon alias
pattern in this config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The protocol module now owns the cross-boundary record vocabulary: the
SafeEvent/SafeEventTarget projection, the template bundle with its typed
render-dependency union (authored-component | trusted-export |
literal-value), and version/feature assertions. Bundles carry a protocol
version and are validated before reification; an unknown dependency kind
rejects the whole bundle. The execution engine asserts the semantic
protocol version and required features on every render record, so an
unsupported record fails closed to last-known-good instead of rendering
an unknown shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BoxelRenderRecord now has exactly one producer shape. A new pure module,
lib/boxel-projection.ts, owns the semantic projection of a canonical
instance (description, resolved fields with linked values as
BoxelValueReference references, merged field configuration, field
descriptions, instance presentation) plus the trusted-semantics document
projection, which now returns a new document instead of mutating the
request in place. buildBoxelRenderRecord() is the one assembly point and
derives the model from the resolved fields, so tiers cannot disagree
about declared values.

Direct consumes the shared pipeline directly and remains the reference
implementation. Capsule adopts the Host projection per instance (wired
through the execution engine), so trusted-Base semantics materialize
once, Host-side, and cross as data, while the Capsule evaluator keeps
ownership of authored templates, getters, computeVia, and actions; its
Host-less fallback now conforms to the shared shapes (reference-shaped
values, format inventory from the declared vocabulary rather than a
hard-coded list, writability defaulting to false absent a Host grant).
The Sandbox child already builds records with the Direct runtime, so it
inherits the shared pipeline unchanged.

The record-diff conformance test renders one fixture through Direct and
Capsule and deep-diffs the BoxelDescription, resolved fields, instance
identity, and presentation, alongside shape probes that pin the
reference semantics themselves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vendored currency-code-symbol-map data exists solely so the Host can
shim the esm.run package of the same name, which deployed realm code
(including base/currency.gts) imports. Base's Currency field goes back
to importing that package identifier, served by the Host-owned shim in
every tier's loader, and the map is no longer re-exported from the
runtime-common index, so nothing can reach for it as a semantic
substitute for trusted Base behavior. Trusted getters such as
CurrencyField.symbol cross execution boundaries as data via the Host
projection pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Direct is the reference implementation, so correctness is enforced by
three layers: an equivalence suite proving the protocol's Direct tier
renders identically to main's legacy CardRenderer path, a conformance
suite exercising routing, the format cascade, computed fields, links,
error chrome, and the Direct-vs-Capsule semantic record diff, and a
statement<->test bijection checker that fails CI when a test cites a
statement the spec does not make (RP-0.3). Coverage of the reverse
direction stays report-only until the spec reaches NORMATIVE status
(--strict); skipped tests never count as coverage.

BoxelExecutionRenderer now seeds both default-format axes from the
caller's format exactly as main does (RP-1.5), with the Capsule
child-format cascade (RP-2.6) supplied by a provider scoped to the
Capsule slot. Verified by the equivalence suite: 14/14 against the
live test stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isolated HTML is rendered and stored on prerendered_html.isolated_html
during indexing, but it was not queryable through the entry/search APIs:
isolated was absent from PRERENDERED_HTML_FORMATS, so assertHtmlQuery
rejected format=isolated with a 400 and store.fetchCardEntry's format
param would not typecheck, and the search projection never selected the
column so the enumerators had nothing to emit.

Add isolated to the format allowlist, select ph.isolated_html in the
renderSet projection that feeds searchEntries (hence /_search and
/_federated-search), and emit an isolated candidate from the row and
file rendering enumerators. isolated is a scalar column rendered at the
row's native type, so it behaves like atom/head.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ence

Browser comparison of real cards between this branch's host and the
deployed host surfaced four defect families; each is fixed at its root:

- Projection collapsed every card-shaped value to a reference stub.
  contains/containsMany composites and loaded, side-loaded links now
  expand recursively (cycle-guarded, depth bounded by what the document
  side-loads); unloaded links keep the RP-7 reference form. This restores
  nested composite values, computed titles over compound entries, enum
  labels, and authored code that reads a loaded link's fields.
- The sandbox module allowlist was seeded only from the entry module's
  static import graph. Modules declared by the document's own resource
  meta (linksTo/linksToMany targets) are now granted per-document via
  modulesConsumedInMeta, still an exact allowlist.
- A sandbox child failure after bootstrap was structurally silent: the
  parent removed its only control-port listener when bootstrap settled,
  and a hung child left renders pending forever. The control protocol
  gains a runtime-error variant with a persistent listener, renders get
  a bounded timeout, and the child installs error/unhandledrejection
  reporters wired to that contract, so failures surface as the standard
  error presentation instead of a blank card.
- Scoped CSS with network-bearing values (@import/url) classified to
  Capsule, whose CSS policy rejects those spellings, and the prerendered
  placeholder path swallowed the rejection silently. The classifier now
  routes network-bearing scoped CSS to the Sandbox tier using the same
  shared pattern the policy enforces, and a rejected placeholder
  stylesheet is logged instead of vanishing.

The prerendered placeholder for isolated surfaces now prefers the
isolated-format entry (falling back to embedded), building on the
isolated prerender exposure cherry-picked from main. New unit and
integration coverage accompanies each fix, including the first
RP-15.3-cited sandbox conformance tests (19 of 91 statements covered).
Suites are authored and type-checked; the full browser test battery runs
next as its own step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Analysis of the frozen reference branch's instant-reload system for
iframe rendering, mapped onto this branch's transport and authority
seams: inventory, mechanism walkthrough, the hard-won edge cases that
must be honored, per-piece portability verdicts, and an ordered
extraction plan. Notable scope findings: the iframe tier needs no
DOM-adoption primitive (that machinery is Capsule-only), and the module
authority's observe()-grown admission already matches the reference
behavior, leaving generations, acknowledgements, and last-known-good
retention as the work to implement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Volatility is a mode orthogonal to execution tier: cards run stable by
default and promote to a volatile Capsule/Sandbox session — draft
buffers, HMR generations, a lease-based quiet-period demotion — only
while a user or agent is actively editing their source. The plan states
the isolation requirement (an edit session shadows only the edited
modules and never invalidates loaders shared with other mounted cards),
the vite-like refresh/flash budget, and the sequencing after sandbox
HMR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Once a module goes volatile it stays volatile in the loader until the
tab closes; the next session starts stable from persisted state. This
removes the quiet-period lease and its lease-expiry-mid-save race from
the design — per-tab volatile state is bounded and evaporates with the
tab, and stable-graph isolation keeps the rest of the workspace
unaffected either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes verified against real cards rendered side by side with the
deployed host, plus two named residuals:

- Relationship values: linksToMany no longer silently drops
  mid-resolution slots (membership state is read through the sanctioned
  API after the field getter triggers resolution), and the execution
  session re-projects when a pending relationship settles — first paint
  stays immediate with the link absent, settlement republishes one
  fresh generation through the existing update path.
- Sandbox child boot: the credentialless iframe no longer constructs
  MatrixService (a telemetry constructor captured the user id at boot,
  dragging the whole matrix stack into the child); a sandbox-gated
  initializer registers a stub service ahead of any lookup, and the
  auth service-worker initializer is gated on the same boot check.
  Dynamic import() calls in authored modules are rewritten through the
  child Loader instead of evaluating natively against the host bundle's
  base URL, closing an authority bypass. Child errors before ready are
  buffered and reported after; stage breadcrumbs across the
  bootstrap/RPC/render chain log at warn level for live diagnosis.
- Sandbox RPC hangs: all runtime RPCs carry the same bounded timeout
  the render channel got earlier, so a stalled child surfaces the error
  presentation instead of leaving a placeholder up forever.
- Module admission: CDN-agnostic (binary content types are skipped,
  everything else is parsed; the esm.sh hostname carve-out is gone) and
  bare specifiers inside third-party modules are no longer mis-resolved
  into bogus same-origin URLs.
- Theme rendering: trusted-module stylesheets are exempt from the
  shared-document Capsule CSS policy; runtime-generated theme sheets
  (bare theme-scope selectors declaring only custom properties) are
  admitted; the render record carries a computed themeScope token that
  the renderer stamps on the capsule slot, normalized through
  unresolveURL so it matches the compiled stylesheet selector.
- Prerendered placeholders wrap multi-root isolated HTML so
  htmlComponent accepts them.

Residuals, tracked for follow-up: a user-realm theme's stamped token
still misses the installed selector (unresolveURL only maps registered
realms — the token form for unregistered realms needs a decision), and
sandbox child paint remains unverified live (breadcrumbs are in place
to pinpoint any remaining stall). docs/ gains the maintainer-lens
minimality review with its ordered slim-down plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…undary

Relationship family (Capsule): the renderer now consumes every
settle-republished generation into @model (not just the first), the engine
explicitly drives pending linksTo loads through the same StoreService the
classic path uses (card-api's own lazy-load trigger is only store-wired for
the canonical root, not nested contains() sub-instances), and the
Host-owned field portals reproduce main's array-like plural contract
(numeric index, length, iterator) so authored templates that index
@fields.<linksToMany|containsMany> render each item.

Theme family: InstancePresentation carries themeCss and cssImports
alongside the scope token, and the Capsule slot makes the identical trusted
CardContainer invocation main's field-component makes — stamp, scoped theme
stylesheet, @imports, and the container's semantic-token derivation —
verified pixel-equal (background rgb(17,23,19)) against the classic path.

Sandbox lifecycle (RP-15.3): the iframe is born inside its presentation
slot and never re-parented — the parking-lot design reloaded the child's
document on every adopt/park, destroying completed renders. The renderer
classifies early (memoized, pure), reserves the Sandbox process ahead of
materialize(), and awaits mount before the first RPC, breaking the
mount⇄materialize cycle. The prerendered placeholder overlays the live,
never-hidden iframe until the child's first real paint; the child reports
intrinsic height over the surface layout channel; render diagnostics now
measure paint truth (an AND of content and on-screen size, plus body
composition, root rect, offsetParent, visibilityState) and authority
denials name the URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…branch parity

The bijection checker becomes a one-way ratchet: RP-0 (meta, enforced by
this script and CI) and RP-17 (deferred) are exempt; every other statement
counts against a recorded ceiling that CI refuses to let rise and prompts
to tighten whenever coverage lands. Two new suites take coverage from 21 to
59 of the 85 coverable statements — protocol statics (format inventories,
context tokens, version/feature admission, classification and containment
tables, themeScope determinism) and rendered semantics (component entry,
format resolution, arguments, computed values, field configuration, context
plane, presentation statics, identity/documents), each test phrased so a
failure points at the violated spec sentence.

The parity audit distills the frozen reference branch's 42.5k-line delta
through one question — what does the Capsule/Sandbox system necessitate for
existing users and cards — into nine ranked redo items, an explicit
not-ported table, and the projected end-state line budget. It also resolves
the minimality review's F10: the frozen branch's CI collapse is the
empirical evidence that the materialization-purpose split is
protocol-reserved, not dead code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…metry

A card's own mount-time code legitimately assumes real element size — main
guarantees it, and a WebGL card sizes its renderer exactly once from the
mount modifier. The child now waits (bounded) for its render root to have
nonzero geometry before mounting the card's component, and re-measures the
render diagnostic on subsequent root resizes so a post-paint size change
still reports paint truth. Verified live: the Three.js 3MF viewer renders
its scene at full size on both fresh load and reload, and the placeholder
hands off to the painted child.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Un-defers RP-17.1's HMR for the Sandbox tier as spec section RP-18, with
all seven statements conformance-covered and the whole rp- battery green
in a real browser run (100/100).

The mechanism, per the extraction dossier's seven steps:
BoxelExecutionSession.pushDraft(moduleIdentifier, source) re-classifies
the draft, serves its source through the module-read channel's
exact-URL-only override, and drives a process-monotonic generation over
the render wire (echoed on every response; parent state transitions only
on a matching echo). The child drops stale generations on arrival and
after every await, invalidates only the edited module, and re-derives the
card from the same document so instance data survives while component
identity changes. A failed generation retains last-known-good alongside
its error; reloadSandbox() is the sole path that remints iframe identity,
also clearing draft overrides and resetting the module authority. An
ordinary draft never re-enters the placeholder.

Defects the first real execution of these suites surfaced and fixed:
update() re-broadcast the previous generation's record at the top of
every replace (a duplicate-notification bug that would have double-fired
on every future draft too); bootstrap failures surfacing through
materialize() left a stale sandbox slot outranking the error
presentation (all failure paths now converge on clearing the slot,
generation-guarded); and the runtime error reporter dispatched on the
real window, which any host test runner's global handler also observes —
it now takes an injectable event source. The RP-18.2 conformance test
waits on the server's own arrival bookkeeping rather than assuming timer
and MessagePort task-source ordering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First real runs (the test-realm stack was previously unavailable to these
suites) surfaced three tests asserting the spec through illegal or
mistaken sequences: the RP-8.2 mismatch probe forced a second local id
for a claimed remote id — through the Host store the sanctioned
observable of that sequence IS the hard error, now asserted as such and
noted in the spec's layering note; the RP-8.3 fixture omitted the
resource linkage (`data.id`) its own statement names as the side-loading
key, accidentally reproducing the statement's negative; RP-8.4 expected
a raw URL where the execution document correctly carries the canonical
scoped identifier — the expectation now derives from the live
VirtualNetwork mapping. The RP-4.4 fixture's field named `name` tripped
a genuine main-behavior artifact — the fields proxy's ownKeys trap
appends declared names to the component class's built-ins without
dedupe, violating the proxy invariant — recorded as an RP-17.3 gap; the
fixture moves off the colliding spelling.

rp-conformance 14/14 and rp-semantics 25/25 in real browser runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The policy layer over RP-18's source volatility, per the volatile
execution plan: promoteToVolatile(moduleIdentifier) marks a module as
under active editing for the rest of the tab's session — one-way, no
lease, no demotion, inert for trusted Host modules. Volatility is a
routing input that can only strengthen isolation: a promoted module
routes to the Sandbox tier regardless of its Capsule classification,
checked live at materialization so a promotion landing mid-request still
takes effect.

Promotion of a mounted card re-routes it live through the ordinary
generation-replace path. The renderer establishes a per-module tracked
dependency synchronously in its resource's tracking frame — one cell per
module, deliberately not a global counter, so promoting one card never
re-instantiates (or flickers) an unrelated card's renderer. The
isolation guarantee is conformance-tested from both sides: a volatile
session's drafts leave the host loader's cached module identity
untouched, and a neighboring session's subscriber observes zero
notifications during another session's draft cycle.

Spec section RP-19 (four statements, all cited) lands with the
implementation; the ratchet holds at 102 statements / 70 covered.
rp-volatile 7/7, full rp- battery 100/100 in real browser runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The child announces its own boots, but only the parent knows the surface
identity that ties successive boots together — a process created more
than once for the same surface in quick succession is the signature of a
renderer resource re-instantiating on a tracked-state tick, observed in
the wild as a card visibly flashing every ~13 seconds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nstant

The child previously reported its render root's getBoundingClientRect()
height — the laid-out box, bounded by the iframe's current viewport — and
the parent applied it as a min-height. Measuring the box the parent sizes
is a feedback loop: an isolated card grew 150px → 448px → 544px and never
reached its content height. The child now reports the root's scrollHeight
(the content's demand, independent of the current box, exactly what the
frozen branch's realm-iframe-height-service measured), re-measuring on
subtree mutations, fonts.ready, and window resize; SurfaceService owns
what a report MEANS: intrinsic mode sets an explicit clamped (40–2400px)
height, allocated mode (fitted) sets height: 100% and ignores nothing —
the child simply never reports in allocated mode, via the shared
surfaceHeightModeFor() both sides derive from.

The prerendered placeholder is now in-flow while the Sandbox boots — it
SIZES the slot (no white gap, no reflow at handoff) while the iframe sits
absolute behind it at opacity 0, exactly the frozen branch's loading
presentation; the child's first-paint diagnostic swaps them with a fade.
The placeholder fetch moved after classification and is Sandbox-only,
isolated/edit-format only, always fetching the isolated prerender — it
previously fired a network round-trip for every card render, Capsule
included, for a placeholder that was rarely shown.

Also fixes a silent scoped-css hazard found while verifying: a selector
compound containing :global() drops its scoped prefix entirely, so the
booting iframe rules (position: absolute; opacity: 0) were being applied
to EVERY iframe on the page. The full selector now lives inside
:global(...).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adorn/overlays discover rendered cards through exactly one mechanism: the
ElementTracker modifier that operator mode injects via CardContext and
field-component.gts applies to every nested card container. The execution
runtime bypassed all of it — the field portal rendered authored cards
bare, the Capsule's @consume(CardContextName) facade returned a frozen
empty object, and the slot roots carried no card identity — so every
overlay-eligible card silently vanished from operator mode.

Three restorations, mirroring what the frozen branch's
realm-sandbox-render.gts deliberately preserved:
- BoxelFieldPortal re-stamps field-component's contract on its rendered
  root (tracker modifier + data-boxel-card-id/format + data-test-card
  attributes), with the field's own type/name threaded through from
  buildFieldPortals. The stamped element is always parent-document DOM,
  so even a Sandbox-routed nested card stays discoverable.
- The Capsule @context facade now plucks exactly two Host presentation
  capabilities from the renderer-passed projection — the ElementTracker
  modifier and the search rendering surface — instead of freezing
  everything away; no Store, loader, or service authority crosses.
- The Capsule and Sandbox slot roots stamp card identity attributes.

Stack close could permanently die: handleAnimationCompletion's non-Abort
rejection path logged the error but never resolved, wedging
startAnimation's dropTask forever, after which every close click was
silently dropped. The animation promise now always settles, and
dismissStackedCardsAbove bounds its cosmetic animation wait with a 1s
race so no animation can ever gate dismissal. Also honors the
ContentElement modifier's teardown (class modifiers ignore modify()'s
return value), which leaked a MutationObserver per stack item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fonts.ready and late observer tails can invoke the reporter after stop();
a torn-down reporter must never land another layout report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A credentialless iframe strips the browser session that lets main render
private-realm <img>s in-document, so every asset in a sandboxed card
401'd (all 20 TierList logos, per the localhost-vs-staging comparison).
Relative authored URLs additionally resolved against the sandbox origin.

The fix is the frozen branch's media bridge, ported to the new transport:
the child-side SandboxMediaBridge discovers img[src] under the render
root, resolves each against its owning card's data-boxel-card-id (never
the iframe route), strips the src before the browser can race an
unauthenticated request into a card's permanent error fallback, and swaps
in a blob from the fetch channel's new 'media' purpose lane. The parent
side never consults the module graph for media — an image must never
become admitted module state — and bounds the lane to GET, image/*
responses, and the shared size cap. The Host fetch carries the user's
realm authorization, which is the same authority main already grants
every in-document <img> via the browser session. Authored code never
sees the capability or the authenticated response.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… boundary

Cross-view sync is main's core behavior: one instance per id, autotracked
reads in every template, so any field set re-renders every view in place.
The execution runtime broke that twice — first by accident (a tracking
leak re-materialized the whole session per save: the flash), then by
design error (an event-subscription reprojection pipeline whose reader
wrote back to the instance and looped through auto-save, corrupting data;
docs/boxel-sync-root-cause-2026-08-06.md is the full post-mortem).

The correct translation, per the maintainer's rule (study main; only add
what the boundary needs): createLiveBoxelModel — @model becomes a Proxy
whose property reads project the canonical instance's CURRENT value as
cloneable data. Reads are pure (relationships via membership state only;
loading stays materialize/settlement's job; pending subtrees answer with
the materialize-time fallback) and autotracked, so the framework's render
pass is the delivery pipeline — there is nothing to order because there
is only one channel. Two bridge details make the tracking complete:
peekAtField alone tracks TrackedArray items but not the field slot (a
save echo replacing an array froze the model mid-word), so every read
also consumes a per-instance tracked version cell whose subscriber is a
bump-only observer; and the capsule component manager's argument-update
revision now invalidates via microtask, since live args legally change
during a render pass.

With delivery structural, the renderer resource unrepeats the original
sin under untrack(): its tracked dependencies are exactly card identity,
format, and volatility — a save can never again tear down the DOM. The
acceptance bar (RP-20.5) passes live: a sentence typed into a field
lands intact, same element, same focus, while the isolated view of the
same card tracks every keystroke.

RP-20 (Rehydration continuity, 5 statements) specifies the contract; the
rp-continuity suite covers all five (typing stability, two-view
convergence, live-model purity+liveness, scroll retention, media-cache
rehydration — the Sandbox media bridge now caches one blob per source so
re-created images swap in synchronously).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@christse

Copy link
Copy Markdown
Contributor

@habdelra The hosted Sandbox deployment follow-up is now pushed at 59ca6384ff and ready for re-review.

What changed:

  • Added a deployable Cloudflare Worker boundary for per-process 128-bit nonce origins, credentialless Host bootstrap/assets, parent-origin allowlisting, restrictive CSP/referrer/permissions/nosniff policy, and explicit API/auth/service-worker denial.
  • Wired staging and production Host environments to boxelusercontent.dev and boxelusercontent.com respectively.
  • Preserved the prior preview runtime route while adding the current /_boxel-sandbox-runtime route, so the older preview remains usable.
  • Fixed the interactive CardPrerender auth context so a cold protected realm source can silently recover its session without weakening dedicated prerender behavior.
  • Added focused origin-allocation, auth-context, and Worker boundary tests.

Verification:

  • Worker boundary suite: 9/9.
  • Staging and production-preview S3 deployment jobs succeeded for this commit.
  • Both preview parent origins return 200 through the nonce Worker bootstrap.
  • Signed-in staging smoke test: workspace chooser → FORGE Gym Shift Board → Library → isolated card rendered in a nonce boxelusercontent.dev iframe → Edit → Finish Editing → isolated iframe rendered again, with no 401 errors.
  • Production preview is live and its compiled config points to the DNS-ready .dev preview edge.

The PR remains draft and DO NOT MERGE. Real production stays configured for .com; its Worker is deployed and root health is green, but nonce subdomains still need the proxied wildcard DNS record documented in the branch. Only production PR previews temporarily use the .dev edge so both preview builds work now.

Please re-review at 59ca6384ff when you have a chance.

@christse

Copy link
Copy Markdown
Contributor

Small final follow-up: b5076969c2 removes the Sandbox child's unnecessary regular Host Store startup, which had produced a CSP-blocked direct base/card-api fetch even though rendering succeeded through the broker-backed child store.

Both preview deploy jobs succeeded again at the new head. I repeated the signed-in Library → FORGE card → Edit → Finish Editing → isolated-card flow on the rebuilt staging artifact: nonce iframe present, card rendered before and after the toggle, and zero browser-console errors. Please use b5076969c2 for re-review.

@christse

Copy link
Copy Markdown
Contributor

Follow-up fix is deployed at head f94b650. This closes the transient private-realm source 401 that could occur when an interactive Boxel execution source load overlapped background prerender state: interactive source loads now enter a concurrency-safe reauthentication scope, while the dedicated prerender app remains storage-only.

Deployed verification on the production preview URL:

  • Preview Host workflow succeeded for both staging and production S3 deployments: https://github.com/cardstack/boxel/actions/runs/31545190594
  • Live bundle changed from the failing main-6el6aaSJ.js to main-CTKWSYDM.js
  • FORGE Week 32 board rendered in its nonce boxelusercontent.dev iframe on 3 consecutive full reloads
  • Edit -> Finish Editing -> isolated return rendered correctly
  • No Unable to render panel and no source 401 in browser errors
  • Focused authorization middleware tests: 4/4 pass

@habdelra please re-review the latest head when convenient. The PR remains draft and explicitly DO NOT MERGE; the preview is ready for broader corpus testing.

@christse

Copy link
Copy Markdown
Contributor

Google Fonts parity follow-up is pushed and deployed at head 38fe0e6.

Change:

  • Sandbox stylesheet policy now permits only https://fonts.googleapis.com.
  • Sandbox font policy now permits only https://fonts.gstatic.com.
  • No Google wildcards and no change to script, connect, image, media, worker, or frame lanes.
  • Local sandbox headers match hosted behavior.
  • The reviewer guide and Worker README explicitly record the accepted Google-only URL/IP/user-agent egress; credentialless and no-referrer remain in force.

Verification:

  • Worker boundary tests: 9/9, including exact-origin and no-wildcard assertions.
  • Focused Worker ESLint: clean; Host JS/template lint clean. Aggregate Host type lint still reports only the existing unrelated Array.at target errors.
  • Wrangler 4.120.1 dry-runs succeeded for both environments.
  • Staging Worker deployed: ccdeea9e-9352-4754-b7c3-3481a79bfa5e.
  • Production Worker deployed: 066765ab-fc2c-4605-a8fb-e7bd4409d316; root health is green. Production nonce wildcard DNS is still outstanding as documented.
  • Live staging header contains the two exact origins.
  • FORGE Week 32 preview and real staging both report Barlow Condensed and Manrope available, with matching proportional glyph/layout metrics and no font/CSP errors.
  • Fresh staging and production Host preview deployments succeeded: https://github.com/cardstack/boxel/actions/runs/31548037633

@habdelra please include this latest head in the re-review. The PR remains draft and DO NOT MERGE.

@habdelra

Copy link
Copy Markdown
Contributor Author

Most of the host tests failwith the same error:

  Error: Assertion Failed: You cannot use the same root element (#ember-testing) multiple times in an Ember.Application
                at assert (http://localhost:7357/assets/debug-DPwvJxt5.js:216:20)
                at EventDispatcher.setup (http://localhost:7357/assets/index-DaCZcoCY-CxZu_iBx.js:273:57)
                at ApplicationInstance.setupEventDispatcher (http://localhost:7357/assets/destroyable-BH81RkYa.js:644:14)
                at ApplicationInstance._bootSync (http://localhost:7357/assets/destroyable-BH81RkYa.js:569:35)
                at http://localhost:7357/assets/route-C5UGqfZI.js:1123:17
                at initializePromise (http://localhost:7357/assets/service-BmZUuF2w.js:7828:3)
                at new Promise (http://localhost:7357/assets/service-BmZUuF2w.js:8251:30)
                at ApplicationInstance.boot (http://localhost:7357/assets/route-C5UGqfZI.js:1122:23)
                at http://localhost:7357/assets/dist-DZ-M9wRU.js:180:79
                at invokeCallback (http://localhost:7357/assets/service-BmZUuF2w.js:7812:11)

could you have your agent try to address this? it seems critical

@christse

Copy link
Copy Markdown
Contributor

Direct RP regression coverage

This update deliberately moves the affected Host regression tests through the Direct Render Protocol path instead of continuing to instantiate card components through the legacy shortcut.

  • Direct RP is now the canonical trusted execution path exercised by the existing preview, Store live-update, query, loader, indexing, and timestamp regressions.
  • The behavioral assertions are retained: these changes update the entry path, not the expected product behavior.
  • Capsule and Sandbox retain dedicated conformance, equivalence, lifecycle, and boundary suites.
  • Trusted Host/package shims are treated as execution terminals rather than authored realm invalidation edges.
  • The test-document environment invariant is checked explicitly so CI cannot silently autoboot a second Ember application on #ember-testing again.

Local pre-push evidence: Host lint passed; Base lint passed; Direct preview 10/10; RP conformance 15/15; execution engine 20/20; smoke-runner unit tests 9/9; environment-config verification passed. The accompanying wild-corpus runner contains URLs and observations only—no private workspace files.

@habdelra

Copy link
Copy Markdown
Contributor Author

looks good! feel free to push on teh CI failures as much as you want. but i think we are probably over the hump in terms of a prototype

@christse

Copy link
Copy Markdown
Contributor

CI follow-up in fc5bf89:\n\n- Host regression call sites continue to exercise their original product behavior through the Direct RP adapter. The nested-card editor assertion now keys off the actual isolated-format fixture marker instead of assuming the pre-RP direct-child DOM topology.\n- Direct RP now retains a same-card/same-format presentation across resource acknowledgements, with focused tests for DOM/input identity and edit→isolated format transitions.\n- Authored relationship portals keep present authored values on the execution router while delegating only terminal broken-link slots to trusted Base; coverage includes a live present→broken transition and a real realm-backed 404 inside a Capsule.\n- Software Factory treats repeated opencode transport failures as terminal blocked results instead of mistaking a dead child for idle and retrying until the Playwright timeout.\n\nLocal verification: Host lint; Host Vite test build; RP continuity 12/12; focused relationship tests; formerly failing nested create/edit workflow; Software Factory JS/format lint and new transport-liveness tests. Private workspace files and unrelated dirty files were not committed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants