From ce475da173524e6ce8702212009ab2fc99137ebf Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 5 Jun 2026 09:23:07 +0000 Subject: [PATCH] docs(safety): disarm via coda manual_actions, gap storici audit, trigger _safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiornamenti post-incidente 31/05 (kill switch armato 6 giorni da un singolo McpTimeoutError transitorio) e post-ripristino 05/06: - 07: tabella trigger — documentato orchestrator._safe (qualsiasi eccezione di tick → arm CRITICAL al primo errore) e la tolleranza al lag benigno dell'anchor (647e3e5) - 07: sezione Disarm — percorso raccomandato via coda manual_actions a engine acceso; CLI solo a engine fermo (race CLI<->engine = gap permanente nella hash chain) - 07: nuova sezione "Limiti noti" — vincolo single-writer e i 7 gap storici benigni del log di produzione (01/05 e 29/05); audit verify full-chain fallisce alla riga 11 by design - 06: nota in Flusso 5 — la tolleranza "3 fallimenti" vale solo per i probe health, _safe arma al primo errore su ogni altro tick Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/06-operational-flow.md | 8 +++++ docs/07-risk-controls.md | 67 +++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/docs/06-operational-flow.md b/docs/06-operational-flow.md index d4da5e1..bf588d8 100644 --- a/docs/06-operational-flow.md +++ b/docs/06-operational-flow.md @@ -156,6 +156,14 @@ Trigger: ogni 5 minuti. Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga scritto: silenzio > 15 min → kill switch via SQLite e alert. +> **Nota — la tolleranza "3 fallimenti" vale solo per i probe del +> health check.** Tutti i tick schedulati (entry, monitor, snapshot, +> …) girano dentro `orchestrator._safe`, che escala **qualsiasi** +> eccezione non gestita a `alert_manager.critical` → kill switch +> armato al **primo** errore, anche transitorio. È il percorso che ha +> fermato il bot il 31/05/2026 (singolo `McpTimeoutError` nel tick +> entry). Vedi la tabella trigger in `07-risk-controls.md`. + ## Flusso 5b — Manual actions consumer Trigger: cron `*/1 * * * *` (job APScheduler `manual_actions`). diff --git a/docs/07-risk-controls.md b/docs/07-risk-controls.md index 583f241..c3239c6 100644 --- a/docs/07-risk-controls.md +++ b/docs/07-risk-controls.md @@ -36,26 +36,51 @@ infrastrutturali o decisioni umane fuori posto. | MCP `cerbero-deribit` non risponde per 3 health check consecutivi | Sì | `runtime/health_check.py` | Severity HIGH | | MCP `cerbero-macro` / `cerbero-hyperliquid` / `cerbero-sentiment` non risponde per 3 health check consecutivi | Sì | `runtime/health_check.py` | Severity HIGH | | `mcp-deribit.environment_info.environment` ≠ `strategy.execution.environment` | Sì | `runtime/orchestrator.boot` + health check | Severity CRITICAL al boot, HIGH a runtime | -| Mismatch tra il tail del file `data/audit.log` e `system_state.last_audit_hash` (truncation o tampering) | Sì | `runtime/orchestrator._verify_audit_anchor` | Severity CRITICAL al boot | +| Mismatch tra il tail del file `data/audit.log` e `system_state.last_audit_hash` (truncation o tampering) | Sì | `runtime/orchestrator._verify_audit_anchor` | Severity CRITICAL al boot. Dal fix `647e3e5` il **lag benigno** dell'anchor (anchor indietro rispetto al tail, ma antenato genuino — vedi `safety/audit_log.tail_continues_from`) è tollerato e ri-sincronizzato senza armare | +| Eccezione non gestita in **qualsiasi** tick schedulato (`entry`, `monitor`, `health`, snapshot, backup, …) | Sì | `orchestrator._safe` → `alert_manager.critical` | Severity CRITICAL. Attenzione: anche un errore **transitorio una tantum** arma in modo permanente. Incidente 31/05/2026: un singolo `McpTimeoutError` su `sentiment.get_cross_exchange_funding` nel tick entry → bot fermo 6 giorni nonostante il servizio si fosse ripreso in pochi secondi. Possibile hardening futuro: tolleranza N-consecutivi o auto-pause temporanea per errori MCP transitori | | Stato SQLite incoerente con il broker (recovery non risolutivo) | Sì | `runtime/recovery.py` | Severity CRITICAL al boot | | `place_combo_order` di chiusura respinto dal broker | Sì | `runtime/monitor_cycle.py` | Severity CRITICAL; la posizione torna in `open` per ritentare | | `place_combo_order` di apertura respinto dal broker | Sì | `runtime/entry_cycle.py` | Severity HIGH; la posizione viene marcata `cancelled` | -| Hash chain audit non verifica (`audit verify` fallisce) | Manuale per ora; CLI `audit verify` segnala l'anomalia con exit 2 | `cli.py audit verify` + `safety/audit_log.verify_chain` | Severity CRITICAL quando integrata nel boot | +| Hash chain audit non verifica (`audit verify` fallisce) | Manuale per ora; CLI `audit verify` segnala l'anomalia con exit 2 | `cli.py audit verify` + `safety/audit_log.verify_chain` | ⚠️ Il log di produzione ha 7 gap storici benigni (vedi «Limiti noti» sotto): `audit verify` full-chain fallisce alla riga 11 by design. Non integrare il full-chain verify nel boot senza prima ruotare il log con un nuovo genesis | | Comando manuale via `cerbero-bite kill-switch arm` | Sì | `cli.py kill_switch_arm` | Severity HIGH (operator-initiated) | ### Disarm +Due percorsi, a seconda che l'engine sia in esecuzione o fermo. + +**Engine in esecuzione → coda `manual_actions` (raccomandato).** +Si accoda una riga `kind="disarm_kill"` (dalla GUI, o a mano via +SQLite); il job `manual_actions` (cron `*/1`) la consuma e chiama +`KillSwitch.disarm` **in-process** entro un minuto: + +```sql +INSERT INTO manual_actions (kind, payload_json, created_at) +VALUES ('disarm_kill', '{"reason": ""}', + strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +``` + +**Engine fermo → CLI.** + ```bash cerbero-bite kill-switch disarm --reason "" \ --db data/state.sqlite \ --audit data/audit.log ``` +⚠️ **Non usare il CLI con l'engine in esecuzione**: il CLI appende +all'audit log da un processo separato e va in race con gli append +dell'engine — il risultato è un fork/gap permanente nella hash chain +(`prev_hash` che non aggancia la riga precedente). I 7 gap storici +del log (01/05 e 29/05/2026, vedi «Limiti noti» sotto) sono stati +causati esattamente da questo pattern. Il percorso a coda non ha il +problema perché l'unico writer resta l'engine. + L'operazione è transazionale: SQLite `system_state.kill_switch = 0` + una linea `KILL_SWITCH_DISARMED` nella audit chain con il motivo. Il disarm non riavvia automaticamente lo scheduler; è il prossimo tick naturale (entry giornaliero o monitor 12h) a far ripartire la -decisione. +decisione. Il disarm **persiste attraverso i restart** del container, +così come l'arm (verificato 29/05 e 05/06/2026). ## Cap di rischio (oltre alle regole di strategia) @@ -156,6 +181,42 @@ il tail del file: in caso di mismatch (truncation, sostituzione, file mancante) viene armato il kill switch CRITICAL prima che qualsiasi ciclo trading parta. +Dal fix `647e3e5` (29/05/2026) il check distingue il **lag benigno** +dal tampering: se l'anchor persistito è indietro rispetto al tail ma è +un *antenato genuino* (la chain dall'anchor al tail verifica — +`safety/audit_log.tail_continues_from`), il boot ri-sincronizza +l'anchor e prosegue senza armare. L'anchor è infatti persistito +best-effort e può restare indietro sotto write contention SQLite. Un +anchor assente dal file o una chain post-anchor rotta restano +tampering e armano CRITICAL (verificato con test negativo il 29/05). + +### Limiti noti: gap da scritture concorrenti + +La chain assume un **single writer** (l'engine). Un processo separato +che appende mentre l'engine gira (CLI `kill-switch arm/disarm`, +script di resync) legge il tail, calcola `prev_hash` e scrive — ma se +l'engine appende nel frattempo, una riga viene persa o la chain si +biforca: la riga successiva ha un `prev_hash` che non aggancia nulla. + +Il file `data/audit.log` di produzione contiene **7 gap storici di +questo tipo, tutti benigni e attribuiti** (verifica completa del +05/06/2026): + +| Righe | Data | Causa | +|---|---|---| +| 11, 16 | 01/05/2026, primo boot | write contention durante il caos env mismatch testnet/mainnet | +| 8130, 8262, 8287 | 29/05/2026 | restart/resync manuali durante il debug anchor (8287 ha persino timestamp fuori ordine) | +| 8301, 8323 | 29/05/2026 | disarm via CLI con engine in esecuzione (race CLI ↔ engine) | + +Conseguenza operativa: `cerbero-bite audit verify` (full-chain dal +genesis) fallisce **per sempre** alla riga 11 — è atteso, non è +tampering. Il controllo operativo in vigore è quello dell'anchor al +boot (tail-continuity), che resta pienamente efficace per truncation +e tampering del tail. Eventuali gap **nuovi** (oltre ai 7 elencati) +vanno invece investigati. Mitigazione: usare la coda `manual_actions` +per arm/disarm a engine acceso (mai il CLI), così l'unico writer +resta l'engine. + ## Dry-run mode Il comando `cerbero-bite dry-run --cycle entry|monitor|health` esegue