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.
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) native/ # compiled full-trust native tool modules 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, source}, 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. To make an imported page your local
fork, strip its plugin provenance; later upgrades leave that page untouched.
On a version bump, pages still owned by the plugin update in place, preserving page ids, explicit node ids, page-version history, and state derived from those ids. If a source path disappears, its still-owned page moves to trash, is marked retired, and has SRS disabled; any derived flashcards clear. Provenance-stripped forks survive both updates and removals.
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, domains?, bindingDomains?}] resolved host-side so the token never enters the sandbox. |
The installed manifest always stores capabilities.host explicitly: omitting it
materializes host: []. Plugin functions never fall back to the curated core grant
used by bare, user-authored code/ functions, so plugin authors must list every host
method they need.
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”Plugins own domain declarations; the core owns privileged engines. The read-only Subspace help agent, MLOps, CRM, meetings, spaced repetition, and outreach each ship as an in-repo plugin, installed and enabled idempotently on every boot. A fresh home and an upgraded home converge on the same default set, and a plugin you disable stays disabled across reboots. See distribution for the bundled set and the boot-time reconcile.