Skip to content

Authoring a plugin

Authoring a plugin means writing a subspace-plugin.json manifest next to a pages/, views/, and workflows/ tree, then handing the directory (or an npm spec) to plugins.install. The manifest is the whole contract: it names what the plugin ships and what authority it needs, a Zod schema validates it, and the install review card renders it for approval before anything runs. This page covers the manifest shape, page types, inter-plugin dependencies, contracts, and the review card. For the artifacts themselves, see plugin tools, views and surfaces, and distribution.

subspace-plugin.json is Zod-validated, and unknown keys are stripped rather than rejected, so a manifest written against a newer SDK still installs on an older host.

{
"name": "@acme/reading-tracker",
"version": "1.2.0",
"sdk": 1,
"capabilities": {
"host": ["kb.query", "kb.append", "collections.put", "collections.query"],
"fetch": ["api.openlibrary.org"],
"credentials": [{ "name": "openlibrary_key", "into": { "header": "Authorization", "format": "Bearer {{secret}}" } }],
"fetchMaxBytes": 1048576,
"fetchTimeoutMs": 8000
},
"tools": [{ "name": "lookupBook", "page": "code/lookup-book" }],
"nativeTools": [{ "module": "native/telegram.js", "names": ["tg.sendMessage"] }],
"surfaces": [{ "slug": "reading", "src": "views/dashboard.js", "title": "Reading" }],
"viewers": [{ "id": "book-card", "src": "views/book-card.js" }],
"workflows": [{ "dir": "workflows", "names": ["digest"] }],
"pageTypes": [{ "namespace": "books", "type": "book", "propsSchema": "schemas/book.schema.json", "defaultView": "views/book-card.js" }],
"dependencies": { "@subspace/mlops-core": "^1.0.0" },
"provides": { "code.runner": { "tools": { "run": "fn.dispatchRun" } } },
"consumes": ["comms.send"]
}

name  string (npm-style)

The package identifier. Scoped names install into an escaped directory (@acme/x becomes acme__x@<version>).

version  semver

The version. Installs are immutable per version; an upgrade installs a sibling directory.

sdk  major integer

The SDK major the plugin targets. Shipping nativeTools or workflows requires pinning sdk: 1; the install is rejected otherwise.

capabilities  object

host (host-API allowlist), fetch (domain allowlist), credentials (brokered secrets), and the optional fetchMaxBytes / fetchTimeoutMs caps. See plugin tools.

tools  [{ name, page }]

Function tools, each mapping a tool name to the code/ page holding its source.

nativeTools  [{ module, names }]

The full-trust native-module lane. See native tool modules.

surfaces  [{ slug, src, title? }]

Special-page routes at /p/<slug> rendered by an ESM component. See surfaces.

viewers  [{ id, src, meta? }]

ESM code viewers for run trees and page types. See views and surfaces.

workflows  [{ dir, names }]

Compiled WDK bundles. See distribution.

pageTypes  [{ namespace, type, propsSchema?, template?, defaultView? }]

Namespace conventions declared as data. See page types.

dependencies  { name: semver range }

Other plugins this one requires. See dependencies.

provides / consumes  object / string[]

The contract registry. See contracts.

pageTypes declares namespace conventions as data, so a directory of pages gains a shape without any code patching the SPA. Each entry names a namespace (a directory slug), a type, and optionally a propsSchema, a template under pages/, and a defaultView. plugins.pageTypes exposes the enabled declarations to the app, and the SPA renders a page type’s defaultView component automatically between its title and outline. See wiring default views.

When a page under a declared namespace changes, an outbox consumer re-validates its metadata/props against the declaring plugin’s propsSchema JSON-Schema files. Validation is advisory and anyOf across the namespace’s types: the page passes if it satisfies at least one declared type.

Schemas are compiled with ajv and cached per immutable plugin directory, so validation is free after the first page in a version. The bundled MLOps plugin ships run and experiment schemas as a worked example.

A plugin declares other plugins it needs in dependencies (package name to semver range). Dependencies are verified at both install and enable: a dependency must be installed, in range, and enabled for the dependent to enable. Enabling therefore proceeds in topological order.

Disabling runs the other direction: disabling a plugin cascade-disables its transitive dependents in the same transaction, one outbox event each, so you can never leave a dependent enabled against a missing provider. Plugins never import each other’s code: composition happens at the registries, not through Node module resolution.

Contracts let a consumer call a capability without knowing which plugin provides it. A provider maps a contract’s methods to its own tool names in provides:

"provides": { "comms.send": { "tools": { "send": "fn.sendViaTelegram" } } }

Install validates every fn.* mapping against the tools the plugin actually ships. A consumer resolves a provider at call time with resolveProvider(contract, provider?) (tRPC plugins.resolveProvider) over the enabled manifests:

  • a pinned provider narrows to that plugin;
  • a single matching provider resolves automatically;
  • ambiguity throws, and you pin one in the calling page’s configuration.

Resolved calls run through the tool registry like any other, so gates, claims, and audit apply unchanged.

Every install renders a review card before anything runs, splitting the plugin’s artifacts into trust lanes so you approve exactly what you are granting.

Sandboxed lane

Function tools and their capabilities: the capabilities.host methods, the capabilities.fetch domains, and each brokered credential rendered as its secret name plus destination header (never the token). Client-code artifacts, surface /p/<slug> and viewers, list here too, since a viewer has DOM access but no remote script or cross-origin channel.

Full-trust lane

Native tool modules and workflow bundles, which run host code. Every declared native tool renders as native tool <name> (<module>), and the whole lane is stamped with a source-tree hash of the version directory so you approve specific bytes.

The card is how the capability model becomes a human decision: a purely declarative plugin (pages, view specs, page types) carries no new trust and reviews cleanly, while anything that reaches the network, holds a credential, or runs host code is surfaced explicitly.