Skip to content

feat(core): render HTML artifacts inline without a remote container - #5

Merged
zebster-cmd merged 1 commit into
requesty-provider-specfrom
merge/v0.27.0
Jul 26, 2026
Merged

feat(core): render HTML artifacts inline without a remote container#5
zebster-cmd merged 1 commit into
requesty-provider-specfrom
merge/v0.27.0

Conversation

@zebster-cmd

Copy link
Copy Markdown
Owner

What

Renders AI HTML artifacts locally instead of loading the remote page https://affine.run/static/container.html and posting the HTML into it.

That remote page was the isolation boundary — it ran artifact scripts on a foreign origin. The cost was a hard runtime dependency on affine.run for what is otherwise a purely local render, which means blank previews on a self-hosted deploy, offline, or on any network hiccup.

How

  • Local srcdoc replaces the remote src + cross-window postMessage handshake.
  • Sandbox grants allow-scripts but deliberately omits allow-same-origin. The frame gets an opaque origin, so artifacts stay interactive while losing all access to the host document, storage, cookies, and auth. This is stricter than what it replaces — the old flow ran with allow-same-origin, and the remote container performed no origin check on incoming messages.
  • Injected bootstrap reports content height / readiness / errors over postMessage. The host authenticates by window handle (event.source === iframe.contentWindow), because an opaque guest must post with a wildcard target and the marker alone is guessable.
  • Sizing gated behind a new autoResize prop. The chat preview panel stretches the frame to height:100% via CSS, so writing an inline height there would override that rule and break the panel — code-artifact.ts passes autoResize=false. Doc-inline previews size to content, clamped to 640px, replacing the hard-coded 544px.
  • Artifact state now survives the Code/Preview toggle. The old code swapped between two different lit templates, which tore down the iframe every toggle. Both views now stay mounted, toggled with [hidden] and keyed by toolCallId.

Verification

Unit + real-Chromium integration (artifact-sandbox.integration.spec.ts), 74 new tests:

  • Scripts run, forms accept input, zero network requests
  • window.origin is 'null'; guest cannot reach parent.document, host localStorage, or cookies
  • State survives a hidden toggle; resets on re-creation (the old behaviour, kept as a test)
  • A dedicated group adds allow-same-origin in the test only to prove that flag is what would collapse the boundary; a third test asserts production never grants it

Checked on this branch:

Check Result
blocksuite specs 162 passed
yarn affine @affine/web build exit 0 (clean rebuild)
Shipped bundle contains affine.run/static/container absent
Shipped bundle contains affine-artifact bootstrap present
tsc -b 2 pre-existing Electron-only errors (@affine/auth .d.ts not built locally); no Electron job in the self-host image

Notably, the browser enforces this harder than specified: Chromium throws SecurityError on document.cookie in the guest rather than returning empty.

Deploy impact

Merging this updates requesty-provider-spec, which triggers requesty-selfhost-image.yml → pushes ghcr.io/zebster-cmd/affine:requesty for Coolify to redeploy.

Known limitations

  • Not verified in the assembled app. Both call sites (chat artifact panel, doc affine:code preview block) are verified at the renderer level, not by driving a running AFFiNE instance with a live AI response.
  • Rust/Docker build not validated locally — this change is frontend-only and touches no Rust, but "web build passes" is not "image builds".
  • Artifacts relying on localStorage will now throw instead of silently working, since the guest is on an opaque origin. Judged acceptable for one-shot generated artifacts.

Planning artifacts: openspec/changes/add-inline-html-renderer/

🤖 Generated with Claude Code

Artifact previews loaded https://affine.run/static/container.html and
posted the HTML into it. That remote page was the isolation boundary, so
every preview depended on affine.run being reachable — blank previews on
self-hosted deploys, offline sessions, or any network hiccup.

Render locally via srcdoc instead, with a sandbox that grants
allow-scripts but deliberately omits allow-same-origin. The frame gets an
opaque origin, so artifacts stay interactive while losing all access to
the host document, storage, cookies, and auth. This is stricter isolation
than the remote flow it replaces, which ran with allow-same-origin and
performed no origin check on incoming messages.

An injected bootstrap reports content height, readiness, and errors over
postMessage. The host authenticates those by window handle
(event.source === iframe.contentWindow), since an opaque guest must post
with a wildcard target and the marker alone is guessable.

Sizing is gated behind a new autoResize property. The chat preview panel
stretches the frame to height:100% via CSS, so writing an inline height
there would override that rule and break the panel; code-artifact.ts
passes autoResize=false. Doc-inline previews size to content, clamped to
640px, replacing the hard-coded 544px.

Code/Preview toggling previously swapped between two different lit
templates, which tore down the iframe and discarded artifact state on
every toggle. Both views now stay mounted, toggled with [hidden] and
keyed by toolCallId.

Verified in real Chromium (artifact-sandbox.integration.spec.ts): scripts
and forms work, zero network requests, window.origin is 'null', the guest
cannot reach parent.document or host storage/cookies, and state survives
a hidden toggle but resets on re-creation. A dedicated group adds
allow-same-origin in the test only to prove that flag is what would
collapse the boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zebster-cmd
zebster-cmd merged commit 445e05e into requesty-provider-spec Jul 26, 2026
57 of 60 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant