diff --git a/.env.example b/.env.example index f728170..0c27052 100644 --- a/.env.example +++ b/.env.example @@ -38,5 +38,9 @@ CHLOVA_LOG_LEVEL=info # gatekeeper + cycle need-review. Toute valeur autre que "2" retombe sur 1. CHLOVA_PHASE=1 CHLOVA_DB_PATH=./data/chlova.db # SQLite : table assets (need-review, Phase 2) +# Alertes (Phase 3) : URL du webhook n8n qui envoie le mail (workflow +# workflows-n8n/chlova-alerts.v1.0.0.json). Vide = alertes log-only (fail-safe). +# Peut contenir un token de chemin → secret, jamais commité. +ALERT_WEBHOOK_URL= # ex. http://n8n:5678/webhook/chlova-alert # Phase 1 : aucun port publié (Telegram en long-polling). Renseigné en P3+ si API/UI. # CHLOVA_HTTP_PORT=8080 diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e5d5f..ed5fba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi ## [Unreleased] +## [0.18.0] — 2026-06-23 — fin Phase 3 (alertes) +### Added +- `workflows-n8n/chlova-alerts.v1.0.0.json` : workflow n8n webhook → mail (formate + selon le type d'alerte). Exporté en JSON (le dépôt fait foi). +- `docs/assets/workflow-chlova-alerts.md` : doc d'asset (rôle, IO, deps, sécurité, + rollback) — palier `privileged`. +### Changed +- `.env.example` + compose : variable `ALERT_WEBHOOK_URL` (vide = log-only). +- `docs/need-review.md` : section alertes marquée implémentée. Compose revalidé. + ## [0.17.0] — 2026-06-23 ### Added - Config `ALERT_WEBHOOK_URL` (optionnel, traité comme secret) → `HttpAlertSender`, diff --git a/docs/assets/workflow-chlova-alerts.md b/docs/assets/workflow-chlova-alerts.md new file mode 100644 index 0000000..d7a84f4 --- /dev/null +++ b/docs/assets/workflow-chlova-alerts.md @@ -0,0 +1,40 @@ +## CHLOVA Alerts + +- **Type** : `workflow-n8n` +- **Version** : `v1.0.0` +- **Palier de risque** : `privileged` (envoie des mails / sortie externe) +- **Statut (need-review)** : `n/a` (créé à la main, hors auto-extension) +- **Lien commit** : voir tag `v0.18.0` +- **Créé le** : 2026-06-23 +- **Fichier** : `workflows-n8n/chlova-alerts.v1.0.0.json` + +### Rôle +Reçoit les alertes du backend CHLOVA (webhook) et envoie un mail. Met en œuvre la +stratégie anti-fatigue : tentative bloquée, 1ʳᵉ exécution provisoire, rappel J-1, +digest quotidien (voir `docs/need-review.md`). + +### Entrées / sorties +- **Entrée** : `POST /webhook/chlova-alert`, corps JSON + `{ source, ts, kind, ... }` (`kind` ∈ blocked_attempt | first_provisional_exec | + countdown_j1 | daily_digest). Jamais de secret. +- **Sortie** : un mail (sujet + corps formatés par le node *Format mail*). + +### Dépendances +- n8n (instance existante) + credential **SMTP** (`SMTP CHLOVA`, à créer dans n8n). +- Backend : variable `ALERT_WEBHOOK_URL` = URL de ce webhook (par **référence**). + +### Sécurité +- Le webhook ne reçoit que des métadonnées (ids/types/statuts/dates). +- Destinataire et identifiants SMTP vivent dans n8n (jamais dans le dépôt) : + remplacer `toEmail` et le credential `smtp` après import. +- Palier `privileged` : sortie mail externe. + +### Rollback +- Désactiver/supprimer le workflow dans n8n ; ou vider `ALERT_WEBHOOK_URL` côté + backend → retour au log-only (`NullAlertSender`), sans rupture. +- `git revert` du commit pour retirer l'export. + +### Tests / vérification +- Importer le JSON, brancher SMTP, exécuter un `POST` de test avec + `{"kind":"daily_digest","blocked":1,"provisional":0,"items":[]}` → mail reçu. +- Le dépôt fait foi : ré-exporter le workflow ici après toute modif (bump version). diff --git a/docs/need-review.md b/docs/need-review.md index 07420e2..2b89f62 100644 --- a/docs/need-review.md +++ b/docs/need-review.md @@ -50,8 +50,18 @@ peut alors valoir `false`). - `git revert` du commit de l'asset fautif ; redeploy version N-1 (GitOps). - Un asset REFUSÉ reste tracé (audit) ; suppression via op SQL manuelle si besoin. -## Reste à faire (Phase 3+) -- Alertes mail via n8n (1ʳᵉ exéc provisoire, digest quotidien, rappel J-1, - alerte BLOQUÉ) — actuellement un simple hook de log `onBlockedAttempt`. +## Alertes (Phase 3 — implémenté) +Stratégie anti-fatigue (`src/alerts/`) : +- **tentative bloquée** → alerte immédiate (`onBlockedAttempt`) ; +- **1ʳᵉ exécution d'un asset PROVISOIRE** → alerte immédiate (`onFirstProvisionalExec`) ; +- **digest quotidien** + **rappel J-1** → `startAlertScheduler` (`runAlertCycleOnce`). + +Transport : `HttpAlertSender` POST vers `ALERT_WEBHOOK_URL` (webhook n8n) ; sans +URL, `NullAlertSender` log-only (fail-safe). Best-effort : une panne d'alerte ne +casse jamais l'agent. Le mail est envoyé par le workflow +`workflows-n8n/chlova-alerts.v1.0.0.json` (doc : `docs/assets/workflow-chlova-alerts.md`). +Le payload ne contient aucun secret. + +## Reste à faire (Phase 4+) - Auto-extension : CHLOVA génère commit + version + doc d'un asset **avant** de le passer en need-review (Phase 4). diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 0101872..5079fa1 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -85,6 +85,7 @@ services: environment: CHLOVA_ENV: ${CHLOVA_ENV:-production} CHLOVA_PHASE: ${CHLOVA_PHASE:-1} # 1 = lecture seule (défaut) ; 2 = écriture sous review + ALERT_WEBHOOK_URL: ${ALERT_WEBHOOK_URL:-} # Phase 3 : vide = alertes log-only OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434} MCP_N8N_URL: ${MCP_N8N_URL} # endpoint MCP natif de n8n MCP_PORTAINER_URL: ${MCP_PORTAINER_URL:-http://mcp-portainer:3000} diff --git a/workflows-n8n/.gitkeep b/workflows-n8n/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/workflows-n8n/chlova-alerts.v1.0.0.json b/workflows-n8n/chlova-alerts.v1.0.0.json new file mode 100644 index 0000000..e5d7d81 --- /dev/null +++ b/workflows-n8n/chlova-alerts.v1.0.0.json @@ -0,0 +1,53 @@ +{ + "name": "CHLOVA Alerts v1.0.0", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "chlova-alert", + "responseMode": "onReceived", + "options": {} + }, + "id": "11111111-1111-4111-8111-111111111111", + "name": "Webhook CHLOVA", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [240, 300], + "webhookId": "chlova-alert" + }, + { + "parameters": { + "language": "javaScript", + "jsCode": "// Construit sujet + corps du mail selon le type d'alerte CHLOVA.\nconst a = $json;\nlet subject, lines = [];\nswitch (a.kind) {\n case 'blocked_attempt':\n subject = `[CHLOVA] Tentative BLOQUÉE : ${a.tool}`;\n lines = [`Asset: ${a.assetId}`, `Outil: ${a.tool}`, `Statut: ${a.status}`, 'Action: review requise (/approve ou /refuse).'];\n break;\n case 'first_provisional_exec':\n subject = `[CHLOVA] 1ère exécution (provisoire) : ${a.tool}`;\n lines = [`Asset: ${a.assetId}`, `Outil: ${a.tool}`, 'Sursis de 7 jours en cours.'];\n break;\n case 'countdown_j1':\n subject = `[CHLOVA] Rappel J-1 : ${a.items.length} asset(s) expirent bientôt`;\n lines = a.items.map(i => `${i.id} [${i.riskTier}] expire ${new Date(i.expiresAt).toISOString().slice(0,10)}`);\n break;\n case 'daily_digest':\n subject = `[CHLOVA] Digest : ${a.blocked} bloqué(s), ${a.provisional} provisoire(s)`;\n lines = a.items.map(i => `${i.id} [${i.riskTier}/${i.status}]`);\n break;\n default:\n subject = `[CHLOVA] Alerte: ${a.kind}`;\n lines = [JSON.stringify(a)];\n}\nreturn [{ json: { subject, text: lines.join('\\n') } }];" + }, + "id": "22222222-2222-4222-8222-222222222222", + "name": "Format mail", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [480, 300] + }, + { + "parameters": { + "fromEmail": "chlova@example.com", + "toEmail": "REMPLACER_PAR_DESTINATAIRE", + "subject": "={{ $json.subject }}", + "text": "={{ $json.text }}", + "options": {} + }, + "id": "33333333-3333-4333-8333-333333333333", + "name": "Envoi mail", + "type": "n8n-nodes-base.emailSend", + "typeVersion": 2.1, + "position": [720, 300], + "credentials": { + "smtp": { "id": "REMPLACER", "name": "SMTP CHLOVA" } + } + } + ], + "connections": { + "Webhook CHLOVA": { "main": [[{ "node": "Format mail", "type": "main", "index": 0 }]] }, + "Format mail": { "main": [[{ "node": "Envoi mail", "type": "main", "index": 0 }]] } + }, + "settings": { "executionOrder": "v1" }, + "meta": { "chlova": { "asset": "workflow-chlova-alerts", "version": "1.0.0", "riskTier": "privileged" } } +}