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:
fetchinside it carries the user's cookies, and your CSP needsworker-src blob:. - SES /
lockdown+Compartment. A Compartment withoutlockdownis 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/Blobby 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.