✓ Microprogettazione v1.1 — APPROVATO (24 aprile 2026). Doc canonico approvato, allineato con l'Architettura v1.1 e con il Dialogo sugli executor e la memoria distribuita. Riferimento normativo per l'implementazione. Sostituisce il vecchio neuron.html (deprecato).
← Indice documentazione Microprogettazione › executor

Metnos

executor — anatomia di una capacità eseguibile
Microprogettazione v1.1 — 24 aprile 2026
Allineata con Architettura v1.1 (cap. 9) e Dialogo sugli executor.
Sostituisce neuron.html (rinomina).

Pubblico: chi implementerà il loader, la firma, la sandbox.
Lettura: 18 minuti.

Indice

  1. Scopo e confini
  2. Cos'è un executor
  3. Anatomia: i sei artefatti
  4. Identità: manifest, firma, versione
  5. Profilo di sandbox
  6. Lifecycle: seed, attivo, fuso, quarantena, archiviato
  7. Sintesi (synt): da proto-mnest a executor
  8. Esecuzione: chiamata dal gateway
  9. Audit e osservabilità
  10. Esempi: tre seed e un builtin
  11. Rinomina: neurone → executor
  12. Executor remoti: stesso contratto, processo altrove
  13. Domande aperte

1. Scopo e confini

Questo documento definisce cosa è un executor, come è fatto, come si autentica, come si isola, come nasce, come muore. È il primo dei tre doc canonici introdotti dal Dialogo sugli executor e la memoria distribuita (24 aprile 2026), insieme a mnest e mnestoma. Sostituisce il vecchio neuron.html: tecnicamente è lo stesso oggetto, ma il nome «neurone» portava una metafora biologica che diventava ingombrante quando si scendeva nel dettaglio implementativo. Executor è più preciso: è codice eseguibile.

Confini

Il documento copre:

Non copre, e demanda altrove:

2. Cos'è un executor

Un executor è un'unità di codice che Metnos può eseguire come passo del proprio ragionamento. Una funzione con un contratto pubblico, un'identità firmata, un profilo di isolamento, un manifest che descrive cosa fa e a quali risorse accede. Nient'altro. Non è un agente, non è un microservizio, non è un plugin: è un piccolo blocco eseguibile, sotto firma del progetto.

La definizione operativa. Un executor è ciò che il gateway può invocare. Tutto il resto (identità, sandbox, audit) serve a rendere quell'invocazione sicura e tracciabile. Quando un dubbio progettuale si pone, la domanda di chiarezza è sempre la stessa: «questa cosa va invocata, oppure va decisa?» Se va decisa, non è un executor: è politica.

Tre categorie, una stessa anatomia esterna

CategoriaDescrizioneVive inFirma
Seed Quelli che Metnos trova al primo avvio: una ventina, scritti a mano nel repo, indispensabili al funzionamento di base. Esempi: fs_read, fs_write, shell_exec, web_fetch, telegram_send. workspace/executors/<n>/ Ed25519 per-istanza
Sintetizzati Nascono col tempo dal synt (cap. 7) per coprire un buco rilevato dall'uso. Hanno la stessa anatomia dei seed e nascono attraverso la stessa pipeline di firma e approvazione. workspace/executors/<n>/ Ed25519 per-istanza
Builtin Parte del runtime, non artefatti del workspace. Implementano servizi che hanno bisogno del clock di sistema, del loop async del gateway, di stato persistente trasversale (es. scheduler). Possono avere capability che nessun executor utente otterrebbe. sorgente di Metnos release-signed

La distinzione fra seed e sintetizzati è storica, non strutturale: un seed è generato manualmente prima del primo avvio, un sintetizzato è generato automaticamente in vita. Una volta firmati, il gateway li tratta esattamente nello stesso modo.

I builtin sono diversi: condividono il contratto esterno con seed e sintetizzati (stesso run(args, ctx) -> dict, stesso audit, stesso schema I/O), ma non hanno manifest.toml separato, non hanno profile.lock per istanza, non passano per la pipeline di sintesi. Vivono nei sorgenti di Metnos e vengono firmati come parte della release. Il synt non può mai generare un builtin (cap. 7): sono fondazionali, non sintetizzabili.

3. Anatomia: i sei artefatti

Un executor vive sotto workspace/executors/<nome>/. La directory contiene sei artefatti, in questo ordine di lettura:

ArtefattoFileDescrizione
Manifest manifest.toml Identità, versione, autore, descrizione, profilo di sandbox dichiarato, contratto di errore. Vedi cap. 4.
Firma manifest.sig Firma Ed25519 del manifest e dell'hash dell'eseguibile. Senza firma valida, il gateway rifiuta l'invocazione.
Codice main.py L'implementazione. Una sola funzione di ingresso esposta come def run(args: dict, ctx: ExecutorCtx) -> dict. Niente import dinamico, niente eval, niente exec.
Schema I/O schema.json JSON Schema dei parametri d'ingresso e del risultato. Validato sia in ingresso (gateway) sia in uscita (executor host process).
Test di nascita tests/birth.py Una manciata di casi che dimostrano il comportamento atteso. Il sintetizzatore (cap. 7) li impone come barriera per l'approvazione di un nuovo executor; il loader li ri-esegue ad ogni boot per garantire continuità.
Profile lock profile.lock Hash congelato del profilo di sandbox effettivo (cap. 5) calcolato al momento della firma. Se al runtime il profilo concreto non corrisponde all'hash, l'invocazione fallisce: protezione contro modifiche silenti.

4. Identità: manifest, firma, versione

Il manifest

Il manifest è il documento di identità di un executor. Esempio per fs_read:

[executor]
name        = "fs_read"
version     = "1.0.0"
created_at  = 2026-04-25T08:12:00Z
created_by  = "seed"               # oppure: "synt:<run_id>"
summary     = "Leggi un file dal workspace e ritornane il contenuto."

[contract]
input_schema  = "schema.json#/definitions/Input"
output_schema = "schema.json#/definitions/Output"
error_classes = ["NotFound", "PermissionDenied", "TooLarge"]
idempotent    = true
side_effects  = false

[sandbox]
profile = "fs-read-workspace"      # vedi sandbox.html
hash    = "blake3:abc123..."       # impronta del profilo

[audit]
trace_topic = "executor.fs_read"

Tre campi sono normativi:

La firma

Il file manifest.sig contiene la firma Ed25519 (curva 25519, chiave privata in ~/.config/metnos/keys/ con permessi 600, non versionata) sui seguenti byte concatenati:

blake3(manifest.toml) || blake3(main.py) || blake3(schema.json) || profile.lock

Il loader del gateway, all'avvio e a ogni richiesta, verifica:

  1. la firma è valida con la chiave pubblica del progetto;
  2. i quattro hash corrispondono ai file su disco;
  3. il manifest è semanticamente coerente (schema valido, profilo esistente).

Se anche un solo controllo fallisce, l'executor è rifiutato. Non viene caricato, non viene invocato, non viene cancellato: viene messo in quarantena (cap. 6) e segnalato all'utente.

Versionamento

Una versione di executor non si modifica mai «sul posto». Per fare una modifica si crea una nuova versione (es. 1.0.02.0.0) con la sua firma. Il gateway può tenere caricate più versioni in parallelo finché ce ne sono mnest che le citano (vedi mnest.html). La promozione di una nuova versione a default è un atto esplicito e firmato: viene scritto in workspace/executors/<nome>/CURRENT.

5. Profilo di sandbox

Il profilo è il punto in cui l'identità di un executor incontra la safety del perimetro (Architettura cap. 5). La sezione è più lunga del normale perché due distinzioni vanno fissate bene, e la domanda «l'executor può leggere fuori dal sandbox?» deve avere una risposta operativamente chiara, non slogan.

5.1 Profilo dichiarato vs sandbox applicata

Sono due cose diverse, accoppiate, che spesso vengono confuse:

StratoCosa èVive in
Profilo dichiarato Una dichiarazione, scritta nel manifest dell'executor, di quali risorse l'executor ha bisogno: quali path leggere, quali path scrivere, quali domini contattare, quali binari eventualmente eseguire, con quali tetti di tempo e memoria. manifest.toml (statico, firmato).
Sandbox applicata L'imposizione tecnica di quei limiti al processo che esegue il codice dell'executor. Su metnos-server usiamo bubblewrap + landlock + namespace mount + seccomp + interfaccia di rete dedicata. Quel processo, fisicamente, non vede e non può chiamare nulla che il profilo non gli abbia concesso. Configurazione runtime del gateway, generata dal profilo.

I due strati sono tenuti in sincrono dal profile lock descritto al cap. 3: l'hash del profilo concreto, calcolato al momento della firma, è congelato in profile.lock. A ogni invocazione, il loader del gateway ricalcola l'hash della sandbox che sta per applicare e la confronta con il lock; se diverge, l'invocazione fallisce. Così un cambio silente del profilo — di chiunque — viene intercettato.

Perché due strati e non uno solo. Si potrebbe pensare che basti la sandbox: in fondo è lei che conta. Il profilo dichiarato serve però a tre cose distinte. (a) È leggibile dal vaglio prima dell'invocazione, quindi una chiamata che chiede ovviamente troppo (un fs_read su /etc/passwd) viene fermata prima ancora di aprire il sandbox. (b) Permette al synt di generare profili minimi al momento della sintesi (cap. 7). (c) È documentazione viva: chi rilegge il manifest sa subito cosa l'executor pretende, senza dover leggere la configurazione del runtime.

5.2 Le cinque dimensioni del profilo

DimensioneEsempi di policy
Filesystem lettonessuno; workspace/inbox/; workspace/<sotto> letto-solo; HOME letto-solo.
Filesystem scrittonessuno; workspace/<sotto>; cartella temporanea privata.
Esecuzione shellvietata; consentita solo per binari elencati; libera (sconsigliato).
Rete in uscitanessuna; allowlist di domini; libera (sconsigliato).
Risorsememoria max, CPU max, durata max.

5.3 Può leggere fuori dal sandbox? No, mai. Perché.

Risposta breve: no. Quello che il profilo non concede, il processo dell'executor non lo vede e non lo può vedere. Non è una raccomandazione di stile, è una limitazione del kernel.

Risposta lunga, in tre passaggi:

  1. Mount namespace. bubblewrap apre il processo in un mount namespace privato in cui sono visibili solo i path elencati nel profilo (montati read-only o read-write a seconda del campo). Tutto il resto del filesystem semplicemente non esiste dal punto di vista del processo. Una open('/etc/passwd') ritorna ENOENT (file non trovato), come se il file non ci fosse mai stato.
  2. Landlock. Sopra al mount namespace, il kernel applica una policy landlock che restringe ulteriormente: anche path montati read-only non possono essere aperti per scrittura, anche path scrivibili non possono essere aperti con flag pericolosi. Una violazione restituisce EACCES.
  3. Seccomp. Le syscall di rete sono filtrate da seccomp: l'executor che non ha rete in profilo riceve EPERM su connect(). L'executor che ha rete su allowlist passa per un proxy locale che blocca tutto fuori dalla allowlist.

È difesa in profondità: il primo livello (mount namespace) già basterebbe per i percorsi non elencati; il secondo (landlock) gestisce le sfumature di scrittura/lettura; il terzo (seccomp) chiude il lato di rete. Tre meccanismi indipendenti del kernel, configurati dal profilo. Aggirarli richiederebbe un bug del kernel, non un bug dell'executor.

5.4 Esempio operativo: fs_read chiamato male

Supponiamo che il gateway riceva la richiesta: fs_read({path: "/etc/passwd"}). L'executor fs_read ha profilo fs-read-workspace che concede sola lettura su workspace/. Cosa succede?

#ComponenteCosa fa
1Validazione policy nel gatewayIl gateway, prima di aprire qualsiasi sandbox, confronta l'argomento path con il profilo dichiarato. /etc/passwd non è sotto workspace/: la chiamata viene rifiutata con un errore strutturato PolicyViolation. Il sandbox non viene neppure aperto.
2(Difesa in profondità)Se per ipotesi assurda il gateway si fosse confuso e avesse aperto la sandbox, il processo dell'executor sarebbe stato in un mount namespace senza /etc/. open('/etc/passwd') sarebbe ritornato ENOENT. L'executor avrebbe ritornato NotFound.
3(Difesa in profondità, ulteriore)Se per assurdo il path /etc/passwd fosse stato per errore montato, landlock avrebbe rifiutato il read() con EACCES.
4AuditIn tutti i casi, il gateway scrive nell'audit log la richiesta, l'esito (PolicyViolation, NotFound, PermissionDenied), il sender, il timestamp.

Il design corretto è che l'errore venga intercettato al passo 1 (policy-time), non al passo 2 o 3 (kernel-time): è più veloce, più informativo per il chiamante, e non lascia tracce nel sandbox. I passi 2 e 3 sono la rete di sicurezza che esiste se il primo dovesse fallire per un bug nostro.

5.5 Forbidden paths del nucleo: sopra al profilo

Esistono percorsi che nessun profilo può concedere, nemmeno se il manifest li dichiarasse. Sono i forbidden paths del nucleo, hard-coded nel codice del gateway, irriducibili, modificabili solo per rito (modifica del codice, revisione, rilascio):

La differenza concettuale: il profilo è modulazione dichiarata per executor; i forbidden paths del nucleo sono divieti universali del sistema. Vivono su due piani diversi dell'asse 2 della safety del perimetro (Architettura cap. 5): il guscio modulabile (profili) e il nucleo duro (forbidden paths). Il livello di autonomy Full può allargare il guscio, mai il nucleo.

Il principio di minimo privilegio è l'unico ammesso. Un executor che chiede «rete libera» quando gli serve solo api.openai.com non viene approvato. Il synt, in fase di sintesi (cap. 7), prova a generare il profilo più stretto che ancora soddisfa i test di nascita; se non riesce, sospende la sintesi e chiede a Roberto di restringere a mano.

5.6 Posso derogare? Path fuori dal workspace, e il caso «riordina le foto»

Sì. Niente nel design vieta a un profilo di dichiarare path fuori da workspace/. Il profilo è una dichiarazione esplicita: si può dichiarare ~/Immagini/, ~/Documenti/lavoro/, una cartella di rete montata, una partizione di backup. Quello che il profilo dichiara, la sandbox concede; quello che il nucleo vieta resta vietato comunque (cap. 5.5).

La deroga, però, è un atto pesato. Ha tre vincoli:

  1. È esplicita. Va dichiarata nel manifest, non implicita. Un revisore che legge il manifest deve vedere subito che questo executor tocca ~/Immagini/ e non solo il workspace.
  2. È firmata. Il profilo ampliato cambia l'hash di profile.lock; cambiare profilo significa cambiare versione, e cambiare versione richiede una nuova firma e una nuova approvazione. Non si può allargare il sandbox di nascosto.
  3. Resta sotto il nucleo. Anche un profilo che dichiara ~/ letto+scritto non può accedere a ~/.ssh/, ~/.gnupg/, ~/.config/myclaw/: i forbidden paths del nucleo (cap. 5.5) sono mascherati dalla mount namespace anche dentro un profilo «ampio».

Caso d'uso: riordina le foto

È un caso reale. Le foto vivono in ~/Immagini/ (sul server) o in C:\Users\Roberto\Pictures\ (sul laptop). Le due situazioni sono distinte e si risolvono in modo distinto.

CasoCome
Riordino su metnos-server (foto in ~/Immagini/ del server) Si crea un executor photo_organize con profilo dedicato: fs-read-write su ~/Immagini/; nessuna shell; nessuna rete; durata max 30s; output limitato. Il profilo è esplicito, firmato, soggetto a sintesi col synt e ad approvazione esplicita di Roberto. Il nucleo continua a vietare ~/.ssh ecc., quindi anche un bug dell'executor non può uscire da ~/Immagini/.
Riordino sul laptop (foto in C:\Users\Roberto\Pictures\, fuori da metnos-server) Caso degli executor remoti: il processo che esegue l'azione gira sul laptop, non sul server, perché sincronizzare gigabyte di foto solo per riordinarle non ha senso (vedi Architettura cap. 4 per la motivazione completa). La logica di sandboxing cambia: vedi cap. 12 di questo doc.
La regola pratica. Il workspace è il default sicuro: tutto quello che si può fare nel workspace, si fa nel workspace. Si esce dal workspace solo quando l'operazione perde senso a restarci dentro — tipicamente perché i dati vivono fisicamente altrove (foto, archivio musicale, codice di lavoro sincronizzato da un IDE). In quei casi si dichiara la deroga in modo esplicito, e la deroga vive nel manifest perché chiunque rilegga il sistema sappia esattamente cosa quell'executor può toccare.

6. Lifecycle: seed, attivo, fuso, quarantena, archiviato

Seed ~20 al boot Attivo in uso Fuso A+B → AB In quarantena firma rotta o test fallito Sotto soglia uso minimo, ager attivo Archiviato non attivo boot fusione drift disuso approva archivia con motivazione
Figura 1 — Macchina a stati di un executor. Le frecce blu sono transizioni di vita ordinaria; le rosse richiedono motivazione esplicita. Il modello formalizza i quattro stati discussi nel Dialogo sugli executor, Giornata IV.
StatoSignificatoTransizione in uscita
SeedExecutor presente al primo avvio, scritto a mano nel repo.→ Attivo (dopo verifica firma).
AttivoCaricato dal gateway, invocabile, con almeno un mnest che lo cita.→ Fuso, In quarantena, Sotto soglia.
FusoDue executor con tracce sovrapposte sono stati uniti dal synt in un nuovo executor che li copre entrambi. I vecchi restano caricati finché ci sono mnest residui, poi vengono archiviati.→ Archiviato.
In quarantenaFirma non valida, test di nascita fallito al boot, profilo lock disallineato. Non invocabile, ma non cancellato. Mostra un avviso a Roberto.→ Attivo (dopo riapprovazione) o → Archiviato.
Sotto sogliaUso negli ultimi N giorni inferiore alla soglia configurata. L'ager propone l'archiviazione. Resta caricato finché Roberto non conferma.→ Attivo (uso ripreso) o → Archiviato.
ArchiviatoSpostato in workspace/executors/.archive/, non caricato dal gateway, ma conservato per traccia storica e per possibile riabilitazione.→ Attivo (riabilitazione esplicita).
Reversibilità. Tutte le transizioni sono reversibili (cap. 14 dell'Architettura): archiviare un executor non lo elimina, lo sposta in .archive/; defuse di una fusione ricarica i due executor originali; un quarantenato torna attivo dopo una nuova firma valida. L'unica cosa irreversibile è la traccia in audit log.
Lifecycle ridotto per i builtin. I builtin non passano per la macchina a stati sopra. Hanno solo due stati operativi: attivo (parte del runtime in esecuzione) e disabilitato in config (riconosciuto dal gateway ma non avviato). Niente quarantena (non possono drift: la firma è quella della release di Metnos), niente archiviazione (la rimozione richiede una nuova release), niente fusione (non sono sintetizzabili). Questa riduzione è il prezzo della loro posizione: sono fondazionali, e i meccanismi di evoluzione del pool non li toccano.

7. Sintesi (synt): da proto-mnest a executor

Il synt è il processo che fa nascere nuovi executor quando l'uso lo richiede. La fonte d'innesco è un proto-mnest ricorrente: una traccia di desiderio non soddisfatto («l'output di A doveva andare a B che non esiste ancora») che si ripete più volte nello stesso mese.

La pipeline ha sette stadi. Ogni stadio può sospendere e chiedere all'utente.

#StadioCosa fa
1Pattern detectL'ager conta i proto-mnest ricorrenti; se uno supera la soglia di ricorrenza, propone al synt una scheda di motivazione.
2SpecificaIl synt formula una specifica testuale (cosa l'executor deve fare, quali ingressi, quali uscite, quali errori). Roberto approva o emenda.
3ScheletroGenera il main.py e il schema.json chiamando l'LLM con la specifica come contesto.
4ProfiloCalcola il profilo di sandbox più stretto compatibile con i test di nascita. Se non ci riesce, sospende e chiede.
5Test di nascitaGenera o accetta dall'utente i casi del file tests/birth.py. Esegue: tutti devono passare con il profilo del punto 4.
6Approvazione umanaRoberto vede manifest, codice, test, profilo. Approva, emenda o rifiuta. Se approva, parte la firma.
7Firma e installazioneCalcola gli hash, genera la firma Ed25519, scrive profile.lock, sposta l'executor in workspace/executors/<nome>/ e lo segnala come Attivo.

La separazione è netta: il synt propone, l'utente approva. Niente sintesi senza filtro umano; questo è il terzo dei sei principi (cap. 14 dell'Architettura). Il dettaglio della pipeline vive in synthesizer.html (da riscrivere).

7.1 La cascata: comporre prima, generare poi

La pipeline a sette stadi descritta sopra è il braccio della generazione: produce un executor nuovo. Ma non è il primo strumento che il synt usa quando un proto-mnest ricorre. La sequenza canonica è una cascata:

QuandoStrategiaCosa faCosto LLM frontier
Reattivo (turno utente) Comporre Cerca una catena di executor attivi che chiuda il proto-mnest. Se la trova, la propone come orchestrato — nessun executor nuovo, nessuna firma da emettere. Lascia un proto-mnest verso la composizione: se ricorre, diventa candidata a promozione (passo introvertivo). zero
Reattivo (fallback) Generare Pipeline a sette stadi (sopra). Si attiva solo se la composizione non esiste o è troppo lunga per restare leggibile. ~1 chiamata Spec + 1–2 Bozza
Introvertivo (omeostasi notturna) Fondere Due executor con tracce sovrapposte e profili compatibili vengono uniti in uno. Vedi lifecycle fuso (cap. 6). 1 chiamata Spec del fuso
Introvertivo Generalizzare N executor specializzati con stessa forma vengono proposti come un executor parametrico ampio. Innesco: soglia di proto-mnest ricorrenti su dimensioni vicine. 1–2 chiamate
Introvertivo (raro) Specializzare Da un executor generale si trae una versione specializzata per un caso caldo. Solo se misura di beneficio reale (non ottimizzazione preventiva). 1 chiamata
Perché la cascata. Il principio guida è operativo, non estetico: una libreria di executor cresce bene se ogni nuovo pezzo ha una ragione di esistere che non è già coperta da una composizione di pezzi vecchi. Comporre per primo paga il duplice fine di non spendere chiamate frontier inutilmente e di tenere piccolo il pool. La generazione resta lecita ma diventa l'eccezione documentata.
Telos di non-rinuncia. La cascata è il meccanismo con cui il synt onora il telos «coltivare gli strumenti» (cap. 11 dell'Architettura): finché la richiesta dell'utente è dentro la costituzione e dentro il budget, il synt esaurisce le strategie disponibili prima di rispondere «non posso». Il fallimento resta lecito ma tracciato, mai una rinuncia silenziosa. La soglia di abbandono (max 3 retry per stadio, sospensione su richieste pattern-rifiutate) resta valida: la non-rinuncia è coraggio, non testardaggine.

I tre passi introvertivi (fondere, generalizzare, specializzare) non hanno ancora un doc dedicato; la collocazione naturale è un capitolo aggiuntivo dentro il futuro synt riscritto, oppure un nuovo doc consolidator.html. Decisione aperta.

8. Esecuzione: chiamata dal gateway

Il flusso di un'invocazione dal gateway a un executor passa attraverso otto passi. Ne diamo qui la sequenza alta; il dettaglio dei passi intermedi (Policy, Vaglio, sandbox) vive nei rispettivi doc, da riscrivere.

  1. Il gateway riceve un'azione: chiamata di executor X con input {...}.
  2. Risolve X in workspace/executors/X/CURRENT; se non c'è CURRENT o è in quarantena, fallisce.
  3. Verifica firma e profilo lock. Se rotta, quarantena e fallimento.
  4. Valida l'input contro schema.json. Se non valido, fallimento con errore strutturato.
  5. Apre il sandbox col profilo dichiarato. Le quattro Leggi entrano qui come ulteriore filtro.
  6. Esegue run(args, ctx). Tempo limite, memoria limite, rete limitata.
  7. Valida l'output contro schema.json. Se non valido, fallimento.
  8. Scrive nell'audit log: chi ha chiamato, con cosa, quanto è durato, cosa ha restituito (in forma normalizzata, con eventuali segreti redatti).

Lungo il flusso, il gateway aggiorna due strutture:

9. Audit e osservabilità

Ogni invocazione produce una riga JSONL in workspace/.audit/executors/YYYY-MM-DD.jsonl:

{
  "ts":         "2026-04-25T08:14:33.881Z",
  "trace_id":   "01HW...",
  "turn_id":    "01HX...",
  "executor":   "fs_read",
  "version":   "1.0.0",
  "caller":     {"kind": "user", "sender": "roberto", "channel": "telegram"},
  "input":      {"path": "workspace/notes/giornaliero.md"},
  "output":     {"size": 4231, "sha":"blake3:..."},
  "duration_ms": 18,
  "exit":       "ok"
}

Tre invarianti che il loader e il runtime garantiscono:

  1. append-only: il file di audit non si modifica mai; gli sbagli si annotano scrivendo una riga successiva di rettifica.
  2. autodescrittivo: ogni riga contiene tutto ciò che serve per ricostruire l'invocazione, senza dover incrociare con altre fonti.
  3. redatto: i segreti riconosciuti (chiavi API, password, token) sono sostituiti da un placeholder con il loro hash, mai dal valore in chiaro.

10. Esempi: tre seed e un builtin

Tre seed di esempio

fs_read

Legge un file dal workspace. Profilo: filesystem letto-solo su workspace/; nessuna shell, nessuna rete; durata max 2 secondi; output max 4 MB. Errori: NotFound, PermissionDenied, TooLarge. Idempotente.

web_fetch

Scarica una pagina web e la restituisce come testo o markdown. Profilo: nessun filesystem, rete in uscita su HTTP/HTTPS verso domini in allowlist (configurabile per istanza); durata max 15 secondi; output max 2 MB. Errori: DnsError, Timeout, Forbidden, TooLarge. Idempotente.

telegram_send

Invia un messaggio al chat fidato dell'utente. Profilo: nessun filesystem, rete in uscita solo verso api.telegram.org; durata max 5 secondi. Effetti collaterali: sì (manda un messaggio). Errori: RateLimited, Unauthorized, Network. Non idempotente: due chiamate con lo stesso input mandano due messaggi.

Side effects e idempotenza. Un executor con side_effects = true e idempotent = false è soggetto a Vaglio rinforzato: il gateway chiede conferma anche all'interno del livello Full. La asimmetria precauzionale (cap. 5 dell'Architettura) ne è il fondamento.

Un builtin di esempio: scheduler

Lo scheduler è il primo (e per ora unico) executor builtin proposto: invoca un altro executor (o catena) a un ritmo definito. È il meccanismo che fa esistere «richieste con schedulazione» senza inventare una categoria di design separata. Sostituisce l'idea sbagliata di «routine concordate» come oggetto a parte.

Contratto

CampoTipoSignificato
target_executorstr (o catena)L'executor da invocare ad ogni firing.
argsdictArgomenti passati al target.
schedulestrEspressione cron-like ("0 8 * * *") o NL ("ogni mattina alle 8") tradotta in cron al momento della creazione.
delivery_channelstrCanale per il risultato (Telegram, mail, voce, …).
countint | NoneNone = invocazioni illimitate; 1 = one-shot; N ≥ 1 = esattamente N firing.
expirytimestamp | NoneScadenza temporale. La prima fra count ed expiry chiude la schedule.
on_failureenumskip / retry / notify / cancel. Default notify dopo 1 retry.
max_consecutive_failuresint | NoneAuto-cancel dopo N fallimenti consecutivi. Default 3, None per disabilitare.
pausedboolSospensione manuale senza cancellare. Default false.

Output: {schedule_id: ULID, next_fire: timestamp}.

Operazioni gemelle, anch'esse builtin: scheduler.list (elenca le schedule attive), scheduler.cancel (chiude una schedule), scheduler.modify (cambia ritmo, expiry, count o canale). Ogni modifica passa per il gate umano se cambia ritmo o target_executor.

Profilo (dichiarato in codice, non in manifest)

Audit

Ogni firing emette una riga JSONL come ogni altra invocazione executor, con caller = {"kind": "scheduler", "schedule_id": "…"}. La storia di una schedule è ricostruibile per schedule_id: creazione, modifiche, ogni firing, eventuale chiusura.

Lifecycle ridotto in pratica. Lo scheduler, come tutti i builtin, non passa per la macchina a stati di cap. 6: ha solo attivo e disabilitato in config. Le sue schedule però (le righe nello schedule store) hanno il proprio piccolo lifecycle: active, paused, completed (count o expiry raggiunto), cancelled (da utente o da on_failure=cancel). Lifecycle interno al builtin, non riflesso nel grafo del mnestoma.

Lo scheduler è il primo builtin perché risolve un caso ricorrente (presidio di una casella di posta, riassunto giornaliero, controlli periodici) con un meccanismo che si compone naturalmente con la cascata di synt: una schedule reiterata diventa candidata di fusione (synt cap. 5.1) in un singolo executor che incorpora la catena.

11. Rinomina: neurone → executor

La microprogettazione precedente usava il termine neurone (neuron.html, ora deprecato e marcato untrusted). Il Dialogo sugli executor e la memoria distribuita ha rinominato il termine in executor per due ragioni:

  1. Precisione. Un neurone biologico non è codice eseguibile sotto firma, né ha un manifest, né un profilo di sandbox. La metafora aiutava il primo brainstorm ma diventava ingombrante nei doc tecnici.
  2. Allineamento con l'esecuzione. Il gateway parla con executor: codice eseguibile invocato per produrre un risultato. Il nome ricorda quel verbo.

Il termine biologico continua a funzionare bene per descrivere il mnestoma emergente come un grafo: questo è un altro livello di metafora (il grafo come tessuto), su cui torneremo nei doc dedicati.

12. Executor remoti: stesso contratto, processo altrove

La topologia (Architettura cap. 4) dichiara che alcuni executor, in futuro, non gireranno sul metnos-server ma sulle macchine dell'utente: il laptop in primo luogo. Questo capitolo dice cosa cambia, in concreto, per il microdesign dell'executor quando il processo non vive più sul server.

Cosa NON cambia

Il contratto di un executor remoto è identico a quello di un executor locale. Stessa anatomia (cap. 3): manifest TOML, firma Ed25519, codice main.py, schema I/O, test di nascita, profile lock. Stessa pipeline di sintesi (cap. 7). Stessa identità gateway-side (cap. 4). Stesso lifecycle (cap. 6). Stesso audit log centrale, su metnos-server (cap. 9): chi chiama un executor remoto e cosa riceve indietro è tracciato esattamente come per uno locale.

Il manifest dell'executor remoto vive comunque sul server metnos-server, sotto workspace/executors/<nome>/. Il server è la fonte canonica del «chi» e del «cosa». Sul laptop vive solo l'esecuzione: un piccolo processo confinato, lanciato e supervisionato dal lato server attraverso il canale Headscale (vedi Architettura cap. 4).

Cosa cambia, e perché

DimensioneSu metnos-serverSul laptop (executor remoto)
Sandbox tecnica Forte: bubblewrap + landlock + seccomp + namespace mount. Tre meccanismi indipendenti del kernel Linux. Debole: Windows non ha equivalenti diretti. AppContainer/Job Object/permessi NTFS aiutano ma non danno gli stessi confini. Il sandbox è più un processo confinato user-space che una vera prigione del kernel.
Forbidden paths del nucleo Imposti dal sistema: ~/.ssh, ~/.gnupg, ecc. Imposti due volte: una volta dichiarati nel profilo (server-side, parte della firma), una volta verificati di nuovo dal piccolo runtime sul laptop prima di ogni operazione. Non ci si fida di un solo livello quando il sandbox tecnico è debole.
Pairing Implicito: il processo gira sulla stessa macchina del gateway, di cui è figlio. Esplicito: il dispositivo (laptop) deve essere pairato con una cerimonia firmata dall'utente da un canale già fidato. Un nuovo laptop non può eseguire executor remoti finché non è ammesso.
Autenticazione del canale Non serve: tutto in-process. mTLS dentro l'overlay Headscale, oppure WireGuard preshared key + token firmato per chiamata. Il gateway rifiuta chiamate non autenticate.
Idempotenza Consigliata, non strettamente necessaria. Obbligatoria. La rete fra metnos-server e laptop può cadere a metà chiamata. Ogni invocazione porta un identificativo univoco; due invocazioni con lo stesso id producono lo stesso esito (la seconda è un no-op).
Reversibilità Modello generico (cap. 6). Modello «prima/dopo»: prima di eseguire un'operazione che modifica lo stato del laptop, il gateway scrive su metnos-server un manifest del «prima» (lista dei path toccati, hash, dimensioni). Se Roberto dice «annulla», lo stesso schema viene applicato al contrario sul laptop. Vedi Architettura cap. 4 sez. «Il pattern prima/dopo».
Audit Una sola riga JSONL su metnos-server. Due righe corrispondenti: una sul server (chi ha chiamato, quando), una sul laptop (cosa è stato fatto sul filesystem, quando). Le due si correlano per trace_id.
Drift di versione Una sola directory workspace/executors/<nome>/. Due copie del binario: una sul server (canonica), una sul laptop (cache). Ad ogni invocazione, il gateway verifica che l'hash del binario sul laptop combaci con il manifest firmato; in caso contrario, sospende e richiede aggiornamento.

Esempio: photo_organize sul laptop

L'esempio canonico del cap. 5.6: riordinare un archivio fotografico di diecimila file che vivono in C:\Users\Roberto\Pictures\. Sincronizzarli sul server, riordinarli, risincronizzarli indietro implica gigabyte di traffico, doppia occupazione disco, finestre temporali in cui lo stesso file esiste in due stati su due macchine. Una rename sul posto, eseguita direttamente dal laptop, risolve in un secondo. Però il processo che esegue quella rename deve essere fidato e tracciato come ogni altro executor di Metnos.

Il flusso, in concreto:

  1. Sintesi. Il synt (cap. 7) propone photo_organize con manifest che dichiara: input = lista di regole di sort, output = numero di file riordinati, profilo = read-write su C:\Users\Roberto\Pictures\ (escluse sottocartelle riservate), nessuna shell, nessuna rete. Roberto approva, il manifest viene firmato sul server.
  2. Pairing del dispositivo. Roberto, da Telegram, conferma che il laptop «Acme-15» può eseguire executor remoti. La cerimonia genera una chiave pairing che vive nel mnestoma e nel piccolo runtime sul laptop.
  3. Distribuzione. Il gateway invia il binario firmato di photo_organize al laptop, che lo verifica contro il manifest e lo scrive in cache locale.
  4. Invocazione. Roberto dice «riordina le foto del 2024 per data di scatto». Il gateway prepara una chiamata con trace_id univoco, scrive su metnos-server il manifest del «prima» (lista dei path che verranno rinominati, hash), poi invia la chiamata al laptop via Headscale + mTLS.
  5. Esecuzione locale. Il piccolo runtime sul laptop verifica autenticità e firma, apre il sandbox debole (AppContainer/permessi NTFS) col profilo dichiarato, esegue la rename, ritorna l'esito.
  6. Audit. Due righe JSONL: una su metnos-server (chi ha chiamato, quando, esito), una sul laptop (lista dei path effettivamente toccati, hash dopo). Correlati per trace_id.
  7. Eventuale annullamento. Se Roberto si pente, il gateway legge il manifest del «prima» e invia al laptop la chiamata inversa. La reversibilità è preservata.
Non al day one. Tutto questo è direzione futura: gli executor remoti sono fase 5+ della roadmap (Architettura cap. 17), e si introducono uno alla volta, motivati da un caso d'uso concreto. Le cinque domande di sicurezza dell'Architettura cap. 4 (callout finale) vanno tutte risolte prima del primo executor remoto reale. Questo capitolo dice come si dovrebbe fare, non cosa è già pronto.

13. Domande aperte

  1. Chiave di firma. La chiave Ed25519 vive sotto ~/.config/metnos/keys/. Quando un nuovo dispositivo deve poter firmare nuovi executor (es. il laptop di Roberto in viaggio), come si autorizza? Pairing del dispositivo (cap. 4 dell'Architettura) + delega a tempo? Aperta.
  2. Condivisione fra istanze. Se in futuro Metnos diventa multi-istanza (executor remoti su un secondo dispositivo), come si propaga la firma di un executor nuovo? Replica con verifica sul lato remoto? Aperta.
  3. Versioni in sospeso. Una nuova versione di un executor in fase di test convive con la vecchia. Per quanto? La promozione è manuale o ha una soglia automatica? Aperta, vedi roadmap fase 4.
  4. Manifest validation. Useremo un parser TOML che esponga errori posizionali? Esiste un test di compatibilità col toolchain? Aperta.

prossimo della triade
mnest
La traccia di co-attivazione fra due executor.
terzo della triade
mnestoma
Il grafo emergente di tutti i mnest.
livello 1
Architettura, cap. 9
Il quadro d'insieme: executor, mnest, mnestoma.
indice
Microprogettazione
Tutti i doc.

Metnos — executor microprogettazione v1.1 — 2026-04-24
Doc canonico nuovo. Sostituisce neuron.html (deprecato).