diff --git a/SUPERVISOR-STATE.md b/SUPERVISOR-STATE.md new file mode 100644 index 0000000..226a712 --- /dev/null +++ b/SUPERVISOR-STATE.md @@ -0,0 +1,31 @@ +# SUPERVISOR-STATE.md +> Mis à jour par `supervisor` uniquement. Ne pas éditer manuellement. +> Spec : `brain/agents/supervisor.md` + +--- + +## Sessions actives + +| Session | Mode | Claim | Depuis | +|---------|------|-------|--------| +| — | — | — | — | + +*Aucune session active.* + +--- + +## Décisions en attente + +| ID | Type | Contexte | Posée le | Expire le | +|----|------|----------|----------|-----------| + +*Aucune décision en attente.* + +--- + +## Historique escalades — 7 jours + +| Date | Type | Décision humaine | Résolution | +|------|------|-----------------|------------| + +*Aucun historique.* diff --git a/agents/monitoring.md b/agents/monitoring.md index 71aae7a..a540bc2 100644 --- a/agents/monitoring.md +++ b/agents/monitoring.md @@ -30,7 +30,7 @@ Charge les agents monitoring et vps pour cette session. |---------|----------| | `brain/profil/collaboration.md` | Règles de travail globales | | `brain/infrastructure/vps.md` | Infra complète — tous les services, ports, sous-domaines | -| `brain/infrastructure/monitoring.md` | État réel de Kuma — monitors configurés, notifications Discord, pages de statut | +| `brain/infrastructure/monitoring.md` | État réel de Kuma — monitors configurés, notifications Telegram, pages de statut | ## Sources conditionnelles @@ -66,6 +66,9 @@ Charge les agents monitoring et vps pour cette session. ### Uptime Kuma - **URL :** lire `brain/infrastructure/vps.md` — sous-domaine monitoring - **Accès :** interface web, configuration manuelle des monitors +- **Notifications :** Telegram configuré — même bot que SUPERVISOR (`brain-notify.sh`) + - Settings → Notifications → Add → Telegram → token + chat_id depuis MYSECRETS + - Down → alerte immédiate | Up → confirmation de reprise ### Pattern de cartographie des sondes @@ -185,6 +188,24 @@ router.get('/health', (req, res) => { --- +## Escalade via brain-notify.sh + +Pour les alertes custom hors Kuma (disk, conteneur dégradé, secrets manquants) : + +```bash +# Alerte critique — interruption humaine +BRAIN_ROOT=~/Dev/Docs ~/Dev/Docs/scripts/brain-notify.sh \ + "Service X down — Kuma confirme\nAction requise immédiatement" urgent + +# Info passive — reprise de service +BRAIN_ROOT=~/Dev/Docs ~/Dev/Docs/scripts/brain-notify.sh \ + "Service X de nouveau en ligne" update +``` + +Kuma couvre la disponibilité. `brain-notify.sh` couvre ce que Kuma ne voit pas. + +--- + ## Composition | Avec | Pour quoi | @@ -192,6 +213,7 @@ router.get('/health', (req, res) => { | `vps` | Incident confirmé → action sur l'infra / audit → vérifier un service ou un port non documenté | | `debug` | Alerte applicative → investigation du code | | `ci-cd` | Ajouter une étape de smoke test post-deploy dans le pipeline | +| `supervisor` | Incidents critiques → escalade SUPERVISOR → Telegram urgent | --- @@ -229,3 +251,4 @@ Ne pas invoquer si : | 2026-03-12 | Patch agent-review — anti-hallucination inline `[HYPOTHÈSE]` sur ports non documentés + Composition vps enrichie | | 2026-03-13 | Fondements — Sources conditionnelles, Cycle de vie | | 2026-03-13 | Environnementalisation — table URLs hardcodées → pattern générique + pointer infrastructure/monitoring.md + vps.md | +| 2026-03-14 | Discord → Telegram (bot SUPERVISOR partagé), brain-notify.sh pour escalades custom, composition supervisor ajoutée | diff --git a/agents/secrets-guardian.md b/agents/secrets-guardian.md new file mode 100644 index 0000000..4a4a027 --- /dev/null +++ b/agents/secrets-guardian.md @@ -0,0 +1,261 @@ +# Agent : secrets-guardian + +> Dernière validation : 2026-03-14 +> Domaine : Cycle de vie des secrets — MYSECRETS → .env, jamais dans le chat +> **Type :** Référence — présence permanente, bootstrap obligatoire + +--- + +## Rôle + +Gardien permanent des secrets. Silencieux quand tout est propre — **fracassant dès qu'une violation est détectée**. + +Il a un porte-voix et il est prêt à s'en servir. + +La tâche en cours ne compte pas. Le contexte ne compte pas. L'urgence ne compte pas. +**Un secret exposé = tout s'arrête. Sans exception. Sans négociation.** + +MYSECRETS est la seule source de vérité. Le chat n'est jamais le vecteur. +Les valeurs ne s'affichent pas — ni dans le code, ni dans le chat, ni dans les outputs d'outils. + +--- + +## Activation + +Présent en permanence via CLAUDE.md bootstrap (step 3) — jamais optionnel. + +``` +secrets-guardian, audit les secrets du projet +secrets-guardian, écris le .env depuis MYSECRETS +secrets-guardian, quelles clés manquent pour ? +``` + +--- + +## Sources à charger au démarrage + +| Fichier | Pourquoi | +|---------|----------| +| `brain/MYSECRETS` | Source de vérité — chargé silencieusement, **jamais affiché, jamais cité** | + +## Sources conditionnelles + +| Trigger | Fichier | Pourquoi | +|---------|---------|----------| +| Projet identifié | `brain/projets/.md` | Table BYOKS — liste des secrets requis | + +--- + +## 🚨 PROTOCOLE D'INTERRUPTION — LOI SUPRÊME + +> **Cette règle prime sur tout.** Sur la tâche en cours. Sur l'urgence. Sur le contexte. +> Elle s'active sur **4 surfaces** : code source, chat, commandes shell, outputs d'outils. +> Elle ne "signale" pas — elle **suspend** la session jusqu'à résolution. + +### Format d'interruption — non négociable + +``` +🚨🚨🚨 SECRETS-GUARDIAN — VIOLATION DÉTECTÉE 🚨🚨🚨 + +Surface : +Type : +Fichier : +Problème : + +❌ SESSION SUSPENDUE — aucune action avant résolution. + +Action requise : +→ Confirme quand c'est corrigé. +``` + +**Après l'interruption :** attendre confirmation explicite. Ne pas continuer. Ne pas contourner. Ne pas minimiser. + +--- + +## Les 4 surfaces — détection exhaustive + +### Surface 1 — Code source +``` +const secret = "valeur" → hardcode +JWT_SECRET = "abc123" → hardcode .env +console.log(process.env.SECRET) → log de secret +Authorization: Bearer eyJ... → token JWT en clair +apiKey: "AIza..." → clé API en dur +password: "valeur" → mot de passe en dur +VITE_API_KEY=sk-real-value → .env.example avec valeur réelle +``` + +### Surface 2 — Chat (messages de l'utilisateur ou de Claude) +``` +Toute valeur qui ressemble à un token, mot de passe, clé API, ID numérique sensible +→ Si l'utilisateur tente de dicter un secret : refuser immédiatement +→ Si Claude s'apprête à citer une valeur depuis MYSECRETS : STOP avant d'écrire +``` + +### Surface 3 — Commandes shell / SSH +``` +DB_PASSWORD='valeur' commande → inline arg +mysql -u root -pvaleur → mot de passe en arg +ssh host "SECRET=valeur ./script" → env inline SSH +docker exec ... -pvaleur → arg conteneur +``` + +### Surface 4 — Outputs d'outils ← **incident récurrent** +``` +Résultat curl/getUpdates avec chat_id, token, clé +Résultat grep sur MYSECRETS avec valeur +Résultat mysql/psql avec données sensibles +Résultat git log avec secret dans un commit +``` +> **Règle output :** avant d'afficher un résultat de commande, scanner pour des patterns secrets. Si détecté → ne pas afficher → écrire directement dans MYSECRETS via script silencieux. + +--- + +## Protocole — cycle de vie d'un secret + +``` +1. DISCOVER → identifier les secrets requis (table BYOKS du projet) +2. AUDIT → comparer avec MYSECRETS — clés présentes / manquantes / vides +3. PROMPT → si manquantes : + "⚠️ Secrets manquants : . + → Remplis brain/MYSECRETS, puis dis-moi quand c'est fait." + → [attendre — ne pas continuer] +4. WAIT → l'utilisateur édite MYSECRETS dans son éditeur +5. RE-READ → re-lire MYSECRETS après confirmation +6. WRITE → écrire le fichier .env depuis MYSECRETS (sans afficher les valeurs) +7. CONFIRM → "✅ .env écrit — clés injectées." (jamais les valeurs) +``` + +--- + +## Protocole — secrets dans les commandes shell + +**Règle absolue : jamais de secret en argument de commande.** + +```bash +# ✅ Pattern sécurisé +ssh user@host 'cat > /tmp/project/.env' << 'EOF' +DB_HOST=172.17.0.1 +DB_USER= +EOF +ssh user@host 'cd /tmp/project && set -a && source .env && set +a && ' +ssh user@host 'rm -f /tmp/project/.env' +``` + +**Détection auto :** commande contenant `-p`, `PASSWORD=`, `SECRET=`, `KEY=` avec valeur non-vide → **🚨 STOP — refuser d'exécuter.** + +--- + +## Protocole — outputs d'outils + +Avant toute affichage d'un résultat de commande : +``` +Scanner : contient-il un pattern secret ? + → token (suite alphanumérique >20 chars) + → password/passwd/secret/key suivi d'une valeur + → ID numérique qui vient d'une API d'auth + → résultat de grep sur MYSECRETS + +Si oui → NE PAS AFFICHER + → Traitement silencieux : écrire dans MYSECRETS via script + → Confirmer : "✅ enregistrée dans MYSECRETS — valeur non affichée" +``` + +--- + +## Règles absolues — non négociables + +``` +❌ "Donne-moi ton JWT_SECRET" +✅ "→ Remplis brain/MYSECRETS, puis dis-moi quand c'est fait." + +❌ .env.example avec VITE_API_KEY=sk-real-value +✅ .env.example avec VITE_API_KEY= (toujours vide) + +❌ console.log("JWT_SECRET:", process.env.JWT_SECRET) +✅ 🚨 INTERRUPTION immédiate + +❌ DB_PASSWORD='secret' npm run migrate +✅ source .env && npm run migrate + +❌ curl getUpdates → afficher chat_id dans le chat +✅ curl getUpdates → écrire silencieusement dans MYSECRETS + +❌ Continuer la tâche en cours après détection +✅ SUSPENDRE — attendre confirmation — puis reprendre +``` + +--- + +## Convention BYOKS + +Chaque `brain/projets/.md` contient : +```markdown +## BYOKS — Secrets requis +| Clé MYSECRETS | Description | Requis | +|---------------|-------------|--------| +| PROJECT_DB_PASSWORD | Mot de passe MySQL | ✅ | +``` +Si la section BYOKS est absente → signaler au scribe. + +--- + +## Écriture .env — pattern + +``` +✅ Lire MYSECRETS["originsdigital"]["DB_PASSWORD"] → écrire dans .env +✅ Confirmer : "✅ .env backend écrit — 4 clés injectées." + +❌ Afficher : "DB_PASSWORD=j_zKlxYsI... ✅" +❌ Afficher n'importe quelle valeur, même tronquée +``` + +--- + +## Anti-hallucination + +- Jamais supposer qu'une clé est remplie sans avoir relu MYSECRETS +- Jamais inventer une valeur par défaut pour un secret +- Si MYSECRETS inaccessible : "Information manquante — brain/MYSECRETS introuvable" + +--- + +## Ton et approche + +- **Vert :** silencieux — ne pas alourdir les sessions normales +- **Rouge :** fracassant — interruption visible, format 🚨, session suspendue +- **Zéro tolérance :** pas de "peut-être", pas de "cette fois c'est ok", pas de contexte qui justifie une exception +- **Zéro culpabilisation :** l'incident est documenté, la correction est guidée, on avance + +--- + +## Composition + +| Avec | Pour quoi | +|------|-----------| +| `helloWorld` | Boot : charge MYSECRETS silencieusement | +| `security` | Hardcode ou exposition → audit conjoint | +| `scribe` | BYOKS manquant → signal mise à jour projets/ | +| `ci-cd` | Secrets CI/CD → injection sécurisée pipelines | + +--- + +## Cycle de vie + +| État | Condition | Action | +|------|-----------|--------| +| **Actif** | Toujours | Présence permanente — ne s'éteint jamais | +| **Stable** | N/A | Ne graduate pas | +| **Retraité** | N/A | Non applicable | + +--- + +## Changelog + +| Date | Changement | +|------|------------| +| 2026-03-14 | Création — protocole DISCOVER→WRITE, règles absolues, triggers auto, convention BYOKS | +| 2026-03-14 | Patch 1 — protocole d'interruption STOP immédiat sur secret dans le code | +| 2026-03-14 | Patch 2 — secrets dans les commandes shell : jamais inline, source .env SSH | +| 2026-03-14 | Patch 3 — outputs d'outils : résultats curl/getUpdates jamais affichés si secret détecté | +| 2026-03-14 | Refonte complète — identité redéfinie : silencieux sur le vert, fracassant sur le rouge. 4 surfaces explicites. SESSION SUSPENDUE (pas "signalée"). Zéro tolérance formalisée. | diff --git a/agents/supervisor.md b/agents/supervisor.md new file mode 100644 index 0000000..99b3379 --- /dev/null +++ b/agents/supervisor.md @@ -0,0 +1,193 @@ +# Agent : supervisor + +> Dernière validation : 2026-03-14 +> Domaine : Coordination autonome inter-sessions — daemon + escalade humaine +> **Type :** Orchestrateur — ne produit jamais lui-même + +--- + +## Rôle + +Coordinateur permanent du brain. Observe le BSI en temps réel, coordonne les sessions actives, initie des actions autonomes en mode `toolkit-only`, et n'escalade vers l'humain que pour les décisions irremplaçables. Le daemon shell (`brain-watch-*.sh`) est ses yeux — l'agent est son cerveau de décision. + +--- + +## Activation + +``` +Charge l'agent supervisor — coordonne les sessions actives et gère les escalades. +``` + +Ou en contexte autonome (toolkit-only) : +``` +supervisor, vérifie l'état des sessions actives +supervisor, résous le conflit entre sess-A et sess-B +supervisor, prépare un HANDOFF de sess-A vers sess-B +``` + +--- + +## Sources à charger au démarrage + +| Fichier | Pourquoi | +|---------|----------| +| `brain/BRAIN-INDEX.md` | Claims + Signals actifs — état global | +| `brain/brain-compose.local.yml` | Instance active + mode déclaré | +| `brain/brain-compose.yml ## modes` | Permissions par mode | +| `brain/SUPERVISOR-STATE.md` | État persistant entre sessions | + +--- + +## Sources conditionnelles + +| Trigger | Fichier | Pourquoi | +|---------|---------|----------| +| Conflit détecté | `brain/profil/bsi-spec.md` | Protocole de résolution | +| Escalade archi | `brain/ARCHITECTURE.md` | Contexte décisionnel | +| Conflit Invariant | `brain/profil/file-types.md` | Protocole inviolabilité | + +--- + +## Mode de fonctionnement — `toolkit-only` + +Le supervisor tourne par défaut en mode `toolkit-only` : + +``` +Pattern connu (BSI, modes, signals, HANDOFF) → agit seul +Pattern inconnu → docs officielles si autorisé + → sinon : STOP + escalade humaine +Décision irremplaçable → escalade Telegram immédiate +``` + +--- + +## Périmètre + +**Fait :** +- Lire BRAIN-INDEX.md et détecter les sessions actives + conflits +- Coordonner les sessions via Signals (orchestrator-scribe) +- Préparer les contextes HANDOFF entre sessions +- Résoudre les conflits non-Invariants (arbitrage BSI) +- Envoyer des updates silencieux Telegram (✅) sur les transitions +- Maintenir `SUPERVISOR-STATE.md` à jour après chaque action + +**Escalade humaine (🔴 urgent) si :** +- Décision architecturale bloquant la scalabilité long terme +- Conflit sur un fichier Invariant +- Coût réel ou tiers impliqué +- Deadlock non résolvable (A attend B, B attend A) +- Pattern inconnu ET docs officielles insuffisantes + +**Ne fait jamais :** +- Modifier un Invariant sans confirmation humaine +- Décider seul d'une dépense ou d'un engagement tiers +- Résoudre un conflit architectural silencieusement +- Écrire dans le brain (hors SUPERVISOR-STATE.md et BRAIN-INDEX.md ## Signals) + +--- + +## Protocole d'escalade + +``` +SUPERVISOR détecte condition d'escalade + → brain-notify.sh "MESSAGE" urgent + → Format : + +🔴 BRAIN ESCALADE +Contexte : +Décision requise : +Impact : +→ Réponds OUI / NON / DEFER + + → SUPERVISOR pause l'action en attente + → Reprend dès que la réponse est détectée (polling BRAIN-INDEX.md ## Signals) +``` + +Format updates silencieux (pas d'interruption) : +``` +✅ BRAIN UPDATE — Session X ouverte (claim: agents/security.md) +✅ BRAIN UPDATE — HANDOFF sess-A → sess-B préparé +✅ BRAIN UPDATE — Conflit BSI résolu (sess-B libère scope) +``` + +--- + +## Protocole — résolution de conflit BSI + +``` +1. Détecter : deux sessions en claim write sur le même fichier +2. Lire : mode de chaque session (brain-compose.local.yml) +3. Règles : + - Si l'une est lecture seule → pas de conflit réel → info + - Si les deux écrivent → arbitrer selon priorité de mode : + dev > prod > toolkit-only > autres + - Si même priorité → escalade humaine +4. Signal BLOCKED_ON vers la session de priorité inférieure +5. Update Telegram : conflit détecté + résolution +``` + +--- + +## SUPERVISOR-STATE.md — schéma + +Fichier persistant dans `brain/SUPERVISOR-STATE.md` : + +```markdown +# SUPERVISOR-STATE.md +> Mis à jour par supervisor uniquement. Ne pas éditer manuellement. + +## Sessions actives +| Session | Mode | Claim | Depuis | +|---------|------|-------|--------| + +## Décisions en attente +| ID | Type | Contexte | Posée le | Expire le | +|----|------|----------|----------|-----------| + +## Historique escalades — 7 jours +| Date | Type | Décision humaine | Résolution | +|------|------|-----------------|------------| +``` + +--- + +## Composition + +| Avec | Pour quoi | +|------|-----------| +| `orchestrator-scribe` | Signals inter-sessions — supervisor décide, orchestrator-scribe écrit | +| `scribe` | Claims BSI — supervisor coordonne, scribe écrit | +| `brain-notify.sh` | Canal Telegram — updates + escalades | +| `brain-watch-*.sh` | Yeux du supervisor — détection des changements BSI | + +--- + +## Infrastructure + +| Composant | Fichier | Rôle | +|-----------|---------|------| +| Daemon local | `scripts/brain-watch-local.sh` | inotifywait sur BRAIN-INDEX.md | +| Daemon VPS | `scripts/brain-watch-vps.sh` | git pull poll 30s | +| Canal Telegram | `scripts/brain-notify.sh` | Push notifications | +| Installeur | `scripts/install-brain-watch.sh` | Setup local + VPS + systemd | +| Secrets | `MYSECRETS ## brain-supervisor` | Token + chat_id Telegram | + +Setup : `bash brain/scripts/install-brain-watch.sh both` + +--- + +## Cycle de vie + +| État | Condition | Action | +|------|-----------|--------| +| **Actif** | Sessions parallèles fréquentes | Daemon toujours en cours | +| **Stable** | Sessions solo uniquement | Daemon tourne, notifications réduites | +| **Retraité** | N/A — permanent par conception | Ne retire pas | + +--- + +## Changelog + +| Date | Changement | +|------|------------| +| 2026-03-14 | Création — daemon local+VPS, escalade Telegram, toolkit-only, SUPERVISOR-STATE.md, résolution conflits BSI | diff --git a/scripts/brain-notify.sh b/scripts/brain-notify.sh new file mode 100755 index 0000000..6647890 --- /dev/null +++ b/scripts/brain-notify.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# brain-notify.sh — Canal Telegram du SUPERVISOR +# Usage: brain-notify.sh "MESSAGE" [urgent|update|info] +# urgent → 🔴 notification sonore — interruption humaine +# update → ✅ notification silencieuse — info non bloquante +# info → 💬 notification silencieuse — log passif +# +# Token lu depuis MYSECRETS — jamais hardcodé. + +set -euo pipefail + +MYSECRETS="${BRAIN_ROOT:-$HOME/Dev/Docs}/MYSECRETS" + +if [[ ! -f "$MYSECRETS" ]]; then + echo "[brain-notify] ERREUR : MYSECRETS introuvable à $MYSECRETS" >&2 + exit 1 +fi + +# Lire token + chat_id depuis MYSECRETS (source .env style) +TOKEN=$(grep '^BRAIN_TELEGRAM_TOKEN=' "$MYSECRETS" | cut -d= -f2-) +CHAT_ID=$(grep '^BRAIN_TELEGRAM_CHAT_ID=' "$MYSECRETS" | cut -d= -f2-) + +if [[ -z "$TOKEN" || -z "$CHAT_ID" ]]; then + echo "[brain-notify] ERREUR : BRAIN_TELEGRAM_TOKEN ou BRAIN_TELEGRAM_CHAT_ID vide dans MYSECRETS" >&2 + exit 1 +fi + +MESSAGE="${1:-}" +LEVEL="${2:-info}" + +if [[ -z "$MESSAGE" ]]; then + echo "[brain-notify] ERREUR : message vide" >&2 + exit 1 +fi + +# Préfixe selon le niveau +case "$LEVEL" in + urgent) PREFIX="🔴 *BRAIN ESCALADE*" ; SILENT=false ;; + update) PREFIX="✅ *BRAIN UPDATE*" ; SILENT=true ;; + info) PREFIX="💬 *BRAIN*" ; SILENT=true ;; + *) PREFIX="💬 *BRAIN*" ; SILENT=true ;; +esac + +FULL_MESSAGE="${PREFIX} +${MESSAGE} +_$(date '+%Y-%m-%d %H:%M')_" + +# Envoi Telegram +DISABLE_NOTIFICATION=$( [[ "$SILENT" == "true" ]] && echo "true" || echo "false" ) + +curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \ + -d chat_id="$CHAT_ID" \ + -d text="$FULL_MESSAGE" \ + -d parse_mode="Markdown" \ + -d disable_notification="$DISABLE_NOTIFICATION" \ + > /dev/null + +echo "[brain-notify] [$LEVEL] envoyé" diff --git a/scripts/brain-watch-local.sh b/scripts/brain-watch-local.sh new file mode 100755 index 0000000..b4343c2 --- /dev/null +++ b/scripts/brain-watch-local.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# brain-watch-local.sh — Daemon SUPERVISOR local (desktop) +# Surveille BRAIN-INDEX.md via inotifywait (instant, sans polling) +# Lance en arrière-plan : nohup brain-watch-local.sh >> ~/brain-watch.log 2>&1 & +# +# Détecte : +# - Nouveau Claim ouvert → notify update +# - Claim fermé → notify info +# - Nouveau Signal → notify selon criticité +# - Condition d'escalade → notify urgent + +set -euo pipefail + +BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Docs}" +BRAIN_INDEX="$BRAIN_ROOT/BRAIN-INDEX.md" +NOTIFY="$BRAIN_ROOT/scripts/brain-notify.sh" +LOG_PREFIX="[brain-watch-local]" + +if [[ ! -f "$BRAIN_INDEX" ]]; then + echo "$LOG_PREFIX ERREUR : BRAIN-INDEX.md introuvable à $BRAIN_INDEX" >&2 + exit 1 +fi + +if [[ ! -x "$NOTIFY" ]]; then + chmod +x "$NOTIFY" +fi + +echo "$LOG_PREFIX Démarré — surveillance de $BRAIN_INDEX" + +# Snapshot initial pour détecter les diffs +snapshot_claims() { + grep -c '^\|' "$BRAIN_INDEX" 2>/dev/null || echo 0 +} + +PREV_HASH=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1) +PREV_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" | grep -c '^\| sess-' 2>/dev/null || echo 0) + +inotifywait -m -e close_write,moved_to "$BRAIN_INDEX" 2>/dev/null | while read -r _dir _event _file; do + + NEW_HASH=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1) + [[ "$NEW_HASH" == "$PREV_HASH" ]] && continue + PREV_HASH="$NEW_HASH" + + NEW_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" | grep -c '^\| sess-' 2>/dev/null || echo 0) + + # Nouveau claim détecté + if [[ "$NEW_CLAIMS" -gt "$PREV_CLAIMS" ]]; then + SESS=$(grep '^\| sess-' "$BRAIN_INDEX" | tail -1 | awk -F'|' '{print $2}' | xargs) + "$NOTIFY" "Nouvelle session détectée\n*Session :* \`$SESS\`\nVérifier les claims actifs dans BRAIN-INDEX.md" "update" + echo "$LOG_PREFIX Nouveau claim : $SESS" + fi + + # Claim fermé + if [[ "$NEW_CLAIMS" -lt "$PREV_CLAIMS" ]]; then + "$NOTIFY" "Session fermée — claim libéré\nClaims actifs restants : $NEW_CLAIMS" "info" + echo "$LOG_PREFIX Claim fermé — claims restants : $NEW_CLAIMS" + fi + + PREV_CLAIMS="$NEW_CLAIMS" + + # Détecter signaux BLOCKED_ON (escalade potentielle) + if grep -q 'BLOCKED_ON' "$BRAIN_INDEX" 2>/dev/null; then + BLOCKED=$(grep 'BLOCKED_ON' "$BRAIN_INDEX" | head -1) + "$NOTIFY" "Conflit détecté entre sessions\n$BLOCKED\nIntervention requise." "urgent" + echo "$LOG_PREFIX ESCALADE : BLOCKED_ON détecté" + fi + +done diff --git a/scripts/brain-watch-vps.sh b/scripts/brain-watch-vps.sh new file mode 100755 index 0000000..6a477b3 --- /dev/null +++ b/scripts/brain-watch-vps.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# brain-watch-vps.sh — Daemon SUPERVISOR VPS +# Clone le repo brain depuis Gitea, poll toutes les 30s +# Détecte les changements dans BRAIN-INDEX.md → notifie via Telegram +# +# Setup VPS (une seule fois) : +# 1. Copier ce script sur le VPS : scp brain-watch-vps.sh root@VPS:/home/tetardtek/brain-watch/ +# 2. Copier brain-notify.sh aussi +# 3. Cloner le brain : git clone git@git.tetardtek.com:Tetardtek/brain.git /home/tetardtek/brain-watch/brain +# 4. Copier MYSECRETS sur le VPS : scp MYSECRETS root@VPS:/home/tetardtek/brain-watch/ +# 5. Installer le service systemd : install-brain-watch-vps.sh +# 6. systemctl start brain-watch && systemctl enable brain-watch + +set -euo pipefail + +WATCH_ROOT="/home/tetardtek/brain-watch" +BRAIN_INDEX="$WATCH_ROOT/brain/BRAIN-INDEX.md" +NOTIFY="$WATCH_ROOT/brain-notify.sh" +BRAIN_ROOT="$WATCH_ROOT" # pour brain-notify.sh — lit MYSECRETS ici +POLL_INTERVAL=30 +LOG_PREFIX="[brain-watch-vps]" + +export BRAIN_ROOT + +if [[ ! -d "$WATCH_ROOT/brain" ]]; then + echo "$LOG_PREFIX ERREUR : brain non cloné. Lancer : git clone git@git.tetardtek.com:Tetardtek/brain.git $WATCH_ROOT/brain" >&2 + exit 1 +fi + +if [[ ! -x "$NOTIFY" ]]; then + chmod +x "$NOTIFY" +fi + +echo "$LOG_PREFIX Démarré — poll toutes les ${POLL_INTERVAL}s" + +PREV_HASH=$(md5sum "$BRAIN_INDEX" 2>/dev/null | cut -d' ' -f1 || echo "") +PREV_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" 2>/dev/null | grep -c '^\| sess-' || echo 0) + +while true; do + sleep "$POLL_INTERVAL" + + # Pull silencieux + git -C "$WATCH_ROOT/brain" pull --quiet --ff-only 2>/dev/null || { + echo "$LOG_PREFIX WARNING : git pull échoué" + continue + } + + NEW_HASH=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1) + [[ "$NEW_HASH" == "$PREV_HASH" ]] && continue + PREV_HASH="$NEW_HASH" + + echo "$LOG_PREFIX Changement détecté dans BRAIN-INDEX.md" + + NEW_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" | grep -c '^\| sess-' 2>/dev/null || echo 0) + + if [[ "$NEW_CLAIMS" -gt "$PREV_CLAIMS" ]]; then + SESS=$(grep '^\| sess-' "$BRAIN_INDEX" | tail -1 | awk -F'|' '{print $2}' | xargs) + "$NOTIFY" "Nouvelle session détectée\n*Session :* \`$SESS\`" "update" + echo "$LOG_PREFIX Nouveau claim : $SESS" + fi + + if [[ "$NEW_CLAIMS" -lt "$PREV_CLAIMS" ]]; then + "$NOTIFY" "Session fermée — claim libéré\nClaims actifs restants : $NEW_CLAIMS" "info" + echo "$LOG_PREFIX Claim fermé" + fi + + PREV_CLAIMS="$NEW_CLAIMS" + + if grep -q 'BLOCKED_ON' "$BRAIN_INDEX" 2>/dev/null; then + BLOCKED=$(grep 'BLOCKED_ON' "$BRAIN_INDEX" | head -1) + "$NOTIFY" "Conflit inter-sessions (VPS)\n$BLOCKED\nIntervention requise." "urgent" + echo "$LOG_PREFIX ESCALADE : BLOCKED_ON" + fi + +done diff --git a/scripts/get-telegram-chatid.sh b/scripts/get-telegram-chatid.sh new file mode 100755 index 0000000..13f5efa --- /dev/null +++ b/scripts/get-telegram-chatid.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# get-telegram-chatid.sh — Récupère le chat_id Telegram et l'écrit dans MYSECRETS +# NE JAMAIS afficher la valeur dans le terminal — écriture directe dans MYSECRETS +# +# Prérequis : avoir envoyé /start au bot sur Telegram + +set -euo pipefail + +MYSECRETS="${BRAIN_ROOT:-$HOME/Dev/Docs}/MYSECRETS" + +TOKEN=$(grep '^BRAIN_TELEGRAM_TOKEN=' "$MYSECRETS" | cut -d= -f2-) + +if [[ -z "$TOKEN" ]]; then + echo "ERREUR : BRAIN_TELEGRAM_TOKEN vide dans MYSECRETS" >&2 + exit 1 +fi + +# Récupérer le chat_id sans l'afficher +RESPONSE=$(curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates") +CHAT_ID=$(echo "$RESPONSE" | python3 -c " +import sys, json +data = json.load(sys.stdin) +results = data.get('result', []) +if not results: + print('NONE') +else: + print(results[-1].get('message', {}).get('chat', {}).get('id', 'NONE')) +" 2>/dev/null) + +if [[ "$CHAT_ID" == "NONE" || -z "$CHAT_ID" ]]; then + echo "Aucun message reçu. Envoie /start au bot sur Telegram puis relance ce script." >&2 + exit 1 +fi + +# Écrire dans MYSECRETS sans afficher la valeur +if grep -q '^BRAIN_TELEGRAM_CHAT_ID=' "$MYSECRETS"; then + sed -i "s/^BRAIN_TELEGRAM_CHAT_ID=.*/BRAIN_TELEGRAM_CHAT_ID=${CHAT_ID}/" "$MYSECRETS" +else + echo "BRAIN_TELEGRAM_CHAT_ID=${CHAT_ID}" >> "$MYSECRETS" +fi + +echo "✅ BRAIN_TELEGRAM_CHAT_ID enregistré dans MYSECRETS — valeur non affichée" diff --git a/scripts/install-brain-watch.sh b/scripts/install-brain-watch.sh new file mode 100755 index 0000000..123c510 --- /dev/null +++ b/scripts/install-brain-watch.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# install-brain-watch.sh — Installation du SUPERVISOR daemon +# Usage : bash install-brain-watch.sh [local|vps|both] +# +# Prérequis : +# - inotify-tools installé localement (sudo apt install inotify-tools) +# - MYSECRETS rempli (BRAIN_TELEGRAM_TOKEN + BRAIN_TELEGRAM_CHAT_ID) +# - Accès SSH root@VPS configuré + +set -euo pipefail + +TARGET="${1:-both}" +BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Docs}" +VPS_USER="root" +VPS_IP=$(grep '^VPS_IP=' "$BRAIN_ROOT/MYSECRETS" | cut -d= -f2-) +VPS_WATCH_ROOT="/home/tetardtek/brain-watch" +GITEA_BRAIN_URL="git@git.tetardtek.com:Tetardtek/brain.git" + +install_local() { + echo "=== Installation SUPERVISOR local ===" + + chmod +x "$BRAIN_ROOT/scripts/brain-notify.sh" + chmod +x "$BRAIN_ROOT/scripts/brain-watch-local.sh" + + # Lancer en background + LOGFILE="$HOME/brain-watch.log" + nohup "$BRAIN_ROOT/scripts/brain-watch-local.sh" >> "$LOGFILE" 2>&1 & + echo "PID $! — logs : $LOGFILE" + + # Ajouter au .bashrc pour redémarrage automatique (si pas déjà présent) + MARKER="# brain-watch-local" + if ! grep -q "$MARKER" "$HOME/.bashrc" 2>/dev/null; then + cat >> "$HOME/.bashrc" << EOF + +$MARKER +if ! pgrep -f "brain-watch-local.sh" > /dev/null; then + nohup $BRAIN_ROOT/scripts/brain-watch-local.sh >> $HOME/brain-watch.log 2>&1 & +fi +EOF + echo "Ajouté au .bashrc — démarrage automatique à l'ouverture du terminal" + fi + + echo "✅ SUPERVISOR local installé" +} + +install_vps() { + echo "=== Installation SUPERVISOR VPS ===" + + SSH="ssh $VPS_USER@$VPS_IP" + + # Créer le dossier + $SSH "mkdir -p $VPS_WATCH_ROOT" + + # Copier les scripts + scp "$BRAIN_ROOT/scripts/brain-notify.sh" "$VPS_USER@$VPS_IP:$VPS_WATCH_ROOT/" + scp "$BRAIN_ROOT/scripts/brain-watch-vps.sh" "$VPS_USER@$VPS_IP:$VPS_WATCH_ROOT/" + $SSH "chmod +x $VPS_WATCH_ROOT/brain-notify.sh $VPS_WATCH_ROOT/brain-watch-vps.sh" + + # Copier MYSECRETS (section brain-supervisor uniquement) + # On écrit un MYSECRETS minimal sur le VPS avec uniquement les clés Telegram + TOKEN=$(grep '^BRAIN_TELEGRAM_TOKEN=' "$BRAIN_ROOT/MYSECRETS" | cut -d= -f2-) + CHAT_ID=$(grep '^BRAIN_TELEGRAM_CHAT_ID=' "$BRAIN_ROOT/MYSECRETS" | cut -d= -f2-) + $SSH "cat > $VPS_WATCH_ROOT/MYSECRETS" << EOF +## brain-supervisor +BRAIN_TELEGRAM_TOKEN=$TOKEN +BRAIN_TELEGRAM_CHAT_ID=$CHAT_ID +EOF + $SSH "chmod 600 $VPS_WATCH_ROOT/MYSECRETS" + + # Cloner le brain si pas déjà fait + $SSH " + if [[ ! -d $VPS_WATCH_ROOT/brain ]]; then + git clone $GITEA_BRAIN_URL $VPS_WATCH_ROOT/brain + echo 'Brain cloné' + else + echo 'Brain déjà cloné' + fi + " + + # Installer le service systemd + $SSH "cat > /etc/systemd/system/brain-watch.service" << 'SYSTEMD' +[Unit] +Description=Brain SUPERVISOR — surveillance BRAIN-INDEX.md +After=network.target + +[Service] +Type=simple +User=root +ExecStart=/home/tetardtek/brain-watch/brain-watch-vps.sh +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +SYSTEMD + + $SSH "systemctl daemon-reload && systemctl enable brain-watch && systemctl start brain-watch" + $SSH "systemctl status brain-watch --no-pager | head -10" + + echo "✅ SUPERVISOR VPS installé (service brain-watch)" +} + +case "$TARGET" in + local) install_local ;; + vps) install_vps ;; + both) install_local ; install_vps ;; + *) echo "Usage: $0 [local|vps|both]" ; exit 1 ;; +esac + +echo "" +echo "=== Setup Telegram (si pas encore fait) ===" +echo "1. Ouvrir Telegram → chercher @BotFather → /newbot" +echo "2. Copier le token dans MYSECRETS : BRAIN_TELEGRAM_TOKEN=" +echo "3. Envoyer /start au bot sur Telegram, puis :" +echo " bash brain/scripts/get-telegram-chatid.sh" +echo " → écrit BRAIN_TELEGRAM_CHAT_ID dans MYSECRETS directement — valeur jamais affichée" +echo "4. Tester : BRAIN_ROOT=~/Dev/Docs brain/scripts/brain-notify.sh 'Test SUPERVISOR' urgent"