* refactor(plugins): group panel and worker modules by owner * chore(tests): prune retired plugin supervision timeout allowance * refactor(plugins): consolidate duplicate owner imports
12 KiB
Isolated plugin frames
Design contract for running plugin UI in isolated guests. Components (TSX) panels run in the same guests; see plugin-declarative-ui.md.
Why
Panel and custom-editor frames are srcdoc iframes sharing the main window's session. CSP is the only
network barrier, so DNS prefetch and WebRTC (via a nested frame's fresh RTCPeerConnection) still reach the
network, and the opaque origin forces authors to inline every asset and shim workers, WASM, storage and blob:.
Isolated guests (desktop)
Every plugin frame on desktop is an Electron <webview>:
- Partition:
persist:orca-plugin-<key>,<key>= first 16 hex chars ofsha256(publisher.id). One session, one renderer process and one storage area per plugin. - Origin:
orca-plugin-frame://<key>/<path>, a privileged scheme (standard,secure,supportFetchAPI,stream) handled on that partition's session. Paths resolve inside the plugin root with the same containment and content-integrity checks asreadPanelEntry; anything else is 404.orca-plugin-frame://<key>/__orca/*is reserved for files Orca ships. - CSP (response header on every HTML document):
default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' data: blob:; connect-src 'self' data: blob:; worker-src 'self' blob:; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'. - Network is denied at the session, not by CSP. "No network" means the frame itself makes no
connections, requests or DNS lookups. The one way data leaves is a URL the person agrees to open:
orca.shell.openExternalneeds a click or key press, and Orca asks before opening a host this plugin was not allowed before (see Host APIs below).webRequest.onBeforeRequestcancels every URL that is notorca-plugin-frame://<key>/,data:,blob:ordevtools:.setProxypins a loopback proxy that drops every connection, with<-loopback>so loopback targets are proxied too, so traffic webRequest never sees (preconnect, WebRTC TURN-over-TCP) has nowhere to go; preconnect hints go through it and are never resolved.<link rel="dns-prefetch">resolves on the system resolver, outside webRequest and the proxy, sowill-attach-webviewpins the guest renderer's--blink-settings=dnsPrefetchingEnabled=false(PLUGIN_FRAME_GUEST_RENDERER_ARGUMENTS), covering static, script-inserted and child-frame hints.X-DNS-Prefetch-Control: off, the matching<meta>and the app-widedns-prefetch-disableswitch were measured and do not stop it.
- Storage: uninstalling a plugin clears its partition (
clearStorageDataandclearCache).- Guest
setWebRTCIPHandlingPolicy('disable_non_proxied_udp'). - Permission request/check handlers deny everything, so
navigator.clipboardreads and writes are refused;document.execCommand('copy'|'cut')works only inside a user gesture, as on any web page; HID, serial, USB and Bluetooth choosers are cancelled. will-downloadis cancelled;setWindowOpenHandlerdenies; navigation away from the plugin origin is cancelled.
- Guest
- Admission:
will-attach-webviewonly admits anorca-plugin-partition whosesrcorigin matches its key, whose plugin is installed, enabled and consented, and whose session main has already configured (main configures it, proxy included, beforereadPanelEntryreturns the guest URL). It force-sets the guest preload,sandbox,contextIsolation,nodeIntegration: false,webSecurity, no popups, anddisableDialogs(alert/confirm/promptdo nothing in a plugin frame). - Bridge: every HTML document the protocol serves starts with the same frame-API script as the iframe
shell (
api,host.call/has,on,shell,files,document.get/onDidChange/applyEdit), so the two cannot diverge. The guest preload only hands that script a one-shot transport (deleted before plugin code runs; offered only totext/htmltop documents, so an SVG or XML page Orca does not wrap never gets one) that caps bytes and rate in the isolated world and relays the{orca:1, kind, …}envelopes withipcRenderer.sendToHost('orca-plugin-frame', msg, userActivation)/ipcRenderer.on('orca-plugin-frame');userActivationis read in the isolated world, so page script cannot forge it. Why notcontextBridge.exposeInMainWorld('orca', …): it dropserror.code/retryableon rejected calls and freezes getters such asorca.api, and a sandboxed preload cannot share code chunks with main. The host renderer useswebview.sendand theipc-messageevent. Envelope validation, budgets, sessions and capability gating are unchanged. A custom editor's declaredentrygets the editor API; every other document gets the panel API. - Theme: main bakes the host's current
--orca-*tokens andlight/darkclass into every document it serves (the renderer sends its theme with each entry read), so the first paint and boot-time token reads match;helloand thethemeevent keep it current without a reload. - Reloads: the guest URL carries
rev=: the installed content hash, or for a dev folder a per-run counter the dev watcher bumps on each change it reports under that folder (plugin-dev-revision.ts; the tree is never hashed for this). An update or an asset-only dev edit remounts the guest. - Input: Orca's window shortcuts are forwarded from a focused guest through the browser tab path, minus
browser-page chords (address bar, find, reload, history, page zoom); a custom editor's save chord still
rides
editor.keydown, so it fires once. Editable fields get Orca's Cut/Copy/Paste context menu. - Switch (default on): rolling back is the constant
PLUGIN_ISOLATED_FRAMES_DEFAULTinsrc/main/plugins/panels/guest-isolation/plugin-frame-isolation.ts.ORCA_PLUGIN_ISOLATED_FRAMES=0turns guests off for one machine and=1on, overriding theexperimentalPluginIsolatedFramessetting; all are main-process only. Off, frames use thesrcdociframe (never offeredorca.shell/orca.files), and components panels say they are not available. - Compatibility: existing single-file plugins load unchanged. Browser clients (serve/web) have no
<webview>and keep thesrcdociframe; multi-file assets need the desktop app. With isolation on, a desktop guest that cannot be set up fails closed (the panel shows its load error and its session is revoked); the iframe is never a fallback for a setup failure. On the guest path nosrcdocHTML is read, hashed or sent over IPC; the guest's own navigation is the one integrity check per open. - Serving: images, fonts and media stream from disk (one
Range: bytes=answered with206, HEAD fromstat), up to 32 MiB a file; a session holds at most 32 requests at once (503past that). Documents, scripts, styles, JSON, SVG and WASM are read whole (16 MiB cap). A declared HTML entry is a document whatever its extension. Path segments ending in a dot or space are refused. - Source modules:
.ts,.tsx,.mtsand.jsxare read whole (1 MiB cap), digest-checked like any script, then transpiled with sucrase on one shared, lazily started worker thread (types stripped, automatic JSX fororca, lines kept, inline source map) and served astext/javascript. Relative imports are rewritten to exact files from the verified file list (the disk, for a dev tree);orcaand URLs are left to the import map and CSP; any other bare import is served as a module that throws, naming the file and import. Results are cached in main by sha256 of the source, transpiler version and options. A transform still running after 2 s is cut short: the worker is terminated and the next file starts a fresh one. A file that fails or times out is served as a throwing module, and that result is cached too (plugin-frame-module-transform.ts,plugin-source-transpile-client.ts). A declared.ts/.tsxframe entry is served as a generated page whose only script is that file with?orca-module, which gets the JavaScript. - Diagnostics: each guest's
console-messageerrors and warnings (uncaught exceptions included) go to its plugin's log buffer with URLs reduced to plugin-relative paths, capped at 20 lines per plugin per 10 s and 2000 characters a line (plugin-frame-guest-diagnostics.ts).plugins.devInspect(CLIorca plugin dev inspect) opens detached DevTools for live guests; headless hosts refuse it. - Integrity: a navigation re-hashes the plugin tree, at most every 2 s per tree; any other request
(including
fetchof an.htmlfile) reuses a check from the last 10 s. The hash records each file's digest, and documents, scripts, styles, JSON, SVG and WASM are served only if their bytes still match it, so a file swapped after the check is refused.
Host APIs added for frames
No new capability; both require transient user activation in the frame and are rate-limited. The shell
checks navigator.userActivation first; the frame host re-checks the activation that arrived with the
message: only a guest's preload stamp counts. A srcdoc iframe shares the host window, whose activation any
keystroke in Orca sets, so iframes (browser clients) are never offered these methods and get
not_permitted. Main runs them only for calls from its own window over IPC, so a dialog or browser tab
never opens on another machine. A components panel's hidden guest never has activation of its own; only a
press on a stationary Orca-drawn control grants one call (see
User gestures).
orca.shell.openExternal(url):http:/https:/mailto:only, opened in the OS browser. Anything in the URL leaves the machine, so for anhttp:/https:host this plugin has not been allowed, Orca first shows " wants to open " with the URL (truncated to 300 characters), Open / Cancel, and "Always allow for this plugin". The choice is kept per plugin and host (with port) in main's settings (pluginExternalHostGrants, which the renderer cannot write) and cleared on uninstall.mailto:opens without asking: it fills a draft the person still has to send. Resolves{ opened: false }when the person cancels.orca.files.saveAs({ suggestedName, mimeType, data }): Orca shows its save dialog; the plugin never sees the chosen path.datais a string orArrayBuffer, capped at 64 MiB. Resolves with{ status: 'saved' }, or{ status: 'cancelled' }when the person cancels. AnymimeTypeis accepted; the dialog filter comes from the suggested name's extension. Executable, script, installer, shortcut and disk-image types are refused, whether suggested or typed (plugin-save-as-extensions.ts, Chromium's dangerous download types plus.iso .img .vhd .vhdx, which shed the download mark when mounted). The written file is marked as downloaded: aZone.Identifierstream withZoneId=3on Windows,com.apple.quarantineon macOS; Linux has no such mark.- Revocation reaches the side effect: each call re-checks that its plugin is still installed, enabled, consented and not kill-listed right before it writes, opens a tab or sends terminal input, so a save dialog left open while the plugin is disabled or uninstalled writes nothing. Dialogs and rate budgets bind to the window whose frame made the call (its WebContents travels with the IPC call).
orca.locale: Orca's UI locale fromhello(guests'navigator.languagefollows the OS).