Distribution
Plugins travel over npm, install without a rebuild, and reconcile declaratively at boot. This page covers how a plugin reaches an instance: the npm resolver and pacote fetch, the boot-time reconcile, the bundled set that ships built-ins, the adoption seam that migrates data when a core feature becomes a plugin, and the version-pinned workflow bundle build. For the manifest and the install review, see authoring; for exact-state fleet control, see GitOps.
npm as transport
Section titled “npm as transport”plugins.install accepts a local directory or an npm spec: name[@range] or a
file:tarball.tgz. An npm spec is fetched with pacote straight into
~/Subspace/plugins/<name>@<version>/, integrity-checked. Crucially, npm never touches the
server’s node_modules: a plugin is copied into its own immutable version directory and
imported from there, so installing a plugin cannot perturb the host’s own dependencies and
there is no rebuild. An npm install also resolves the manifest’s
dependencies first, recursively and
topologically, and the install response returns the resolved dependency tree with each
already-satisfied node marked satisfied: true.
Boot-time reconcile
Section titled “Boot-time reconcile”Instances converge on a desired plugin set declaratively at boot rather than through imperative install commands.
-
SUBSPACE_PLUGINS
The
SUBSPACE_PLUGINSenvironment variable (comma- or space-separated npm specs or local paths) reconciles first: missing plugins install into the volume, satisfied ones are left untouched, and a failing spec logs and never bricks boot. -
Bundled set
The bundled plugin sweep runs next, installing and enabling the in-repo set.
-
GitOps
The GitOps
plugins.jsonsync runs last, as the outer authority.
The bundled set
Section titled “The bundled set”plugins/bundled.json ({"plugins": [<in-repo plugin directory names>]}) lists the plugins
that install and enable idempotently on every trusted-plane boot, after the
SUBSPACE_PLUGINS reconcile and before the GitOps sync. This is how domain features ship as
plugins with zero UX regression: MLOps, CRM,
meetings, spaced repetition, and
outreach are bundled plugins that a fresh home and an upgraded home
both converge on.
The first-enable adoption seam
Section titled “The first-enable adoption seam”When a feature that used to live in core becomes a plugin, the plugin needs to adopt the
data core already wrote. A plugin registers a one-time, core-implemented migration step by
name (for example, copying existing SRS card rows into a collection).
installPlugin runs the step inside the install transaction, claim-guarded by an
ops.plugin_state (plugin, 'adopted') row.
Workflow bundles
Section titled “Workflow bundles”A plugin’s workflows: [{ dir, names }] ships compiled WDK standalone bundles that run on
the durable agent engine. Every installed version directory
imports at boot (superseded and disabled included, so in-flight runs resume against their
pinned id), while the unversioned alias <plugin>/<fn> resolves to the newest enabled
version’s workflow//<plugin>@<version>//<fn> only at run start, so a mid-flight upgrade
never mutates a running body. syncPluginWorkflows reconciles aliases on boot and on every
plugins event: it hot-imports on install, repoints only once the import completes, drops
aliases on disable (the bundles stay for draining), and refuses non-package-mode ids
(relative ./ specifiers). Shipping workflows requires pinning sdk: 1. An
agent definition binds a workflow through its workflow: <alias>
field.
Package-mode builds
Section titled “Package-mode builds”The @subspace/plugin-sdk build command compiles a plugin’s use workflow sources into the
version-pinned bundle the host requires:
subspace-plugin build [pluginDir] [--dir workflows] [--shared-external]The stock builder emits relative ids (workflow//./…) that the host refuses. The SDK
forces package mode: it runs the standalone builder with the module root pointed at a
throwaway package whose package.json lists the plugin as a dependency, so the sources
resolve as a workspace package and produce ids like workflow//<name>@<version>//<fn>
(name and version read from the plugin’s package.json). It writes the {"type":"commonjs"}
marker the host’s import() needs, and an assertion fails the build if any id came out
relative.
GitOps and fleet control
Section titled “GitOps and fleet control”For declarative fleet management, GitOps runs a plugins.json in exact-state mode as the
outer authority: it runs after the bundled sweep and can disable bundled entries to enforce a
precise plugin set across an instance. See GitOps for the full
reconcile model, and self-hosting for where these files live.