Controls and plugin dependencies
A plugin resolving ctx.control(key) must declare reads: ['controls'] and include that key in consumesControls. An absent or missing key is refused with a local diagnostic and returns undefined. Privileged keys independently enforce the host's authority allowlist; declaration alone never grants credentials, process launch or artifact access. Resolve the control on each use. A disabled, missing or incomplete owner returns undefined. Sandbox artifacts remain core-only. The Files plugin declares sandbox and skillResources as a real consumer.
Controls and plugin dependencies
A control is a live domain interface, not a plugin-name lookup. Publish a key
with ctx.registerControl(name, control) and declare it in
provides.controls. Consume a sibling with ctx.control(name) and declare
reads: ["controls"]. Resolution happens at call time and returns
undefined when the provider is disabled or unavailable. Treat that as a
real state and fail honestly. Never cache the returned object.
Owning code: src/plugins/registry.ts (control, registerControl) and
src/plugins/api.ts (PluginContext). A consumer also needs its key in
consumesControls when the manifest has that field. Without it, or without
reads: ["controls"], ctx.control() returns undefined and logs a warning
(src/plugins/registry.ts, control). A provider whose registration lacks a
method that core calls for that key counts as absent (KNOWN_CONTROL_METHODS in
src/plugins/registry.ts).
The bundled Files plugin declares skillResources in consumesControls for managed Read support files.
Core provides that authority from the live, grant-, owner- and override-filtered skill catalog. Only Files
may consume it, and only canonical directory-form skill roots authorize a read; flat skills do not grant
access to their shared loader folder. A hidden skill or missing provider grants no host resource access.
Real consumer: plugins/files/index.mjs calls ctx.control('skillResources'); the
provider is registered in src/daemon/brainCore.ts.
requiresControls names control contracts needed before enablement. The host
uses the manifest declarations to report missing providers without coupling one
plugin to another plugin name. Declare a key here when the plugin cannot do its
job without its provider; a conditional feature, such as managed-project access
in a plugin that also serves host projects, stays in consumesControls only.
Core-owned keys, such as skillCatalog or machineRuntimeGateway, are not plugin
dependencies. A registration may also set
registerControl(name, control, { requires }) for a live control dependency.
Enablement enforces the declaration. When no enabled provider publishes a
required key, the enable request answers 409 with error: "missing plugin dependency" and controls: [{ key, providedBy }] (src/api/routes/plugins/index.ts,
enablePlugin; missingControls in src/api/routes/plugins/marketplace.ts).
An unknown key has no provider and therefore blocks enablement.
The licence server derives optional catalog requiresControls: string[],
provides.controls: string[] and label: string from the manifest archived for
that exact version, alongside its compatibility requirements. Never hand-write
these dependency fields into the registry. Omitted fields make no claim; older
core catalog parsers tolerate unknown fields. Core reads these fields in
src/plugins/marketplace.ts. The derivation runs on the licence server, which is
not part of this repository.
Admin GET /plugins and GET /plugins/marketplace add dependencies: one row per
resolved provider plugin, shaped as { name, label?, i18n?, enabled }, where
i18n carries installed manifest locale labels. Installed manifests override
catalog declarations for the same provider. Catalog-only, disabled and
soft-removed providers have enabled: false. Multiple controls provided by the
same plugin yield one row; self-provided controls yield no pill. Unknown keys
still refuse enablement, but produce no invented plugin identity. Unreadable
installed manifests have no dependency claims to display.
The same resolveControlDependencies helper matches control keys for the enable
gate and both listings. A key is satisfied when any installed enabled provider
publishes it; enabling a candidate also counts its own controls. The gate keeps
its existing 409 controls: [{ key, providedBy }] response. Listing resolves
against the licence-filtered catalog and installed manifests, with one catalog
refresh/cache read and one API-side plugin scan per request and no per-dependency
network work. An unavailable catalog retains known installed providers. The helper
lives in src/plugins/controlDependencies.ts; the listing code is in
src/api/routes/plugins/index.ts and src/api/routes/plugins/marketplace.ts.
Settings/Plugins shows the resulting providers beside each card's version,
including Available and soft-removed bundled cards. OneDrive's microsoftIdentity
requirement resolves to Microsoft Teams. Outline Badge tones are muted for
enabled providers and warning for unavailable ones; shared HelpTip explains
the required display names on hover, focus or tap. No requirements or no known
provider means no pill. The pills render in DependencyPills in
web/modules/settings/PluginsSection.tsx. Both OneDrive (requiresControls) and
Microsoft Teams (provides.controls) are plugins in the plugin registry, not in
this repository.
The microsoftIdentity control gives OneDrive a fresh drive-scoped Graph client through
driveGraphFor(accountId). An absent binding or typed missing sign-in returns null without a warning;
verification and transport failures reject. Consumers keep their current link state on an outage rather
than requesting another sign-in. No provider means no client or request; the existing delegated-session
verification owns tokens and network cost, and the consumer owns failure reporting.
The bundled elowen-docs plugin publishes docs: search(query, limit) returns the best sections of
the shipped manual and plugin docs as { path, title, heading, text } (heading is the place within the
page, without repeating its title), ranked exactly like DocsSearch
(semantic with an embedding model, keyword overlap without). Core reads it in POST /search/ask, where the
command palette's Ask AI grounds a short answer in the top eight sections; the lookup gets five seconds of
the ask's fifteen. With the plugin disabled, failing or slow, the ask still picks pages and simply shows
no answer text. A plugin may read it too by declaring it in consumesControls. It costs one query
embedding per call once the index exists; the first call after an upgrade builds the index.
Code: plugins/elowen-docs/index.mjs (registerControl('docs')); consumer
lookupDocs in src/api/routes/search.ts; the constants SEARCH_ASK_PASSAGES (8),
SEARCH_ASK_DOCS_TIMEOUT_MS (5000) and SEARCH_ASK_TIMEOUT_MS (15000) are in
src/search/siteSearchAsk.ts.
Controls currently used by the built-in plugin set include delegation, terminal, cron, workflow, MCP, LSP, code mode, sandbox, identity, GitHub, site gateway, browser capture, the skill catalog, and documentation search. Use domain keys, not current plugin names.
The table maps those domains to their keys and to where each provider is registered. Some providers are plugin-registry plugins, not bundled in core, and are marked as such.
| Domain | Key | Provider | Registered in |
|---|---|---|---|
| Delegation | subagent, workflow | Bundled subagent plugin | plugins/subagent/index.mjs, plugins/subagent/lib/workflow.mjs |
| Terminal | terminal | Bundled terminal plugin | plugins/terminal/index.mjs |
| Code mode | codeMode | Bundled code-mode plugin | plugins/code-mode/src/index.ts |
| Sandbox | sandbox, sandboxArtifacts | Bundled sandbox plugin | plugins/sandbox/index.mjs |
| Documentation search | docs | Bundled elowen-docs plugin | plugins/elowen-docs/index.mjs |
| MCP | mcp | Bundled MCP plugin | plugins/mcp/index.mjs |
| Cron | cron | Plugin registry (cronjob) | plugins/cronjob/lib/filing.mjs in the registry |
| Browser capture | browserCapture | Plugin registry (browser) | plugins/browser/src/index.ts in the registry |
| GitHub | github | Plugin registry (github) | plugins/github/src/index.ts in the registry |
| Identity | microsoftIdentity | Plugin registry (Microsoft Teams) | Microsoft Teams plugin in the registry |
| Site gateway | publishedSitesGateway | Core host, when configured | src/daemon/brainCore.ts |
| Skill catalog | skillCatalog, skillManagement, skillResources | Core host | src/daemon/brainCore.ts |
| Machine runtime | machineRuntimeGateway | Core host | src/daemon/brainCore.ts |
Core host keys are not plugin dependencies, so they never produce a pill.