Host API

Every plugin calls the same methods. In JavaScript they are api.host.<group>.<method>(params) (camelCase); on the wire, which a plugin's runtime speaks for it, they are host/<group>.<method> (snake_case). All params and results are JSON objects. Ids are strings.

api.host.call(method, params, { signal }) and api.host.kernel(method, params, { signal }) accept an AbortSignal. An abort rejects with its reason and sends host/cancel for the outstanding request; a signal already aborted sends no request. Completed calls remove the listener. Kernel method names omit the kernel/ prefix.

Agent approvals have no general host method. A plugin may answer them only through the manifest's tool_approval hook with the dangerous approvals.auto grant (see Hooks); the kernel card handles user answers.

The files, network, programs and environment a plugin reaches go through the broker methods at the end of the table. JavaScript calls them through api.fs, api.net, api.process and api.env (ui-plugins.md), which also wrap their streams; the table is the wire.

Each plugin has a data folder of its own, <data dir>/plugin-data/<id> (the id with / as __), which the data scope of its fs.read and fs.write grants names. The host makes it before the plugin loads and gives a JavaScript plugin its absolute path (api.paths.data). The first time an official JavaScript plugin loads in the place of the native provider of the same folder name (before the providers' port to JavaScript), the host moves what that provider kept in <data dir>/agents/<folder>/ into the data folder as legacy/, once; the plugin reads it from there.

Permissions

The host checks every call before it runs it. The plugin must be enabled, and it must hold a grant for the permission in the last column (see ../AGENTS.md, Permissions). A method with no permission needs only an enabled plugin. A call without the grant fails, and nothing happens:

  • in JavaScript, the Promise rejects with an Error whose name is PermissionNotGranted (or NeedsReview, see plugins.set_enabled), with permission and scope set to what was missing, and a message such as PermissionNotGranted: tabs has no grant for fs.read /etc/hosts;
  • on the wire, the answer is an error with code -32001, that message, and data: { code, message, permission?, scope? }, which the runtime turns into that Error.

Any other failure has the name Error. A method that does not exist fails with unknown host method <method>.

fs.* methods check the path itself: a path relative to a workspace resolves inside that workspace root (it cannot leave it with .., an absolute path or a symlink), and the grant must cover the real file after symlinks are resolved. The broker then opens the file inside the folder the grant names, so a symlink swapped in after the check cannot lead it out either.

Method Params Result Permission
workspaces.list {} { workspaces: Workspace[], colors: PresetColor[] } (icon: the project's icon file when the folder has a favicon, icon or logo; found once per minute per folder. color: the folder's preset colour, given in turn to each folder without an icon (all twelve before one repeats); its mark is a folder-solid in that colour when there is no icon or mark, or with useColor) chats.read
workspaces.add { path } Workspace chats.control
workspaces.remove { workspaceId } {} chats.control
workspaces.set_color { workspaceId, color } {}: the mark becomes a folder in color (one of colors), or with color: null the project icon again chats.control
chats.list { workspaceId? } { chats: Chat[] } chats.read
chats.create { workspaceId, agentId, title?, taskId?, inUse? } Chat (reuses an empty draft, never one named in inUse: drafts the caller holds unsent text for or has queued) chats.control
chats.transcript { chatId } { items: TranscriptItem[], running, pendingApprovals, pendingQuestions, plan, usage } chats.read
chats.send { chatId, text } or { chatId, blocks: ContentBlock[] } {} (progress arrives as events) chats.control
chats.cancel { chatId } {} chats.control
chats.cancel_task { chatId, taskId } {} — stops one subagent (capabilities.cancelTask) chats.control
chats.respond_question { chatId, questionId, answer: { values, cancelled } } {} chats.control
chats.update { chatId, title?, archived?, completed?, agentId?, workspaceId? } Chat (agentId and workspaceId only before the first prompt; completed checks the chat off or opens it again) chats.control
chats.delete { chatId } {} chats.control
chats.set_option { chatId, optionId, value } { options: ConfigOption[] } (the choice is also remembered for the chats that have not started) chats.control
chats.options { chatId } { options: ConfigOption[] } (current values: a chat that has not started shows the last choices made in any chat of its agent, a started chat keeps the values it started with) chats.read
chats.commands { chatId } { commands: SlashCommand[] } (the agent's) chats.read
chats.skills { chatId } { skills } (the agent's) chats.read
chats.checkpoints { chatId } { checkpoints: [{ itemId, createdAt }] } (the prompts the workspace can be put back to) chats.read
chats.checkpoint_coverage { chatId, itemId } { degraded, expired, unknownPaths } (what restoring that checkpoint could not undo) chats.read
chats.restore_checkpoint { chatId, itemId } {} (the files and the transcript go back to before that prompt) chats.control
chats.edit_message { chatId, itemId, text } or { chatId, itemId, blocks } {} (restores to before that prompt and sends the new one) chats.control
chats.compact { chatId } {} (asks the agent to summarize the conversation) chats.control
chats.fork { chatId, title? } Chat (a new chat with the same history) chats.control
chats.turn_changes { chatId } { itemId, workspaceId, paths } or null: the files the chat's last finished turn changed, relative to the workspace (itemId is that turn's prompt); null when no finished turn had a checkpoint before it chats.read
chats.import { workspaceId, agentId } { imported } chats.control
chats.load_history { chatId } { items } (how many the transcript now holds; 0 when nothing was replaced). Reads the agent's own record of the session and replaces the stored transcript when the two differ, keeping the chat's revert points on their prompts. Skipped while a run is active, and for an agent without sessionHistory. The chat plugin calls this when a chat is opened, so a chat carried on in another client of the same agent catches up, and an imported session fills for the first time. chats.control
inputs.list { chatId } InputSnapshot: durable inputs, attachment generation, CAS revision, running/settling state, last settled run and opaque policyState chats.read
inputs.enqueue { chatId, id, blocks, intent: send | steer | queue, origin?, append? } InputSnapshot; persists structured bytes without sending; id is an idempotency key chats.control
inputs.update { chatId, revision, inputId?, blocks?, intent?, remove?, pauseReason?: string | null, policyState? } InputSnapshot; CAS checks the snapshot revision. Content edits/removal require held or positively rejected state chats.control
inputs.dispatch { chatId, inputId, revision, policy?, scope? } InputSnapshot; claims an attempt before provider I/O. Omit policy for an explicit manual action; automatic dispatch must use the selected scheduler token chats.control
inputs.policy {} { token: string | null }; authority of the live convergence/input-scheduler service registration chats.read
agents.list {} { agents: AgentInfo[] } (enabled only)
agents.all {} { agents: (AgentInfo & { enabled })[] }
agents.options { agentId, workspaceId } { options: ConfigOption[] }
agents.refresh_usage { agentId } { limits: UsageLimits | null }; fresh native read, without UI cache throttling. Null does not mean allowed chats.control
agents.set_enabled { agentId, enabled } {} agents.manage
agents.authenticate { agentId, method } {} agents.manage
agents.logout { agentId } {} agents.manage
agents.update { agentId } {} (upgrades the agent's program with the installer that owns it) agents.manage
agents.changed { agentIds? } (a request, or a notification) {}: the plugin registered or removed agents after it loaded; the host lists its agents again (agents/list), serves the new ones and stops the ones it no longer lists (agent-protocol.md). A JavaScript plugin's runtime sends it for api.agents.register and api.agents.unregister after the load. agentIds names agents of the plugin whose initialize answer is out of date: the host forgets it and asks the enabled ones again agents.provide
fs.read { workspaceId?, path, encoding?: utf8 | base64 } { text }, or { data } (base64) fs.read covering the file
fs.write { workspaceId?, path, text } or { workspaceId?, path, encoding: "base64", data } {} fs.write covering the file
fs.search { workspaceId, query?, limit? } { paths } fs.read covering the workspace
fs.list { workspaceId?, path } { entries: [{ name, kind: file | dir | symlink | other, size, modified }] } (modified in milliseconds since 1970) fs.read covering the folder
fs.stat { workspaceId?, path } { kind, size, modified } fs.read covering it
fs.mkdir { workspaceId?, path, recursive? } {} fs.write covering it
fs.remove { workspaceId?, path, recursive? } {} fs.write covering it
fs.rename { workspaceId?, from, to } {} fs.write covering both
fs.watch { workspaceId?, path } { watchId }; changes arrive as the notification fs/changed { watchId, paths } fs.read covering it
fs.unwatch { watchId } {} the plugin that watches
net.fetch { url, method, headers (lower-case names), body? (base64), bodyStream? } { status, statusText?, headers, url?, stream }: the body arrives on stream. A bodyStream id is sent up with stream.write and stream.close while the call is out net covering the host (checked again at each redirect, at most 5)
net.websocket { url, protocols } { stream, protocol? }: messages arrive on stream, stream.write sends net covering the host
process.spawn { program, args, cwd?, env? } { pid, stdin, stdout, stderr } (stream ids); the end arrives as process/exit { pid, code, signal }. stdout and stderr carry everything the child wrote, also what it wrote just before it exited process naming the program, or process.any for a path or an interpreter
process.kill { pid, signal? } {} the plugin that started it
process.which { program } { path, realPath }: where spawn finds the program, and that file with its links followed; both null when it is not there as process.spawn for that program
env.get { name } { value } (from the login environment; null when unset) env naming the variable
stream.write { stream, data } (base64) or { stream, text } (a text message of a socket) {} once the host took it. The writes and the close of one stream run one at a time, in the order they came, whether or not the plugin waits for each answer; different streams run side by side the stream's plugin
stream.close { stream } {}; also a notification, to stop reading the stream's plugin
stream.ack { stream, bytes } (notification) the plugin took that much; the host sends at most 1 MiB ahead of it the stream's plugin
slash.run { name, input, context } { action: prompt | message | none, text? }: runs another plugin's slash command in that plugin chats.control
git.status { workspaceId } { branch, upstream, ahead, behind, isRepo, entries: [{ path, origPath?, staged, unstaged }] } (paths relative to the repository root; staged/unstaged are the porcelain letters, ? in staged for an untracked file; untracked folders are listed file by file) git.read
git.diff { workspaceId, path?, origPath?, staged? } { diff } (an untracked file comes back as an addition; origPath shows a staged rename as one) git.read
git.watch { workspaceId } {}: watches that workspace's repository and stops watching the one before; changes arrive as git_changed git.read
git.run { workspaceId, args } { stdout, stderr, code } (runs at the repository root, where status paths point) git.write
storage.get { key } { value } (private to the plugin)
storage.set { key, value } {}
tools.list {} { mode: code|direct, tools: [{ plugin, name, exposedAs, description, enabled }], rules: [{ plugin, file, text, enabled }] } settings.read
tools.call { agentId, sessionId?, workspace?, name, input, callId? } MCP CallToolResult (providers pass on their agent's tool calls; see tools.md) agents.provide
mcp.serve { agentId, sessionId?, workspace?, tools, url? } { url }: the host serves tools as a streamable HTTP MCP server on the loopback address, each tools/call run like tools.call, for agents that take extra tools only as MCP servers; with url (one this plugin was given) the same address serves the new scope; it stops when the plugin unloads (agent-protocol.md) agents.provide
mcp.close { url } {}: stops serving an mcp.serve address of this plugin agents.provide
services.provide { name, version, access, methods: { m: { params, result, errors } }, events: { e: schema } } (JSON Schema 2020-12) {}: registers a service the manifest declares in provides. access is "any", "official" or a list of plugin ids. The schemas are the contract: the host validates every call's params and result and every event's payload against them. One plugin per name: the user's choice (the serviceProviders setting), an official plugin, the first by name; another provider is refused with ProvidedElsewhere the manifest's provides
services.call { name, method, params } the method's result, or a service error (code -32010, data._tag: ServiceUnavailable, NotAllowed, InvalidParams (with issues), ContractViolation, Interrupted, or the provider's own declared error). Cancellable like any request (host/cancel), which cancels it in the provider the manifest's inject/optional, and the provider's access list
services.emit { name, event, payload } (notification) the provider's event, to the plugins that declared the service and subscribed with services.subscribe the provider
services.subscribe { name, event } (notification) hear that event as services/event { name, event, payload } the manifest's inject/optional
services.unsubscribe { name, event } (notification) stop hearing it
subagent.start { scope, title, name?, agent?, model?, effort?, prompt? } { taskId }: a subagent of the chat, nested under the tool call scope came with (tools.md, Subagents) a scope of this plugin's running tool call
subagent.update { scope, taskId, title?, activity?, summary?, usage?, toolUses?, model?, effort?, status? } {}; a final status (completed, failed, cancelled) ends it. The runtime sends it as a notification, and the end as a request as subagent.start
subagent.event { scope, taskId, event } {}: text_delta, reasoning_delta, tool_call_started, tool_call_updated, usage or notice in the subagent's own transcript; a notification, so events keep their order as subagent.start
subagents.types {} { types: [{ plugin, id, title, description, inputSchema }] }: every subagent type of a plugin with models.use
subagents.run { type, input, scope? } { result }: runs the type (id or plugin/id) in its plugin (subagent/run), under the caller's tool call when scope names it models.use
models.list { workspaceId? } { agents: [{ id, name, icon?, model, effort, models: [{ value, name, group?, efforts: [{ value, name }] }], error? }] }: the enabled agents models.use
models.prompt { agentId, model?, effort?, prompt | blocks, workspaceId?, promptId?, display?: { scope, taskId } } { text, usage, status, message?, agentId, model, effort } after the run; its events come first as models/event { promptId, event } notifications. The agent must be enabled; the session is hidden and closed after the run; display shows the run in one of the caller's subagents, whose approvals and questions then go to the chat. Cancelling the request cancels the run models.use
tools.changed {} (notification) the plugin's tool list changed; the host asks tools/list again (JavaScript plugins send it for you) tools.provide
settings.get {} Settings settings.read
settings.update partial Settings Settings settings.write
updates.status {} UpdateStatus plugins.manage
updates.check {} UpdateStatus, after asking the update server plugins.manage
updates.apply_plugins {} { report: { updated, added, removed, modified, skipped }, chatId? }: default plugins the user left alone are replaced; chatId is the chat where an agent merges the update into the changed ones plugins.manage
updates.install_app {} { restart }: the app update is downloaded and ready; when restart is true, quit (api.host.kernel("quit")) to finish plugins.manage
plugins.list {} { plugins: PluginSummary[] } plugins.manage
plugins.request_enable { name, chatId? } { requestId }: asks the app to show its native card for the plugin: the enable card when it is not enabled, the permission card when it is enabled and asks for more. requestId is null when there is nothing to review. With chatId the card sits in that chat; without it the card is a modal over the window. It grants nothing: only the user's answer on the card does. plugins.manage
plugins.set_enabled { name, enabled } {}. Off always works. On works only for a plugin the user enabled before with nothing new to review; otherwise it fails with NeedsReview, and the caller uses plugins.request_enable plugins.manage
plugins.revoke { name, permission, scope? } {}: takes one grant back; the plugin keeps running and calls that need the grant fail from now on. Refused for official plugins (turn them off instead) plugins.manage
plugins.reload { name } {} (the plugin loads again; for one its limits stopped, this is "Start again") plugins.manage
plugins.settings { name } the plugin's settings section: { name, id, title, properties: [{ key, type, title, description?, default?, options?, minimum?, maximum?, kind?, placeholder?, value, secretSet?, locked, error? }], notices, view }. value is what the plugin reads (a secret has only secretSet); locked: a permission refers to the setting; error: its value cannot fill that permission; notices: values an update put back to their defaults; view: the plugin's settings view module plugins.manage
plugins.set_setting { name, key, value } { value, requestId }: the user changed the setting (null goes back to the default). For a setting a permission refers to whose new value needs a grant the plugin does not have, the value is saved and requestId names the change card, which shows the old and the new scope; the old grant stays until the user answers, and Cancel puts the old value back. A secret is only cleared here (value: null); it is typed into the native field (ui.secretInput), never sent through a plugin plugins.manage
own_settings.get {} { values }: the calling plugin's own settings, without secrets (api.settings.get)
own_settings.set { key, value } { values }: writes one of the calling plugin's own settings, checked against its type; refused (PermissionNotGranted for settings.own) for a setting a permission refers to, which only the user changes. A secret goes to the system credential store
own_settings.secret { key } { value }: one of the calling plugin's own secrets, from the system credential store, or null
security.pending { chatId } { requests: SecurityRequest[] }: the requests whose cards sit in that chat chats.read
market.search { query?, sort?, page?, category? } the registry's search page { results, page, pageSize, total } (sort: relevance, downloads, rating, recent, name; category: official, agents, tools, interface, git, terminal, web, mods) market
market.listing { id } the listing (id, name, description, publisher: { name, displayName, verified }, latestVersion, downloads, rating, basedOn, mods, permissions, versions) plus installed: { folder, path, version, enabled, source } | null market
market.read { id, version?, path? } without path the release: { id, version, official, publisher, basedOn, permissions, targets, yanked, files: [{ path, size, sha256, executable, binary }] }; with path one file: { path, size, sha256, text, truncated } or { ..., binary: true }. Read from the verified package; nothing is saved market
market.similar { id } publishers and plugins with similar names, with creation dates, install counts and the verified flag market
market.categories {} { categories: [{ id, title, … }] } from the registry the app uses (marketplaceUrl, else the default) market
market.comments { id } { threads }: the listing's comments, from the same registry market
market.install { id, version?, chatId? } { id, version, folder, path, official, enabled: false, yanked, basedOn, permissions, requestId }: verifies, downloads and extracts into the writable plugins folder, not enabled; with chatId the enable card shows in that chat market
market.updates { cached? } { updates: MarketUpdate[] } (with cached, the last check's answer, without asking the registry) market
market.update { name } { updated: true, id, folder, from, to } for an unmodified official plugin (replaced after the official signature check), else { needsAgent: true, id, folder, path, from, to, official, modified, baseline, changedFiles, permissions: { added, removed, changed }, newPermissions, basedOn, read: { id, version } } market
market.publish { name, visibility?, share? } { jobId, state, id, version, basedOn, permissions, error?, errorFile?, errorLine? }. For cvg and the app's own publish row; the marketplace plugin never calls it (its market_publish tool asks for the row) market
market.publish_setting { name, setting? } { setting }: the plugin's publish setting, kept in its install record (never in its folder); with setting (an object, or null) replaced. { mode: "never" } is "Never for this plugin" market
market.request_publish { name, chatId, visibility?, share? } { requestId }: the publish row for that plugin in that chat, for a user who asked an agent to publish. visibility and share only fill in the row's form; the plugin is published only from the row. Refused for an official plugin as shipped market
market.request_report { id, version?, reason, files?, kind?: agent | user, chatId? } { requestId }: the report card, showing exactly what is sent (in the chat, or a modal without chatId); the report is sent only from the card market
market.report { id, version?, reason, files?, kind? } { id }: sends a report at once (cvg report); the marketplace plugin uses market.request_report market
market.unpublish { id } { ok } market
market.yank { id, version } { ok } market
market.share { id, add?, remove? } { added, removed } market
market.login {} { userCode, verificationUri, verificationUriComplete, expiresIn, interval }: a device sign-in; show the code, and open the page when the user clicks. The outcome arrives as market_account market
market.logout {} {} market
market.whoami {} { signedIn, registry, account?, expired? } (no network when nobody is signed in) market
market.workspace {} { workspaceId, path }: the Plugins workspace, added when the user has none market
market.bug_draft { chatId? } { plugins: [{ name, id, version, official, listed, enabled, changed, baseline, suggested, reasons }], note, registry, signedIn, appVersion }: the bug report form's plugins, the suggested ones first (changed against their release, changed by the chat's last turn, named in an error of the log) market
market.bug_preview { plugins, fullSource?, screenshots? } { plugins, attachments: BugAttachment[], note }: every attachment those choices send, with its full content (text, or path of a screenshot) market
market.request_bug_report { text, plugins, contactEmail?, attachments?, fullSource?, screenshots?, chatId?, kind?: user | agent } { requestId }: the send card with the full preview (in the chat, or a modal without chatId); attachments are the ids to send (the default ones when left out); the report is sent only from the card, which answers market_bug_report market
market.bug_reports {} { reports: [{ id, number, registry, text, plugins, source, createdAt, requestId? }] }: the reports this app sent market
market.link_fix_chat { chatId, report } { reportId, number }: the chat fixes that report (bug_… or #N): a turn there that changes a marketplace plugin leaves the attach row instead of the publish row market
market.request_attach_fix { report, folders?, chatId? } { requestId, plugins }: the attach card for the diffs of folders (every changed marketplace plugin when left out) against their base versions; the fix is attached only from the card market
market.open_bug_form { plugins?, text? } {}: opens the bug report form (the market plugin hears market_bug_form); for Settings and the Help menu plugins.manage
ui.notify { message, level? } {}
ui.context {} { workspaceId?, chatId?, agentId? }
ui.invalidate_panel { panel } {} (declarative panels)
ui.open_chat { chatId } {} chats.read
log { level, message } {} (writes to the app log; console.* sends it)
cancel { id } (notification) the plugin gave up its request id (a fetch aborted before its headers): the host stops the work, and the request's answer is an error nobody waits for
events.subscribe / events.unsubscribe {} {} (the runtime subscribes for a JavaScript plugin; api.on listens)

The market.* methods belong to the official marketplace plugin (market is reserved to official plugins; ../market/ is the plugin). JavaScript calls them through api.host.call("host/market.search", params). None of them enables, publishes from a chat or sends a report behind the user's back: the plugin asks for the enable card (plugins.request_enable), the publish row (market.request_publish), the report card (market.request_report), the bug report's send card (market.request_bug_report) and the attach card (market.request_attach_fix), and only the app's own buttons act on them.

chats.respond_approval is gone. Only the app's own card answers an agent's approval, so a plugin cannot allow a tool call the user did not see: the chat plugin places that card with ui.securityCard (see ui-plugins.md), and a call to the old method fails with unknown host method host/chats.respond_approval. Questions stay with the plugin: answering one (chats.respond_question) is user input to the agent, as chats.send is.

Types:

UpdateStatus  { enabled, target, appVersion, plugins?: { version, build, pluginApi }, checking, busy?, lastChecked?,
                error?, appUpdate?: { version, releaseNotes?, url, sha256, size?, kind }, pluginUpdate?: { version, build,
                pluginApi, releaseNotes?, url, sha256, size? } }
              `enabled` is false when the app runs its plugins from a source tree. See `scripts/RELEASES.md`.
Workspace     { id, path, name, position }
Chat          { id, workspaceId, agentId, sessionId?, title, createdAt, updatedAt, archived, options, status, completedAt? }
              completedAt: set when the user checks the chat off, or when a started chat had nothing running for a day; a
              prompt (or a new run) clears it.
              status: idle | running | waiting_approval | waiting_question | failed
Settings      { appearance, theme, fontSize, enabledAgents, agents, keybindings, autoApprove, checkpoints,
                checkpointRetentionDays: number | null, checkpointMaxMegabytes: number | null,
                tools: { mode: code | direct, disabled: ["plugin/tool"], disabledRules: [plugin] }, marketplaceUrl? }
              `marketplaceUrl`: the registry the marketplace uses (a self-hosted or local one); unset means the
              default, or `CONVERGENCE_REPO_URL` when that is set.
              (`disabledPlugins` is read once, into the plugins' install records, and is empty after that.
              `keybindings` is not read: the user's keys are in `<data>/keybindings.json`, see `ui-plugins.md`,
              Commands and keys. A plugin's own settings are not here either: see `plugins.settings`.)
AgentInfo     { id, name, description, version?, capabilities, authMethods, status: { state, message? }, icon? }
              icon: the agent's mark as inline SVG; pass it to `ui.icon(name, { svg })`
ConfigOption  { id, name, description?, category: mode|model|reasoning|approval|<other>, kind: select|toggle, value, choices: [{ value, name, description?, group?, reasoningLevels }] }
TranscriptItem { id, createdAt?, role: user|assistant|reasoning|tool|plan|notice, ...body }
PluginSummary { name, id, version, description, official, claimsOfficial, source: official-bundle | marketplace | local,
                enabled, state: loaded | disabled | not_enabled | failed | reload_pending, error?, ui,
                sandboxed: bool | null, basedOn?: { id, version }, permissions: PermissionEntry[],
                pending: PermissionEntry[], settings?: string, commands: number }
              `name` is the folder name, `id` the manifest id (`local:<folder>` without one). `disabled` is a plugin
              the user turned off; `not_enabled` one nobody enabled yet. `failed` with the error `Stopped: too much memory` or
              `Stopped: busy loop` is a plugin its limits stopped (`plugins.reload` starts it again). `ui`: runs as
              JavaScript (`main`). `claimsOfficial`: a `convergence/` id on a plugin that is not official.
              `sandboxed`: whether the OS
              sandbox holds the plugin's runtime (`false` where the system has none: Windows for now; `null` before
              any runtime started). `pending`: what the manifest asks for that nobody reviewed yet. `settings`: the
              title of its section of the settings page, when it declares settings; `commands`: how many it declares.
PermissionEntry { permission, scope?, reason, risk: always | low | medium | high | dangerous | unsafe | reserved, granted,
                  setting?, settingTitle? }
              `setting`: the scope is that setting's value (`${settings.<key>}` in the manifest), parsed; the card
              says so with the setting's title.
              A reserved permission in a plugin that is not official has `risk: "reserved"`, is never granted and
              never pending.
SecurityRequest { id, chatId?, afterItemId?, kind: "enable", plugin: PluginSummary, createdAt, stale }
              | { id, chatId?, afterItemId?, kind: "permissions", plugin: PluginSummary, new: PermissionEntry[], createdAt, stale }
              | { id, chatId, afterItemId?, kind: "publish", plugin: PluginSummary, changed: string[], visibility?, share?,
                  createdAt, stale: false }
              | { id, chatId?, afterItemId?, kind: "report", report: { id, version?, reason, files, kind, registry },
                  plugin?: PluginSummary, createdAt, stale: false }
              | { id, kind: "setting", plugin: PluginSummary, key, title, from, to,
                  changes: [{ permission, from?, to?, risk, reason }], createdAt, stale }
              | { id, chatId?, afterItemId?, kind: "bug_report", report: { text, plugins: BugPlugin[], contactEmail?,
                  attachments: BugAttachment[], source: app | agent, registry, account?, appVersion }, createdAt, stale: false }
              | { id, chatId?, afterItemId?, kind: "attach_fix", fix: { reportId, number?, registry, plugins: [{ folder, id,
                  baseVersion, official, files, permissionsAdded, permissionsRemoved, permissionsChanged, diffSize }] },
                  createdAt, stale: false }
              `afterItemId` is the chat's last item when the request was made. `stale`: the manifest on disk changed
              since, and the card offers to review again. `publish` is the publish row (`MARKETPLACE.md` §10.6): after a
              turn that changed files (`changed`, relative to the plugin's folder) of a plugin with no publish setting
              that is not an official plugin as shipped; it goes below the chat's latest response only, and the next
              prompt, or a newer row, takes it away. `report` holds exactly what the report sends. `setting` is the
              change card (`MARKETPLACE.md` §15.3): the user changed a setting a permission refers to, from `from` to
              `to`, and `changes` are the scopes that change; it is a modal, `stale` once the value changed again.
              `bug_report` is a bug report's send card (`MARKETPLACE.md` §14.1) and `attach_fix` a fix's attach row
              (§14.2): a request never carries an attachment's content or a diff; only the kernel's card shows them.
BugPlugin     { folder?, id, version?, official }   (`id` is `local:<folder>` for a plugin without one)
BugAttachment { id, kind: diff | source | config | diagnostics | screenshot, title, name, pluginId?, size,
                removed: [{ line, kind }], defaultOn }   (`market.bug_preview` adds `text`, or `path` and `mimeType`)
MarketUpdate  { id, folder, installed, latest?, official, direct, malicious, yanked, reason? }
              `direct`: an unmodified official plugin that `market.update` replaces at once; anything else goes to an agent.

Durable input and scheduler authority

inputs.enqueue acknowledges persistence, not delivery. append: true adds blocks to the last held steer, separated by a newline; attachments keep their order and bytes. The resulting input may retain that earlier steer's id. Ordinary user input supersedes editable generated_continue input; that origin is admitted only when no unconsumed input exists, so generation cannot race a user's queue. Removed and amended enqueue ids remain idempotency keys.

Input states are held, dispatching, delivery_unknown, rejected, native_admitted, and observed_consumed. A write acknowledgement does not prove native admission or model pickup. Never automatically replay uncertain/admitted input. A correlated provider event can establish consumption after the dispatch call returns. Rejection positively establishes non-admission; preserve the same logical input id when retrying it. Refresh the snapshot before a CAS retry; never retry provider delivery just because a request timed out.

running includes preparation. settling lasts through required transcript and checkpoint finalization. Use run_settled, not run_finished, as a release boundary. The host never drains an ordinary queue: a scheduler chooses an input and calls dispatch. Native steers share the initiating run's checkpoint; simulated steering reserves one replacement, cancels the original prompt, and waits for real settlement in the same session. Stop/edit/remove can revoke that held reservation; a cancellation timeout cannot authorize a new prompt.

Any plugin may provide convergence/input-scheduler with the service APIs. The live provider's registration yields the opaque token returned by inputs.policy; automatic dispatch validates that token against the caller and registration. Reload/disable/replacement invalidates it. The stock chat service contract is chat/scheduler-contract.ts; consumers need not use the stock FIFO policy. policyState is durable JSON the host does not interpret. Alternative policies may release inputs in a different order, while claims, CAS, lineage and delivery evidence remain host-owned. An optional scope coordinates a claim across chats until actual run settlement; a recovered ambiguous run retains its claim.

Availability reads are observations, not permission inferred from a meter. Use UsageLimits.recovery with native account identity, quota scope, observation time and availability. A missing reset cannot invent a timer; null/unknown/unsupported does not override a fresh rejection. Stock recovery uses one bounded attempt at a trustworthy future reset and holds when identity or delivery cannot be reconciled.

See agent-protocol.md for ToolCall, ApprovalRequest, QuestionRequest and the event payloads.

Source: plugins/docs/host-api.md in the Divergence repository, built with this site.