Initial deliverable of the architecture phase: 16 ADRs (stack, CRDT collaboration, plugin sandbox, import/export, backups, CI/CD), data model, permission model, real-time collaboration and plugin concepts, deployment/operations/security documentation, and the milestone roadmap that the implementation issues are derived from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
4.2 KiB
Markdown
82 lines
4.2 KiB
Markdown
# ADR 0008: Sandboxed iframe plugins with a message-based API
|
|
|
|
- Status: accepted
|
|
- Date: 2026-07-04
|
|
|
|
## Context
|
|
|
|
The vision requires plugins that range from simple styling (colored section
|
|
backgrounds) to complex features (table of contents, page index, diagrams,
|
|
embedding blocks from other pages), installable at runtime (directory upload
|
|
or GUI) without redeploying. Kickoff decision: only Site Admins install
|
|
plugins, and plugins run **sandboxed** — an uploaded plugin must not be able
|
|
to compromise the server or exfiltrate data beyond what the viewing user may
|
|
see.
|
|
|
|
## Decision
|
|
|
|
- **Plugins are client-side packages only** (v1). No plugin code executes on
|
|
the server. A plugin is a ZIP containing:
|
|
- `manifest.json` — id, name, version, `apiVersion`, declared extension
|
|
points, declared permissions, i18n strings;
|
|
- `plugin.js` — a single ES module bundle;
|
|
- optional assets (CSS, images).
|
|
- **Two plugin classes, by trust needs:**
|
|
1. **Declarative style plugins** — manifest + CSS only, no JavaScript.
|
|
They define named "section styles" (e.g. colored background boxes)
|
|
applied as attributes on standard container nodes. No sandbox needed;
|
|
CSS is served sanitized and scoped.
|
|
2. **Code plugins** — run inside a **sandboxed `<iframe>`**
|
|
(`sandbox="allow-scripts"`, **without** `allow-same-origin`, so the
|
|
frame has an opaque origin: no cookies, no host DOM, no storage).
|
|
Communication with the host app happens exclusively via `postMessage`
|
|
RPC defined in `packages/plugin-sdk`.
|
|
- **Extension points (v1):**
|
|
- `block`: a custom block node type (registered in the editor schema by
|
|
the host); the plugin renders/edits the block content inside its iframe
|
|
(diagram editors, embeds, …). Block data is stored as attributes/content
|
|
of the node in the page document.
|
|
- `pageTool`: read-only widgets over page data — table of contents, page
|
|
index, "embed block from another page". They query data through the
|
|
plugin API only.
|
|
- `sectionStyle`: the declarative class above.
|
|
- **Plugin API & security:** the host mediates every request. Plugins get a
|
|
capability object scoped to the **viewing user's permissions** — e.g.
|
|
`listPages(pondId)`, `getPageOutline(pageId)`, `getBlock(pageId, blockId)`
|
|
return only what the current viewer could read anyway (enforced by the
|
|
API server, not the client). Network access from the iframe is blocked by
|
|
CSP; plugins requesting external resources must declare them in the
|
|
manifest and route them through a host-controlled allowlist (post-v1).
|
|
- **Lifecycle:** Site Admin uploads via GUI (or drops the ZIP into the
|
|
`plugins/` volume; a watcher registers it). The API validates the
|
|
manifest, stores metadata (ADR 0002), and serves the bundle. Site Admin
|
|
sets each plugin `disabled` / `optional` / `required` per instance;
|
|
Pond Admins toggle optional plugins per pond. Activation is immediate —
|
|
no redeploy, clients pick up the plugin list on next page load.
|
|
- **Versioning:** `apiVersion` in the manifest is checked against the host's
|
|
supported range; incompatible plugins are refused at install time.
|
|
|
|
## Consequences
|
|
|
|
- A malicious plugin can, at worst, render nonsense inside its own iframe
|
|
and read data the current viewer could read anyway — it cannot touch
|
|
cookies, other pages' DOM, or the server.
|
|
- Complex "server-ish" features (e.g. scheduled jobs, new storage) are not
|
|
possible for plugins in v1; they become core features or a future,
|
|
separately-decided trusted-plugin tier.
|
|
- Rendering plugin blocks costs one iframe each; acceptable at wiki page
|
|
scale, and `pageTool` widgets are lazy-loaded.
|
|
- Exports (PDF/Word) render plugin blocks as their declared static fallback
|
|
(manifest field `fallback`: image/text) — documented in
|
|
`plugin-architecture.md`.
|
|
|
|
## Alternatives considered
|
|
|
|
- **Trusted server-side plugins (WordPress model)**: maximum power, but one
|
|
bad upload owns the instance; contradicts the sandbox decision.
|
|
- **Web Workers as sandbox**: no DOM rendering, which block plugins need;
|
|
iframes give both isolation and rendering.
|
|
- **WASM sandbox on the server**: strong isolation for server-side logic,
|
|
but big complexity budget; revisit only if plugin demand outgrows the
|
|
client-side model.
|