Capabilities and tools
How Fermix represents, registers, filters, and executes tools: built-in, plugin, and MCP-discovered capabilities through a single shared registry, and how skills fit alongside.
Tools are the actions the agent can take: reading a file, running a shell command, searching the web, and so on. Every tool the model can call (whether it is built into Fermix, registered by an installed plugin, or discovered from an MCP server, a standard way for outside programs to expose tools to an AI agent) is one entry in a single shared list, the Capabilities.Registry. There is no separate set of “main agent tools” vs “voice tools” vs “scheduled-job tools.” Trust level and policy filters decide which subset each call site sees. Skills are the one exception: they are instruction packages, not provider-visible tools, and surface through a compact catalog in the prompt plus the skill_view and skill_run built-ins — see skills.
Capability shape
Each registered capability is a struct with these fields:
%Capability{
name: "file_read",
description: "Read a UTF-8 text file from the workspace.",
parameters: %{ # JSON Schema object
type: "object",
required: ["path"],
properties: %{
path: %{type: "string", description: "..."}
}
},
kind: :builtin, # :builtin | :skill | :mcp
executor: {FermixCore.Tools.FileRead, :execute, []},
hidden_from_agent?: false,
policy_class: :read_only,
owner_only?: false,
metadata: %{
category: :file,
when_to_use: "...",
examples: [...],
failure_modes: [...]
}
}
The parameters map is a JSON Schema object. AgentLoop passes the formatted schema to the configured provider verbatim. The executor tuple is called with the parsed argument map when the model issues a tool call. Two fields carry the trust boundary: policy_class bounds what a caller may do, while owner_only? marks a tool whose return value is the owner’s own data and keeps it out of any guest surface entirely — prompt, provider wire, and dispatch by name.
Three kinds of capability
| Kind | Source | Registered by |
|---|---|---|
:builtin |
Pure-Elixir tool modules compiled into Fermix, sandbox preset commands, and the tools of ready plugins. | BuiltinSeeder at boot; Sandbox.CommandCapabilities for preset shell commands; Plugins.Capabilities for plugin tools (tagged plugin_owned? in metadata; a plugin that runs a local MCP process contributes its tools as :mcp instead). |
:skill |
Reserved in the schema for skills, but the live runtime deliberately does not register one capability per skill — skills surface through the prompt’s skill catalog and the skill_view / skill_run built-ins. |
SkillRegistry keeps skills in its own store and clears any stale :skill entries from the capability registry on reload. |
:mcp |
Outbound MCP server tools/list response, registered as mcp_<server>_<tool> (a plugin-owned MCP server uses <plugin>_<tool>). |
The per-server Capabilities.MCP.Server process when its client connects; each of its tools is unregistered when the connection goes down. |
unregister_kind/3 accepts a metadata match map so a whole family of capabilities can be removed atomically — for example, every plugin_owned? tool when plugins reload, or every sandbox preset command when the sandbox config changes.
For more on skills, see skills. For plugins, see plugins. For MCP integration, see MCP.
Policy classes
Every capability carries one policy_class that describes its effect category:
| Class | Meaning | Example built-ins |
|---|---|---|
:read_only |
Reads data without modifying anything. | file_read, content_search, glob_search, git_read, memory_recall, tool_help, list_jobs, memory_sources_list, send_attachment |
:read_write |
Writes or mutates local state. | file_write, file_edit, git_write, memory_store, schedule_job, pause_job, resume_job, remove_job, skill_create, model_routing_config |
:exec |
Spawns an OS process or delegated run. shell goes through the sandbox. |
shell, skill_view, skill_run |
:network |
Outbound HTTP, validated by Net.Guard. |
web_fetch, web_search, place_search, browser |
:external_api |
Calls a third-party API, typically an LLM provider. All discovered MCP tools default to this class (a per-tool policy_class override exists in [mcp.servers.<server>.tools.<tool>]). |
subagents, generate_image, every :mcp capability |
:gui_control |
Drives the host desktop by screenshot and mouse/keyboard. Operator-only; never delegated to subagents (helper agents the main agent spins up for a piece of work). | computer_use (experimental, off by default) |
Subagent workers always run with the parent trust level’s classes minus :read_write and :gui_control: delegated work can read, browse, and call APIs, but never writes local state or touches the desktop.
Trust model
Trust is stamped at ingress (the point where a message enters Fermix, for example a chat channel or the CLI) and flows through MainAgent to AgentLoop to CapabilityRegistry.list_for/2, which applies the default policy filter for that trust level.
| Trust | Default allow | Default deny | Where it is set |
|---|---|---|---|
:operator |
[:read_only, :read_write, :exec, :network, :external_api, :gui_control] |
[] |
The human owner via CLI, daemon socket (the local connection to the long-running Fermix background process), voice click-to-talk, or the channel’s owner_user_id. Also applies to scheduled jobs created by the operator and bundled or operator-installed skills. |
:guest |
[:read_only] minus owner_only? capabilities |
[:read_write, :exec, :network, :external_api, :gui_control] |
Non-owner senders in allowed_user_ids; plugin-shipped skills (under ~/.fermix/plugins/) whose code the operator has not reviewed. |
nil |
(treated as :guest) |
Any call path that did not set :trust. A missing trust value degrades to read-only; it never silently grants full access. |
A guest additionally never receives an owner_only? capability — a tool whose return value is the owner’s own data. Workspace reads (file_read, glob_search, content_search, git_read), scheduled-job reads (list_jobs, list_job_runs, get_job_run), and memory_sources_list are absent from a guest’s surface entirely: not in the prompt, not on the provider wire, and not dispatchable by name — a guest calling one gets the same “Tool not found” error as any unregistered tool. Read-only bounds what a guest may do; the owner-only filter bounds whose data comes back. This filter is code-enforced and applies only to explicit :guest trust — operators, subagent workers, and scheduled runs are unaffected.
A few tools go further and require an attended turn — one a present owner typed or spoke, rather than one machinery generated. Each gated family carries its own condition, and every one of them is code-enforced.
| Tools | What the turn must be |
|---|---|
event_store, event_update, event_remove, reminder_snooze |
Operator trust, at the top level (not a delegated sub-agent), on an interactive, CLI, or live-voice surface, and not inside a synthesized coding-run continuation. |
event_list |
The same, or a scheduled job the owner created — so a digest job can read stored dates while the four tools that change something stay attended-only. |
| Every coding agents tool | An operator turn at the top level with a live reply path, or a scheduled job naming that exact tool in its own allowed_tools. There the job’s allowlist is the authorization; a broad tool policy is deliberately not enough. |
request_directory_access |
An operator turn at the top level on a channel that has both a live reply path and slash commands, because the owner completes the approval with /confirm. A surface without slash commands never sees it. |
Absence fails closed: a turn with no trust marker is treated as a guest, and a nesting depth other than zero is treated as delegated, so a malformed context can only narrow one of these gates, never widen it. The gate runs twice for the event and reminder family — once when the tool list is built, so the model is never offered them, and again when a call is dispatched, so a call by name from an unattended context is refused rather than executed.
A list/2 call with no :trust, :policy, or :policy_classes option bypasses the default filter and returns every registered capability. Internal callers such as fermix capabilities use this primitive path.
See ingress and trust for how trust is assigned per channel and per sender.
Filter parameters
CapabilityRegistry.list_for/1,2 accepts these options:
filter() :: [
allowed_tools: [String.t()] | nil,
policy: policy_spec(),
policy_classes: [policy_class()] | nil,
trust: :operator | :guest | nil,
kind: :builtin | :skill | :mcp | :all,
excluded_categories: [atom()] | nil,
excluded_names: [String.t()] | nil,
include_hidden?: boolean()
]
Filters compose in this order: kind, then policy, then the owner-only filter (applied only for :guest trust), then allowlist (the explicit list of tool names permitted), then name exclusion, then category exclusion, then hidden filter.
Key semantics:
allowed_tools: []means deny all.allowed_tools: nilmeans no narrowing by name.policy: [:read_only]is shorthand for[allow: [:read_only], deny: []].excluded_categories: [:scheduling]removes every tool whosemetadata.categorymatches, without listing each tool name.include_hidden?: truesurfaces capabilities flaggedhidden_from_agent?: true. Default isfalse. Operators use this flag to keep an MCP tool configured but invisible to the model.
Registry API
| Call | Notes |
|---|---|
register/2 |
Insert a capability. Returns {:error, {:duplicate_name, name}} if a capability with that name already exists. |
unregister/2 |
Remove by name. |
unregister_kind/2,3 |
Remove every capability of a kind, optionally filtered by a metadata match map. Used to clear capability families atomically: plugin tools on reload, sandbox preset commands on config change. |
find/2 |
ETS lookup by name. Hot path; bypasses the GenServer. |
list/1,2 / list_for/1,2 |
List all capabilities, with optional filtering. |
refresh/2 |
Currently a no-op for every kind. Real re-syncs go through the owning components: plugin reload, skill_reload, and MCP connect/disconnect. |
resolve_policy/2 |
Translate (trust, policy) into the effective allow/deny list. Public API. |
Built-in tools
Forty-one built-in tools ship unconditionally with Fermix, plus three bridges for deferred tools (tool_search, tool_describe, tool_call) when tool-schema deferral is enabled (on by default). Deferral, explained below, keeps rarely-used tool definitions out of each request to save tokens. Each implements the Builtin.Tool behaviour. One of them, place_search, is credential-gated: seeded with the rest but advertised to the model only while a Brave Search API key resolves, with execution re-checking the key either way. Eight more built-ins are seeded conditionally. computer_use is experimental and off by default, and registers only when computer use is enabled in config and its native helper is installed (see the table row and the note below). The seven coding agents tools — codex_run, claude_code_run, codex_cloud_run, list_coding_runs, get_coding_run, cancel_coding_run, stop_tracking_coding_run — seed only when the coding harness is enabled and a vendor CLI (Codex or Claude Code) is installed, and none is advertised to the model until the owner approves coding agents once (see the note below the table).
| Name | Policy class | Category | What it does |
|---|---|---|---|
shell |
exec |
system | Run a shell command via CommandRunner with sandbox-resolved working directory and env. |
file_read |
read_only |
file | Read a UTF-8 text file, gated by the sandbox read-path check. |
file_write |
read_write |
file | Write a file, gated by the sandbox write-path check. |
file_edit |
read_write |
file | Replace an exact-string span in a file. |
glob_search |
read_only |
file | Pattern-glob the workspace. |
content_search |
read_only |
file | Regex-search file contents. |
git_read |
read_only |
git | Run read-only git queries: status, log, diff, branch, show. branch lists only — flags that modify a ref and bare operands (which would create one) are both refused, in every spelling git accepts, so a branch flag’s value goes in the = form. |
git_write |
read_write |
git | Run mutating git commands: add, commit, checkout, pull. Pushing is deliberately excluded. Flags that read a file of the caller’s choosing into the repository (-F, --file, --pathspec-from-file) are refused — only the repo path is sandbox-checked, not the arguments — so a commit message comes inline with -m. |
web_fetch |
network |
web | Fetch a public URL through Net.Guard, with a body cap and IP pinning. Returns the page’s readable text, or the JSON verbatim when the endpoint serves JSON. |
web_search |
network |
web | Web search with DuckDuckGo as the default keyless backend; six configurable backends (Brave, Exa, Tavily, Parallel, Perplexity, Firecrawl) are supported when set up. A hard error on a configured backend degrades once to DuckDuckGo, loudly. |
place_search |
network |
web | Structured place lookup on Brave Place Search — hours, ratings, addresses, links, one thumbnail per place. Advertised only while the shared Brave API key resolves; one provider request per lookup; fails loud with named errors and never falls back to web_search or another provider. |
browser |
network |
web | Drive a supervised local browser for JavaScript-rendered pages, forms, and interactive content. The screenshot action returns the captured page as an image the model can see, not just a saved path; the managed browser caps live tabs and auto-closes the oldest non-active tab past the cap. |
memory_recall |
read_only |
memory | Look up a stored memory by key, list this conversation’s memories, or search stored memories and past conversation history. |
memory_store |
read_write |
memory | Persist a memory candidate through the admission pipeline. |
memory_sources_list |
read_only |
memory | List configured memory sources. |
subagents |
external_api |
delegation | Run one or more temporary subagents for delegated work, concurrently up to a cap. |
skill_create |
read_write |
skill_admin | Scaffold a new local Fermix skill with SKILL.md and starter evals. |
skill_reload |
read_write |
skill_admin | Reload a skill from disk without restarting the daemon. |
skill_view |
exec |
skill_admin | Show an installed skill’s instructions and metadata. |
skill_run |
exec |
skill_admin | Run an installed skill by name. |
skill_list |
read_only |
system | List installed skills available to the agent. |
schedule_job |
read_write |
scheduling | Create a cron-style scheduled agent run. The effective capability policy comes from the creator’s trust level, not the model’s. |
update_job |
read_write |
scheduling | Update a scheduled job’s schedule, prompt, or config. |
list_jobs |
read_only |
scheduling | List existing scheduled jobs. |
list_job_runs |
read_only |
scheduling | List past runs for a scheduled job. |
get_job_run |
read_only |
scheduling | Fetch detail for one scheduled job run. |
run_job_now |
read_write |
scheduling | Trigger an immediate out-of-schedule run of a job. |
pause_job |
read_write |
scheduling | Pause a scheduled job. |
resume_job |
read_write |
scheduling | Resume a paused scheduled job. |
remove_job |
read_write |
scheduling | Delete a scheduled job. |
event_store |
read_write |
scheduling | Store a personal date (one-time or yearly) and the finite reminder plan Fermix delivers for it through the configured default background channel. Attended owner turns only. |
event_list |
read_only |
scheduling | List or search stored dates, their plans, and the last delivery result. Defaults to active dates from today onward. Attended owner turns, plus scheduled jobs the owner created. |
event_update |
read_write |
scheduling | Edit a stored date, replace its reminder plan, or rebind it to the current default delivery target. Attended owner turns only. |
event_remove |
read_write |
scheduling | Soft-cancel a stored date and its unsent reminders; with no id, cancels the date behind the last reminder delivered in this conversation. Attended owner turns only. |
reminder_snooze |
read_write |
scheduling | Defer one delivered reminder to a later time without touching the stored plan. Attended owner turns only. |
model_routing_config |
read_write |
config | Read or update model routing config (provider, model, effort). |
generate_image |
external_api |
media | Create or edit a raster image from a text prompt. The result is written under the sandbox media/ floor and sent to the current chat automatically. Backend is openai, openai_codex, xai, or google, configured via [fermix_core.tools.generate_image]. openai_codex runs on a connected ChatGPT/Codex login instead of an image API key — billed to that subscription. It is opt-in and fails loudly if the login is missing or not entitled; it never silently falls back to another backend. |
tool_help |
read_only |
system | Return full documentation for one registered capability. |
tool_search |
read_only |
system | Keyword search (using BM25, a standard text-relevance ranking) over deferred plugin/MCP tool schemas by description. Registered when tool-schema deferral is enabled (default on). |
tool_describe |
read_only |
system | Fetch the full schema for one deferred tool by name. Registered when tool-schema deferral is enabled (default on). |
tool_call |
read_only |
system | Invoke a deferred tool by name; the loop unwraps it before policy and dispatch, so the real tool’s own policy class is what gets enforced. Registered when tool-schema deferral is enabled (default on). |
request_directory_access |
external_api |
system | Ask the owner, in chat, to approve access to a directory the sandbox currently denies. Surfaces only on attended operator turns; the owner confirms with a single-use 60-second /confirm token, the grant persists to [sandbox] allowed_roots, and on chat channels the original request resumes automatically. Guest, scheduled, and delegated runs never see it, and unsafe roots (all of $HOME, ~/.ssh, OS roots) are refused before the owner is even prompted. See sandbox. |
send_attachment |
read_only |
channel | Send a local file through the active channel reply port. URLs are not fetched. |
react |
read_only |
channel | React to the user’s message with a single fitting emoji instead of a text reply, for pure acknowledgements (“ok”, “thanks”, 👍). Advertised only on turns whose channel supports message reactions (on restricted channels the emoji choice is constrained to the channel’s allowed set); a delivered reaction with no accompanying text ends the turn without a further model call. |
computer_use |
gui_control |
computer | Drive the host desktop by screenshot and mouse/keyboard, one action per call. Experimental and off by default. Registers for the model only when computer use is enabled and the native helper is installed; operator-only and never delegated to subagents. |
codex_run |
exec |
harness | Run a background coding task in a repository through the Codex CLI. Seeded when the coding harness is enabled and the Codex CLI is installed; advertised only once coding agents are approved. |
claude_code_run |
exec |
harness | Run a background coding task in a repository through the Claude Code CLI. Same seeding and approval gate as codex_run. |
codex_cloud_run |
exec |
harness | Submit a coding task to a Codex cloud environment and track it by bounded polling. Seeded only when cloud_enabled = true. |
list_coding_runs |
read_only |
harness | List coding runs, their status, and delivery state, with dead-lettered runs grouped separately. |
get_coding_run |
read_only |
harness | Fetch one coding run in full: status, diagnostics, usage, and delivery. |
cancel_coding_run |
read_write |
harness | Cancel an active local coding run by id. |
stop_tracking_coding_run |
read_write |
harness | Stop tracking a Codex cloud run (the vendor task keeps running). Seeded only when cloud_enabled = true. |
The browser tool controls a supervised local browser process; web automation belongs there. The computer_use tool is a separate, experimental capability that drives the host desktop itself — the real mouse and keyboard of the logged-in session, guided by screenshots. It is off by default, and it registers for the model only when two things hold: computer use is enabled in config and the native helper is installed. The setup Plugins page (the “Computer Use” card) does both — it sets [fermix_core.computer_use] enabled = true and fetches the helper. The helper is fetched from the compux project’s own signed release and verified against a SHA-256 checksum baked into your Fermix build, then cached under ~/.fermix/plugins/compux/; fermix plugins does not manage it. You can also set the config key yourself: a daemon that has computer use enabled but is missing the matching helper downloads it on its next start (daemon boot only, bounded to about 30 seconds, and fail-soft — a failed fetch leaves computer use off until the next restart or the setup card). Supported on Apple Silicon (M-series) macOS and Linux x86_64. Config changes apply on the next daemon restart. Operating-system permissions (macOS Screen Recording and Accessibility) are a separate diagnostic, not part of that gate — fermix doctor’s computer-use check reports what is missing, so an ungranted permission informs the operator rather than silently hiding the tool. How far it may act follows the sandbox mode (strict is look-only), and a session only starts from an attended origin — an interactive chat, fermix ask, or voice — never from a scheduled job. Before any action that would move the pointer or type — clicking, dragging, scrolling, moving the mouse, typing, pressing a key, or pasting — Fermix checks how long the machine has been idle. If you are actively using it, the agent waits a few seconds for a gap, then either takes its turn or holds the action back and tells you it stepped aside. Idle detection is macOS-only. Where it is unavailable the action proceeds, so this is a courtesy that keeps the agent from fighting you for the cursor, not a safety boundary. To take the machine back yourself, /pause hands the cursor and keyboard back to you: the computer-use session stays alive and refuses further actions until you /resume — unlike /stop, which tears the session down. See computer use for the full platform, permission, action-loop, and safety model.
The seven coding-agent tools follow the same conditional-registration pattern, with an added consent gate — both gates code-enforced. The coding agents feature (internally, the coding harness) hands repository work — reviewing changes, fixing bugs, building features — to the operator’s own Codex or Claude Code CLI, run inside the target repository. Each run tool seeds at daemon boot only when its vendor CLI is on the daemon’s PATH (the history tools seed when either CLI is present); installing a CLI takes effect on the next restart. Seeding alone advertises nothing: no harness tool is offered to the model until the owner approves coding agents once, on the setup Coding Agents tab or with [fermix_core.harness] approved = true. Until then the whole family is absent from the prompt and Fermix does coding work with its own file and shell tools — there is no in-chat consent prompt, and Fermix can never grant the approval itself. The tools stay dispatchable by name while unadvertised, so a run recorded earlier can still be read or cancelled; a by-name launch on an unapproved machine is refused with a typed error naming the fix. default_vendor filters visibility, not capability: with both CLIs installed, only the preferred vendor’s run tool is advertised and the other stays callable by name. The two cloud tools, codex_cloud_run and stop_tracking_coding_run, are not seeded at all unless cloud_enabled = true. Every harness tool is additionally authorized at execute time: the caller must be a live attended operator turn, or a scheduled job that names the exact tool in its allowed_tools — guests, delegated subagents, unattended runs, and voice calls are refused with typed errors.
For per-tool parameter schemas, failure modes, and usage examples, see the tool reference.
Tool categories
Categories let a runtime context exclude a class of tools without naming each one individually (excluded_categories: [:scheduling]).
| Category | Tools |
|---|---|
:system |
shell, tool_help, skill_list, tool_search, tool_describe, tool_call, request_directory_access, sandbox preset commands |
:file |
file_read, file_write, file_edit, glob_search, content_search |
:git |
git_read, git_write |
:web |
web_fetch, web_search, place_search, browser |
:media |
generate_image |
:memory |
memory_recall, memory_store, memory_sources_list |
:scheduling |
schedule_job, update_job, list_jobs, list_job_runs, get_job_run, run_job_now, pause_job, resume_job, remove_job, event_store, event_list, event_update, event_remove, reminder_snooze |
:delegation |
subagents |
:skill_admin |
skill_create, skill_reload, skill_view, skill_run |
:config |
model_routing_config |
:channel |
send_attachment, react |
:computer |
computer_use |
:harness |
codex_run, claude_code_run, codex_cloud_run, list_coding_runs, get_coding_run, cancel_coding_run, stop_tracking_coding_run |
:plugin |
Every tool registered by a ready plugin |
How tools are presented to the model
MainAgent fetches the filtered capability list for the current trust level and conversation context, then formats each capability’s JSON Schema into the provider-specific tool-call format before passing it to AgentLoop. The loop calls the provider, parses any tool calls in the response, looks up each capability in the registry by name, and dispatches to the executor. Results are appended to the message list and the loop continues until the model issues a final text response or the iteration cap is reached (default 100 for interactive, sub-agent, and scheduled-job turns; these caps are internal system settings, not config.toml keys).
Sandbox enforcement applies to every shell call regardless of how it reaches the executor. See sandbox for path grants, env allow/deny, and command presets.
Tool-schema deferral
By default, the full definitions (schemas) for plugin and MCP tools are left out of the request sent to the provider to save tokens; only the tool names remain listed in the runtime prompt. The model fetches a full definition on demand when it actually needs that tool. Three bridge tools handle these deferred tools:
| Bridge | What it does |
|---|---|
tool_search |
BM25 search over the deferred catalog to find tools by description. |
tool_describe |
Fetch the full schema for one deferred tool by name. |
tool_call |
Invoke a deferred tool by name; the loop unwraps it so traces and policy see the real tool name. Direct calls by name also work. |
Deferral is on by default. To disable it, set [fermix_core.tools.tool_search] enabled = false in config.toml. With deferral off, all schemas are included inline on every call.
Discovery honors the caller’s own ceiling. tool_search, tool_describe, and tool_help only reveal tools the caller could actually dispatch: a guest cannot enumerate or read the schema of an owner-only or above-ceiling tool, and a policy-narrowed scheduled job or subagent worker sees only its own allowed classes. A capability outside the caller’s ceiling answers with the same “Unknown capability” error as one that does not exist — the answer never confirms what the caller may not see.
Skills and MCP as capability sources
Skills are not registered as provider-visible tools. They live in their own SkillRegistry; the model discovers them through a compact skill catalog in the runtime prompt and interacts with them via the skill_view and skill_run built-ins. Each skill’s SKILL.md still defines its own allowed-tool boundary (allowed_tools), so a skill’s sub-turns can be restricted to a subset of built-ins. Bundled and locally installed skills run at operator trust; plugin-shipped skills run at guest trust. A skill’s name may not collide with a registered capability name.
Ready plugins register their tools directly into the same registry (as :builtin kind, tagged plugin_owned? in metadata so they can be swapped atomically on plugin reload). A plugin that runs a local MCP process contributes its tools through MCP discovery instead, named <plugin>_<tool>.
Outbound MCP tools enter as :external_api policy class by default (overridable per tool with policy_class in [mcp.servers.<server>.tools.<tool>]) and are removed when the MCP connection drops. Operators can set hidden_from_agent = true on any MCP tool in config.toml to keep it configured but invisible to the model.
See skills, plugins, and MCP for setup and configuration details.