ADR 007: Plugins: one package, optional server and client halves, trusted ESM in v1
Date: 2026-09-10. Status: accepted.
Decision#
- A plugin is a directory with a
nookletmanifest inpackage.json(id, API version, optionalserverandcliententries, JSON-schema settings, permissions, declared contributions), or a single*.plugin.tsfor quick scripts. Shared code is an ordinary shared module. - The data API (
blocks,pages,query,transact) is the same TypeScript interface on both sides: server halves call core services, client halves go through the HTTP API and the local replica. A client half calls its server half over RPC. - v1 runtime is trusted: the host bundles each entry with esbuild and
import()s it; the trust boundary is the plugins directory. The UI states that plugins run with full access. - The API is written so a sandbox can be added later without changing it: every call is async
with JSON-serializable arguments and results; callbacks enter the host only through
register(...)functions returning disposables; client renderers may berender(source, el)(trusted) orhtml(source) => string(sandboxable). Upgrade path: server halves in aworker_thread(v1.x), untrusted client halves in a Web Worker with sandboxed iframes (v2). - Extension points are enumerated in
PLAN.mdsection 13 and kept small. Slots are named and host-owned; plugins getHTMLElements, never framework components. - Built-in optional features ship as internal plugins to keep the API honest.
Why#
- Obsidian's simple trusted
Pluginclass with auto-cleanup registration is what made its ecosystem; SilverBullet's sandboxed portable runtime was abandoned in 2025 because two runtimes were "a persistent burden" for authors. nooklet needs server-authoritative features (MCP, jobs, hooks), so two explicit halves with one shared interface beats one portable runtime. - Sandboxes with real teeth (QuickJS-wasm, workers) all cost DOM and sync access, which is what
renderers and slash commands want;
isolated-vmis in maintenance with a 2026 escape CVE;node:vmis not a security boundary; ShadowRealm has shipped nowhere.
Consequences#
- Node cannot evict ES modules, so in-process hot reload leaks old module graphs; acceptable in development and solved by the worker host later.
- MCP is reached through
ctx.mcp, never by importing the SDK directly, so SDK upgrades stay the host's problem.
Confirmation#
User confirmed trusted plugins for v1 on 2026-09-10.