/opt/metnos/executors/ and ones generated on the fly by Synt in
~/.local/share/metnos/executors/ (kept apart by design).
Manifest signed with Ed25519, runtime fence built on the manifest itself, life cycle managed by the loader.
See also agent_runtime
(planner loop), sandbox
(the fence) and synt
(automatic generation).
executor — how it is madeAn executor is a small specialised utility that does one thing only: reads mail, finds a file, takes a snapshot of the clock, sends a message. It lives on its own, in its own folder, and talks to Metnos through a precise 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:
An executor is a folder on disk. Inside there are at most five files, each with its own job. Nothing more.
The three required files are the heart. The code does the work. The manifest describes the code so that the planner can decide whether to use it. The signature proves the manifest has not been tampered with since the author wrote it.
| File | What it holds | Who reads it |
|---|---|---|
manifest.toml | Name, description, arguments, examples, output schema, sandbox profile | the planner (to choose), the loader (to load) |
manifest.toml.sig | Ed25519 signature of the manifest bytes | the loader, when the catalog comes up |
<name>.py | An invoke(args) function that performs the work | the runtime, when the tool gets called |
manifest.lang_state.json | Per-language description hashes (used by the translation system) | the nightly translation daemon |
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.
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.
An executor does not roam free on the system. It lives inside a fence, built from the permissions its manifest declares. The fence sets: what it can read on disk, what it can write, which network hosts it can speak to, which system files are off limits even if the executor were to ask for them.
The golden rule is simple: the fence is always tighter than the manifest.
If a manifest asks to read /home, the fence may still cut the request down
to /home/user/notes. If it asks to talk to any domain, the fence may
restrict it to imap.example.com. The difference matters: the manifest is
what the author hoped for, the fence is what the system decided after
looking at who is calling, in which context, with what trust level.
| What | Where it is written |
|---|---|
| What the executor would like to do | in the manifest, capabilities block |
| What it actually may do in the current turn | in the fence applied by the sandbox at invocation time |
| What no one may ever do | in the «core rules», non-negotiable |
/etc/shadow (the system password file), here is what happens: the
manifest does not request that permission, the sandbox catches the call and refuses it,
the executor receives a «permission denied» error. Even if the code were briefly
compromised, there is no way to step outside the fence.
An executor is not born active and does not stay active forever. It moves through a series of states the system manages on its own. Knowing these states helps you read the catalog: if a tool is not being picked anymore, it might be because it slipped into quarantine, not because it is broken.
| State | Meaning | In pool? |
|---|---|---|
| proposed | only the manifest exists, the code is missing | no |
| synthesised | code present and tests green, awaiting signature | no |
| active | signed and visible to the planner | yes |
| quarantine | replaced by another or under suspicion, but recoverable | visible in audit, kept out of new picks |
| archived | out of the system; kept only for historical audit | no |
All executors share the same external anatomy, but they are born in four different ways. The distinction is not cosmetic: it changes who writes them, where on disk they live, and where the ideas behind them come from.
The author crafts them with care. They form the stable core, the seed from which everything else grows.
Folder: /opt/metnos/executors/
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/
Examples: specialisations or generalisations of existing seeds.
Kept apart: they can never overshadow handwritten seeds.
A public skill (e.g. from agentskills.io) describes in plain English how to use a third-party service. A deterministic translator turns 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 runtime services. They have no folder of their own; they live within Metnos.
Folder: runtime/system/
Examples: undo_last_turn, scheduler, admin.
System services, not regular tools.
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
/opt/metnos/executors/: imported executors live in a separate
folder, under watch, and still pass through the vetting
before every call.
One operational detail: imported executors that need credentials (an API key for an external service, the session file of an account) do not make them up. The first time you call one, it opens a dialog window, asks you for the missing data, encrypts it in a local vault, and proceeds. Subsequent calls find the credential already in place. No passwords in code, no configuration files to edit by hand.
Three 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).
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 Lucia»Sends one or more messages over Telegram (today) or email (coming soon). 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 broader permissions (net:write), they go through a check
before execution, and their effect stays tracked so it can be undone. 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 |
| observability (what happened, why, when) | observability |
For design decisions (why this choice and not another) there is a registry in
/opt/metnos/decisions/. Examples: why generated executors live
in a different folder from handwritten ones; how to ask for a password
without ending up with it in plain text in the logs; how to index
thirty thousand photos without blocking everything else.
Metnos — executor, didactic introduction