approval_ux — i flussi di approvazione/undoL'approvazione è dove l'utilità (agisco in fretta), l'intelligenza (so cosa chiedere e come) e l'autonomia (non disturbo se non serve) si incontrano in un'unica superficie. Sbagliare qui non è un bug, è un fallimento di prodotto: gli utenti cliccano "ok" riflessi, gli agenti abusano, e la Supervised mode diventa una firma in bianco.
Questo documento è il contratto di comportamento dei flussi di approvazione: quando si chiede, come si presenta, quanto si aspetta, come si batcha, come si revoca, come si rompe il pilota automatico. È la reificazione delle tensioni 1 e 2 del giudizio di fase 0.
ApprovalRequest (pending → granted/denied/timeout/batch_auto_granted)./undo).policy.html).ApprovalRequest: è del Gateway (→ gateway §4.1). Qui definiamo solo la forma, il ciclo di vita e l'UX di consegna.sandbox.html).observability.html).ApprovalRequest, Approval, BatchGrant, UndoResult sono definiti
qui (§9). Consumatori: gateway (costruzione), channel (rendering), policy
(draft), observability (audit). Modifiche richiedono aggiornamento di types §3.
Prima di descrivere il design, nominiamo tre modelli di approvazione che abbiamo esplicitamente rifiutato. Nominare il fallimento previene la sua sottigliezza futura.
| Antipattern | Perché fallisce |
|---|---|
| "Rubber-stamp yes" | Pulsante "Approva" sempre identico, sempre immediato, testo generico "Procedo?". Dopo 10 interazioni l'utente clicca senza leggere. L'approval fatigue è un effetto noto in UX di sicurezza (cookie consent, TLS warning). Contromisura: pausa forzata (§5) + testo specifico per azione. |
| "Everything or nothing" | L'utente concede autonomy Full "per non essere disturbato" e la sicurezza svanisce in blocco. Oppure Supervised chiede per ogni singola scrittura di file e l'utente abbandona dopo un giorno. Contromisura: batching per classi (§4), non per tutto. |
| "Modal sincrono" | L'agente blocca tutto finché l'utente non risponde. Se Roberto è in
riunione, l'operazione muore. Contromisura: timeout
espliciti per canale + outcome user_aborted pulito + "riprendi
più tardi" se senso. |
Ogni volta che la Policy restituisce "approve_required", nasce una
ApprovalRequest che attraversa cinque stati. Il runtime del §8 del
doc agent_runtime si sospende finché la richiesta non è decisa
(qualunque il verdetto).
ApprovalRequest. Da Pending si va a cinque destinazioni. La transizione a Never registra una blacklist esplicita: quella classe di azione non chiederà mai più approvazione a questo sender, viene sempre negata.Il messaggio presentato all'utente non è mai "Procedo?" generico. È strutturato in 4 pezzi obbligatori:
| Pezzo | Esempio |
|---|---|
| Azione | fs_write · shell_run · telegram_send_to ... |
| Bersaglio / effetto | "scrivere /home/roberto/notes.md" · "eseguire apt update" |
| Motivo (perché lo sto facendo) | "per aggiungere quello che mi hai dettato due minuti fa" |
| Reversibilità | reversibile · ⚠ irreversibile · reversibile con costo |
La singola richiesta "approvi questa operazione?" è una firma singola. Il batching permette di dare una firma per classe: "per i prossimi 10 minuti, tutte le operazioni simili sono approvate per default".
La classe è fissata da Policy e ha granularità intenzionalmente stretta:
effect_class: "fs_write:~/downloads/*" # scrittura in ~/downloads e sotto
effect_class: "shell_run:apt_family" # apt / apt-get / aptitude
effect_class: "telegram_send:@famiglia" # messaggi al gruppo famiglia
effect_class: "fs_delete:~/trash/*" # eliminazione file in ~/trash
Una classe non è "qualunque fs_write". Quella è una scorciatoia pericolosa: includerebbe scrittura in ~/.bashrc. La classe lega verbo + ambito.
rapporto_marzo.pdf da drive.google.com
in ~/downloads/?L'ultimo pulsante è il batching. Premendolo, myclaw annota internamente:
BatchGrant(
sender="telegram:@roberto",
effect_class="fs_write:~/downloads/*",
expires_at="2026-04-21T22:24:00Z",
created_by_request_id=UUID(...)
)
Per i 10 minuti successivi, richieste che matchano effect_class
e sender transitano da Pending direttamente a
Batch auto-granted senza presentazione all'utente. Ma
la trace continua a registrarle (Legge 3, tracciabilità).
/etc o simili, si torna in Pending.myclaw approvals list e revocabili con myclaw approvals revoke <id>.È il cuore della mitigazione contro approval fatigue e dual-process theory. Il pulsante "Approva" non è cliccabile per 3 secondi dopo la visualizzazione della richiesta. Suona paternalistico: lo è, volutamente.
In letteratura HCI (Nielsen, Norman) la "speed bump" è un pattern noto: l'introduzione di un attrito minimo per rompere l'automazione ha riduzione del 30-50% di errori su azioni rischiose.
config/default.yaml.
Il pulsante è reso visivamente disabilitato (grigio, italico "attendi 3s...") con un countdown. A click durante la pausa, il click è ignorato e un micro-haptic/visual feedback segnala "non ancora". A fine pausa, il pulsante cambia aspetto e accetta input.
Nelle CLI (§8) la pausa si traduce in: il prompt [y/N] non
accetta input per 3 secondi; i caratteri digitati prima vengono scartati.
Nessuna magia: è lo stesso effetto.
/undo
La pausa di lettura previene l'errore prima del click. La revoca lo
ripara dopo. Un comando /undo (Telegram), un ctrl-c
esteso (CLI) o un vocale "fermati" (voce futura) arresta l'esecuzione in corso
e, se possibile, inverte l'effetto.
| Caso | Cosa fa /undo |
|---|---|
| Azione in preparazione (sandbox sta per partire) | Cancella prima che il tool venga eseguito. Outcome user_aborted. |
| Azione in esecuzione | Invia SIGTERM al processo sandbox. Il tool ha 5s per chiudere pulitamente, poi SIGKILL. |
| Azione completata reversibile | Inverte l'effetto se il tool espone revert(). Esempi: fs_write con snapshot, telegram_send che diventa telegram_delete (se entro 48h). |
| Azione completata irreversibile | Non fa nulla ma registra l'intento e l'impossibilità nell'audit log. Avverte l'utente. |
Il comando /undo agisce sulla trace più recente
dello stesso sender. Per agire su una trace specifica: /undo <trace_id_short>.
Paradosso della selezione: più un neurone (o una classe di azione) è "affidabile", meno l'utente lo controlla, più automation bias cresce. Il tutor mode introduce una rottura deliberata ogni K approvazioni consecutive.
tutor_caught che segnala "il tutor ha prevenuto un errore potenziale".Inline buttons, messaggio editabile, emoji sobrio. Il pulsante Approva si abilita dopo 3s via edit del messaggio.
rapporto_marzo.pdfdrive.google.com~/downloads/ (~2.4 MB)Pausa di lettura = TTS lento del summary (non accelerato). La conferma vocale richiede parola-chiave riconosciuta (es. "procedi") per evitare false conferme da rumori. Un "sì" generico non basta: serve intent esplicito.
| Canale | Timeout default | Razionale |
|---|---|---|
| CLI | 30 secondi | Utente davanti al terminale, attivo. |
| Telegram (DM) | 2 minuti | Utente può essere momentaneamente occupato. |
| Telegram (canale di casa) | 5 minuti | Più permissivo, meno urgenza. |
| Voce | 15 secondi | Conversazione sincrona. |
from typing import Protocol, Literal
from dataclasses import dataclass
from datetime import datetime
from uuid import UUID
@dataclass
class ApprovalRequest:
request_id: UUID
trace_id: UUID
sender: str # es. "telegram:@roberto"
channel: str
tool_name: str
args: dict
effect_class: str # es. "fs_write:~/downloads/*"
summary: str # one-line human readable
reversibility: Literal["reversible", "reversible_with_cost", "irreversible"]
risk: Literal["medium", "high"]
requested_at: datetime
timeout_at: datetime
@dataclass
class Approval:
request_id: UUID
granted: bool
granter: str # sender identity
granted_at: datetime
via: Literal["explicit", "batch", "timeout_deny", "never_list"]
batch_window_expires_at: datetime | None = None
reason: str | None = None # motivazione, se negato
class ApprovalBroker(Protocol):
async def request(self, req: ApprovalRequest) -> Approval:
"""Presenta la richiesta, attende, restituisce esito."""
...
async def active_batches(self, sender: str) -> list["BatchGrant"]:
"""Elenco dei batch grant attivi per il sender."""
...
async def revoke_batch(self, sender: str, batch_id: UUID) -> None:
"""Invalida un batch attivo."""
...
async def undo(self, sender: str, trace_id: UUID | None = None) -> UndoResult:
"""Tenta la revoca dell'ultima azione (o di una specifica)."""
...
@dataclass
class BatchGrant:
batch_id: UUID
sender: str
effect_class: str
created_at: datetime
expires_at: datetime
approval_counter: int # quante richieste batched so-far
source_request_id: UUID
@dataclass
class UndoResult:
trace_id: UUID
status: Literal["undone", "partial", "irreversible", "already_completed"]
message: str
| Alternativa | Perché scartata |
|---|---|
| Approvazione vocale in CLI ("dì sì o no") | Ridondante con la tastiera, introduce ambiguità di riconoscimento. Rimandata al canale voce dedicato. |
| Scala graduale 1-5 invece di approva/nega | Overhead cognitivo × 5, nessun beneficio misurabile. Un binario + "mai più" basta. |
| Approvazione delegata ad altri utenti di casa (moglie, figlio) | Fase 3+, con multi-utente. Prima serve il pairing multiplo. |
| AI critic che approva al posto dell'utente | Ricadrebbe in self-judge ottimistico (Huang 2023). La cosa che rende utile l'approvazione è che l'umano guardi. |
| Batch perpetuo ("approva fs_write:~/downloads/* per sempre") | Collassa verso autonomy Full mascherata. Massimo 60 minuti. Per permanenza serve modificare la Policy. |
| Invariante | Test |
|---|---|
| Pausa di lettura rispettata | Click prima di 3s non risolve la richiesta (resta Pending). Deve accettare al 3.01s. |
| Batch non estende a forbidden | BatchGrant per fs_write:~/* non deve auto-approvare una fs_write:/etc/hosts (forbidden). |
| Batch non per azioni irreversibili | Richieste con reversibility=irreversible non espongono il pulsante batch. |
| Tutor mode ogni 10 | Dopo 10 granted consecutivi per stessa effect_class, l'11° ha pause_s=10 e flag tutor=true nella trace. |
| Reset tutor su altro esito | Se tra i 10 consecutivi c'è un deny o timeout, il contatore riparte da 0. |
| Timeout rispettato per canale | ApprovalRequest su CLI con timeout 30s chiude in Timeout se nessuna risposta. |
| Undo su azione in corso | SIGTERM al sandbox entro 50ms dal comando /undo; SIGKILL entro 5.1s. |
| Autonomy change invalida batch | Cambio da Supervised a ReadOnly cancella tutti i batch attivi di quel sender. |
| Append-only nell'audit | Ogni Approval (granted o denied) produce una riga JSONL; nessuna modifica retroattiva. |
| Never list persiste | Dopo "mai più chiederlo" su effect_class X, future richieste X risultano sempre via=never_list, granted=false. |
| Riferimento | Cosa abbiamo preso |
|---|---|
| Nielsen Norman Group, "Speed Bumps for UX" | La pausa di lettura 3s come anti-click-riflesso (§5). |
| Kahneman, Thinking Fast and Slow | Dual-process theory: la pausa forza il passaggio system 1 → system 2 (§5). |
| Parasuraman & Manzey 2010, Complacency and Bias in Automation Use | Automation bias: la giustificazione scientifica del tutor mode (§7). |
| Cookie consent UX studies (varie, 2018-2023) | Come non fare approval fatigue: classe, non singolo evento (§4). |
| Giudizio di fase 0 — critica bloccante #5 | Il mandato di scrivere questo doc. |
| Tensioni 1 e 2 del giudizio di fase 0 | Antropomorfizzazione e automation bias: mitigate qui in modo strutturale. |
ApprovalRequest è innescata: §4 di agent_runtime, transizione "policy decision = approve_required". I due doc si parlano da vicino.eval.html, terzo dei trasversali.
myclaw — approval_ux microprogettazione v1.0 — 2026-04-21
Secondo dei tre doc trasversali di fase 1. Prossimo: eval.html.