Plugin runtime environment
What capture hooks and the transform may use inside the sandbox, and what does not exist there.
Package code runs in a capability-free sandbox: QuickJS compiled to WebAssembly, with an empty import table. fetch, require, process, and the Node built-ins are not merely blocked; they do not exist as symbols. Memory and wall-clock are bounded. The same sandbox ships in the server and the Desktop app, so a package behaves identically wherever it runs.
Entry points
| Module | Exports | Called with |
|---|---|---|
transform.hooks.file | normalize (optional), materializeRecords, buildView | extract items; the root record and its related records |
capture.hooks.file | the functions listed in capture.hooks.exports | the page API, limited to the declared capabilities |
Entry point names are fixed by the runtime id (fruitful-page-plugin@1); the manifest never restates them.
Optional field tracing
The transform operations receive (input, context, trace?). ContentPipelineModule
types this optional collector; it is created inside the sandbox and grants no
host, DOM, storage, or network access. Existing packages can ignore it and keep
their existing return shapes.
| Operation | Collector method | Source and target |
|---|---|---|
normalize | linkExtractField({ item, path, sources }) | Input items → returned normalized item |
materializeRecords | linkRecordField({ record, path, sources }) | Input items → returned semantic record |
buildView | linkViewField({ view, path, sources }) | Root/supporting records → returned View |
Each item source is { item, path }; each record source is { record, path }.
Use actual input and returned objects. Paths are JSON Pointers, relative to
item.fields for extracts and to the JSON root for records and Views. A context
source is { context: true, path: '/capturedAt' }. Multiple sources remain multiple
contributors. Calls apply only during their operation; retained collectors are inactive.
Fruitful records the captured HTML digest, exact locked binding, original entry
and field match, selector, and match occurrence automatically. The package
declares how its JavaScript connects fields; those claims should have fixture
coverage. A missing normalizer and the required View /record identity need no
extra annotations. Other unreported relationships remain gaps.
Tracing does not change binding files, semantic Lexicons, or the manifest format. Publish annotations as a new immutable plugin version through the usual generate and validation workflow. Older captures retain their original package resolution and available evidence; an updated plugin does not retrospectively change them. See normalization and DevTools.
Always available
Bare ECMAScript: JSON, Math, Date, RegExp, String, Array, Object, Map, Set, encodeURIComponent, decodeURIComponent, parseInt, parseFloat.
Available when declared
stdlib on the hooks block links pure, pinned polyfills. Declare only what the module uses:
"hooks": { "file": "blog-plugin.js", "stdlib": ["url", "intl", "base64"] }| Module | Provides |
|---|---|
url | URL, URLSearchParams, TextEncoder, TextDecoder |
intl | Intl.NumberFormat, Intl.PluralRules, Intl.Locale, Intl.getCanonicalLocales, Number.prototype.toLocaleString |
base64 | atob, btoa, Uint8Array.fromBase64, Uint8Array.prototype.toBase64 |
The sandbox reads no ambient locale: pass one explicitly (value.toLocaleString('en-US')). To read a base64url identifier, use Uint8Array.fromBase64(value, { alphabet: 'base64url' }) and TextDecoder, never Buffer.
Not available
Buffer, require and every node: built-in, process, fetch, setTimeout and other timers, crypto, fs. The bundler builds with esbuild in browser mode and bundles everything, so a node: import fails plugin generate with "Could not resolve"; a typecheck against Node types will not catch it earlier.
Capture hook operations
A capture hook's import table is exactly the operations its declared capabilities grant:
| Capability | Operations |
|---|---|
browser.dom.read | waitForVisible(selector), count(selector) |
browser.dom.click | click(selector) |
browser.dom.type | type(selector, text); refuses password fields and credential-looking input |
Each operation is clamped by a timeout (10 s by default, 30 s at most). An operation the export did not declare is absent at link time, not denied at call time. Hooks are written against CaptureApi from workspace.packages.universal.lib.
Marshalling
Arguments and results cross the boundary as JSON values. No host objects or functions enter the guest; nothing the guest creates can hold a reference to the host. A guest that throws surfaces as a sandbox error with the message; a guest that exceeds the time limit is terminated.