"""Actions d'écriture avec graduation de risque (v1.7.0). Deux classes, un seul canal d'exécution (SSH à commande forcée vers /usr/local/bin/vektor-actions sur le PVE, qui re-valide un format fermé) : 1. CRITIQUE — double confirmation obligatoire (historique v1) : 1. L'utilisateur demande une action (« redémarre jellyfin »). 2. `detect_action()` la reconnaît (whitelist stricte) et renvoie une PROPOSITION. 3. Rien n'est exécuté : Vektor demande de confirmer avec « OUI ». 4. `confirm_pending()` n'exécute que si l'utilisateur renvoie exactement OUI dans la même conversation, et uniquement l'action proposée (jamais déduite du texte de confirmation). 2. AUTONOME — exécution immédiate sans confirmation (v1.7.0) : réservée aux actions RÉVERSIBLES ou strictement informationnelles (`AUTONOMOUS_ACTIONS`) : snapshots LXC à expiration automatique 7 jours, inventaire des snapshots, scan/inventaire des mises à jour Docker (aucun conteneur redémarré), fstrim. Le pire cas est un LV snapshot qui disparaît tout seul dans 7 jours. `execute_autonomous()` REFUSE toute action hors de cette classe : une critique ne peut jamais passer par le chemin autonome. """ from __future__ import annotations import asyncio import os import re import time ACTIONS_SSH_KEY = os.environ.get( "VEKTOR_ACTIONS_SSH_KEY", "/app/secrets/vektor_actions_ed25519" ) ACTIONS_SSH_HOST = os.environ.get("PVE_SSH_HOST", "PVE_HOST") ACTIONS_SSH_USER = os.environ.get("PVE_SSH_USER", "root") # Whitelist : mot(s)-clé FR -> (libellé humain, action SSH formatée) ACTION_PATTERNS: list[tuple[re.Pattern, str]] = [ ( re.compile(r"\bred[ée]marr\w*\s+(?:le\s+)?(?:conteneur\s+|container\s+)?jellyfin\b", re.I), "docker_restart_102_jellyfin", ), # Redémarrage Tdarr (transcodage) — libère la charge disque/CPU du PVE ( re.compile(r"\bred[ée]marr\w*\s+(?:le\s+)?(?:serveur\s+|n[oe]ud\s+|node\s+|conteneur\s+|transcodage\s+)?tdarr\b", re.I), "docker_restart_102_tdarr-node", ), ( re.compile(r"\bred[ée]marr\w*\s+(?:le\s+)?(?:ct|conteneur|lxc)\s*(101|102|103|104|105|106)\b", re.I), "lxc_restart_{0}", ), # Rescan de bibliothèque Sonarr / Radarr ( re.compile(r"\b(?:relance|rerafra[îi]ch\w*|rescan\w*|reindex\w*|re[\s-]?scan\w*)\s+(?:la\s+)?(?:biblioth[èe]que\s+(?:de\s+)?)?(sonarr|radarr)\b", re.I), "{0}_rescan", ), ( re.compile(r"\b(?:scan|rerafra[îi]ch|rescan)\w*\s+(?:la\s+)?biblioth[èe]que\b", re.I), "sonarr_rescan", ), # Mises à jour des images Docker (scan puis apply sous confirmation OUI) ( re.compile(r"\b(?:check|verifie|v[ée]rifie|regarde|cherche|liste|scan)\w*\s+(?:les\s+)?(?:mises?\s+[àa]\s+jour|updates?|nouvelles?\s+(?:versions?|images?))\b", re.I), "docker_updates_scan", ), ( re.compile(r"^/(checkupdates|updates)\b", re.I), "docker_updates_scan", ), # Inventaire lecture seule des versions (commande /majlist) ( re.compile(r"^/majlist\b", re.I), "docker_updates_list", ), ( re.compile(r"\b(?:liste|montre|affiche)[\w-]*\s+(?:les\s+)?(?:conteneurs?|images?)\b", re.I), "docker_updates_list", ), ( re.compile(r"\bversions?\s+(?:actuelle?s?\s+et\s+disponibles?\s+)?(?:des?\s+)?(?:conteneurs?|images?|stacks?)\b", re.I), "docker_updates_list", ), ( re.compile(r"\b(?:applique|installe|d[ée]ploy\w*)\w*\s+(?:les\s+)?(?:mises?[\s-]?à?[\s-]?jours?|updates?|maj|mises?[\s-]?à?[\s-]?jours?)\b", re.I), "docker_updates_apply", ), ( re.compile(r"\b(?:mets?|met)[\s-]+à[\s-]+jour\w*\s+(?:les\s+)?(?:conteneurs?|images?|stacks?|docker|tout)\b", re.I), "docker_updates_apply", ), ( re.compile(r"\b(?:update|upgrade)\w*\s+(?:the\s+)?(?:containers?|images?|stacks?|docker|all)\b", re.I), "docker_updates_apply", ), ( re.compile(r"^/(applyupdates|maj)\b", re.I), "docker_updates_apply", ), # ── Classe autonome : snapshots réversibles + inventaires (v1.7.0) ── # /snap [CT] : snapshot thin du disque du CT, expiration auto 7 jours. # Sans CT explicite : CT 103 (stack arr, la plus souvent mise à jour). # Le script PVE n'accepte que CT 101-107 et un label [a-z0-9-]{1,24}. ( re.compile(r"^/snap\s+(101|102|103|104|105|106|107)\s*$", re.I), "lxc_snap_{0}_pre-update", ), ( re.compile(r"^/snap\s*$", re.I), "lxc_snap_pre-update", ), ( re.compile( r"\bsnapshot\s+(?:du\s+|de\s+|sur\s+)?(?:CT\s*)?(101|102|103|104|105|106|107)\b", re.I, ), "lxc_snap_{0}_pre-update", ), ( re.compile(r"\b(?:fais?|prend|cr[ée]e|lance)\w*\s+(?:un\s+)?snapshot\b", re.I), "lxc_snap_pre-update", ), # Inventaire des snapshots à expiration (lecture seule) ( re.compile(r"^/snapls\s*$", re.I), "lxc_snapls", ), ( re.compile(r"\b(?:liste|montre|affiche)\w*\s+(?:les\s+)?snapshots?\b", re.I), "lxc_snapls", ), # Pause / reprise globale qBittorrent (commandes slash et langage naturel) ( re.compile(r"^/(pause)\b", re.I), "qb_pause_103", ), ( re.compile(r"^/(resume)\b", re.I), "qb_resume_103", ), ( re.compile(r"\b(?:mets?|stoppe?|arr[êe]te?|pause|sus?pends?)\w*\s+(?:en\s+pause\s+)?(?:tous\s+|les\s+|des\s+|tout(?:es)?\s+)*(?:les\s+|des\s+)?t[ée]l[ée]chargements?\b", re.I), "qb_pause_103", ), ( re.compile(r"\b(?:reprends?|relance|r[ée]active|red[ée]marre?)\w*\s+(?:tous\s+|les\s+|des\s+|tout(?:es)?\s+)*(?:les\s+|des\s+)?t[ée]l[ée]chargements?\b", re.I), "qb_resume_103", ), # ── Diagnostics lecture seule (v1.8.0 — agent de diagnostic) ── # /diag [cible] : rapport d'état nommant le maillon défaillant. # Cibles : pve, net, media, services, failover, docker_, disk_ ( re.compile(r"^/diag\s*$", re.I), "diagnose_pve", ), ( re.compile(r"^/diag\s+(pve|net|media|services|failover)\s*$", re.I), "diagnose_{0}", ), ( re.compile(r"^/diag\s+(?:docker\s+)?(CT\s*)?(101|102|103|104|105|106|107|108|109|110|111)\s*$", re.I), "diagnose_docker_{1}", ), ( re.compile(r"^/diag\s+disque?s?\s+(?:CT\s*)?(101|102|103|104|105|106|107|108|109|110|111)\s*$", re.I), "diagnose_disk_{0}", ), ( re.compile(r"\b(?:diagnosti[qc]u?e?|[ée]tat\s+d[ée]taill[ée]|sant[ée])\w*\s+(?:du\s+|de\s+la\s+|de\s+l')?(?:pve|hyperviseur|syst[èe]me)\b", re.I), "diagnose_pve", ), ( re.compile(r"\b(?:diagnosti[qc]u?e?|[ée]tat|test)\w*\s+(?:du\s+|de\s+la\s+)?r[ée]seau\b", re.I), "diagnose_net", ), ( re.compile(r"\b(?:diagnosti[qc]u?e?|[ée]tat)\w*\s+(?:du\s+|de\s+la\s+)?(?:m[ée]dia|jellyfin|transcodage)\b", re.I), "diagnose_media", ), ( re.compile(r"\b(?:diagnosti[qc]u?e?|[ée]tat)\w*\s+(?:du\s+)?failover(?:\s+dns)?\b", re.I), "diagnose_failover", ), ] # Classe autonome (v1.7.0) : actions exécutables SANS confirmation OUI. # Critères stricts : réversible (snapshot avec expiration auto) ou purement # informationnelle (inventaires, scans sans effet sur les conteneurs). # Les actions générées `lxc_snap__