Skip to content

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.

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.

Instances converge on a desired plugin set declaratively at boot rather than through imperative install commands.

  1. SUBSPACE_PLUGINS

    The SUBSPACE_PLUGINS environment 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.

  2. Bundled set

    The bundled plugin sweep runs next, installing and enabling the in-repo set.

  3. GitOps

    The GitOps plugins.json sync runs last, as the outer authority.

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.

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.

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.

The @subspace/plugin-sdk build command compiles a plugin’s use workflow sources into the version-pinned bundle the host requires:

Terminal window
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.

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.