Skip to content

Plugins

A plugin is a self-contained bundle that extends a live Subspace instance without a redeploy. You install a versioned directory, the host imports its artifacts through the same command pipeline everything else runs on, and new tools, views, page types, and surfaces appear in the running process. Nothing is compiled into the server: a plugin is data plus sandboxed code, governed by an explicit capability grant, so you can add, upgrade, disable, or remove one on a running instance and watch the graph change under you.

Plugins are how Subspace stays a small privileged core while shipping a large surface area. The engine owns the durable machinery (the command model, the outbox dispatcher, the agent engine, gates and claims and audit). Plugins own domain declarations layered on top. Several features you already use, MLOps, CRM, meetings, spaced repetition, and outreach, are themselves bundled plugins, installed and enabled on boot rather than wired into the core.

A plugin is an installable, immutable, versioned directory on the data volume:

~/Subspace/plugins/<name>@<version>/
subspace-plugin.json # the Zod-validated manifest
pages/ # OKF files → kb pages (fns, agent/skill/mcp definitions, templates)
views/ # ESM viewer + surface bundles (*.js)
workflows/ # compiled WDK standalone bundles

The name is an npm-style package name; scoped names escape into the directory (@acme/x becomes acme__x@1.0.0). Versions never change once written: an install never overwrites an existing version directory, and an upgrade installs a sibling directory and repoints the database row. Because superseded versions stay on disk, an in-flight agent run keeps running against the exact bytes it started on while the newest enabled version serves fresh calls.

Installing imports the plugin’s pages/**/*.md through the ordinary command pipeline: the file’s subdirectory becomes the page namespace (pages/code/x.md becomes the page code/x), every imported page is provenance-tagged with metadata.plugin = {name, version}, and one transaction upserts the ops.plugins (name, version, manifest, enabled) row and emits a plugins outbox event. A slug that collides with a page the plugin does not own is skipped, never clobbered, so your local edits fork the page rather than being lost.

A single manifest can ship any mix of the following. Each has its own page.

A plugin starts with nothing. Every reach beyond its own pages is named in the manifest capabilities block and enforced host-side, so the sandbox never holds authority it was not granted.

Capability Grants
capabilities.host An allowlist of host API methods a function tool may call (kb.query, kb.append, collections.*, http.fetch, and so on).
capabilities.fetch The domain allowlist http.fetch may reach. Requests are https-only, size- and time-capped, and redirects stay closed.
capabilities.credentials Brokered secrets [{name, into: header template}] resolved host-side so the token never enters the sandbox.

Because grants are pinned into the tool snapshot at install, a function inherits exactly the plugin’s declared reach, and a bare code/ page you write by hand keeps the default minimal grant no matter what a manifest claims. See security and trust for the full trust model and plugin tools for how the broker injects credentials.

Plugins are installed once and toggled thereafter. Disabling is Subspace’s uninstall: the row and the imported pages stay, but the plugin stops contributing tools, views, surfaces, and workflow aliases.

Action tRPC Effect
Install plugins.install {path} | {spec} Copies into the immutable directory, imports pages, upserts the row, emits plugins.
Enable plugins.enable {name} Verifies dependencies are installed, in-range, and enabled, then flips enabled.
Disable plugins.disable {name} Leaves data in place; cascade-disables transitive dependents in the same transaction.
List plugins.list Returns every installed row with its manifest and enabled flag.

A disabled plugin’s function tools degrade to the default capability grant, never a wider one, so turning a plugin off can only reduce authority. Plugins are barred from DDL: durable plugin state lives in knowledge-base pages, in metadata props, in a namespaced ops.plugin_state (plugin, key, value) row, or in collections, each cascade-deleted with the plugin row.

The doctrine is that plugins own domain declarations and the core owns privileged engines. Domain features package cleanly as bundled plugins: MLOps, CRM, meetings, spaced repetition, and outreach each ship as an in-repo plugin that is installed and enabled idempotently on every boot. A fresh home and an upgraded home converge on the same default set with zero UX regression, and a plugin you disable stays disabled across reboots. See distribution for the bundled set and the boot-time reconcile.