Skip to content

PATTERN Cited by 2 sources

Capability-manifest plugin isolation

Pattern

Isolate each third-party plugin (or agent-authored extension, LLM-generated tool, untrusted script) in a capability-based sandbox. The plugin ships with a declarative capability manifest naming the hooks it observes and the capabilities it requires. At install time, the administrator inspects the manifest; at runtime, the sandbox enforces that the plugin cannot exceed what the manifest declared. The manifest is the contract; the sandbox is the enforcer.

When to use

  • Third-party extensions run in the same process / host as trusted application code (classic plugin systems: WordPress, Jenkins, Jira, Postgres extensions).
  • You want a plugin ecosystem without an expensive marketplace-vetting apparatus (concepts/plugin-marketplace-lock-in).
  • You want license-independent plugins — authors pick their own license, ship proprietary / closed-source builds without compromising host security.
  • You want agent-authored extensions (LLM writes a plugin at runtime; you need to constrain what it can do).

Ingredients

  1. A capability-based runtime. The plugin executes in a sandbox that starts with no ambient authority — no filesystem, no arbitrary network, no credentials, no access to host memory. Dynamic Workers / V8 isolates / WASM / native sandboxing primitives all work.
  2. A capability lexicon. A documented, versioned set of capability names the runtime understands. Each capability has a scope-narrow definition — not "network" but "network access to hostname X"; not "content" but "read this content type".
  3. A declarative manifest format. Machine-parseable; structured (not free-form); ideally colocated with the plugin code so install-time inspection is trivial.
  4. Runtime enforcement at the boundary. Capability checks happen at the runtime-API boundary (can this isolate call this binding?), not in a separate policy layer that the plugin code sits above.
  5. Install-time UX. The manifest is shown to the administrator during install; approval or denial is per-capability or per-manifest.

Worked example — EmDash plugin

EmDash plugins run as Dynamic Workers. Manifest + code in one TypeScript object:

import { definePlugin } from "emdash";

export default () =>
  definePlugin({
    id: "notify-on-publish",
    version: "1.0.0",
    capabilities: ["read:content", "email:send"],
    hooks: {
      "content:afterSave": async (event, ctx) => {
        if (event.collection !== "posts" ||
            event.content.status !== "published") return;
        await ctx.email!.send({
          to: "[email protected]",
          subject: `New post: ${event.content.title}`,
          text: `"${event.content.title}" is live.`,
        });
      },
    },
  });

Runtime enforcement:

  • Hook registration (content:afterSave) — only runs on the declared hook; not invoked for other content events.
  • ctx.email!.send — available because email:send is declared; absent otherwise.
  • No fetch to arbitrary origins — the Dynamic Worker's globalOutbound: null posture means unrelated network calls simply fail.
  • If network is needed — the manifest names the exact hostname ("it can specify the exact hostname it needs to talk to, as part of its definition, and be granted only the ability to communicate with a particular hostname").

Worked example — Project Think agent extension

Project Think agents can author their own tools at runtime. The runtime-generated extension comes with a manifest:

{
  "name": "github",
  "permissions": {
    "network": ["api.github.com"],
    "workspace": "read-write"
  }
}

Different use-case (LLM-generated code, not human-authored plugin), same pattern. The agent declares what it needs; the Dynamic Worker enforces.

Non-example — WordPress plugins

A WordPress plugin's header block contains metadata (name, version, author, license) but no capabilities: the plugin's PHP can call any WordPress API, read any table, write any file. There is no install-time inspection surface and no runtime boundary to enforce one against. This is the structural gap plugin marketplace lock-in characterises.

Granularity matters

The pattern's strength depends entirely on how narrow the capability lexicon is. Coarse capabilities make the manifest theatre:

  • email:send where send means "to arbitrary recipients with arbitrary body" is almost as dangerous as the plugin having direct SMTP access.
  • read:content where content is the whole site is weaker than read:content/<collection>.
  • network where the scope is "any hostname" is barely better than ambient authority.

The EmDash post's hostname-scoped network grant is the good-granularity example. Capability design is a continuous discipline, not a one-off lexicon commit.

Composition risks

Two individually-narrow capabilities can combine into a broader one:

  • read:content + network: ["attacker-hostname.example.com"] = exfiltration of arbitrary content to the attacker's host.
  • read:users + email:send with to: as a free text field = mass spam.

Manifest inspection must make composition risks legible — ideally the runtime lints combinations and flags suspicious pairings at install time.

Seen in

Last updated · 200 distilled / 1,178 read