Plugin decisions and verification
Decision table
| I want to... | Use this seam |
|---|---|
| Add model-callable domain behavior | ctx.registerTool |
| Add progressive-disclosure instructions | ctx.registerSkill |
| Add a slash macro | ctx.registerCommand({ kind: "prompt" }) |
| Open a local browser chooser | web.chatPickers and a picker command |
| Add stable agent instructions | ctx.registerSystemPromptFragment |
| Add data that changes on every turn | ctx.registerTurnContext(..., { placement: "after-user" }) |
| Transform only explicit current-turn input | ctx.registerInputTransform |
| Add a reminder during a long tool loop | ctx.registerStepContext |
| Learn skills from a finished turn in the background | ctx.registerSkillPostTurnReviewer |
| Append a note or ask for one more agent run when a reply ends | ctx.registerHook with brain.run.beforeSettle |
| Observe or narrowly patch a wired lifecycle point | ctx.registerHook |
| Expose a user-facing authenticated endpoint | ctx.registerApiRoute |
| Receive an external callback | ctx.registerHttpRoute |
| Rate-limit or audit a public plugin webhook by canonical network origin | PluginHttpRequest.origin; authenticate the webhook separately |
| Start an account-scoped synthetic platform turn and observe live progress | PlatformControlApi.relay(src, text, observer) |
| Stream bytes to a browser or integration | ctx.registerWebSocketRoute plus issueWebSocketTicket |
| Share a domain capability with another plugin | registerControl, control, and manifest consumesControls |
| Add a Project row indicator | registerProjectIndicators or web.projectRows |
| Add fields to Brain status | registerBrainStatusProvider |
| Add cleanup or periodic work | registerBootReconcile, registerService, or registerInterval |
| Render a plugin card, rail section, metric, or history branch | web.chatCards, web.chatRailSections, web.dashboardMetrics, or web.historyBranches |
| Keep something of the conversation's Project visible in its chat | web.chatDock |
| Persist domain data | ctx.db().migrate and ctx.dataDir() |
| Use a core security boundary | The matching ctx.host accessor, never a copied ACL/path/process implementation |
If the value changes every turn, it belongs in turn context with
placement: "after-user". It never belongs in the system prompt, because
the system prompt is the cached prefix. Every provider request still pays for
the volatile text as input; system-prompt bytes are paid again only after a
cold or invalidated prefix. Host-only details such as toolTrace and
ownRow do not cost model tokens.
Where the seams are implemented
- The
PluginContextinterface, with the signatures and rules of everyregister*method, is insrc/plugins/api.ts. Platform relays use thePlatformControlApiinterface in the same file (line 1273). - The implementations live in
src/plugins/registry.ts, in the object that builds each plugin's context (about lines 1450-2366). Tools, platforms and HTTP, API and WebSocket routes are checked against the manifest first: an undeclared one is refused with a warning. Commands are checked for their name, collisions and prompt content instead. - Manifest validation for
webslots, such aschatDockandchatCards, is insrc/plugins/manifest.ts; the contributions reach the browser throughsrc/plugins/pluginWebUi.ts. - The rows of the seam catalog are in the "Complete seam catalog" section of
docs/PLUGIN_DEV.md.tests/contract/pluginSeamCatalog.test.tsfails when a newregister*method orwebslot has no catalog row.
Local development and verification
For a bundled plugin, install dependencies in the Elowen checkout and run the normal plugin build:
npm ci
npm run build:plugins-web
npm run build
The web build is needed when a plugin has browser sources. The main build
compiles the daemon, builds browser bundles, and copies bundled plugins into
the distribution tree. npm run build runs prebuild (language checks, then
it cleans dist/), build:ts, build:plugins-web, build:desktop-agent, and
build:assets (which copies plugins/ into dist/), and then postbuild,
which fails when a plugin folder has no manifest or an expected file is
missing (package.json, lines 68-74). A plugin developed outside the catalog builds and tests against a local core checkout:
depend on the host packages with file: dependencies (elowen, elowen-plugin-shared,
elowen-plugin-ui-kit) instead of the published versions, then run the host contract checks.
Host-lent SDKs
A registry plugin does not need to vendor an SDK the host already ships. After
every install and update, the marketplace installer symlinks the plugin's own
node_modules to the host's (linkHostModules in
src/plugins/marketplace.ts), so the plugin's bare imports resolve against
the host's copy at runtime. That is why grammy (Telegram), baileys and
qrcode (WhatsApp), botframework-connector (Teams), and puppeteer-core
and proxy-chain (Browser) have zero importers anywhere in this checkout: the
host carries the dependency so it can be lent, not because it is dead. They
are excluded from the dead-dependency sweep for the same reason, in
knip.json's ignoreDependencies; do not remove one from package.json
without first checking whether an installed registry plugin still imports it.
node-html-parser (Parts: the CoraHB e-shop and PartsLink24 HTML parsing) is
lent the same way, but the bundled Sandbox guest also imports it, so it is not in
ignoreDependencies. It must stay in dependencies, not devDependencies, or
a production install does not carry it and the plugin cannot import it.
Use focused checks first:
npx vitest run tests/plugins
npx vitest run tests/api/pluginUiRoutes.test.ts
npx vitest run tests/plugins/marketplace.test.ts
npx vitest run tests/contract/pluginApiSubpath.test.ts
npx vitest run tests/contract/pluginSharedPackage.test.ts
npx vitest run tests/contract/pluginBundleContracts.test.ts
npx vitest run tests/contract/pluginSeamCatalog.test.ts
npm run check
npm test
For a manifest change, verify that the manifest parses, every declared entry exists, every declared browser entry and stylesheet exists, and the runtime contribution report matches the intended registration. For a route or grant change, run the focused API and plugin-grant tests. For a hook change, run the hook integration tests. Inspect the plugin log after a restart when a plugin is skipped.
Before publishing, test the exact built tree, not only source files. Do not commit generated output unless the owning repository requires it.