|
|
|
@@ -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": "<motivo>"}',
|
|
|
|
|
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Engine fermo → CLI.**
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cerbero-bite kill-switch disarm --reason "<motivo>" \
|
|
|
|
|
--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
|
|
|
|
|