From 44b18e799c6e68829fdaa5c044521d1f44129e3d Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 19 May 2026 13:45:30 -0700 Subject: [PATCH 1/2] plan: sandboxed artifact rendering Design doc for a custom element that renders untrusted HTML artifacts inside a nested-iframe sandbox. Outer iframe holds a fixed bootstrap under sandbox="allow-scripts" with no allow-same-origin; inner iframe inherits the outer's opaque origin via srcdoc so the artifact's scripts can reach parent.tonk synchronously. The outer constructs a MessageChannel and posts port2 up to the host; subscriptions return transferred ReadableStreams so the host keeps no subscription-lifecycle state. --- plan/tonk-portal.md | 639 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 plan/tonk-portal.md diff --git a/plan/tonk-portal.md b/plan/tonk-portal.md new file mode 100644 index 00000000..87ac7915 --- /dev/null +++ b/plan/tonk-portal.md @@ -0,0 +1,639 @@ +# `` — sandboxed artifact rendering + +## Context + +`` renders a single entity by querying a `view` row +on the branch and interpolating its `display` HTML against the +entity's fields. The HTML is authored by trusted curators of the +branch and ends up running as a child of the host page: same +origin, same SW, same DOM. + +`` is the analogue for untrusted HTML — artifacts +contributed by anyone on the branch, served whole-document, that +a host page embeds without granting them access to the +surrounding page, the network, or anything beyond a defined data +API. + +The element renders the artifact inside a nested iframe pair: + +- An outer iframe with `sandbox="allow-scripts"` (unique opaque + origin, no network, no SW reach) holds a fixed bootstrap. +- The bootstrap creates an inner iframe with `srcdoc` set to the + artifact's HTML verbatim. The inner inherits the outer's + opaque origin (a property of `srcdoc`), so the two are + same-origin to each other and the artifact's scripts can call + `parent.tonk` directly. +- The outer brokers data flow to the top frame via a + `MessageChannel` it constructs and posts up. The top frame + performs SW-backed queries on the artifact's behalf. + +Target usage: + +```html + +``` + +## The artifact's content claim + +`` resolves the document by subscribing to a single +claim: + +``` +(the = xyz.tonk.artifact/content, of = ) +``` + +No concept descriptor, no `artifact!` concept on the branch — the +single claim is enough. Authors assert an artifact by writing the +claim directly: + +```yaml +artifact!: &welcome + xyz.tonk.artifact/content: !text/html | + + + Welcome + +

Welcome

+ + + + + +``` + +(The exact notation form is decided in the worker / notation +layer; the design only depends on the claim shape: +`(the = xyz.tonk.artifact/content, of = , is = )`.) + +The artifact author writes a full HTML document, including +``, ``, and ``. The portal does not wrap or +modify it. The artifact's scripts reach the data API via +`parent.tonk`. + +## The `source` attribute + +`source` is an entity URI — specifically a DID. The element +subscribes directly to +`(the = xyz.tonk.artifact/content, of = )` and uses the +resulting `is` value as the inner iframe's `srcdoc`. No Phase-1 +concept resolution, no name lookup. + +Rejected at validation: + +- empty string, +- any value that doesn't parse as a DID. + +A `data-state="error"` reflects the failure; lifecycle event +`tonk-portal:error` carries `{ kind: 'descriptor', message }`. + +Live subscription: editing the artifact's `content` claim on the +branch updates the inner iframe (see "Content-change strategy" +below). + +Name-based lookup (resolving a human-readable name to an artifact +entity) is out of scope here. Callers that have only a name +resolve it themselves — via a `Name` query or any other lookup — +and pass the resulting DID to ``. This keeps the +element's resolution path single-source and its failure modes +narrow. + +## Element shape + +```html + + +``` + +Attributes (all observed; changing any restarts flows): + +| Attribute | Required | Meaning | +|---|---|---| +| `source` | yes | DID of the artifact entity. Must parse as a DID; anything else is an error. | +| `space` | no | Defaults to `"home"`. | +| `branch` | no | Defaults to `"main"`. | + +No children — the portal owns the outer iframe in its light DOM +(no shadow root; mirrors ``'s posture). + +## DOM state signalling + +Same convention as `` — `data-state` on the host +reflects lifecycle for stylesheets: + +| State | Reflected as | +|---|---| +| Initial / resolving | `` | +| Inner iframe loaded with artifact | `` | +| Artifact not found / empty stream | `` | +| Concept / network / parse failure | `` | + +Error detail is still dispatched as a custom event. + +## Frame topology + +Three frames, two trust boundaries: + +``` +┌────────────────────────────────────────────────────────────────┐ +│ Top frame — host page (real origin H, has SW) │ +│ │ +│ │ +│ └─