Docs · 01 / 13

Why a frame and a worker

The code a model writes has to run somewhere with the user's session, or it cannot do anything useful. Running it in your page with new Function gives it everything your page has: fetch with cookies, localStorage, the DOM, every global. Any text the model reads can then run script on your origin.

The alternatives, and why this library does not use them:

  • A Worker created from your page. It has no DOM, but it is same-origin with your page: fetch inside it carries the user's cookies, and your CSP needs worker-src blob:.
  • SES / lockdown + Compartment. A Compartment without lockdown is not a boundary; with it, your page's intrinsics are frozen, which breaks most apps. Fine as a hardening layer, not as the boundary.
  • QuickJS in WASM. A real separate engine with its own memory and an interrupt handler. Costs a ~1 MB payload, a different JavaScript engine from the page's, and re-encoding File/Blob by hand.
  • An iframe alone. A sandboxed frame has no cookies, but its window can still navigate (location = "https://…?" + data), and a synchronous loop cannot be stopped from outside.

A <iframe sandbox="allow-scripts"> has an opaque origin: no cookies, no storage, same-origin with nobody. Its document carries a CSP with connect-src 'none', and a Worker created from a blob: inside it inherits that CSP: fetch, XMLHttpRequest, WebSocket, EventSource, sendBeacon, importScripts and dynamic import() are all refused by the browser before any code of ours runs. The worker has no DOM and no navigable location, and terminate() ends a while (true) {} mid-loop. The library adds a prelude that removes the same globals (so a failure reads fetch is not defined), a bridge that only accepts the message types it defines, and caps on the host side.