executor — anatomia di una capacità eseguibileQuesto 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.
Il documento copre:
Non copre, e demanda altrove:
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.
| Categoria | Descrizione | Vive in | Firma |
|---|---|---|---|
| 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.
Un executor vive sotto workspace/executors/<nome>/.
La directory contiene sei artefatti, in questo ordine di lettura:
| Artefatto | File | Descrizione |
|---|---|---|
| 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. |
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:
name e version formano l'identità che il gateway usa per risolvere ogni invocazione. name è immutabile per tutta la vita dell'executor; version segue semver con una regola dura: ogni cambiamento al codice o al contratto incrementa il major.contract è lo schema d'uso. Non si può aggiungere o rimuovere un campo d'ingresso senza incrementare la versione.sandbox.hash aggancia l'identità al profilo. Cambiare profilo significa cambiare identità.
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:
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.
Una versione di executor non si modifica mai «sul posto».
Per fare una modifica si crea una nuova versione (es. 1.0.0
→ 2.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.
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.
Sono due cose diverse, accoppiate, che spesso vengono confuse:
| Strato | Cosa è | 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.
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.
| Dimensione | Esempi di policy |
|---|---|
| Filesystem letto | nessuno; workspace/inbox/; workspace/<sotto> letto-solo; HOME letto-solo. |
| Filesystem scritto | nessuno; workspace/<sotto>; cartella temporanea privata. |
| Esecuzione shell | vietata; consentita solo per binari elencati; libera (sconsigliato). |
| Rete in uscita | nessuna; allowlist di domini; libera (sconsigliato). |
| Risorse | memoria max, CPU max, durata max. |
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:
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.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.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.
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?
| # | Componente | Cosa fa |
|---|---|---|
| 1 | Validazione policy nel gateway | Il 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. |
| 4 | Audit | In 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.
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):
~/.ssh/, ~/.gnupg/ — chiavi crittografiche dell'utente;~/.config/myclaw/, ~/.config/metnos/ — cassaforte dei segreti del progetto;workspace/.audit/ — il registro append-only delle azioni; nemmeno modificabile dall'audit log writer stesso, che apre il file in modalità append via syscall dedicata;/etc/, /proc/, /sys/, eccezioni esplicite per file innocui letti dal runtime Python).
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.
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.
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:
~/Immagini/ e non solo il workspace.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.~/ 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».
È 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.
| Caso | Come |
|---|---|
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. |
| Stato | Significato | Transizione in uscita |
|---|---|---|
| Seed | Executor presente al primo avvio, scritto a mano nel repo. | → Attivo (dopo verifica firma). |
| Attivo | Caricato dal gateway, invocabile, con almeno un mnest che lo cita. | → Fuso, In quarantena, Sotto soglia. |
| Fuso | Due 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 quarantena | Firma 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 soglia | Uso negli ultimi N giorni inferiore alla soglia configurata. L'ager propone l'archiviazione. Resta caricato finché Roberto non conferma. | → Attivo (uso ripreso) o → Archiviato. |
| Archiviato | Spostato in workspace/executors/.archive/, non caricato dal gateway, ma conservato per traccia storica e per possibile riabilitazione. | → Attivo (riabilitazione esplicita). |
.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.
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.
| # | Stadio | Cosa fa |
|---|---|---|
| 1 | Pattern detect | L'ager conta i proto-mnest ricorrenti; se uno supera la soglia di ricorrenza, propone al synt una scheda di motivazione. |
| 2 | Specifica | Il synt formula una specifica testuale (cosa l'executor deve fare, quali ingressi, quali uscite, quali errori). Roberto approva o emenda. |
| 3 | Scheletro | Genera il main.py e il schema.json chiamando l'LLM con la specifica come contesto. |
| 4 | Profilo | Calcola il profilo di sandbox più stretto compatibile con i test di nascita. Se non ci riesce, sospende e chiede. |
| 5 | Test di nascita | Genera o accetta dall'utente i casi del file tests/birth.py. Esegue: tutti devono passare con il profilo del punto 4. |
| 6 | Approvazione umana | Roberto vede manifest, codice, test, profilo. Approva, emenda o rifiuta. Se approva, parte la firma. |
| 7 | Firma e installazione | Calcola 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).
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:
| Quando | Strategia | Cosa fa | Costo 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 |
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.
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.
{...}.X in workspace/executors/X/CURRENT; se non c'è CURRENT o è in quarantena, fallisce.schema.json. Se non valido, fallimento con errore strutturato.run(args, ctx). Tempo limite, memoria limite, rete limitata.schema.json. Se non valido, fallimento.Lungo il flusso, il gateway aggiorna due strutture:
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:
fs_readLegge 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_fetchScarica 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_sendInvia 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 = 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.
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.
| Campo | Tipo | Significato |
|---|---|---|
target_executor | str (o catena) | L'executor da invocare ad ogni firing. |
args | dict | Argomenti passati al target. |
schedule | str | Espressione cron-like ("0 8 * * *") o NL ("ogni mattina alle 8") tradotta in cron al momento della creazione. |
delivery_channel | str | Canale per il risultato (Telegram, mail, voce, …). |
count | int | None | None = invocazioni illimitate; 1 = one-shot; N ≥ 1 = esattamente N firing. |
expiry | timestamp | None | Scadenza temporale. La prima fra count ed expiry chiude la schedule. |
on_failure | enum | skip / retry / notify / cancel. Default notify dopo 1 retry. |
max_consecutive_failures | int | None | Auto-cancel dopo N fallimenti consecutivi. Default 3, None per disabilitare. |
paused | bool | Sospensione 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.
workspace/.runtime/scheduler.sqlite (lo schedule store).system:scheduler (accesso al loop async del gateway, non concedibile a nessun seed o sintetizzato).
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.
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.
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:
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.
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.
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).
| Dimensione | Su metnos-server | Sul 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. |
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:
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.photo_organize al laptop, che lo verifica contro il manifest e lo scrive in cache locale.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.rename, ritorna l'esito.metnos-server (chi ha chiamato, quando, esito), una sul laptop (lista dei path effettivamente toccati, hash dopo). Correlati per trace_id.~/.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.
Metnos — executor microprogettazione v1.1 — 2026-04-24
Doc canonico nuovo. Sostituisce neuron.html (deprecato).