dorfteich/docs/architecture/adr/0008-plugin-sandbox.md
Claude Fable 5 0629411966 Add architecture documentation, ADRs, and operations concept
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>
2026-07-04 14:36:16 +02:00

4.2 KiB

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.