NAVIGATION
ELOWEN DOCUMENTATION

Last updated: 10 October 2026

Browse documentation · Manifest identity and contributions
Developer reference

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

  • name is 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.
  • version is the plugin release version. It is also part of loader cache busting and marketplace update detection.
  • apiVersion is the breaking plugin API contract. The current value is "2" and must match exactly.
  • requiresCore is 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 a needs-newer-core skip report and advances on a later run once the core is new enough.
  • requiresSharedApi is an optional positive integer for the elowen-plugin-shared contract. 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 exposes elowen-plugin-shared/transport for markTransportFailure and isTransportFailure, plus elowen-plugin-shared/configNumber for clampConfig(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 use ctx.host.voice() with an explicit incoming-message SessionSource; core owns transcription, speech preparation, credentials and billing. The host checks the linked account, voice grant, enabled Voice mode and spend limit. Declare capabilities.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.
  • label is the human-facing plugin name. If it is absent, surfaces use name.
  • description is the plugin description shown in management surfaces.
  • entry is the relative path to the ESM module exporting register(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 a rootMount.
  • 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 in provides.tools and 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 in provides.tools and selects this plugin's tools that carry external registration metadata. This is distinct from planSafe.
  • desktop: true when the plugin registers a desktop provider through registerDesktopProvider (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

  • icons maps tool names or patterns to display icons.
  • showOutput lists exact names or prefix* patterns whose successful output is shown in the transcript. Failures remain visible even when a tool is omitted.
  • planSafe lists exact read-only tool names allowed in plan mode. Patterns are not accepted. Each name must also appear in provides.tools; otherwise it is ignored with a warning (src/plugins/registry.ts:1196-1210). An undeclared tool is treated as mutating.
  • deferLoading lists exact names or prefix* 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.
  • planMode registration metadata is checked for conflicts with manifest planSafe; it does not widen host-owned plan-mode admission. Core defaults and validated manifest planSafe declarations remain authoritative. There is no registration option that keeps a tool out of deferral: only the locked core set does.
  • external registration 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.
  • icon is an optional SVG path relative to the plugin directory. When omitted, the host looks for icon.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:

  • entry is the built browser ESM path.
  • css is an optional plugin-owned stylesheet.
  • requiresApiVersion is the browser runtime version required by the bundle.
  • adminOnly hides navigation and browser assets from non-admins.
  • label names the plugin's navigation world.
  • navKind is domain or infrastructure.
  • layout is document or workbench.
  • wordmark set to false closes 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.
  • presentation is page or overlay. overlay keeps 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 receives surface="deck" because the host owns the outer title and frame. A plugin that depends on this presentation must set requiresCore to the first core release that provides it.
  • nav lists main-navigation entries with label, optional icon, and optional route.
  • account, user, and project list panel metadata with id, label, and optional icon. Account panels also accept placement: "section" | "linkedAccount". The host draws each user panel 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 a userStatus component under the same id (UI API 46, see the catalog row for web.user); it is mounted while the block is closed, so it must fetch only what it shows. Without it nothing changes in the plugin.
  • projectRows declares a project-register indicator contribution.
  • settings lists sections with id, label, optional icon, optional placement: "page" | "pluginDetail".
  • strings is a flat map of English view strings.