executor — how it is madeAn 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.
Ask 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.
The words local, remote, intelligent, and parallel do not describe four alternative families. They answer three independent questions. The same executor can, for example, be deterministic, eligible for a paired device, and still remain serial.
How to read this card. Placement grants no new permissions; intelligence grants no new authority; parallelism is not a promise of speed. These are facts declared by the contract and enforced by the runtime. For the protocol between server and PC, see remote executors; for executors with an internal reasoning loop, see intelligent executors.
get_processesServer or device · deterministic · serial.
find_filesServer or device · deterministic · verified class 3.
act_sitesServer · agentic · serial.
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.
get_now manifest, shortened
for readability. It shows representative fields, not the complete manifest
schema.About affinity. It is
an intentionally language-independent list of canonical routing hints, not
user-facing prose. The multilingual validator checks its structure and overlap;
natural-language recognition instead belongs to the versioned detection
lexicon, where coverage and review policy are explicit. See
Language and internationalisation.
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 | The server fails closed before journaling and before the subprocess. Only the exact byte-pinned undo broker has a direct path. |
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. A distributed release may load an exact generation that was
already admitted, but no new or revised generation becomes active
merely because its source is reviewed. Every origin crosses the same Executor
Birth boundary before activation.
| 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.
Executor Birth takes a private snapshot of the complete candidate, assigns stable identities to its exact bytes and admission context, and applies every mandatory structural, policy, localisation, routing, and test check. A model-authored or imported revision also receives independent semantic review; an exact human approval is required where policy says so. A failed or unavailable mandatory check stops the transition.
Only the Birth commit boundary can send an admitted candidate to the
immutable contract store. It then rereads the published manifest and code
before exposing the generation as active, pre-exercise, or quarantined. A
signature by itself, a copied directory, or a service restart cannot make
changed bytes operational. Maintainer updates use
runtime/stack_reconcile.py deploy --executor <name> --sign;
the compatibility flag submits the candidate to Executor Birth and does not
select the former direct publisher.
During transition, the inventory and first cold catalog load use the same author public-key set already authenticated by Birth. In exercise, readers use the authorities of the installed Birth context; separate administrative processes authenticate them through the current ownership chain. In the closed store, missing keys or an invalid context stop the read: the legacy key directory is never consulted as a fallback.
Before requiring the new distribution, the transition binds the candidate's authenticated bytes to its durable receipts. It does not prematurely require the build archive published only after the certificate, but rejects an already present conflicting archive. The contract-receipt catalog and the service catalog have distinct identities: both are checked, without using either as proof of the other or advancing publication.
The service-recipe check also binds writable paths to the account's signed home directory. Relocating that directory does not change the allowed recipe: the relative data and state paths remain exact. Additional or different paths are rejected even when their hashes have been recomputed.
For code dependencies between executors, the parent process selects only the public keys that authenticated the requested contracts. The Linux sandbox exposes these keys in a private read-only mount, separate from configuration paths; the mount stays empty when there are no dependencies. The child cannot add keys or replace the mount by creating a nested isolation environment. Explicitly permitted writes and the temporary directory remain available.
The Linux distribution includes the Playwright driver in its managed Python environment and preserves executable permissions for programs supplied by its packages; browsers remain separately installed resources. After the transition, readiness checks observe the units and scope bound by the current signed catalog, without changing services or broadening control commands. An invalid catalog stops the check: it is not silently replaced with the previous configuration.
The signed catalog starts HTTP with METNOS_ENGINE=v3, without
inheriting arbitrary unit drop-ins (.service.d) or launch-environment
additions. Private settings remain in the existing
$METNOS_USER_CONFIG/runtime.toml: default_account in
[mail] is a nonempty string, defaulting to
metnos_system; nightly_enabled in [telos]
is a boolean, defaulting to false.
METNOS_DEFAULT_MAIL_ACCOUNT and METNOS_TELOS_NIGHTLY
override their respective file settings; only the exact environment value
1 enables nightly Telos activity.
HTTP resolves the SMTP account once, after the Birth check and before workers start: executors inherit the resolved name without receiving the private file, while an explicit invocation account still takes precedence. An empty or wrongly typed default account and a non-boolean Telos file setting are rejected rather than silently replaced with another value. This precedence does not authorize changes to signed units or put personal values in the public catalog.
The service configuration is signed before its timers start. The automatic activation and ordering links of an exact signed timer may appear when it is loaded or started without requiring another signature. Equivalent disabled values of an unconfigured watchdog also have one canonical representation. For a configured watchdog, systemd's initial runtime sentinel is accepted only when the same observation proves the service inactive, dead, without main or control processes and never started. The exact signed setting remains the canonical value; a disabled or different watchdog after start is rejected. Unexpected triggers, new dependencies and changes to explicitly configured values remain subject to strict verification.
The service reads the signed ownership chain without opening the private root-owned writer lock; it still verifies the lock's exact metadata. Administrative checks also inspect its marker. Startup checks may reuse the pure source analysis for one identical set of paths and bytes in the same process, but never reuse live file checks, signatures or service observations. Declared imports may also reuse syntax analysis, but their filesystem resolution is always repeated. The service reader uses the same bounded reuse: its before/after observations remain independent and live without repeating syntax analysis of identical bytes. Reuse also binds the exact analysis limits. File-operation and external-command analysis applies only to relevant calls, without changing authority classification. Signal-termination reports are parsed in systemd's native form without changing the signed command or interpreting termination as success.
Every Python service module is resolved from the working directory declared
in the signed catalog. The long-running workload service uses the
runtime subdirectory and does not depend on paths inherited from
a development session. A regression checks this constraint for every declared
Python entry point.
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