Accepted
ADR 0059 — Plugin runtime deferred to v1.x
Install, the capability model, and configuration ship in v1; sandboxed execution of plugin hooks and blocks follows in v1.x
Context
Cusp's plugin system has two halves. The host-side half — installing a signed package through the InstallPackage workflow, verifying its Ed25519 signature and per-file integrity, validating the manifest, recording the capabilities it declares, scoping its per-plugin KV prefix and D1 tables, generating a configuration form from its schema, and upgrading or uninstalling it — is complete and runs entirely in the host Worker. None of it depends on executing plugin code, and all of it works on a deploy.
The runtime half — loading the plugin's compiled code into a Dynamic Worker Loader sub-isolate and dispatching lifecycle hooks and contributed-block renderers to it — was written against an earlier, incorrect understanding of the Worker Loader API. The loader read sub.default off the loader return value and the dispatcher called it in-process as a factory, passing the plugin's capability-bound host env directly. The real Dynamic Worker Loader runs loaded code as a Worker reached over RPC via getEntrypoint(); a bare default export is never directly callable across that boundary, and live host bindings cannot be passed into the sub-isolate by reference.
The theme loader hit the same mistake and was migrated to the real model: a generated WorkerEntrypoint wrapper is loaded as the sub-Worker's entry module and its render method is invoked over RPC. That fix worked cleanly because a theme template is a pure function of (document, context) returning an HTML string — plain data in, a string out, both structured-cloneable. A plugin is not pure: it receives a capability-scoped env (a per-plugin fetch shim with an SSRF guard and hostname allowlist, scoped KV, scoped D1) and calls back into the host through it. Making that work across an isolate boundary requires the host to mediate those capability calls over RPC in both directions — a design problem, not a mechanical mirror of the theme fix.
The gap is invisible in local development and CI: Miniflare exposes no worker_loader binding, so the loader short-circuits with a typed error and the dispatcher degrades gracefully (the hook is logged as failed in plugin_invocations and the request proceeds). The break only manifests on real Cloudflare, where the loader is present but the pre-RPC contract fails.
Decision
Ship v1 with the working host-side surface and mark plugin runtime experimental. Concretely: plugin install, signature and manifest verification, capability scoping, generated configuration forms, upgrade, uninstall, and the admin Plugins UI are all v1 capabilities. A configuration-only plugin — one with no callable surface, like the bundled Cloudflare Web Analytics example, which only surfaces a beacon token as a versioned setting that themes read — is fully functional on a deploy. Plugin runtime — the generic hook dispatcher (ADR 0038), plugin-contributed block and field renderers, and the per-plugin Dynamic Worker Loader sub-isolate — is experimental in v1 and does not execute on a deployed site. The bundled Cusp Webhook example exists to illustrate the lifecycle-hook and capability contract, not to deliver production webhooks.
The manifest, capability, and authoring contract (ADR 0036, ADR 0037, ADR 0038) is held stable: a plugin authored against it today remains valid when the runtime lands, so the deferral is of execution, not of the interface developers build against.
The v1.x work
Finishing plugin runtime needs three pieces, mirroring then extending the theme-loader migration: (1) a generated plugin WorkerEntrypoint wrapper loaded as the sub-Worker's entry module, exposing hook handlers as RPC methods; (2) a capability bridge so the sandbox's env calls — scoped fetch, KV, D1 — are marshalled over RPC back to host-side implementations that enforce the manifest's allowlists, rather than passing live host objects into the isolate; (3) dispatcher changes to construct the entrypoint, call hook methods over RPC with structured-cloneable (payload, config), and keep the existing per-call timeout, graceful-degradation, and plugin_invocations logging. Tracked in the project's GitHub issues.
Consequences
The README, the themes-and-plugins guide, the plugin-authoring guide, the v1.0.0 changelog, and the admin Plugins screen all state the experimental status plainly, so neither a deployer nor a plugin author is misled into expecting hook execution on a deploy. The trade-off accepted is that one of the two bundled example plugins (the webhook) does not do anything observable on a deployed site in v1; this is preferable to either shipping a silent, broken marquee claim or sinking the v1 release into a runtime redesign. The capability model being verified and recorded at install — even though it is not yet exercised at runtime — means the security posture is established up front and the v1.x runtime inherits an already-enforced contract.