Manifest reference
What the app accepts from a plugin’s manifest, field by field. This page is derived from the loader’s validator; when the two disagree the validator wins. The authoring walkthrough — project layout, handler code, hooks, settings, inbound triggers — is under Guides.
A manifest that fails validation is refused with one message per broken rule and the plugin is not loaded.
Top level
Section titled “Top level”| Field | Required | Rule |
|---|---|---|
name |
yes | kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$). Doubles as the plugin’s identity in the installed registry and in the pluginAllowList a folder agent may carry. |
displayName |
yes | free text, shown in Connect. |
version |
yes | semver MAJOR.MINOR.PATCH with an optional -prerelease suffix. |
description, author { name, url? }, repository, icon, license, keywords[], category |
no | descriptive only. |
capabilities[] |
no | flat lowercase tags (^[a-z0-9]+([.-][a-z0-9]+)*$), e.g. channel.telegram, audio.stt. A dot-separated namespace is recommended, not enforced. Consumers resolve a capability to the first plugin tool that declares it; absent means []. |
runtime |
no | "node" (default) — handlers are JavaScript files run in the app’s plugin host; "process" — the plugin is its own executable, see below. |
command, args[] |
process only |
command is required for runtime: "process", must be relative to the plugin’s install directory (absolute on either OS is rejected — manifests are portable), and must not contain ... |
engines.filer |
no | the oldest Filer the plugin runs in, written ">=X.Y.Z" (any other shape is rejected). Compared with the app’s own version: an older Filer refuses to install the plugin — saying which version it needs — and does not load one that is already installed. |
bundled |
no | true: Filer installs the plugin by itself — on its first start, or the first start after the plugin appears in the catalog — and does not reinstall it once the person removes it. Honoured only in the official plugin source; ignored anywhere else. |
contributes |
no | see below. |
permissions |
yes | see Permissions; {} declares a plugin that touches nothing. |
runtime: "process" — not supported
Section titled “runtime: "process" — not supported”The process runtime speaks the plugin’s tools over a process transport; a tool is called by its own id, so a
tool entry must not carry handler. The following are rejected under this runtime, each with its own message:
contributes.hooks (non-empty), contributes.views (non-empty), any contributes.settings[*] of type: "action",
permissions.exec, and a static permissions.fs.read/write rule without "parameter": true — a process plugin does its
own OS I/O and never receives the ctx.fs proxy that enforces static rules, so such a rule would be silently
unenforced; use "parameter": true (dispatch-time enforcement) instead.
contributes
Section titled “contributes”Within one manifest, each tool, command and action id is declared once: the menus and the tool list key their entries by id, so a second entry with the same id was shown as only one row.
tools[] — MCP tools the agent can call
Section titled “tools[] — MCP tools the agent can call”| Field | Required | Rule |
|---|---|---|
id |
yes | the MCP tool name. Must not contain . (dots are reserved for namespace prefixing). Legacy manifests that use name are rejected with a message asking for id. |
title, description |
yes | description is what the model reads when choosing a tool. |
schema |
yes | path to a JSON-Schema file for the tool’s arguments, relative to the plugin root; must not contain ... A property carrying "format": "file-path" (or an array whose items do) is what ties that argument to the fs.read / fs.write parameter: true checks — its value at call time is the path the dispatcher grants and checks against the calling agent’s folder; a path passed under a property without that format gets no grant, so only a static rule could admit it. A property carrying "format": "directory-path" grants that directory and everything below it — for a tool whose input is a set of files rooted in one place (a page and the scripts and data it loads); the directory is checked against the calling agent’s folder at dispatch like any grant, deny patterns still refuse inside it, and nothing beside it is granted. |
handler |
node only |
path to the handler module, relative to the plugin root; must not contain ... Required for runtime: "node", forbidden for "process". |
timeout |
no | e.g. "30s". |
fileExtensions[] |
no | extensions the tool applies to (used by the app’s context menus). |
annotations |
no | MCP tool annotations — { readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }, booleans only, the same names and meaning as the MCP specification; any other key is rejected. They tell the approval gate what the tool does (see Trust and allow-lists). An undeclared tool is not assumed read-only: see there for what it is treated as. |
targetArguments[] |
no | the arguments that name what a call acts on — e.g. ["path"] for a tool that works on one file. The agent then counts a call of this tool on the same target as a repeat whatever its other arguments, so a model that asks about one photo eight times with different questions is stopped as a loop instead of running until it hits its step limit. Non-empty strings, none repeated; each must be a property of the tool’s schema (an unknown name is ignored). Leave it out when another argument moves the call on (a read with offset, a paged search): each page would look like a repeat. Sent to the agent as the tool’s MCP _meta. |
Handler result shape. A node handler’s return value reaches the agent as one MCP text block: a string
verbatim, anything else JSON.stringify‘d. A handler that returns an MCP content envelope —
{ content: [ { type: "text", text }, { type: "image", data: <base64>, mimeType: "image/…" } ] }, every block
well-formed — is forwarded verbatim, so an image it returns reaches the model as an image on providers that accept
image tool results. A malformed envelope falls back to the text wrapping;
nothing is dropped silently. A failure is a failure to the agent: a handler that throws, or returns an object whose
success is false (the bundled plugins’ { success: false, error }), or an envelope carrying isError: true, reaches
the agent as an MCP tool error (isError). The model still reads the text; the run’s record counts the call as failed,
and a run whose every outward send failed is reported as not done. A string result is never read as a verdict. One name, one tool: the agent loop registers built-ins first, then host tools, then
plugin tools, and a later tool whose name is already taken is skipped with a warning that names its source — a
plugin tool cannot shadow a built-in or a host tool of the same name (document-essentials renamed its reader to
read_text_document for this reason).
actions[] — right-click AI actions
Section titled “actions[] — right-click AI actions”| Field | Required | Rule |
|---|---|---|
id |
yes | kebab-case. |
title, description |
yes | shown in the context menu / action dialog. |
scope |
yes | "single-file", "multi-file", "folder", or an array of those. |
kind |
no | "prompt" (default): renders prompt and opens an agent turn in chat. "tool": invokes toolId directly — no agent turn, no model. |
prompt |
prompt kind |
path to the prompt template, relative to the plugin root; must not contain ... Forbidden for kind: "tool". |
toolId |
tool kind |
must match a contributes.tools[].id in the same manifest. |
toolScope |
no, prompt kind only |
"none" (no tools this turn) or "read-only" (only tools that read the folder and its knowledge — nothing that writes, runs a command, reaches the network, or belongs to a plugin). The manifest names a scope, never a tool; the host resolves it into the turn’s tool list and enforces it on the agent loop for that one turn. A prompt that says “read the file at …” needs read-only, not none — with no tools the model cannot read and will narrate instead. Forbidden for kind: "tool" (it runs no agent turn). |
fileExtensions[], fileTypes[] |
no | when the action is offered. |
multiFileMode |
no, tool kind |
"independent" (default): one tool call per selected file. "batch": exactly one call with every selected path under the tool schema’s array-of-paths property — for a tool whose point is to combine files. |
params[] |
no | dialog inputs: { id, label, type: "select" | "text" | "per-file-text", options?, placeholder?, default?, required? }. |
generate |
no | an in-form button { label, actionId, populateParamId, forwardParamIds? } that invokes a sibling kind: "tool" action (actionId must exist in the same manifest) and fills one of this action’s per-file-text params from its result. |
hidden |
no | true keeps the action out of the context menu while it stays invocable as another action’s generate.actionId. |
settings{} — the plugin’s settings card
Section titled “settings{} — the plugin’s settings card”Keyed by setting id. Each value: { type: "string" | "boolean" | "number" | "action" | "info", title, description?, default?, secret?, required?, enum?, multiline?, min?, max?, order?, handler?, variant? }. secret: true stores the
value in Filer’s background service’s encrypted secret store (Windows DPAPI, per user — the same store the AI provider
keys use) and hands it to handlers through ctx.settings, never to the renderer in plain text. type: "action" renders a button that runs
handler (relative path); variant is "primary" | "secondary" | "danger". Not available under runtime: "process".
hooks{} — event handlers
Section titled “hooks{} — event handlers”Keyed by event name; each value is either a handler path or { handler, filter? }. Handler paths must not contain
... Event names, payloads and the filter shape are on Hooks and events (onAgentComplete, onSessionMessage, onToolCall, …). Not available under runtime: "process".
A hook for a folder’s run fires only if that folder allows your plugin. A folder’s plugin allow-list (its settings’
plugin_allow_list, which a parent folder can narrow for everything beneath it) decides which plugins’ tools a run gets,
and the same list decides which plugins’ hooks hear about the run: its completion (onAgentComplete, onSessionMessage),
an approval request (onAgentHitlRequest) and a file-change notice (onFileChangeNotify). A folder with no list reaches
every installed plugin; an empty list reaches none. So a messenger that “notifies on every completion” notifies only for
folders that allow it — if your hook stays silent for one folder, check that folder’s plugin list first. A chat opened in a
folder follows that folder’s list the same way — the plugins its AI may call and the hooks that hear its turns
(onSessionStart, onToolCall, onAgentComplete); a chat opened outside any folder with a list reaches every plugin.
onSessionMessage is the reply to a conversation that came in on your plugin’s channel, raised to your plugin alone and
always with the reply’s text in result — it is not raised for a chat in the app’s own panel, nor for a run started by a
file change, a schedule, a manual start or a hand-off, so none of those is copied to a messenger unasked. A plugin that
offers “send me every result” does it from onAgentComplete, which carries how the run or turn ended: outcome is
completed, unfulfilled (it ended without doing everything asked) or failed, and reason is Filer’s sentence for why
when it is not completed — say what outcome says, never “completed” for a run that failed. Events that belong to no
folder (onAppReady, onRuntimeReady, a channel registered, a file memorized) reach every plugin.
A hook’s failure never stops the other plugins’ hooks, but it is not invisible: an onSessionMessage hook that relays a
run’s reply must throw when it cannot send it (the channel is not configured, the service refused). Filer waits for that
hook on a run that came in on the plugin’s channel and reads a normal return as delivered, a thrown error as not
delivered — the run is then recorded as unfulfilled, with the error’s message as the reason the user sees. A folder
whose plugin list leaves your plugin out gets no reply relayed through it; the run says so as its reason.
Hooks run in Filer’s background plugin runtime, which the background service owns — with or without the app window.
Two lifecycle events differ only in what they announce: onRuntimeReady fires once per runtime start, after the
plugin’s settings are loaded — start a channel listener (polling, IMAP idle) here, and restart it from
onPluginSettingsChanged ({ key, oldValue: null, newValue }, raised to every plugin on each settings save; newValue
is null for a secret) when a key it read changes, or the change waits for the next runtime start; onAppReady fires when the app
window is up — it is announced to the runtime by the app, so it may fire long after onRuntimeReady, or not at all on a
machine where the background service runs without the app. Every other event is raised by the background service where
the fact is made (a file-change notification, a HITL request, a channel registered, a file memorized, a chat turn).
ctx.renderHtmlToPdf renders through headless Microsoft Edge, which Windows ships, so it works in the background
runtime with or without the app window; the page is printed with no network access and no header or footer. Where Edge
is not installed it rejects with “renderHtmlToPdf is not available in this host” — treat it as optional.
commands[] — slash commands
Section titled “commands[] — slash commands”{ id, title, icon?, contexts?[], handler?, when?, fileExtensions?[] }. handler must not contain ...
views[] — plugin-contributed panels
Section titled “views[] — plugin-contributed panels”{ id, title, icon?, location?, definition? }. Not available under runtime: "process".
Icon names
Section titled “Icon names”A view definition names its icons — a stats-row item’s icon and an empty-state’s icon — from the table below. A name
is matched in kebab-case or PascalCase alike (alert-circle = AlertCircle); a name that is not in the table, or a path,
draws no icon and the item renders without one. The names are Filer’s own vocabulary, not an icon library’s export list: a
name keeps its meaning when the glyph set behind it changes, and the right column says which Fluent System Icon the app
draws for it today. A prompt template’s icon takes the same names. The top-level icon, and icon on commands[] and
views[], are descriptive — the app does not draw them today.
| Name | Draws | Name | Draws |
|---|---|---|---|
activity |
Pulse | lock |
LockClosed |
alert-circle |
ErrorCircle | mail |
|
alert-triangle |
Warning | message-square |
Chat |
arrow-right |
ArrowRight | more-horizontal |
MoreHorizontal |
arrow-up-circle |
ArrowCircleUp | pause |
Pause |
book-open |
BookOpen | play |
Play |
bot |
Bot | plus |
Add |
check |
Checkmark | puzzle |
PuzzlePiece |
check-circle-2 |
CheckmarkCircle | refresh-cw |
ArrowSync |
chevron-down |
ChevronDown | rotate-ccw |
ArrowCounterclockwise |
chevron-right |
ChevronRight | rotate-cw |
ArrowClockwise |
chevron-up |
ChevronUp | save |
Save |
clock |
Clock | search |
Search |
copy |
Copy | send |
Send |
external-link |
Open | settings |
Settings |
eye |
Eye | shield-alert |
ShieldError |
file-bar-chart |
DocumentData | shield-check |
ShieldCheckmark |
file-plus |
DocumentAdd | sparkles |
Sparkle |
file-text |
DocumentText | table |
Table |
file-warning |
DocumentError | terminal |
WindowConsole |
folder-open |
FolderOpen | trash-2 |
Delete |
git-branch |
Branch | wrench |
Wrench |
globe |
Globe | x |
Dismiss |
languages |
LocalLanguage | zap |
Flash |
View colours
Section titled “View colours”A stats-row item’s color names a status, and the app draws its icon in the theme’s colour for it: error (or danger),
warning, success, info. Any other value — a CSS colour such as #00ff00 included — draws in the default colour: a fixed
colour from a plugin would ignore the light, dark and contrast themes.
permissions
Section titled “permissions”Required. Everything a plugin may touch is declared here and enforced by the plugin context it receives (ctx). A
manifest without the block is not installed, listed or loaded — there is no undeclared way for a handler to run. An
undeclared capability is not removed from ctx — the member is still there, but calling it throws a permission
error whose message names the plugin and the capability, with not-declared as the reason (settings.*, fs.*,
triggerInbound / listChannels / respondToHitl, renderHtmlToPdf, session.*,
ai.complete, channels.*). Two exceptions: ctx.toast and ctx.store are undefined when not declared, and
ctx.log without log: true silently does nothing — no error, no output. Hook handlers receive the same
wrapped ctx with an empty grant set and no calling-agent folder, so inside a hook parameter: true grants
nothing — only static rules (and deny) apply there.
| Key | Meaning |
|---|---|
fs.read / fs.write |
{ static?: string[], parameter?: boolean, deny?: string[] }. static — glob patterns the handler may touch on its own; parameter: true — the path comes from the call’s arguments (the schema property marked "format": "file-path") and is checked at dispatch against the calling agent’s folder — a parameter write is also refused anywhere inside a folder’s .filer/ except .filer/artifacts/ (reason filer-managed; .filer/ holds the folder’s instructions and settings); deny — always refused. Beyond any deny, Filer’s path denylist holds for every plugin: secret-like files (.env, keys, certificates, credentials) and OS system directories are refused to read, write and exists, and left out of a list result, whatever the manifest declares. The ctx.fs proxy (read, write, exists, …) is where static rules are enforced. Relative static patterns are resolved against the app process’s working directory, not the plugin’s directory — "./cache/**" does not mean the plugin’s own cache/; write "${dataDir}/cache/**" (the plugin’s data directory) or an absolute path. Patterns beginning with * (e.g. **/.env) are matched as globs without resolution. |
fs.list |
{ static?: string[], parameter?: boolean, deny?: string[] } — the same three fields as fs.read, with one difference: static entries are directory paths matched by prefix (a listed directory covers everything below it), not globs. parameter: true lets ctx.fs.list take a directory named by a "format": "file-path" argument, checked at dispatch against the calling agent’s folder exactly as a read is — the shape for any tool that lists a folder the agent chooses. deny patterns are globs and always refuse. Same resolution rule for static as above: use ${dataDir} or absolute paths, not "./data". |
fetch |
{ static?: string[], settings?: string[], deny?: string[] } — allowed hosts, hosts read from named settings, and a deny list; localhost/private ranges are refused regardless (SSRF guard). static / deny entries are host names. A settings entry names a setting whose value must be a full URL (https://api.example.com/v1) — the host is taken from it with a URL parse, so a bare host name stored in the setting parses as nothing and grants nothing. |
store |
true — a per-plugin key/value store. |
session |
{ getActive?, sendMessage? }. |
hostTrigger |
true — ctx.triggerInbound, routing a resolved inbound channel message to the host’s inbound trigger. Separate from fetch on purpose: it targets only the fixed host origin. |
ai |
{ complete?: true } — ctx.ai.complete, a single tool-free completion on the user’s own configured model. Not an agent turn; no HITL gating of its own. Inline images are read by the local vision models on the local route. |
channels |
{ getIntegrationConfig?, setIntegrationConfig? }. |
settings, toast, renderHtmlToPdf, log |
true to expose the corresponding ctx member. |
browser |
true — ctx.browser.launch() resolves { wsEndpoint }: a headless Microsoft Edge the runtime starts for the call (a throwaway profile, no network) with a DevTools endpoint on loopback, for Playwright’s chromium.connectOverCDP. The runtime kills it when the call ends. Rejects with “not available in this host” where Edge is not installed. A node plugin drives a browser only this way. |
exec |
A list of program names (["git"]): letters, digits, ., _, -; no path, no spaces. ctx.exec.run(program, args, { cwd, env? }) starts a listed program — found on PATH by its name, without a shell — and resolves { code, stdout, stderr } (a non-zero exit resolves too; a program that is not installed rejects with ENOENT). cwd is checked like a ctx.fs.list of that folder, and a folder whose contents the path denylist refuses (a .ssh folder) is refused whole. A node plugin starts programs only this way. The program runs with the arguments you pass, as the person’s account — the file rules do not bind what a declared program then does. Not for runtime: "process". |
execution |
{ timeout?, maxTimeout? } — handler time budget. |
Trust and allow-lists
Section titled “Trust and allow-lists”A plugin inherits the folder agent’s trust level for approval gating; a folder’s settings.json may carry a plugin
allow-list, and a tool from a plugin not on it is refused by the host’s plugin MCP endpoint (POST /plugins/mcp) with a
message that names the plugin.
How a tool is gated. The agent’s approval gate sorts every call into a category, and the folder’s trust level
decides whether that category runs, asks or is refused. A plugin tool’s category comes from its annotations:
| Annotation | Category | Asks at |
|---|---|---|
openWorldHint: true |
external — reaches outside the machine (the network, other people, other services) |
every level below Full Auto; refused in Sandbox |
destructiveHint: true |
delete |
per the trust table |
readOnlyHint: false |
modify |
per the trust table |
readOnlyHint: true, or nothing that applies |
read | never |
When several apply, the strictest wins. A tool that declares no annotations is treated by what its plugin may do: a
plugin that declares permissions.fetch, or runs as runtime: "process" (an unsandboxed native process), has its
undeclared tools treated as openWorldHint: true, and the plugin loader logs a warning naming each such tool; a
plugin that declares permissions.fs.write has them treated as readOnlyHint: false. Permissions that only touch
Filer itself (store, session, hostTrigger, channels, settings, …) imply nothing. Declare annotations on
every tool so a read-only tool of a networked plugin does not ask. This gates the agent’s calls only — a plugin’s
hooks, commands and views are not agent turns, and their outward reach is bounded by permissions.fetch and the
allow-list.
