Plugin runtime boundaries
Plugin manifests are executable policy, not descriptive metadata. Developer-controlled native plugins use WiseWig’s in-process PluginRuntime. Third-party sandboxed packages use EmDash’s canonical package and trust contract, compiled into WiseWig’s internal enforcement form; a host-provided sandbox runner owns execution. EmDash’s runner behavior is the compatibility target rather than a WiseWig-specific execution format. See EmDash plugin compatibility and WiseWig AI.
Runner policy
WiseWig follows EmDash’s two-format, pluggable-runner model:
- Cloudflare uses the Worker Loader API (Dynamic Workers) to execute each sandboxed plugin in a separate V8 isolate.
- A future Node adapter may use EmDash’s workerd runner, while documenting that standalone workerd lacks Cloudflare’s per-invocation CPU and memory enforcement.
- If the configured runner is absent or unavailable, sandboxed plugins are not executed. The framework reports them as unavailable; it never falls back to same-process execution.
- Moving a package to the native plugin list is an explicit developer deployment decision and grants native-level trust.
Dynamic Workers require the ordinary Workers Paid plan but not Workers for Platforms. The core and ai self-hosted profiles do not require Dynamic Workers. The plugins and full profiles require them only when executable EmDash sandboxed plugins are enabled. Workers for Platforms remains exclusive to the future managed-isolated topology.
The Cloudflare runner loads verified backend.js by immutable plugin/version/executable-digest identity, creates a fresh request-scoped bridge binding for each invocation, sets globalOutbound: null, applies CPU and subrequest limits in Worker Loader, and enforces a host wall-time limit. The isolate receives no raw D1, R2, Durable Object, secret, session, or environment binding. All content, media, storage, network, email, and AI/MCP operations cross a capability-filtered host bridge.
The first beta execution slice is implemented. Bundle ingestion uses EmDash’s current 256 KiB package and 128 KiB backend ceilings. Before code storage, WiseWig binds the bundle’s slug, version, capabilities, allowed hosts, storage declarations, and hooks to the already-authorized internal manifest; any drift fails closed. The non-public artifact Worker stores that backend immutably under a canonical SHA-256 executable digest and will return it only for the bound plugin identity. The tenant workspace owns the Worker Loader binding. A real-workerd test proves the EmDash 0.31 wrapper contract can execute the backend and that ambient fetch is blocked.
The runtime wrapper adapter is repository-local and explicitly pinned to the MIT-licensed EmDash 0.31 contract. Importing EmDash’s aggregate Cloudflare sandbox entrypoint into a standalone Worker causes Wrangler to traverse Astro-only virtual modules, so it is not a deployable runtime dependency here. Compatibility changes must be mirrored deliberately and exercised through real-workerd plugin fixtures; do not replace this with an import that makes the production dry run depend on virtual:emdash/* or astro:content.
The tenant bridge and lifecycle dispatch are live for declared plugin storage, content, media, and network capabilities. Executable bytes and their canonical digest are covered by the trusted release signature, installed into the immutable artifact service before tenant state changes, and bound to the active site/plugin/version/release digest in tenant SQLite. After-publish dispatch retrieves and revalidates that exact artifact, asks the tenant Durable Object for a 15-second grant, invokes a fresh Dynamic Worker, and revokes the grant in finally.
Grant tokens are random, stored only as SHA-256 hashes, never archived, and revalidated on every bridge call against expiry, revocation, active plugin version, executable digest, enabled state, capability, and declared collection. The Dynamic Worker receives only the request-scoped loopback binding. It cannot choose another tenant Durable Object, site, plugin, release, capability, host, or collection. Real-workerd coverage proves Dynamic Worker → EmDash-compatible context → loopback RPC → tenant SQLite/R2, cross-tenant token denial, wrong-digest denial, undeclared-collection denial, and immediate revocation.
The supported bridge surface is deliberately bounded:
content.readexposes published core pages and plugin-owned structured records.content.writecreates, updates, and deletes only plugin-owned namespaced records; it cannot mutate core pages.media.readexposes ready tenant media.media.writeaccepts at most 5 MiB per upload and coordinates tenant metadata with tenant-prefixed R2 objects.network.fetchaccepts only HTTP(S), rechecks the manifest hostname allowlist after every redirect, strips credentials on cross-origin redirects, rejects literal local/private and metadata targets, and caps time and response size. Host policy is still required to defend against DNS rebinding.- taxonomy, user, and email methods remain fail-closed with
PLUGIN_CAPABILITY_DENIED.
Each invocation uses WorkerLoader.load() rather than a cached worker. Real-workerd testing showed that reusing a worker by executable digest could retain the first invocation’s loopback binding after that grant was revoked. Fresh loading makes the request-scoped authority boundary explicit. The declarative runner remains the fallback only for releases without a bound EmDash executable.
Capabilities
Before a plugin performs an operation, the host calls assertCapability(pluginId, capability). In a sandbox, the stronger rule is preferred: undeclared host APIs are never placed on the plugin context. Native packages retain direct same-process power and therefore are restricted to developer-controlled deployments.
Lifecycle hooks
dispatchHook() invokes only handlers that are both declared in the manifest and registered by the plugin. A missing declared handler fails closed. Hook execution is bounded by a host-configured timeout; a timeout rejects the lifecycle step. beforeSave and beforePublish should perform bounded validation only. Long-running delivery, indexing, and integrations belong after the durable command commit.
Storage namespace
storageKey() permits only manifest-declared collections and safe key segments. It produces an isolated plugin prefix beneath the host’s tenant/site storage scope. The host still enforces tenant/site prefixing, quotas, encryption/retention policy, and capability checks before any backing-store operation.
Sandboxed execution is the default for third-party distribution. The declarative WiseWig artifact contains only a small validation instruction set and is persisted by verified digest in a non-public Worker’s own Durable Object. The host sends one bounded event envelope; missing artifacts, undeclared hooks, timeouts, and failed instructions fail closed. Verified EmDash backends are lifecycle-wired through the bounded bridge above.
The artifact Durable Object exposes only install/status/hook behavior through its non-public Worker. Install reparses the manifest-bound artifact and recomputes its SHA-256 digest. Storage is immutable by digest, expected validation failures return bounded result codes rather than escaping as Durable Object RPC exceptions, and the host can verify release availability before dispatch.
In managed hosting, installation is a trusted dispatcher composition rather than a browser-to-tenant command. After session, membership, plugin.manage, active-tenant, same-origin, idempotency, and exact-confirmation checks, the dispatcher verifies the ECDSA P-256 release signature against its environment-specific public JWK set and recomputes the artifact digest. It installs the release into the isolated sandbox first, then creates a platform-only plugin-install dispatch grant for the already-selected tenant Worker to apply migrations and installation state. Tenant Workers receive neither trust keys nor the sandbox binding, and a normal private grant cannot invoke the install command.
MCP runtime registration
Installed, enabled plugin declarations become real MCP catalog entries through generic resource templates and prompt resolvers. Catalog discovery never grants access. The private gateway reloads current membership, obtains the exact declared permission, and dispatches into the tenant-specific Worker before a resource can read its explicitly named collections. Prompt arguments are restricted to declared names and required values; signed instructions are guidance, not authority to call undeclared tools or cross data scopes. Public plugin capabilities are materialized during publish and contain artifact provenance, but execute only against the bounded public index.