policy — registro delle capacità e concessioniChiedi a Metnos con una richiesta come quella di questo esempio: «Spiegami quali controlli si applicano se chiedo di leggere un file e quali se chiedo di modificarlo». La risposta deve distinguere i controlli attivi del runtime dalla matrice dichiarativa descritta in questa pagina.
runtime/policy.py offre un registro canonico, una matrice
deterministica dei livelli di autonomia e un archivio di concessioni circoscritte a
un obiettivo. Queste API sono disponibili e ispezionabili, ma non costituiscono
ancora il controllo generale del percorso ordinario di esecuzione. Le autorizzazioni
effettive del runtime sono descritte nei documenti dei componenti che le applicano.
Il modulo policy descrive un possibile esito di autorizzazione per una
tripla composta da livello di autonomia, capacità e obiettivo. L'esito può essere
consentito, negato oppure approvazione richiesta. È un
modello deterministico disponibile nel codice; non va confuso con un controllo già
applicato a ogni executor.
Il modello distingue tre responsabilità, affidate a componenti diversi:
Questa separazione è il disegno del sottosistema, non una dichiarazione sull'ordine
del percorso attuale. Nel codice corrente il registro viene usato per convalidare le
capacità dichiarate dai manifest; la chiamata generale a
effective_outcome prima di ogni esecuzione non è ancora presente.
Contratto dichiarativo. I manifest attivi dichiarano capacità
appartenenti al registro canonico. I controlli effettivi restano distribuiti fra
convalida del manifest, vaglio, dialoghi di approvazione, sandbox e regole specifiche
del dispatcher; questa pagina non li attribuisce tutti a policy.py.
Il registro è il dizionario chiuso delle capacità riconosciute dal validatore dei manifest. Le voci canoniche sono definite in runtime/policy.py:CAPABILITY_REGISTRY. Ogni voce è un CapabilitySpec con quattro attributi:
name — il nome canonico (formato famiglia:modo, es. fs:read);critical — True se l'azione è irreversibile o ad alta posta in gioco (write, send, exec);default_approval — classe usata per costruire la matrice: none, per_target o always;target_kind — tipologia del bersaglio: path_glob, host, exact, none.La tabella seguente mostra un gruppo rappresentativo. Il registro nel codice e
l'uscita del comando registry sono le fonti complete e aggiornate.
| name | critical | default_approval | target_kind | descrizione |
|---|---|---|---|---|
compute:pure | no | none | none | calcolo deterministico in memoria senza I/O esterno |
fs:read | no | per_target | path_glob | lettura file dal filesystem locale entro path_glob dichiarati |
fs:write | sí | per_target | path_glob | scrittura/modifica file entro path_glob (critica) |
code:exec | sí | always | exact | esecuzione di un comando shell di una lista bianca (es. gestore di pacchetti) |
network:http | no | per_target | host | HTTP/HTTPS GET/POST verso host autorizzati |
llm:local | no | none | none | chiamata LLM locale (llama-server, llama.cpp), costo zero |
llm:online | no | per_target | none | chiamata LLM online (Anthropic, OpenAI,...), costo > 0 |
mail:read | no | per_target | exact | lettura di messaggi IMAP da una casella di posta autorizzata |
mail:send | sí | always | exact | invio SMTP a destinatari (irreversibile, alta posta in gioco) |
channel:in | no | none | exact | ricezione messaggi da un canale (Telegram, CLI, voice) |
channel:out | no | per_target | exact | invio messaggi a un canale specifico |
time:read | no | none | none | lettura ora corrente e fusi orari |
parse:local | no | none | none | parsing locale di formati noti (PDF, HTML, JSON, CSV) |
calendar:read | no | per_target | exact | lettura eventi da un calendario autorizzato |
index:read | no | per_target | exact | lettura di un indice locale amministrato da Metnos |
metnos:read | no | per_target | exact | lettura di una risorsa locale amministrata da Metnos |
metnos:cache | no | none | none | cache tecnica best-effort in un sottoalbero amministrato da Metnos, senza effetti sui dati utente |
system:read | no | per_target | exact | lettura diagnostica di sistema su host o dispositivo autorizzato |
provider:access | no | per_target | exact | accesso alla rete e alle credenziali di un fornitore autorizzato |
Il registro è chiuso: record_grant rifiuta una capacità che non vi appartiene. Aggiungerne una significa modificare il registro nel codice e convalidare i manifest — non esiste una registrazione dinamica durante l'esecuzione. Il vocabolario delle azioni è quindi un contratto controllato.
provider:access usa come bersaglio esatto il binding della skill. Una
clausola when nel manifest può restringerla a un backend della singola
invocazione; la policy non ricava mai questo permesso dal nome dell'executor o da
un valore libero del piano.
Il registro comprende letture, scritture, accessi a fornitori esterni, operazioni amministrative e interazioni umane. Il flag critical e la classe default_approval contribuiscono alla matrice del modulo. Non dimostrano, da soli, che una richiesta di conferma sia presentata nel percorso di esecuzione corrente.
Per esempio, compute:pure e time:read hanno classe none; fs:write ha classe per_target; mail:send, mail:write, system:admin e drive:permissions hanno classe always. L'elenco completo va letto dal registro, non mantenuto a mano in questa pagina.
La matrice è il prodotto dei tre livelli di autonomia (ReadOnly, Supervised, Full) per le capacità registrate. Per ogni cella produce un esito: allowed, approval_required o denied. È generata da _init_table. Qui sotto compare un estratto; il comando table stampa la matrice completa.
| capability | ReadOnly | Supervised | Full |
|---|---|---|---|
compute:pure | allowed | allowed | allowed |
fs:read | approval | approval | allowed |
fs:write | denied | approval | allowed |
code:exec | denied | approval | approval |
network:http | denied | approval | allowed |
llm:local | allowed | allowed | allowed |
llm:online | denied | approval | allowed |
mail:read | approval | approval | allowed |
mail:send | denied | approval | approval |
channel:in | allowed | allowed | allowed |
channel:out | denied | approval | allowed |
time:read | allowed | allowed | allowed |
parse:local | allowed | allowed | allowed |
calendar:read | approval | approval | allowed |
index:read | approval | approval | allowed |
metnos:read | approval | approval | allowed |
metnos:cache | allowed | allowed | allowed |
system:read | approval | approval | allowed |
provider:access | approval | approval | allowed |
La tabella non è arbitraria: nasce da tre regole, una per livello, che _init_table applica iterando sul registro.
ReadOnly. Le capacità non critiche con classe none sono consentite. Un insieme esplicito di letture e accessi circoscritti — fra cui fs:read, mail:read, calendar:read, network:sites e provider:access — produce approval_required. Le altre capacità critiche o con approvazione predefinita vengono negate.
Supervised. Le capacità con classe none sono consentite; tutte le altre producono approval_required. Questa è la semantica della matrice, non la prova che il dispatcher corrente presenti il relativo dialogo.
Full. Le capacità con classe always producono ancora approval_required; tutte le altre sono consentite. Fra le voci correnti con classe always figurano code:exec, mail:send, mail:write, system:admin e drive:permissions.
Un esito non equivale a un controllo applicato. La matrice conserva
approval_required per le classi always anche al livello Full.
Finché il dispatcher non usa questo esito come vincolo generale, la proprietà va
letta come contratto del modulo e non come garanzia dell'intero sistema.
La reversibilità è un contratto distinto dalla matrice di autonomia. Un executor conforme allo standard che dichiara revertible = true deve indicare un reverse_pattern. Il runtime conserva i dati di esecuzione necessari e undo_last_turn prova ad applicare il percorso inverso, riferendo anche gli eventuali fallimenti.
Il catalogo chiuso in runtime/reverse_patterns.py comprende, fra gli altri:
swap_src_dst — uno spostamento o rinomina si annulla scambiando sorgente e destinazione (path del filesystem e cartelle IMAP allo stesso modo, le seconde agganciate allo stabile Message-ID).delete_created_dirs / delete_created_paths — una creazione si annulla rimuovendo ciò che è stato creato, solo finché è ancora vuoto/intatto.restore_blob_backup — prima di ogni scrittura distruttiva il runtime salva una copia byte della versione precedente in $METNOS_HISTORY_DIR/<turn>/blob/<sha256>.bin; l'undo la ripristina. È la parte «intercetta la modifica e tieni una copia della versione precedente», fatta una volta e in modo deterministico.delete_*_by_id — una creazione su un servizio esterno si annulla usando l'identificatore restituito dall'operazione.Per i manifest conformi allo standard, il validatore controlla la coerenza fra la dichiarazione di reversibilità e il pattern inverso. Restano percorsi di compatibilità per alcuni executor manuali: per questo non è corretto affermare che ogni operazione modificatrice usi soltanto il catalogo.
La presenza di un pattern inverso non concede autorità e non elimina, da sola, un'eventuale approvazione. Reversibilità, autorizzazione e isolamento sono controlli separati.
Il modulo offre anche un archivio SQLite per concessioni circoscritte. Ogni record lega un canale, un mittente, una capacità, un obiettivo e un'eventuale scadenza. L'archivio esiste ed è interrogabile; il percorso ordinario di approvazione non vi scrive ancora concessioni.
Lo schema è definito da runtime/policy.py:SCHEMA:
CREATE TABLE IF NOT EXISTS grants ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel TEXT NOT NULL, sender_id TEXT NOT NULL, capability TEXT NOT NULL, target TEXT NOT NULL, granted_at TEXT NOT NULL, expires_at TEXT, granted_by TEXT, revoked_at TEXT );
Una concessione è identificata dalla quaterna (channel, sender_id, capability, target). Le date granted_at, expires_at e revoked_at sono espresse in UTC. La separazione logica fra utenti dipende dal corretto passaggio di canale e mittente da parte del chiamante.
Il file predefinito è ~/.local/state/metnos/grants.db per l'utente di installazione; METNOS_GRANTS_DB permette di indicarne un altro. Più utenti applicativi serviti dallo stesso processo condividono dunque il contenitore fisico, mentre le righe restano distinte dall'identità fornita all'API.
| funzione | cosa fa |
|---|---|
record_grant(channel, sender_id, capability, target, expires_at=None, granted_by=None) |
Registra una concessione. Solleva ValueError se la capability non è nel registro. Ritorna l'oggetto Grant con id assegnato. |
has_grant(channel, sender_id, capability, target) |
True se esiste un grant attivo (non revocato, non scaduto) per la quaterna. La query confronta expires_at con il tempo corrente. |
list_grants(channel=None, sender_id=None, include_revoked=False) |
Elenca le concessioni, filtrabili per canale e mittente; può includere
quelle revocate. L'ordine è decrescente per granted_at. |
revoke_grant(grant_id) |
Pone revoked_at al momento corrente. Ritorna True se è stato modificato qualcosa, False se il grant era già revocato o non esisteva. |
Le funzioni aprono e chiudono una connessione per chiamata; non mantengono una sessione SQLite globale.
Nel runtime corrente non risultano chiamanti di produzione di record_grant
e il pianificatore non consulta effective_outcome. La tabella può
essere popolata chiamando direttamente l'API del modulo, ma non esiste una
superficie amministrativa attiva che lo faccia e non va descritta come la
memoria delle conferme dell'utente.
effective_outcome)
La funzione effective_outcome combina la matrice e le concessioni in un
unico esito. È disponibile ai chiamanti del modulo e alla CLI; non è attualmente il
punto di accesso del pianificatore.
| tabella dice | grant attivo per (channel, sender, target)? | esito |
|---|---|---|
| allowed | indifferente, non si interroga il DB | allowed |
| denied | indifferente, non si interroga il DB | denied |
| approval_required | sí | allowed |
| approval_required | no (oppure parametri di scope mancanti) | approval_required |
La logica è lineare: se la tabella decide già in modo netto (allowed o denied), il grant non viene neppure consultato; se decide approval_required, un grant attivo lo trasforma in allowed, altrimenti resta approval_required.
Invariante della funzione: una concessione non trasforma mai
denied in allowed. Può invece trasformare
approval_required in allowed.
Limite da conoscere. effective_outcome non distingue fra
per_target e always quando trova una concessione attiva.
Anche una capacità classificata always verrebbe quindi elevata ad
allowed se qualcuno ne registrasse una concessione. Prima di collegare
il modulo al dispatcher occorre impedire questo caso oppure definire esplicitamente
una semantica diversa.
Valutazione del modulo, non esecuzione di un turno.
Il comando check Supervised fs:write, senza identità e obiettivo,
restituisce approval_required. Il comando
check ReadOnly fs:write restituisce denied. Questi risultati
mostrano la matrice; non salvano un file e non aprono un dialogo nella chat.
Il modulo partecipa al runtime nei punti seguenti:
CAPABILITY_REGISTRY per rifiutare nomi sconosciuti;effective_outcome è richiamato dalla CLI, ma non dal percorso ordinario del pianificatore o del dispatcher;approval_registry e il relativo renderer di canale non hanno chiamanti di produzione che creino richieste pendenti;Di conseguenza questo archivio di concessioni non va interpretato come il
meccanismo di autorizzazione attivo del pianificatore. Un eventuale chiamante deve
conservare identità e obiettivo esatti, rispettare l'isolamento per utente,
gestire correttamente le classi always e usare un renderer semantico
localizzato.
Dal percorso principale del repository, il modulo è ispezionabile con l'ambiente di Metnos. Espone cinque sottocomandi.
| comando | cosa fa |
|---|---|
PYTHONPATH=runtime ./.venv/bin/python -m policy registry | stampa una riga JSON per ogni capacità con tutti i suoi attributi. |
PYTHONPATH=runtime ./.venv/bin/python -m policy table | stampa una riga JSON per ogni livello con l'esito di tutte le capacità. |
PYTHONPATH=runtime ./.venv/bin/python -m policy check <level> <capability> [--channel C --sender S --target T] | stampa l'esito di effective_outcome. Senza identità e obiettivo mostra l'esito della sola matrice. |
PYTHONPATH=runtime ./.venv/bin/python -m policy grants [--channel C] [--sender S] [--all] | elenca le concessioni attive; --all include quelle revocate. |
PYTHONPATH=runtime ./.venv/bin/python -m policy revoke <grant_id> | revoca una concessione per identificatore. |
I comandi registry, table e grants producono JSON Lines e possono essere filtrati con jq.
La verifica del modulo comprende:
registry, table e i casi check desiderati;get_approval, che è il meccanismo attivo e non dipende da questo archivio di concessioni.denied; il trattamento delle classi always resta invece da
correggere prima dell'integrazione.Il valore attuale del modulo è circoscritto ma concreto: registro chiuso, matrice deterministica e archivio strutturato. La garanzia pubblica resta limitata a ciò che il runtime collega davvero.
Presentare questa API come filtro generale prima del collegamento al dispatcher sarebbe un'affermazione più ampia del comportamento dimostrato.