Hanabi Developer Hub
/ Build / Talking to the platform
Build

Talking to the platform

The capability bridge and the SDK — files, jobs, the data store, notifications.

Module UI
sandboxed iframe
opaque origin
Capability bridge
  1. 1 declared
  2. 2 granted
  3. 3 scoped
  4. 4 REST call
Platform
REST API
Every call is declared, granted and scoped before it touches the platform.
collection"reports"
{ id: "r-01", title: "Q1", … }
{ id: "r-02", title: "Q2", … }
{ id: "r-03", title: "Q3", … }
putgetlistquerydelete
data.store: JSON documents in named collections.
Status: [live] (Phase 2). The host-side bridge (module-bridge.ts) receives the versioned HANABI_* envelopes and answers them with permission + scope enforcement; the SDK (module-sdk) provides the matching client. A working end-to-end reference lives in examples/modules/capability-demo. Note: HANABI_CREATE_JOB runs the module's server worker. First-party modules register one in-process; a third-party module's worker runs in the isolated worker sandbox once it holds the admin-approved worker.execute permission and a sandbox backend is enabled (Phase 9). A module with no available worker gets a clean error, never arbitrary server execution.

A module UI runs in a sandboxed iframe with an opaque origin: it has no cookies, no access to the shell's DOM, and no direct API access. Everything it needs from the platform goes through `postMessage`, brokered by the host (the shell) which holds the user's authenticated session.

text
 module UI (sandboxed iframe)                     Hanabi shell (trusted parent)
                       
 postHanabiMessage(req)   postMessage   capability bridge
                                                    1. verify event.source is this iframe
                                                    2. check manifest declares the permission
                                                    3. check the user granted it
                                                    4. scope to the module's folders
                                                    5. call the authenticated REST API
        result/event    postMessage   reply { rid, ok, data | error }

Envelope #

Every message is a JSON object with a protocol version and a type:

jsonc
{ "v": 1, "type": "HANABI_<NAME>", "rid": "<correlation-id>", /* payload */ }
  • v — protocol version (currently 1). The host supports a range and replies { ok: false, error: { code: "UNSUPPORTED" } } for types/versions it doesn't know — never a silent drop.
  • rid — correlation id the module generates per request; the host echoes it on the reply. Events pushed by the host (e.g. job progress) omit rid.

Module → host requests #

TypePayloadRequiresHost does
HANABI_READYMarks the module loaded; the reply is the module context (id, name, version, permissions, folders, window, openedFile, and launchAction). openedFile is set ({id, name, mime_type, size_bytes}) when launched via Open with — see File associations. launchAction is set to a quick-action id when launched from a Start-menu jump-list — see Jump-list actions.
HANABI_GET_CONTEXTRe-fetch the module context (same shape as the HANABI_READY reply) at any time.
HANABI_PICK_FILEaccepts?, scope?files.read (own) · consented files.read.all (scope: "user")Default: returns the module's input folder listing. With scope: "user", opens a host file-open dialog over the user's workspace (never other modules' folders) and returns the single picked file. SDK: pickFile() / pickUserFile().
HANABI_READ_FILEfileIdfiles.read (own folders) or consented files.read.all (broad)Returns file bytes (base64) + metadata. Allowed for the module's own files, or — with a consented files.read.all grant — any user file outside other modules' folders.
HANABI_OPEN_MEDIAfileIdfiles.read (own folders) or consented files.read.all (broad)Media streaming. Same scope gate as HANABI_READ_FILE, but instead of returning bytes it returns { url, name, mime_type, size_bytes, expires_in } — a short-lived, range-capable URL the module drops into <video>/<audio>/<img>. The host mints a signed per-file ticket; GET /runtime/media/{ticket} answers Range with 206, so playback can seek without any bytes crossing the bridge. Use this for large or seekable media; use HANABI_READ_FILE for small files you need the bytes of.
HANABI_WRITE_FILEfolderRole, name, bytesB64files.writeWrites into the module's folder of that role (default output); returns the new file ref.
HANABI_READ_OFFICEfileIdfiles.read (own folders or the launch file)In-place office editing. Parses a spreadsheet/document into structured content (sheets + rows, or paragraphs) using the platform's openpyxl/document engine — so a packaged editor can load the file it was opened with. Same scope gate as HANABI_READ_FILE. SDK: readOffice.
HANABI_SAVE_OFFICEfileId, payloadfiles.writeIn-place office editing. Reserialises edited content and overwrites the file in place — the launched file, or one in the module's writable scope. payload is { kind: "spreadsheet", sheets } or { kind: "document", paragraphs }. The first capability that writes back to a file outside the module's own folders, gated strictly to the user-opened file. SDK: saveOffice.
HANABI_FS_LISTfolderRole?, folderId?files.read (own) · consented files.read.all (broad)VFS management. Lists a folder's files + subfolders (defaults to the module's input/root folder). SDK: fs.list.
HANABI_FS_STATfileId? / folderId?files.read (own) · consented files.read.all (broad)VFS management. Metadata for one file or folder. SDK: fs.stat.
HANABI_FS_MKDIRname, parentFolderRole?, parentFolderId?files.manage (own) · consented files.write.all (broad)VFS management. Creates a subfolder under a target folder (defaults to the module's workspace folder). SDK: fs.makeDir.
HANABI_FS_RENAMEfileId? / folderId?, namefiles.manage (own) · consented files.write.all (broad)VFS management. Renames a file or folder. SDK: fs.rename.
HANABI_FS_MOVEfileId? / folderId?, destFolderRole?, destFolderId?files.manage (own) · consented files.write.all (broad)VFS management. Moves a file or folder into a target folder (default output). SDK: fs.move.
HANABI_FS_COPYfileId, destFolderRole?, destFolderId?files.manage + files.read (own) · consented files.write.all (broad)VFS management. Copies a file into a target folder (keeps both on a name clash). SDK: fs.copy.
HANABI_FS_DELETEfileId? / folderId?files.manage (own) · consented files.write.all (broad)VFS management. Moves a file or folder to the Trash (restorable; never a permanent delete). SDK: fs.remove.
HANABI_CREATE_JOBoptions, inputRefs?, stream?jobs.createRuns a module job and returns it. With stream: true (SDK: pass onProgress/onOutput to createJob), drives the module's progress worker and streams live HANABI_JOB_PROGRESS events and, when the worker emits them, HANABI_JOB_OUTPUT output chunks to the module.
HANABI_LIST_JOBSjobs.createThis module's own past jobs for the signed-in user (newest first; completed runs that produced outputs) — backs an in-module history / re-download view. SDK: listJobs.
HANABI_QUEUE_TASKoptions?, inputRefs?jobs.queueBackground task queue (B1). Enqueues a module job that runs in the background (non-blocking) instead of synchronously in the request; returns the queued job immediately. Quota-bound (max in-flight tasks per module). SDK: queue.enqueue.
HANABI_GET_JOBjobIdjobs.queuePolls one of this module's own jobs by id (queued/running/completed/failed). Scoped — a module can only ever read its own jobs. SDK: queue.get.
HANABI_CANCEL_JOBjobIdjobs.queueCancels a still-queued task (a task already running can't be interrupted mid-flight). SDK: queue.cancel.
HANABI_SCHEDULE_LISTjobs.scheduleScheduled tasks (B2). Lists this module's recurring schedules for the user. SDK: schedules.list.
HANABI_SCHEDULE_CREATEname?, triggerType?, trigger, options?, inputRefs?jobs.schedule (admin-approved)Creates a schedule that fires a background job (B1) on an interval (trigger: { seconds }) or cron (trigger: { cron }) trigger — even while the UI is closed. Quota-bound (max schedules + minimum interval); the platform fires it server-side via APScheduler. SDK: schedules.create.
HANABI_SCHEDULE_TOGGLEscheduleId, enabledjobs.scheduleEnables or disables a schedule (disabled stops firing but is kept). SDK: schedules.toggle.
HANABI_SCHEDULE_DELETEscheduleIdjobs.scheduleDeletes a schedule and its trigger. SDK: schedules.delete.
HANABI_STORAGE_USAGEstorage.readThe signed-in user's storage footprint: { limit_mb, used_bytes, remaining_bytes } (limit_mb/remaining_bytes are null when unlimited). Read-only; the user's own aggregates only. SDK: storageUsage.
HANABI_DATA_LIST_COLLECTIONSdata.storeStructured data store (C1). Lists this module's collections with document counts, plus its usage vs the resolved quota. Strictly scoped to (signed-in user, module). SDK: data.collections.
HANABI_DATA_LISTcollection, limit?, offset?data.storeA page of JSON documents in a collection, newest-updated first ({ total, limit, offset, docs }). SDK: data.list.
HANABI_DATA_GETcollection, iddata.storeOne document by id, or null if it doesn't exist. data is the raw JSON string the module stored. SDK: data.get.
HANABI_DATA_PUTcollection, data, id?data.storeUpserts a JSON-serialisable document. Omit id to create with a server-minted id; pass it to overwrite. Enforces the per-module collection / record / byte quotas. SDK: data.put.
HANABI_DATA_QUERYcollection, where?, limit?data.storeEquality filter over a collection's top-level JSON fields, evaluated over a bounded scan (truncated flags when the scan was capped — page large sets with HANABI_DATA_LIST). SDK: data.query.
HANABI_DATA_DELETEcollection, iddata.storeDeletes one document; existed is false if it was already gone. SDK: data.delete.
HANABI_GET_SETTINGkeysettings.selfReturns a value from the module's durable namespaced store — per-user and server-side, so it survives a browser cache clear and follows the user across devices. Falls back to a local cache when the server is unreachable.
HANABI_SET_SETTINGkey, valuesettings.selfPersists a value in the module's durable namespaced store (per-user, server-side, with a local write-through cache). Scoped to this module + user; values are capped at 256 KiB each.
HANABI_OPEN_FOLDERfolderRole?files.read or files.writeOpens File Explorer to one of the module's own folders (default output). Scoped — a module can only reveal its own declared folders.
HANABI_NOTIFYmessage, title?, tone?notificationsDesktop integration. Raises a desktop notification (toast + notification centre). title defaults to the module's name; tone is info (default) / success / warning / error. Title/message are length-clamped so a module can't spoof another app or flood the UI.
HANABI_SET_BADGEcountnotificationsDesktop integration. Sets a count badge on the module's own taskbar icon; count <= 0 clears it (large counts render as 99+). Affects only this module's icon.
HANABI_LIST_MODULESmodules.useInter-module. Lists other installed, launchable modules as {id, name, icon, file_associations} (self excluded). Public catalog facts only — never another module's data, files, or settings.
HANABI_SEND_TO_MODULEmoduleId, fileIdmodules.use + files.readInter-module handoff. Opens another installed module (in a new, user-visible window) with one of this module's own scoped files delivered as its openedFile. The file must be readable by this module; the target gets a launch-scoped read of just that file. No silent invocation, no access to the target's own data.
HANABI_DESKTOP_LISTdesktop.read (consent)Desktop workspace. Read-only snapshot of the user's Desktop: app icons + items (files/folders on the Desktop). SDK: desktop.list.
HANABI_DESKTOP_PLACEfileId? / folderId?, copy?desktop.workspace (consent)Desktop workspace. Places one of the module's own files/folders onto the user's Desktop (copy: true keeps the original, files only; default moves it). The item must be readable by this module. SDK: desktop.place.
HANABI_DESKTOP_REMOVEfileId? / folderId?desktop.workspace (consent)Desktop workspace. Removes from the Desktop a file this module placed there (verified by source_module_id); goes to the Trash. Never touches the user's own desktop items. SDK: desktop.remove.
HANABI_DESKTOP_SHORTCUTlabel?, action?, fileId?desktop.shortcuts (consent)Desktop workspace. Pins a launcher on the Desktop that reopens this module, optionally to a jump-list action and/or one of the module's own files (fileId) as its openedFile. Returns the created shortcut. SDK: desktop.createShortcut.
HANABI_DESKTOP_REMOVE_SHORTCUTshortcutIddesktop.shortcuts (consent)Desktop workspace. Removes a desktop shortcut this module created. SDK: desktop.removeShortcut.
HANABI_DESKTOP_APPEARANCEdesktop.personalize (consent)Desktop workspace. Reads the current workspace appearance — { wallpaper, accent, theme }. SDK: desktop.getAppearance.
HANABI_DESKTOP_PERSONALIZEwallpaper?, accent?, theme?desktop.personalize (consent)Desktop workspace. Sets the workspace wallpaper (a built-in name only — no module-supplied asset), accent (#rrggbb), and/or theme (light/dark). The host validates each field and returns the applied appearance, rejecting unknown values. SDK: desktop.setAppearance.
HANABI_MODULE_REQUESTmethod?, path, body?, stream?first-party module with a server routerModule-owned routes. Calls one of this module's own server routes (e.g. a /state, /parse, or /generate route a first-party module ships in the API). The opaque-origin iframe can't reach /runtime itself, so the host proxies the call (adding the session cookie + CSRF) — clamping path to the module's own /runtime/modules/<id>/ subtree and blocking the generic capability routes (jobs/app/kv/media-ticket). With stream: true the route returns newline-delimited JSON and each line is relayed as a HANABI_MODULE_STREAM event. SDK: moduleRequest / moduleStream.

Host → module replies & events #

  • Reply to any request: { "v": 1, "type": "<TYPE>_RESULT", "rid": "...", "ok": true, "data": {...} } or { "ok": false, "error": { "code": "...", "message": "..." } }.
  • Event, correlated by the rid of the originating streaming request (HANABI_CREATE_JOB or a stream: true HANABI_MODULE_REQUEST). The originating request stays open until its terminal _RESULT reply resolves it:
  • progress — { "v": 1, "type": "HANABI_JOB_PROGRESS", "rid": "...", "jobId": "...", "progressPct": 50, "message": "…" }.
  • streamed output — { "v": 1, "type": "HANABI_JOB_OUTPUT", "rid": "...", "jobId": "...", "seq": 1, "chunk": "…", "encoding": "text" | "base64" }. A progress worker emits these by yielding {"output": "…", "encoding": "text"|"base64"}; use them for live logs, progressive/token output, or incremental results. Chunks are transient (not persisted) and arrive in seq order, interleaved with progress.
  • streamed module-route line — { "v": 1, "type": "HANABI_MODULE_STREAM", "rid": "...", "event": { … } } — one parsed newline-delimited-JSON object from a stream: true HANABI_MODULE_REQUEST (moduleStream). The host reads the route's NDJSON response and relays each line as it lands; a terminal { "error": "…" } line rejects the call.

Error codes

CodeMeaning
PERMISSION_DENIEDThe manifest didn't declare, or the user didn't grant, the permission.
OUT_OF_SCOPEThe request named a file/path outside the module's folders.
NOT_FOUNDTarget file/job doesn't exist for this user.
UNSUPPORTEDUnknown message type or protocol version.
INVALIDMalformed payload.

File associations (Open with) #

A module can declare the file types it opens in its manifest:

json
"desktop": { "file_associations": ["md", "txt"] }

Extensions are lowercased and dot-stripped. An installed module that declares an extension then appears in File Explorer's Open with menu (and can be set as the default app) for matching files. Choosing it launches the module with the file delivered as openedFile in the module context (HANABI_READY).

Because the user explicitly chose “open this file with this module”, that single file is readable for the launch — via HANABI_READ_FILE or HANABI_OPEN_MEDIA using openedFile.id — even though it lives outside the module's own folders. No other out-of-scope file becomes reachable. The module still needs files.read.

Jump-list actions #

A module can contribute quick actions to the Start menu (a jump-list) in its manifest:

json
"desktop": { "jump_list": [{ "id": "new-note", "label": "New note" }] }

Each action appears as an indented sub-row under the module in All programs. Choosing one launches the module with the action's id delivered as launchAction in the module context (HANABI_READY / HANABI_GET_CONTEXT); the module reads it on load and runs the matching action (e.g. open a blank document). Entries are validated and clamped (up to 6 actions; ids and labels are length-limited). No permission is required — a jump-list only ever launches the module that declares it, with no file or data access implied.

Using the SDK (module side) #

The @hanabi/module-sdk ships createHanabiClient(), which handles the versioned envelope and rid correlation so you write await calls instead of wiring postMessage by hand:

ts
import { createHanabiClient } from '@hanabi/module-sdk';

const hanabi = createHanabiClient();

// Read the grant + folder context (also answers HANABI_READY).
const ctx = await hanabi.getContext();

// Pick and read an input file (requires files.read).
const { files } = await hanabi.pickFile(['xlsx', 'csv']);
const file = await hanabi.readFile(files[0].id);   // file.bytesB64

// Write an output file (requires files.write).
await hanabi.writeFile('result.txt', btoa('done'), 'output');

// Clean up the listener when your view tears down.
// hanabi.dispose();

SDK client methods

createHanabiClient() returns a typed client — one method per capability, plus call(type, payload, onProgress?, onOutput?) as the escape hatch:

MethodMessageNeedsReturns
ready() · getContext()HANABI_READY · HANABI_GET_CONTEXTthe module context (openedFile, launchAction, permissions, folders, window)
pickFile(accepts?)HANABI_PICK_FILEfiles.read{ files } from the module's input folder
pickUserFile(accepts?)HANABI_PICK_FILE (scope:"user")consented files.read.all{ file } from a host file-open dialog
readFile(fileId)HANABI_READ_FILEfiles.read{ id, name, mime_type, size_bytes, bytesB64 }
openMedia(fileId)HANABI_OPEN_MEDIAfiles.read{ url, … } — a range-capable stream URL
writeFile(name, bytesB64, folderRole?, subpath?)HANABI_WRITE_FILEfiles.write{ file }; subpath nests it under auto-created sub-folders of the role folder
readOffice(fileId)HANABI_READ_OFFICEfiles.readparsed office content ({ kind, sheets } / { kind, paragraphs })
saveOffice(fileId, payload)HANABI_SAVE_OFFICEfiles.writeoverwrites the file in place; returns the saved ref
createJob(options?, inputRefs?, onProgress?, onOutput?)HANABI_CREATE_JOBjobs.create{ job }; pass onProgress/onOutput to stream
listJobs()HANABI_LIST_JOBSjobs.createthis module's past jobs for the user (newest first)
queue.enqueue(opts?, inputRefs?) · queue.get(id) · queue.cancel(id) · queue.list()HANABI_QUEUE_TASK · HANABI_GET_JOB · HANABI_CANCEL_JOB · HANABI_LIST_JOBSjobs.queuebackground task queue: enqueue non-blocking work, poll, cancel a queued task
schedules.list() · schedules.create(spec) · schedules.toggle(id, on) · schedules.delete(id)HANABI_SCHEDULE_*jobs.schedulerecurring schedules that fire a background job on an interval/cron trigger
storageUsage()HANABI_STORAGE_USAGEstorage.readthe user's storage { limit_mb, used_bytes, remaining_bytes }
data.collections() · data.list(c, opts?) · data.get(c, id) · data.put(c, data, id?) · data.query(c, where?, limit?) · data.delete(c, id)HANABI_DATA_*data.storestructured JSON-document store scoped to (user, module); quota-bound collections
moduleRequest(method, path, body?)HANABI_MODULE_REQUESTfirst-party server routethe route's JSON response
moduleStream(method, path, body, onEvent)HANABI_MODULE_REQUEST (stream)first-party server routeresolves when the NDJSON stream ends; onEvent per line
getSetting(key) · setSetting(key, value)HANABI_GET_SETTING · _SET_SETTINGsettings.selfdurable per-user value
openFolder(folderRole?)HANABI_OPEN_FOLDERfiles.read/files.write{ path, role }
fs.list(target?) · fs.stat(ref)HANABI_FS_LIST · HANABI_FS_STATfiles.read (own) · files.read.all (broad)a folder's { folders, files }; a file/folder's metadata
fs.makeDir(name, parent?) · fs.rename(ref, name) · fs.move(ref, dest?) · fs.copy(fileId, dest?) · fs.remove(ref)HANABI_FS_*files.manage (own) · files.write.all (broad)VFS management — subfolders, rename/move/copy; remove trashes (restorable)
notify(message, { title?, tone? })HANABI_NOTIFYnotifications{ ok }
setBadge(count)HANABI_SET_BADGEnotifications{ ok, count }
onFilesDropped(cb)HANABI_ENABLE_FILE_DROPreceive files the user drags onto the module's window (the host reads the folder and forwards them); returns an unsubscribe
listModules()HANABI_LIST_MODULESmodules.use{ modules }
sendToModule(moduleId, fileId)HANABI_SEND_TO_MODULEmodules.use + files.read{ ok, moduleId }
desktop.list()HANABI_DESKTOP_LISTdesktop.readthe Desktop's { icons, items }
desktop.place(ref, { copy? }) · desktop.remove(ref)HANABI_DESKTOP_PLACE · _REMOVEdesktop.workspaceput the module's files/folders on the Desktop; remove ones it placed
desktop.createShortcut(spec?) · desktop.removeShortcut(id)HANABI_DESKTOP_SHORTCUT · _REMOVE_SHORTCUTdesktop.shortcutspin/unpin a desktop launcher that reopens the module
desktop.getAppearance() · desktop.setAppearance(patch)HANABI_DESKTOP_APPEARANCE · _PERSONALIZEdesktop.personalizeread/set wallpaper, accent, theme (validated)
dispose()remove the message listener (call on teardown)
ts
// Stream a job's progress + live output, then raise a notification when done.
await hanabi.createJob({ mode: 'convert' }, refs,
  (p) => setProgress(p.progressPct),
  (o) => appendLog(o.encoding === 'base64' ? atob(o.chunk) : o.chunk));
await hanabi.notify('Conversion complete', { tone: 'success' });

// Stream a scoped video without copying bytes over the bridge.
const media = await hanabi.openMedia(fileId);
videoEl.src = media.url;            // supports HTTP range / seeking

// Persist a durable, cross-device setting (survives a cache clear).
await hanabi.setSetting('theme', 'dark');

// Edit a spreadsheet in place — the file this module was opened with.
const wb = await hanabi.readOffice(ctx.openedFile.id);          // { kind, sheets }
await hanabi.saveOffice(ctx.openedFile.id, { kind: 'spreadsheet', sheets: wb.sheets });

// Call this (first-party) module's own server route; stream a long one.
const preview = await hanabi.moduleRequest('POST', 'preview', { input_refs, options });
await hanabi.moduleStream('POST', 'generate-stream', { input_refs, options },
  (e) => markDone(e.index, e.total));     // one NDJSON line per finished item

If you can't bundle the SDK (a plain-HTML module), copy the ~20-line inline client from examples/modules/capability-demo/ui/index.html — it speaks the exact same protocol.

Capability tiers & admin governance #

Every permission has a security tier — the single source of truth is the backend capability registry (services/capabilities.py):

TierGranted byDefaultExamples
baselineimplied by installonfiles.read/write/manage, jobs.create, settings.self, notifications, storage.read, data.store, jobs.queue
consentA one-time prompt the user approves to allow an elevated permission like files.read.all.per-user prompt at install / first useofffiles.read.all/write.all, desktop.read/workspace/shortcuts/personalize, client.webgl/gamepad/pointerlock/fullscreen/webcodecs
adminpublish review (binary)offnetwork.fetch, worker.execute, client.wasm, jobs.schedule
admin-HIGHpublish review + an explicit per-capability admin toggle + quotaoffworker.native, worker.service, network.fetch.broad

admin-HIGH capabilities are denied unless an admin individually grants them for a module and sets their quota (resource tier, egress budget, …). Grants are recorded on the module's developer draft and enforced at runtime by enforce_capability (capability_enforcement.py), which also audits each use. Adding or escalating any admin / admin-HIGH capability on an update forces a manual re-review (the module stays live on its prior approved version meanwhile). First-party / built-in modules are trusted.

The three enforcement checks (why this is safe) #

A capability is honored only if all three hold — this is the security spine, and it reuses the boundaries in ../security-model.md:

  1. Declared — the manifest lists the matching permission.
  2. Granted — the installing user consented (auto-granted for first-party; explicit consent UI for third-party — Phase 5).
  3. ScopedClamped to your own Modules/<Name>/ folders for the current user — never another module or user. — file operations are clamped to the module's own Modules/<Name>/… folders for that user. A module can never name a file id or path belonging to another module or another user.

The bridge runs in the trusted parent and calls the same authenticated, CSRF-protected API the rest of the app uses; it never hands the iframe raw storage keys, cookies, or cross-scope data.

Isolation summary #

  • <iframe sandbox="allow-scripts"> without allow-same-origin → opaque origin, no cookie/DOM access.
  • Host validates event.source === iframe.contentWindow and ignores all other senders.
  • Assets served read-only with X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, a locked-down Permissions-Policy, and a per-module Content-Security-Policy. With a distinct module-serving origin (HANABI_MODULE_ORIGIN, Phase 11) the CSP names that origin explicitly in the asset directives (default/script/style/img/media/font/frame-src/worker-src) — the iframe's opaque origin makes a bare 'self' match nothing for sub-resources — while keeping connect-src strict ('self' + any declared network origins) and object-src 'none'. frame-src and worker-src also allow blob:, so a module can frame a blob document it built from its own bytes and run a web worker it bundles inline — e.g. PDF.js, which a module uses to render a PDF preview to a <canvas> because the browser's built-in PDF viewer is blocked in the sandbox; <embed>/<object> stay blocked. The module document is served no-store so a header change (like the CSP) propagates on the next load.
  • Outbound network from a module is denied by connect-src unless its manifest declares http(s) dependencies.services and holds the approved network.fetch permission — then only those origins are allowed (Phase 8).
Live: the bridge round-trip click a call — watch it cross the wall
Call an SDK method
Permission granted — toggle to watch a call get denied
Transcript
Pick a call above to run it.