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.
What a plugin is
Section titled “What a plugin is”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 bundlesThe 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.
Artifact kinds
Section titled “Artifact kinds”A single manifest can ship any mix of the following. Each has its own page.
The capability model
Section titled “The capability model”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.
Enable, disable, upgrade
Section titled “Enable, disable, upgrade”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.
Built-ins are plugins
Section titled “Built-ins are plugins”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.