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.

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)
native/ # compiled full-trust native tool modules
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, 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.

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, 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.

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.

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.