<iframe sandbox="allow-scripts" srcdoc="…"> — no allow-same-origin, no
network egress (the frame inherits the notch’s CSP), no host access.
postMessage is the only channel, and this page is its complete wire format.
Two widget surfaces speak slightly different dialects:
Host → widget
Sent once, when the frame loads:datais the block’sdataprop verbatim (≤ 16,384 chars serialized).argsis the pending tool call’s argument object — confirmation widgets only. A result widget must carry everything it needs indata.theme.modeis the host’s theme. Style from this, never fromprefers-color-scheme(the OS theme is the wrong signal inside the notch).radiusis the corner this host cuts your card at, in px. It is normally 26, but a surface whose clip is concentric with a different shell sends its own value (the pill clips at 25). The kit applies it to--k-radiusfor you; hand-rolled documents should do the same. See The corner.- The message is posted with target origin
"*"— necessarily, since a sandboxed document’s origin is opaque and can’t be named. Isolation comes from the sandbox, not the target origin.
Widget → host
Every message is accepted only whenevent.source is the widget’s own
contentWindow — a message from any other frame is ignored.
voiceos:resize
height 160ms ease).
Non-numeric heights are ignored. Report only real measurements — never while
document.hidden, zero-width, or under 8 px, or one bad sample pins the card
at the 60 px floor.
voiceos:openUrl
noopener,noreferrer). Route your links through this: the sandbox blocks
navigation, so an unrouted <a> is a dead click.
voiceos:updateInput (confirmation widgets only)
Anything malformed is silently dropped. Result widgets have no pending call —
the message is ignored there.
What the Widget Kit does for you
Documents built with the Widget Kit implement the widget side automatically:- Resize: a
ResizeObserverplus load/settle timers report height changes > 1 px, with the collapsed-measurement guard built in. - Hydration: on
init, the theme lands asdata-k-themeon the root, and every[data-k-key]/[data-voiceos-key]element is filled fromargs. - Input relay: a delegated
inputlistener postsvoiceos:updateInputfor any keyed element. - Link routing: clicks on
[data-k-link]elements and plain<a href="https://…">anchors are intercepted and sent viavoiceos:openUrl.
Interaction & focus
The notch is a click-through overlay window; the host grabs keyboard focus when the user mouses down inside your frame, so typing in widget inputs works. There’s nothing to implement — but know that focus follows pointer, not tab order, across the frame boundary.Security invariants (the short version)
- Opaque origin: no cookies, storage, parent DOM, or host APIs.
- No network egress from inside the frame — assets must be inlined.
- Height is clamped on render and on every resize.
- Message
sourceidentity is verified on every widget→host message. - No approve leg exists. Nothing a widget posts can execute a tool.
- Only integration tool results and manifest confirmations can carry
widgets — a model-authored card cannot (the parser drops
widgetblocks unless the integration path explicitly allows them).

