Manifest identity and contributions
Manifest
The manifest file is named elowen-plugin.json. The required fields are
name, version, apiVersion, description, and entry.
This example shows the complete manifest vocabulary:
{
"name": "my-plugin",
"version": "1.0.0",
"apiVersion": "2",
"requiresCore": "0.29.26",
"requiresSharedApi": 8,
"label": "My plugin",
"description": "Adds a small example tool.",
"entry": "index.mjs",
"provides": {
"tools": ["MyTool"],
"readOnlyTools": ["MyTool"],
"skills": ["my-skill"],
"platforms": ["my-platform"],
"destinations": ["my-platform"],
"httpRoutes": ["callback"],
"apiRoutes": ["status"],
"wsRoutes": ["stream"],
"mcpTools": ["my_tool"],
"controls": ["my-domain"]
},
"consumesControls": ["shared-domain"],
"icons": {
"MyTool": "🔧"
},
"showOutput": ["MyTool"],
"planSafe": ["MyTool"],
"deferLoading": ["LargeTool", "Mcp*"],
"icon": "icon.svg",
"configSchema": [],
"retiredConfigKeys": ["obsoleteSetting"],
"userConfigSchema": [],
"userConfigLabel": "My account",
"userConfigPlacement": "account",
"userGrantable": false,
"capabilities": {
"reads": ["db"],
"mutates": ["events"],
"network": true
},
"requiresControls": ["shared-domain"],
"web": {
"entry": "web/index.js",
"css": "web/index.css",
"requiresApiVersion": 17,
"adminOnly": false,
"label": "My plugin",
"navKind": "domain",
"layout": "document",
"nav": [
{ "label": "My plugin", "icon": "Puzzle", "route": "" }
],
"account": [
{ "id": "connection", "label": "Connection", "icon": "Link" }
],
"user": [
{ "id": "details", "label": "Details", "icon": "User" }
],
"project": [
{ "id": "status", "label": "Status", "icon": "Folder" }
],
"projectRows": true,
"dashboardMetrics": true,
"chatPickers": true,
"chatCards": true,
"chatRailSections": true,
"chatDock": true,
"historyBranches": true,
"settings": [
{
"id": "settings",
"label": "Settings",
"icon": "Settings",
"placement": "page"
}
],
"strings": {
"title": "My plugin"
}
}
}
Identity and compatibility fields
nameis the technical id. It must equal the directory name and keys configuration, controls, API mounts, data directories, and stored grants. Renaming it is a migration.versionis the plugin release version. It is also part of loader cache busting and marketplace update detection.apiVersionis the breaking plugin API contract. The current value is"2"and must match exactly.requiresCoreis an optional minimum Elowen version. The curated registry installer rejects a plugin that needs a newer core before copying it into the instance plugin directory. It is a minimum version, not an exact match. The check judges the core the plugin will run on: the daemon's own version for a manual install, the transaction's target (the release being installed, else the running version) for an automatic update. A planned automatic update whose requirement exceeds that target is deferred with aneeds-newer-coreskip report and advances on a later run once the core is new enough.requiresSharedApiis an optional positive integer for theelowen-plugin-sharedcontract. It must match the host exactly. The host checks it before importing the entry, because an incompatible shared package can fail during module linking. Omit it when the plugin does not import that package. The current contract is API 8 and exposeselowen-plugin-shared/transportformarkTransportFailureandisTransportFailure, pluselowen-plugin-shared/configNumberforclampConfig(value, defaultValue, min, max). The latter uses the default for unset, invalid, or zero values and clamps the result to the call site's bounds. API 8 removes plugin-owned speech helpers. Platform adapters usectx.host.voice()with an explicit incoming-messageSessionSource; core owns transcription, speech preparation, credentials and billing. The host checks the linked account, voice grant, enabled Voice mode and spend limit. Declarecapabilities.reads: ['voice']; see the host voice row in the seam catalog for its refusal contract. Answer text is preserved and reasoning uses its separate API channel.applyVisionModel(access, vision)accepts only those two arguments. Consumers declare API 8 and require the core artifact shipping it. The exact-match manifest gate refuses older consumers before entry import; update the core and registry consumers together.labelis the human-facing plugin name. If it is absent, surfaces usename.descriptionis the plugin description shown in management surfaces.entryis the relative path to the ESM module exportingregister(ctx). It must stay inside the plugin directory.
Declared contributions
The optional provides object contains these arrays:
tools: tool names the plugin registers.skills: skill names the plugin contributes.platforms: platform adapter names.destinations: proactive notification destination providers.httpRoutes: public webhook paths.apiRoutes: authenticated API paths, or full absolute paths for arootMount.wsRoutes: WebSocket paths.mcpTools: tools contributed to Elowen's own MCP server.controls: control keys published for other plugins.readOnlyTools: exact plugin tool names eligible for read-only typed-agent composition. Each name must also appear inprovides.toolsand be registered by this plugin; otherwise the entry is ignored with a warning (src/plugins/registry.ts:1215-1237).mcp__*is the only pattern accepted. It must also appear inprovides.toolsand selects this plugin's tools that carryexternalregistration metadata. This is distinct fromplanSafe.desktop:truewhen the plugin registers a desktop provider throughregisterDesktopProvider(src/plugins/manifest.ts:141,src/plugins/api.ts:2551).
consumesControls is not a provides key. It is a top-level manifest field
listing the exact control keys this plugin is authorized to resolve through
ctx.control (src/plugins/manifest.ts:145, src/plugins/loader.ts:416).
This is distinct from requiresControls, which is an enablement dependency.
The runtime registration methods for platforms, destinations, public routes,
authenticated routes, WebSockets, and MCP tools are deny-by-default. Their
names or paths must be declared in the matching provides list. Controls
must be declared as well. There is no provides.hooks field. Hooks are
registered at runtime and their mutations are controlled by
capabilities.mutates.
Tool display metadata
iconsmaps tool names or patterns to display icons.showOutputlists exact names orprefix*patterns whose successful output is shown in the transcript. Failures remain visible even when a tool is omitted.planSafelists exact read-only tool names allowed in plan mode. Patterns are not accepted. Each name must also appear inprovides.tools; otherwise it is ignored with a warning (src/plugins/registry.ts:1196-1210). An undeclared tool is treated as mutating.deferLoadinglists exact names orprefix*patterns deferred by default, even when the session holds too few tools for the automatic threshold. A pattern expands only to tools registered by the same plugin. See "Tool deferral and the core set" below.planModeregistration metadata is checked for conflicts with manifestplanSafe; it does not widen host-owned plan-mode admission. Core defaults and validated manifestplanSafedeclarations remain authoritative. There is no registration option that keeps a tool out of deferral: only the locked core set does.externalregistration metadata identifies externally authored tool schemas, independently of tool names. It controls schema bounds, status-note schema augmentation and redacted cache diagnostic labels, not execution grants.iconis an optional SVG path relative to the plugin directory. When omitted, the host looks foricon.svg.
Tool names become durable permission and event data. Choose stable names before publishing. Renaming a tool requires a coordinated migration of stored rules and references.
Grants and capabilities
Set userGrantable: true only when the plugin must be granted separately to
accounts. It is deny-by-default for every account, including administrators,
until an administrator grants it in Users. Grants filter the plugin's tools,
skills, routes, and browser UI. An administrator can edit their own grant;
with no grant the plugin remains unavailable, even to an administrator.
Grants do not filter prompt fragments, platform prompts, prompt commands, or
hooks. Those reach every session, and the loader warns when a grantable plugin
registers any of them (src/plugins/loader.ts:456-466). A grantable plugin must
not put account-sensitive behavior in those surfaces. Plugin slash commands are
different: the command listing and the picker route check the grant
(src/api/routes/brainChat.ts:369, src/api/routes/brainChat.ts:391-392).
capabilities is deny-by-default:
{
"reads": ["db", "controls", "providers", "embeddings", "alerts"],
"mutates": ["prompt", "turnContext", "tools", "events", "workflow-dag", "users", "runBoundary", "conversations"],
"network": true
}
src/plugins/capabilities.ts owns PLUGIN_MUTATIONS and the TypeBox
PluginCapabilitiesSchema; PluginCapabilities is inferred from that schema.
The manifest validator uses it directly. The public plugin API imports the same
type and retains its separately ordered CONSENT_REQUIRED_MUTATES subset: a
vocabulary declaration does not itself grant operator consent. Absent capabilities
still permit no mutation; reads remain extensible strings. Validation scans only
the declared arrays and adds no policy lookup or I/O.
mutates accepts prompt, turnContext, tools, events,
workflow-dag, users, runBoundary, and conversations. The retired memory value is no longer accepted:
it never granted anything, no bundled, registry, installed or archived manifest declares it, and
a manifest that still names it fails validation like any other unknown value.
A plugin declaring prompt, tool, event, workflow-DAG, user, run-boundary, or conversation mutation requires explicit acknowledgement from the operator. That acknowledgement is STORED per plugin rather than attached to the
request that gave it, so a version whose claims are all inside the stored set is
never asked about again. The doors that judge a candidate before it takes a
place on disk are the install, enable and marketplace-update routes, which
answer 409 grants require consent with the whole consent-required set the
plugin now declares — including grants the operator approved for an earlier
version. Install and update pin that gated artifact in the request queue. The
coordinator rechecks stored consent when staging, including boot-requested repair
of an enabled receipted plugin that is missing or unloaded. That request has no
caller to ask, so a refusal is reported as needs-consent, not installed. A plugin that grows one
of these capabilities in a later version therefore needs the operator to agree
again before the new version can land, and no refusal clears the enabled flag.
A manifest already on disk, loaded as usual at startup, is not re-judged.
turnContext is the one mutation exempt from this acknowledgement because it
reaches only one turn.
reads gates host reads such as db, controls, embeddings,
providers, alerts, prompts, stores, git, project-files,
conversation-files, inference, push, elowen-cli, and
agent-files (Subagent only). Declare only what the implementation uses.
network: true declares network intent and
enables the validated public HTTP transport. It is not permission to use an
arbitrary client or to trust remote data.
Browser metadata
At plugin load, src/plugins/pluginWebUi.ts prepares bundle and stylesheet paths,
content hashes and browser metadata without mutating the live registry. The loader
publishes staged tools, hooks, alert strings and browser metadata only after
registration and asset preparation succeed. An escaping path or a read failure on
an existing asset skips the plugin and discards its staged contributions; siblings
still load. This boundary covers registry publication, not side effects performed
by the plugin's registration code.
For example, MCP declares web.entry: "web/index.js" and web.css: "web/index.css".
Preparation reads each present asset to initialize its independent content hash
and byte snapshot. PluginWebUi.readBundle() and optional readCss() are the single
source for both the URLs advertised by GET /plugins/ui and the bytes served by
GET /plugins/:name/web/:file. Each call stats the file; only a changed mtime or
size causes a new read and SHA-256 hash. The cache retains one byte snapshot per
asset for the lifetime of its registry entry. A stale hash returns 404, and current
URLs cache immutably for a year. Rebuild plugin JS or CSS and reload the page to
fetch the new listing and content-hash URLs without restarting the daemon.
No web declaration means no asset reads or browser metadata.
A missing bundle logs a warning and omits the UI while retaining server
contributions and the declared admin-only policy. A missing stylesheet logs a
warning and retains the UI without plugin CSS. Neither absence rejects the plugin.
The optional web object is declarative metadata plus the bundle entry:
entryis the built browser ESM path.cssis an optional plugin-owned stylesheet.requiresApiVersionis the browser runtime version required by the bundle.adminOnlyhides navigation and browser assets from non-admins.labelnames the plugin's navigation world.navKindisdomainorinfrastructure.layoutisdocumentorworkbench.wordmarkset tofalsecloses the plugin's/p/<plugin>pages without the instance wordmark the host mounts under every other page (except/chat). Use it for a full-height application surface that owns its bottom edge, such as the editor with its panes and status bar. Absent, and on a host too old to know the key, the word closes the page as usual. It rides on the plugin UI listing, so the word never appears first and then vanishes.presentationispageoroverlay.overlaykeeps the plugin's normal primary-navigation entries and/p/<plugin>address, but presents the page in the host's shared reading modal, with the same fullscreen phone mode and history-aware close behavior as Settings and Account. The plugin receivessurface="deck"because the host owns the outer title and frame. A plugin that depends on this presentation must setrequiresCoreto the first core release that provides it.navlists main-navigation entries withlabel, optionalicon, and optionalroute.account,user, andprojectlist panel metadata withid,label, and optionalicon. Account panels also acceptplacement: "section" | "linkedAccount". The host draws eachuserpanel as a collapsible block in the user detail drawer: a header button with the panel's icon and label, closed by default, with the open state remembered in the browser per panel (users.plugin.<plugin>.<id>, shared by every user shown). The panel component is not mounted while its block is closed. A panel that wants a status visible in the closed header registers auserStatuscomponent under the same id (UI API 46, see the catalog row forweb.user); it is mounted while the block is closed, so it must fetch only what it shows. Without it nothing changes in the plugin.projectRowsdeclares a project-register indicator contribution.settingslists sections withid,label, optionalicon, optionalplacement: "page" | "pluginDetail".stringsis a flat map of English view strings.