← Documentation index Architecture guide › executor

Metnos

executor — how it is made
Introductory guide

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.

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.

Contents

  1. What an executor is (in thirty seconds)
  2. How to read executor types: placement, intelligence, and parallelism
  3. Anatomy: manifest, signature, and implementation
  4. The manifest: the calling card
  5. The fence: what it can and cannot do
  6. An executor's life
  7. Four origins: handwritten, generated, imported, builtin
  8. Four concrete examples
  9. Going deeper

1. What an executor is (in thirty seconds)

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 dimensions, not confusing labels

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.

1. PlacementWhere it can run
Serverscope = "server": it runs only where Metnos is hosted. Paired devicescope = "device": it runs on an eligible, recognised device. Server or devicescope = "any" with device_ok = true: it starts from the server and may be sent to the device named in the request.
2. IntelligenceHow it approaches the task
DeterministicApplies code, rules, or an algorithm without an internal model. LLMUses a model for part of the task while retaining the same contract, limits, and controls. AgenticHas a bounded internal observation-and-proposal loop; this is declared explicitly, never inferred from its name.
3. ParallelismWhether it can run alongside others
Serialparallelism_class = 0: the conservative default. Eligible for parallelismClasses 1–3 are an explicit opt-in: the central scheduler decides actual admission from verified equivalence, effect, resource, and limits. Same resource, same queueInvocations that are not read-only and share a concurrency identity 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_processes

Server or device · deterministic · serial.

find_files

Server or device · deterministic · verified class 3.

act_sites

Server · agentic · serial.

arguments {tz: "Europe/Rome"} get_now one thing only: returns date and time manifest signature result 2026-05-06 16:45 what I ask the tool that runs what I get back
An executor is a box with a precise job, an inbound contract, and an outbound contract.

Three things define it:

Why executors are small

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.

2. Anatomy: manifest, signature, and implementation

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.

get_processes/ manifest.toml the calling card: name, what it does, arguments, examples, output schema manifest.toml.sig the cryptographic signature of the calling card: it proves who wrote it get_processes.py the actual code: an invoke(args) function that performs the task manifest.lang_state.json translation state (multilingual): only when descriptions exist in more than one language common layout of the distributed catalog
The layout used by distributed executors: manifest, signature, entry point, and translation state. The manifest remains authoritative for the signed code files.

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.

FileWhat it holdsWho reads it
manifest.tomlName, description, arguments, examples, output schema, declared capabilities, and execution policythe planner (to choose), the loader (to load)
manifest.toml.sigEd25519 signature of the manifest bytes; the manifest also carries the code digestthe loader during catalog admission
<entrypoint>.pyThe subprocess entry point; additional files may be declared in [code].filesthe runtime when the executor is invoked
manifest.lang_state.jsonPer-language description fingerprintsthe 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.

3. The manifest: the calling card

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.

name = "get_now" version = "0.2.0" affinity = ["time", "ora", "data", "now", "what time"] [description] en = "SCOPE: gets the current date and time..." [args.properties.timezone] type = "string" default = "Europe/Rome" [output] schema_inline = "{ ok: bool,... }" [code] files = ["get_now.py"] digest = "sha256:788db417..." [[capabilities]] name = "time:read" identity keywords helps the planner find this tool what it does a clear sentence, translated per language arguments type, default, description result shape what comes out of the tool, field by field code + fingerprint the digest changes if the code is touched permissions what it may touch
An excerpt from the current 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.

What deserves a second look

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.

One execution policy

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.

ClassPortable meaningBehaviour
0No threadStays on the caller thread.
1ModerateUses a small share of the central pool.
2HighRequests more concurrency within resource limits.
3MaximumStill 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.

Why the contract uses TOML

A manifest must remain readable, commentable, and editable during review. TOML preserves that legibility while retaining a structure that the loader can validate rigorously.

4. The fence: what it can and cannot do

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.

LayerCurrent server behaviour
Contractcapabilities, placement, platforms, and execution policy come from the signed manifest.
Application controlsArguments may narrow a signed scope but never widen it; identity, consent, and destination are checked before the subprocess starts.
Bubblewrap activeCode 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 disabledThe 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.

5. An executor's life

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.

StateMeaningIn pool?
proposedTriage metadata without code files; the loader rejects it if it binds code.No; visible only to work and audit surfaces.
synthesizedA 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.
activeAn admitted contract visible to the planner.Yes, unless disabled or dormant because prerequisites are missing.
deprecatedExcluded from new compositions; retained as a compact index for diagnosis and replacement.No.
archivedExcluded 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.

The single birth boundary

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.

6. Four origins: handwritten, generated, imported, builtin

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.

Handwritten

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.

Generated on the fly

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.

Imported from an external skill

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.

Builtin

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.

Precedence of the curated catalog

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.

Why the imported origin exists

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.

7. Four concrete examples

Four executors actually in use, told from the outside. No source code: just what you ask for and what you get.

7.1 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.

7.2 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.

7.3 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.

7.4 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.

8. Going deeper

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 executoragent_runtime
the fence in detail (forbidden paths, exceptions, «sort the photos»)sandbox
how the Synt composes new executorssynt
how an external skill is imported as an executorskill importer
the check that precedes the execution of risky actionsvaglio
how the user sees and approves actionsapproval_ux
the memory that executions leave behindmnest 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 TOMLmodel 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