An executor is an operational unit whose arguments, effects, capabilities, and isolation profile are declared in a signed manifest. The system keeps shipped, generated, and imported executor provenance separate; the generated catalog supplies current counts, domains, and status without duplicating them on this page.
executor — how it is madeAsk Metnos with a request like this example: “Find every PDF in Documents that contains the word contract.” Metnos may compose several executors: one finds the files, another reads their contents, and a third filters the results. The user describes the outcome and does not need to know executor names.
An executor is a small specialised operational unit that does one thing only: reads mail, finds a file, obtains the time, or sends a message. Subprocess executors have their own folders; in-process executors retain the same logical contract.
Think of a toolbox. Every tool is simple and recognisable: the screwdriver tightens screws, the hammer drives nails. Nobody would ask the screwdriver to drive a nail. In Metnos it is the same: every executor is a tool with a clear job. When the user asks for something, the planner picks the right tool and uses it.
Three things define it:
The more tasks a tool combines, the harder it becomes to verify its contract, authority, and effects. Separating file deletion from mail delivery makes every step legible and controllable. When Metnos composes several executors, the plan and each execution retain their own controls.
Executors in the distributed catalog normally use a flat folder containing four files. The contract does not impose that number: a manifest may list multiple code files, and in-process executors keep their signed contracts separate from their implementations.
For an active subprocess executor, manifest, signature, and code form the core. The code does the work, the manifest lets the planner evaluate its contract, and cryptographic verification detects changes made after signing.
| File | What it holds | Who reads it |
|---|---|---|
manifest.toml | Name, description, arguments, examples, output schema, declared capabilities, and execution policy | the planner (to choose), the loader (to load) |
manifest.toml.sig | Ed25519 signature of the manifest bytes; the manifest also carries the code digest | the loader during catalog admission |
<entrypoint>.py | The subprocess entry point; additional files may be declared in [code].files | the runtime when the executor is invoked |
manifest.lang_state.json | Per-language description fingerprints | the tools that keep translations aligned |
The loader does not infer a contract from folder shape. It reads the
manifest, validates the standard, signature, and code digest, checks the entry
point, and only then admits the executor. Contracts for in-process executors
live under runtime/builtin_executor_contracts/ and undergo the same
cryptographic verification.
The manifest is a TOML file. You open it with a text editor and you can make sense of it even without being a programmer. It declares everything the planner needs to know: the executor's name, what it does, which arguments it takes, what the result looks like, a few examples for orientation.
Three things stand out, because the whole system rests on them.
The code fingerprint (digest): a cryptographic fingerprint
computed over the bytes of the .py file. If anyone modifies even a single
comma in the code without recomputing the fingerprint, the loader rejects the executor.
The manifest and the code are bound like a certificate and the document it certifies.
The result shape (output.schema_inline): declares field by
field what the executor returns. It serves whoever composes chains of executors (the
planner does not fly blind: it reads the schema and knows what to expect at the next
step) and whoever auto-generates code.
For facts that must not be softened by generated prose, an executor may also
return an authoritative_presentation with a closed semantic scope.
Exact counts and duplicate groups are examples. The runtime uses those fragments
only when every productive step in the turn is covered; otherwise it keeps the
ordinary finalizer. A display limit therefore remains distinct from the work
performed: a fragment may claim a complete scan only when the executor also
attests that its source was complete. A fragment suppresses the ordinary
truncation notice only when this all-or-nothing composition succeeds for the
whole turn; a covered step cannot hide a limit in an otherwise uncovered
pipeline.
Identifiers may also carry manifest-declared source context. An array can map
entries[*].uid through from_entries_key, while scalar
properties such as account and source folder can declare a conditional
from_entries_required. The projection accepts those values only
when every source entry agrees. A direct call must provide the same applicable
context explicitly, and the invocation choke-point rejects it before any side
effect if that context is absent.
The permissions (capabilities): the manifest does not
decide what the executor may do. The manifest declares what it would need in
order to function; the system then decides whether to grant those permissions and
under which constraints. See sandbox.
[[capabilities]]
name = "provider:access"
hint = ["google-workspace"]
when = { arg = "client", values = ["google_workspace"] }
For a remote backend, the when clause narrows the declaration to one
invocation. Only when the final client value matches does the same binding
enable network access, the read-write credential home, and server placement. An
arbitrary value in client grants nothing.
Every invocation passes through a central runtime scheduler. If the
[execution] section is missing, incomplete, or invalid, the loader
always selects serial execution. Parallelism is therefore an opt-in
property of the signed contract, admitted only after repeated equivalence tests
between serial and concurrent execution.
| Class | Portable meaning | Behaviour |
|---|---|---|
0 | No thread | Stays on the caller thread. |
1 | Moderate | Uses a small share of the central pool. |
2 | High | Requests more concurrency within resource limits. |
3 | Maximum | Still bounded by hardware, backend, and global ceilings. |
At startup the runtime observes visible CPUs and the operational
max_workers setting and establishes one instance ceiling. A signed
class is a reduction of that ceiling, not a fixed thread count:
an executor may only lower its assignment according to available work or its
I/O profile. In one place the runtime applies bounded queues, backpressure,
per-resource pools, and metrics without changing arguments, results, causal
order, permissions, or success criteria. A non-read-only executor may declare a
positive class, but it must also provide a resolvable concurrency identity;
invocations sharing that identity remain serial.
LLM-backed executors follow the same rule. At startup, framework and hardware determine the LLM resource ceiling: a single-slot backend degrades to class 0, whereas a batching backend can admit more requests. Executors do not hide independent competing concurrency pools.
Recursive filesystem searches use one shared visitor: directories form a dynamic queue, idle workers take the next branch, and results are reordered before a limit is applied. Exact duplicate search, for example, filters by size and samples before computing complete SHA-256 digests for the remaining candidates; its display limit does not reduce the compared source.
A manifest must remain readable, commentable, and editable during review. TOML preserves that legibility while retaining a structure that the loader can validate rigorously.
The manifest declares the maximum capabilities requested by the executor. Before invocation, the runtime checks the concrete arguments, resolves only the required resources, and applies identity, vaglio, and policy controls. The sandbox is an additional containment layer; it grants no authority and does not replace those controls.
| Layer | Current server behaviour |
|---|---|
| Contract | capabilities, placement, platforms, and execution policy come from the signed manifest. |
| Application controls | Arguments may narrow a signed scope but never widen it; identity, consent, and destination are checked before the subprocess starts. |
| Bubblewrap active | Code and runtime are read-only, /tmp is private, data resources are mounted with the required access, and the network is separated when no capability needs it. |
| Bubblewrap absent or disabled | Application checks remain, but the command runs directly and must not be described as operating-system isolation. |
Network access is currently binary: when required, the process inherits
the host network; there is no domain allowlist. Some system roots, including
/etc, are visible read-only. See the sandbox
guide for mounts, exceptions, downgrades, and the differences among Linux,
Windows, and macOS.
The lifecycle field separates candidates from executors available
to the planner. It is not a mandatory sequence for every origin: reviewed,
distributed executors may begin as active, while Synt candidates
pass through preparation and admission states.
| State | Meaning | In pool? |
|---|---|---|
proposed | Triage metadata without code files; the loader rejects it if it binds code. | No; visible only to work and audit surfaces. |
synthesized | A candidate with code. When signature verification is enabled, it must already pass signature, digest, and entry-point checks, but the composer still cannot use it. | No; available to the Synt path. |
active | An admitted contract visible to the planner. | Yes, unless disabled or dormant because prerequisites are missing. |
deprecated | Excluded from new compositions; retained as a compact index for diagnosis and replacement. | No. |
archived | Excluded from the operational catalog; its state remains in the lifecycle registry. | No. |
Automatic inactivity ageing applies only to Synt-generated executors: after
30 unused days they become deprecated, and after another 14 days
in that state they become archived. Handcrafted executors,
skill imports, and protected names are not retired merely because they are
rarely used. The thresholds are configurable and restoration is explicit.
All executors expose the same logical contract, but they originate in four different ways. The distinction is not cosmetic: it changes who writes them, where they live, and which provenance is recorded.
The author crafts them with care. They form the stable core, the seed from which everything else grows.
Folder: executors/ in the installation.
Examples: get_now, find_files, read_messages, send_messages.
Tight, robust, reviewed many times.
When the catalog does not cover a request, the Synt composes a new executor in five steps (name, contract, tests, description, code).
Folder: ~/.local/share/metnos/executors/
Example: a narrow candidate prepared through a governed request when the catalog lacks a required capability.
Kept apart: they can never overshadow handwritten seeds.
A public skill describes how to use a third-party service. Parsing and deterministic closed-vocabulary mapping turn it into one or more Metnos executors.
Folder: ~/.local/share/metnos/executors/skills/
Examples: read_events, set_events, delete_events (from a calendar skill).
Same checks as a generated one: no special treatment for coming from outside.
Internal services executed in process. Their implementations live in the runtime, separately from their signed contracts.
Contracts: runtime/builtin_executor_contracts/
Examples: admin, create_tasks, list_skills, describe_images.
System services, not regular tools.
A generated or imported executor cannot use the name of a handwritten executor. Admission rejects the collision. If it is discovered while the catalog loads, the curated executor remains active and the synthesized folder is moved to a recoverable temporary area. Generation or a third-party skill therefore cannot silently replace the catalog's curated core.
A third-party library typically has its own textual documentation explaining
how to use it: «to list the calendar, call gws calendar list;
to create an event, use --summary and --start».
The agentskills.io standard encodes this documentation in a precise
format (a Markdown file with a structured front matter). Metnos's importer
reads that format, translates it into the system's closed vocabulary, and
generates the executor folder as if it had been handwritten.
The upside: any service already documented as a skill (Google calendar,
mail, file storage,...) can be brought into Metnos without rewriting from
scratch. The downside: you have to trust whoever wrote the skill (and its
helper scripts). For this reason the importer does not install anything in
executors/: imported executors live in the separate data
folder, under watch, and still pass through the vetting
before every call.
One operational detail: an executor that requires credentials remains dormant and is excluded from the planner pool until its prerequisites are available. The configuration flow may collect the data through a dialog and store it encrypted; it is not correct to promise that every first call can always continue automatically. See the skill importer guide for the admitted cases.
Four executors actually in use, told from the outside. No source code: just what you ask for and what you get.
get_now — «what time is it?»The simplest tool in the catalog. No required arguments. Returns a dictionary with the current date and time.
call: get_now(timezone="Europe/Rome")
response: { ok: true,
content: "2026-05-06T16:45:23+02:00",
metadata: { timezone: "Europe/Rome", iso8601: "...", epoch:... } }
No network, no file read, no write. Permissions: time:read. It is one of
those tools that look superfluous until you see why they matter:
the planner must never invent the date from training memory. When it
needs to compute «yesterday's mail», it calls get_now first, then
subtracts a day. That way «yesterday» is always today's yesterday, not the
yesterday from when the model was trained.
find_files — «find the photos»Searches files by name or pattern (the classic «extensions»). Returns the list together with basic metadata: path, name, size, last modified, type.
call: find_files(base_path="/home/user/images", pattern="*.jpg")
response: { ok: true,
entries: [
{path: "/home/.../foto1.jpg", size: 2458123,...},
{path: "/home/.../foto2.jpg", size: 1923456,...},...
],
metadata: { count: 247,... } }
The planner uses it when it has to hand the list off to another tool: for example to
keep only the most recent photos, compute total size, compress the older ones. Note
that filtering does not live in find_files: the tool returns the
files and that is it. If you want a subset, you ask via the pattern, or you pipe the
result into a tool that filters. One thing at a time.
filter_lists — «find overlapping events»A tool that works on two lists rather than one. Useful when a user question intersects two sets: "which HLT appointments overlap with MNM ones in the next three months?", or "which files are present in both folders?".
call: filter_lists(op="overlap",
from_step=2, # list A (HLT events)
with_step=3) # list B (MNM events)
reply: { ok: true,
op: "overlap",
entries: [...entries of A that overlap with at least one of B... ],
metadata: { count_a: 4, count_b: 5, count_out: 0 } }
Available operations: intersect (entries common to both
lists, match on a key), union (entries from either, deduped),
difference (entries of A not in B), symdiff
(symmetric difference), overlap (temporal AND: entries of
A whose time window overlaps with at least one in B; auto-detect
start/end), and delta (items that are new or have advanced from a
reference list, used by incremental monitors).
The taxonomy of list operators is closed and readable:
filter_entries reduces a single list (predicates:
where_starts_with, where_contains, where_glob, where_regex),
filter_lists combines two lists with set ops,
compute_entries computes a scalar (sum, average, min, max,
count). The three primitives together cover almost every list
manipulation without inventing new verbs.
send_messages — «send a message to a family member»Sends one or more messages over Telegram or email. Main argument: a list of messages, each with recipient and body.
call: send_messages(messages=[
{to_user: "lucia", body: "Out for a bit, back at seven."}
])
response: { ok: true, ok_count: 1, fail_count: 0,
results: [{to_user: "lucia", channel: "telegram", message_id: "abc123"}] }
A «transformative» tool: it changes the world, it really sends a message. For
this reason, executors that change the world get treated with more care: their
manifest declares mail:send and channel:out, and the
destination is resolved against the bound user and channel. Delivery is
recorded but cannot be undone: the manifest declares
revertible=false. Controls must therefore run before delivery. See
vaglio for pre-execution checks and
approval_ux for how the user is asked to confirm.
This document is an introduction. If you want to understand the mechanisms beneath — how a manifest gets signed, how the fence is applied, how the Synt generates code, how the planner picks a tool — the documents below should be read one at a time.
| To understand… | Read |
|---|---|
| the planner that picks the right executor | agent_runtime |
| the fence in detail (forbidden paths, exceptions, «sort the photos») | sandbox |
| how the Synt composes new executors | synt |
| how an external skill is imported as an executor | skill importer |
| the check that precedes the execution of risky actions | vaglio |
| how the user sees and approves actions | approval_ux |
| the memory that executions leave behind | mnest and mnestome |
| the dialogue with the world (Telegram, web, voice) | channel |
| which models (LLM, embedding, VLM) power the executors and how to change them from a TOML | model virtualization |
| observability (what happened, why, when) | observability |
You do not need to read these guides in order. Start with the concrete question, follow the relevant links, and stop at the level of detail you need.
Metnos — executor, didactic introduction