Un executor è un'unità operativa con argomenti, effetti, capacità e profilo di isolamento dichiarati in un manifest firmato. Il sistema mantiene distinta la provenienza degli executor distribuiti, generati e importati; il catalogo generato fornisce conteggi, domini e stato corrente senza duplicarli in questa pagina.
executor — capire come è fattoChiedi a Metnos con una richiesta come quella di questo esempio: «Cerca nella cartella Documenti tutti i PDF che contengono la parola contratto». Metnos può comporre più executor: uno trova i file, un altro ne legge il contenuto e un terzo filtra i risultati. L'utente descrive il risultato voluto; non deve conoscere i nomi degli executor.
Un executor è una piccola unità operativa specializzata che fa una cosa sola: legge la posta, cerca un file, ottiene l'ora, manda un messaggio. Quelli eseguiti come sottoprocesso hanno una propria cartella; quelli interni al processo conservano lo stesso contratto logico.
Pensa a una cassetta degli attrezzi. Ogni attrezzo è semplice e riconoscibile: il cacciavite stringe le viti, il martello batte i chiodi. Nessuno chiederebbe al cacciavite di battere un chiodo. In Metnos è lo stesso: ogni executor è un attrezzo con un compito chiaro. Quando l'utente chiede qualcosa, il pianificatore sceglie l'attrezzo giusto e lo usa.
Le tre cose che lo definiscono sono:
Più compiti riunisce uno strumento, più diventa difficile verificarne contratto, autorizzazioni ed effetti. Separare la cancellazione dei file dall'invio di posta rende ogni passaggio leggibile e controllabile. Quando Metnos compone più executor, il piano e ciascuna esecuzione conservano i propri controlli.
Gli executor distribuiti nel catalogo principale usano normalmente una cartella piatta con quattro file. Il contratto, però, non impone il numero quattro: il manifest può elencare più file di codice e gli executor interni al processo conservano il contratto firmato separato dall'implementazione.
Per un executor attivo eseguito come sottoprocesso, manifest, firma e codice formano il nucleo. Il codice fa il lavoro; il manifest permette al pianificatore di valutarne il contratto; la verifica crittografica rileva modifiche successive alla firma.
| File | Cosa contiene | Chi lo legge |
|---|---|---|
manifest.toml | Nome, descrizione, argomenti, esempi, schema dell'output, capacità dichiarate e policy di esecuzione | il pianificatore (per scegliere), il loader (per caricare) |
manifest.toml.sig | Firma Ed25519 dei byte del manifest; il manifest contiene anche l'impronta dei file di codice | il loader, durante l'ammissione al catalogo |
<entrypoint>.py | Il punto d'ingresso del sottoprocesso; altri file possono essere dichiarati in [code].files | il runtime, quando l'executor viene invocato |
manifest.lang_state.json | Impronte delle descrizioni per lingua | gli strumenti che mantengono allineate le traduzioni |
Il loader non deduce il contratto dalla forma della cartella: legge il
manifest, verifica standard, firma e impronta del codice, controlla l'entry
point e soltanto allora ammette l'executor. I contratti degli executor interni
al processo si trovano in runtime/builtin_executor_contracts/ e
subiscono la stessa verifica crittografica.
Il manifesto è un file in formato TOML. Lo apri con un editor di testo e ci capisci qualcosa anche senza essere un programmatore. Dichiara tutto quello che il pianificatore deve sapere: come si chiama l'executor, cosa fa, che argomenti accetta, come è fatto il risultato, qualche esempio per orientarsi.
Tre cose meritano un secondo sguardo, perché sono quelle su cui si regge l'intero sistema.
L'impronta del codice (digest): è un'impronta crittografica
calcolata sui byte del file .py. Se qualcuno modifica anche solo una virgola
nel codice senza ricalcolare l'impronta, il loader rifiuta l'executor. Il manifesto e
il codice sono legati come un certificato e il documento che certifica.
La forma del risultato (output.schema_inline): dichiara
campo per campo cosa restituisce l'executor. Serve a chi compone catene di executor
(il pianificatore non vola alla cieca: legge lo schema e sa cosa aspettarsi al passo
successivo) e a chi genera codice in automatico.
Quando un dato non deve essere attenuato dalla prosa generata, l'executor può
restituire anche una authoritative_presentation con un ambito
semantico chiuso: per esempio un conteggio esatto o un insieme di duplicati
verificati. Il runtime usa questi frammenti soltanto se coprono tutti i passi
produttivi del turno; negli altri casi conserva il normale compositore finale.
Il limite di visualizzazione resta così distinto dal lavoro svolto: un frammento
può dichiarare completa una scansione solo se l'executor attesta anche la
completezza della sorgente. Il frammento sostituisce il normale avviso di
troncamento soltanto quando questa composizione integrale riesce per l'intero
turno: un passo coperto non può nascondere il limite di una pipeline che, nel
suo insieme, non è coperta.
Gli identificativi possono inoltre richiedere un contesto di origine dichiarato
nel manifest. Una lista può proiettare entries[*].uid tramite
from_entries_key; proprietà scalari come account e cartella di
origine possono dichiarare un from_entries_required condizionale.
La proiezione accetta questi valori soltanto se tutte le righe concordano. Una
chiamata diretta deve fornire esplicitamente lo stesso contesto applicabile; se
manca, il punto unico di invocazione la rifiuta prima di qualunque effetto.
I permessi (capabilities): non è il manifesto a stabilire
cosa l'executor può fare. Il manifesto dichiara di cosa avrebbe bisogno per
funzionare; poi il sistema decide se concedere quei permessi e con che vincoli.
Vedi sandbox.
[[capabilities]]
name = "provider:access"
hint = ["google-workspace"]
when = { arg = "client", values = ["google_workspace"] }
Per un backend remoto la clausola when restringe la dichiarazione alla
singola invocazione. Solo quando il valore finale di client corrisponde,
lo stesso binding abilita rete, home delle credenziali in lettura-scrittura e
collocazione sul server. Un valore arbitrario in client non concede nulla.
Ogni invocazione passa da uno scheduler centrale del runtime. Se la sezione
[execution] manca, è incompleta o non è valida, il loader sceglie
sempre la modalità seriale. Il parallelismo è quindi una proprietà
opt-in del contratto firmato, ammessa soltanto dopo test ripetuti di
equivalenza fra esecuzione seriale e concorrente.
| Classe | Significato portabile | Comportamento |
|---|---|---|
0 | Nessun thread | Resta sul thread del chiamante. |
1 | Moderata | Usa una quota contenuta del pool centrale. |
2 | Alta | Richiede più concorrenza, entro i limiti della risorsa. |
3 | Massima | È comunque limitata da hardware, backend e tetti globali. |
All'avvio il runtime osserva le CPU visibili e il limite operativo
max_workers, e stabilisce un solo tetto per l'istanza. La classe
firmata è una riduzione di quel tetto, non un numero fisso di
thread: il singolo executor può soltanto ridurre l'assegnazione in base alla
quantità di lavoro o al profilo di I/O. Applica nello stesso punto code limitate,
backpressure, pool per risorsa e metriche, senza cambiare argomenti, risultati,
ordine causale, permessi o criteri di successo. Un executor non di sola lettura
può dichiarare una classe positiva, ma deve anche fornire un'identità di
concorrenza verificabile; invocazioni sulla stessa identità restano seriali.
Gli executor che usano un LLM seguono la stessa regola. All'avvio, framework e hardware determinano il tetto della risorsa LLM: un backend a singolo slot degrada a classe 0, mentre un backend con batching può ammettere più richieste. Non esistono pool concorrenti indipendenti nascosti nei singoli executor.
Le ricerche ricorsive sul filesystem usano un visitor comune: le directory formano una coda dinamica, i worker liberi prendono il prossimo ramo e i risultati vengono riordinati prima di applicare un limite. Per esempio, la ricerca dei duplicati confronta prima dimensione e campioni, poi calcola SHA-256 completo sui soli candidati; il limite di visualizzazione non riduce l'insieme confrontato.
Il manifest deve poter essere letto, commentato e aggiornato durante una revisione. TOML conserva questa leggibilità senza rinunciare a una struttura che il loader può convalidare in modo rigoroso.
Il manifest dichiara le capacità massime richieste dall'executor. Prima dell'invocazione il runtime controlla gli argomenti concreti, risolve le sole risorse necessarie e applica i controlli di identità, Vaglio e policy. La sandbox è un ulteriore livello di contenimento; non concede autorizzazioni e non sostituisce quei controlli.
| Livello | Comportamento corrente sul server |
|---|---|
| Contratto | capabilities, collocazione, piattaforme e politica di esecuzione provengono dal manifest firmato. |
| Controlli applicativi | Gli argomenti possono restringere un ambito firmato, mai ampliarlo; identità, consenso e destinazione vengono verificati prima del sottoprocesso. |
| Bubblewrap attivo | Codice e runtime sono in sola lettura, /tmp è privato, le risorse dati sono montate con l'accesso necessario e la rete viene separata quando nessuna capacità la richiede. |
| Bubblewrap assente o disabilitato | Restano i controlli applicativi, ma il comando gira direttamente: non va descritto come isolamento del sistema operativo. |
La rete è attualmente binaria: quando serve, il processo eredita la rete
dell'host; non viene applicata una lista di domini. Alcune radici di sistema,
fra cui /etc, sono visibili in sola lettura. Per montaggi, eccezioni,
declassamenti e differenze fra Linux, Windows e macOS, vedi la guida alla
sandbox.
Il campo lifecycle separa i candidati dagli executor disponibili
al pianificatore. Non descrive una sequenza obbligatoria per ogni origine: gli
executor distribuiti e revisionati possono nascere direttamente come
active, mentre i candidati di Synt attraversano i passaggi di
preparazione e ammissione.
| Stato | Significato | È in pool? |
|---|---|---|
proposed | Metadati di triage senza file di codice; se dichiara codice, il loader lo rifiuta. | No; visibile soltanto alle superfici di lavoro e audit. |
synthesized | Candidato con codice. Quando la verifica delle firme è attiva deve già superare firma, impronta ed entry point, ma non è ancora ammesso al composer. | No; disponibile al percorso Synt. |
active | Contratto ammesso e visibile al pianificatore. | Sì, salvo disabilitazione o dormienza per prerequisiti mancanti. |
deprecated | Escluso dalle nuove composizioni; resta indicizzato in forma compatta per diagnosi e sostituzione. | No. |
archived | Escluso dal catalogo operativo; lo stato resta nel registro di durata. | No. |
L'invecchiamento automatico per inattività riguarda soltanto gli executor
generati da Synt: dopo 30 giorni senza uso diventano deprecated e,
dopo altri 14 giorni in quello stato, archived. Gli executor curati
a mano, quelli importati come skill e i nomi protetti non vengono ritirati per
il solo fatto di essere usati di rado. Le soglie sono configurabili e il
ripristino richiede un'azione esplicita.
Tutti gli executor espongono lo stesso contratto logico, ma nascono in quattro modi diversi. La distinzione non è cosmetica: cambia chi li scrive, dove vivono e quale provenienza viene registrata.
L'autore li compone con pazienza. Sono il nucleo stabile, il seed da cui parte tutto.
Cartella: executors/ nell'installazione.
Esempi: get_now, find_files, read_messages, send_messages.
Stretti, robusti, rivisti più volte.
Quando il catalogo non copre una richiesta, il Synt compone un nuovo executor con cinque passi (nome, contratto, prove, descrizione, codice).
Cartella: ~/.local/share/metnos/executors/
Esempio: un candidato ristretto preparato, su richiesta governata, quando il catalogo non copre una capacità necessaria.
Tenuti separati: non possono mai fare ombra ai seed scritti a mano.
Una skill pubblica descrive l'uso di un servizio terzo. Il parser e la mappatura deterministica nel vocabolario chiuso la convertono in uno o più executor Metnos.
Cartella: ~/.local/share/metnos/executors/skills/
Esempi: read_events, set_events, delete_events (da una skill calendario).
Stessi controlli di un generato: niente trattamento di favore per il fatto di venire da fuori.
Servizi interni eseguiti nel processo. L'implementazione vive nel runtime e il contratto firmato resta separato.
Contratti: runtime/builtin_executor_contracts/
Esempi: admin, create_tasks, list_skills, describe_images.
Servizi del sistema, non attrezzi normali.
Un executor generato o importato non può usare il nome di un executor scritto a mano. L'ammissione rifiuta la collisione; se questa viene rilevata durante il caricamento, il catalogo conserva l'executor curato e sposta la cartella sintetizzata in un'area temporanea recuperabile. Una generazione o una skill di terzi non può quindi sostituire silenziosamente il nucleo del catalogo.
Una libreria di terzi ha tipicamente una sua documentazione testuale che
spiega come si usa: «per leggere il calendario chiama gws calendar list;
per creare un evento usa --summary e --start». Lo standard
agentskills.io ha codificato questa documentazione in un formato
preciso (un file Markdown con intestazione strutturata). L'importatore di
Metnos legge quel formato, lo traduce nel vocabolario chiuso del sistema, e
genera la cartella dell'executor come se fosse scritta a mano.
Il vantaggio: ogni servizio già documentato come skill (calendario di Google,
posta, drive di archiviazione,...) si può portare in Metnos senza riscriverlo
da capo. Lo svantaggio: bisogna fidarsi di chi ha scritto la skill (e dei suoi
script di supporto). Per questo l'importatore non installa nulla in
executors/: gli executor importati vivono nella directory dati
separata, sotto sorveglianza, e passano comunque dal vaglio
prima di ogni chiamata.
Un dettaglio operativo: un executor che richiede credenziali resta dormant e viene escluso dal pool finché i prerequisiti non sono disponibili. Il flusso di configurazione può raccogliere i dati con un dialogo e conservarli cifrati; non è corretto promettere che qualunque prima chiamata possa sempre proseguire da sola. Vedi la guida allo skill importer per i casi ammessi.
Vediamo quattro executor realmente in uso, raccontati da fuori. Niente codice sorgente: solo cosa chiedi e cosa ottieni.
get_now — "che ora è?"L'attrezzo più semplice del catalogo. Non ha argomenti obbligatori. Ritorna un dizionario con la data e l'ora correnti.
chiamata: get_now(timezone="Europe/Rome")
risposta: { ok: true,
content: "2026-05-06T16:45:23+02:00",
metadata: { timezone: "Europe/Rome", iso8601: "...", epoch:... } }
Niente rete, nessun file letto, nessuna scrittura. Permessi: time:read.
È uno di quegli attrezzi che sembrano superflui finché non si capisce perché servono:
il pianificatore non deve mai inventarsi la data dalla memoria di addestramento.
Quando deve calcolare "le mail di ieri", chiama prima get_now, poi calcola
"ieri" sottraendo. Così "ieri" è sempre quello vero, non quello del giorno in cui il modello è stato addestrato.
find_files — "trovami le foto"Cerca file per nome o per modello (le classiche "estensioni"). Restituisce la lista con i metadati di base: percorso, nome, dimensione, data ultima modifica, tipo.
chiamata: find_files(base_path="/home/user/images", pattern="*.jpg")
risposta: { ok: true,
entries: [
{path: "/home/.../foto1.jpg", size: 2458123,...},
{path: "/home/.../foto2.jpg", size: 1923456,...},...
],
metadata: { count: 247,... } }
Il pianificatore lo usa quando deve passare la lista a un altro attrezzo: per esempio
filtrare le foto più recenti, calcolare la dimensione totale, comprimere quelle più vecchie.
Notare che la filtrazione non sta in find_files: l'attrezzo torna i
file e basta. Se vuoi un sottoinsieme, glielo chiedi col modello, oppure passi il
risultato a un altro attrezzo che filtra. Una cosa sola alla volta.
filter_lists — "trovami eventi che si sovrappongono"Un attrezzo che mette in relazione due liste invece di una. Lo usi quando la domanda dell’utente incrocia due insiemi: «quali appuntamenti HLT si sovrappongono a quelli MNM nei prossimi tre mesi?», oppure «quali file sono presenti sia in questa cartella sia nell’altra?».
chiamata: filter_lists(op="overlap",
from_step=2, # lista A (eventi HLT)
with_step=3) # lista B (eventi MNM)
risposta: { ok: true,
op: "overlap",
entries: [...gli eventi di A che si sovrappongono ad almeno uno di B... ],
metadata: { count_a: 4, count_b: 5, count_out: 0 } }
Le operazioni disponibili sono sei. intersect tiene
solo le entries presenti in entrambe le liste (il confronto avviene su
una chiave indicata, ad esempio il percorso del file).
union unisce le due liste eliminando i duplicati.
difference tiene le entries di A che non compaiono in B.
symdiff è la differenza simmetrica (quel che c’è
solo in A o solo in B). overlap è un’operazione
temporale: tiene gli eventi di A che cadono in un intervallo
sovrapposto a qualche evento di B, riconoscendo da solo i campi di
inizio e fine. delta individua invece elementi nuovi o avanzati
rispetto a una lista di riferimento e viene usato nei monitor incrementali.
Le tre primitive che lavorano sulle liste hanno ruoli complementari e
ben separati: filter_entries riduce una lista applicando
un predicato a un campo (where_starts_with,
where_contains, where_glob,
where_regex); filter_lists combina due liste
con le operazioni di insieme appena viste; compute_entries
calcola un singolo numero a partire da una lista (somma, media,
minimo, massimo, conteggio). Le tre primitive coprono insieme la
quasi totalità delle manipolazioni che servono, senza dover
introdurre verbi nuovi.
send_messages — "manda un messaggio a un familiare"Spedisce uno o più messaggi via Telegram o email. Argomento principale: una lista di messaggi, ognuno con destinatario e testo.
chiamata: send_messages(messages=[
{to_user: "lucia", body: "Sono uscito, torno alle 7."}
])
risposta: { ok: true, ok_count: 1, fail_count: 0,
results: [{to_user: "lucia", channel: "telegram", message_id: "abc123"}] }
Un attrezzo "trasformativo": modifica il mondo, manda davvero un messaggio. Per questo
gli executor che cambiano qualcosa al mondo vengono trattati con più cautela: il loro
manifesto dichiara le capacità mail:send e
channel:out, e la destinazione viene risolta rispetto all'utente e
al canale associati. L'invio è tracciato, ma non è annullabile: il manifest
dichiara infatti revertible=false. I controlli devono quindi
precedere la consegna.
Vedi vaglio per i controlli pre-esecuzione e
approval_ux per come si chiede conferma all'utente.
Questo documento è un'introduzione. Se vuoi capire i meccanismi che stanno sotto — come si firma un manifesto, come si applica il recinto, come il Synt genera il codice, come il pianificatore sceglie un attrezzo — i documenti seguenti vanno letti uno alla volta.
| Per capire… | Leggi |
|---|---|
| il pianificatore che sceglie l'executor giusto | agent_runtime |
| il recinto in dettaglio (forbidden paths, deroghe, «riordina le foto») | sandbox |
| come il Synt compone executor nuovi | synt |
| come si importa una skill esterna come executor | skill importer |
| il controllo che precede l'esecuzione di azioni rischiose | vaglio |
| come l'utente vede e approva le azioni | approval_ux |
| la memoria che le esecuzioni lasciano dietro di sé | mnest e mnestoma |
| il dialogo con il mondo (Telegram, web, voce) | channel |
| quali modelli (LLM, embedding, VLM) muovono gli executor e come si cambiano da un TOML | virtualizzazione dei modelli |
| l'osservabilità (cosa è successo, perché, quando) | observability |
Non occorre leggere queste guide in ordine. Parti dalla domanda concreta, segui i collegamenti pertinenti e fermati quando hai raggiunto il livello di dettaglio che ti serve.
Metnos — executor, introduzione didattica