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 common manifest vocabulary. Fields it leaves out are
described in the sections below or on the page for their feature, for example
service in Independent services, liveConfig and operationDock
in the seam catalog, and alerts in
Plugin settings and internationalization.
{
"name": "my-plugin",
"version": "1.0.0",
"apiVersion": "2",
"requiresCore": "0.30.4",
"requiresSharedApi": 14,
"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"],
"controls": ["my-domain"]
},
"consumesControls": ["shared-domain"],
"icons": {
"MyTool": "🔧"
},
"showOutput": ["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",
"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 14; the seam catalog names the subpaths and helpers it carries. 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 14 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: complete authenticated API patterns, or full absolute patterns for arootMount. Both allow lowercase literal segments and:nameparameters. Undeclared suffixes never reach a handler.wsRoutes: WebSocket paths.controls: control keys published for other plugins.readOnlyTools: exact plugin tool names that only read. This is the one read-only declaration: plan mode admits these tools, read-only delegated sub-agents (explore,plan,review,read_only: true) receive them, and a sub-agent continued from a plan-mode turn may run them. Declare a tool only when it changes nothing outside the agent's own working state; a conversation's own task list counts as working state. Each name must also appear inprovides.tools; an undeclared name is ignored with aplugin.tool_metadata_invalidwarning (setReadOnlyToolsinsrc/plugins/registry.ts). A declared tool the plugin leaves unregistered, for example while it is not configured yet, is skipped silently.mcp__*is the only pattern accepted. It must also appear inprovides.toolsand selects this plugin's tools that carryexternalregistration metadata; read-only sub-agents receive those, but plan mode never admits anexternaltool, because such a declaration vouches for a remote server's whole tool list, write tools included (planModeToolNamesinsrc/brain/toolMetadata.ts).desktop:truewhen the plugin registers a desktop provider throughregisterDesktopProvider(src/plugins/manifest.ts,src/plugins/api.ts). Registration also requires the desktop API paths inprovides.apiRoutesand thedesktop/vncWebSocket declaration.
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, src/plugins/loader.ts); it also needs
capabilities.reads: ['controls']. 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.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. A pattern that matches no name inprovides.toolsis ignored with aplugin.tool_metadata_invalidwarning; a declared tool the plugin leaves unregistered while it is not configured is skipped silently. See "Tool deferral and the core set" below.- 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:458-468). 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:366, src/api/routes/brainChat.ts:392).
tests/plugins/grantableContributions.test.ts pins the bundled grantable set to Terminal and loads it through the authoritative registry with an in-memory plugin database. Successful registration is proven by Terminal's exact four tools and control, together with the empty global prompt, slash-command and hook assertions; a skipped plugin or unknown grantable plugin cannot satisfy the registration proof. Services stop before the fixture database closes.
capabilities is deny-by-default:
{
"reads": ["db", "controls", "providers", "alerts"],
"mutates": ["prompt", "turnContext", "tools", "events", "workflow-dag", "users", "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, 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, 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,
providers, alerts, prompts, stores, git, project-files,
conversation-files, inference, push, usage, voice, 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.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"and an optional integerorder, the panel's place in the Account menu on the scale the core sections use (Account 100, Computers 260, Models 400, then 500, 600 and so on up to Terminal 1100; Sandbox's Desktops declares 250). Withoutordera panel takes 200, right after Profile, where plugin panels always sat. A non-integerorderfails manifest validation. Equal orders draw core sections first, then plugin panels by plugin name and then panel id, so the menu is the same on every load. Cores before 0.29.118 ignore the field. Project panels acceptplacement: "tab" | "overview" | "overviewTop"(defaulttab): atabis a text tab in the Project drawer's header,overviewrenders inside its Overview tab after the host's facts, andoverviewTopat the top of Overview. The host draws eachuserpanel as its own header tab of the User detail drawer, labelled with the panel's label (tabs carry no icons). The panel component is not mounted until its tab is chosen. A panel that wants a status mark registers auserStatuscomponent under the same id (UI API 46, see the catalog row forweb.user); the host draws it as the first line of the tab, right-aligned beside the panel's label. It is mounted with the tab, so it must fetch only what it shows. Without it the tab starts directly with the panel.projectRowsdeclares a project-register indicator contribution.settingslists sections withid,label, optionalicon, optionalplacement: "page" | "pluginDetail".stringsis a flat map of English view strings.projectEditIcon,dashboardMetrics,chatPickers,chatCards,chatRailSections,chatDockandhistoryBranchesare boolean flags. Each one tells the host to load the browser registration for that surface, and the seam catalog describes what each surface does.operationDocktakes{ "desktopOnly": true }and declares that the plugin contributes items to the shared operation dock;desktopOnly: truekeeps them off a phone. A plugin without the declaration gets no dock request.