From e163c7d7e330bb0c2e37bffdb69964ad7f161cd2 Mon Sep 17 00:00:00 2001 From: sylvain Date: Mon, 16 Mar 2026 07:18:47 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20confirmations=20cron/systemd,=20renforc?= =?UTF-8?q?ement=20script=20skill,=20=C3=A9diteur=20de=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - base_agent: _pending_confirmations + intercepteur oui/non dans _on_xmpp_message - cron: add/remove/clear demandent confirmation (requêtes XMPP directes) - systemd: start/stop/restart/enable/disable/mask/unmask/daemon-reload demandent confirmation - script: _safe_name strip toutes les extensions, extensions système interdites, contenu vide rejeté, nouvelle commande edit | - README mis à jour Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 +- skills/script.py | 60 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 689e580..704a19e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ systemctl enable --now agent_deploy | `deploy` | Déploie un agent depuis le catalogue sur un serveur distant (SSH) | | `catalog` | Consulte le catalogue des agents déployables | | `ssh` | Connexion SSH et exécution de commandes distantes | -| `script` | Bibliothèque de scripts bash (save/list/show/exec/run/delete) | +| `script` | Bibliothèque de scripts bash (save/list/show/edit/exec/run/delete) | | `agents_status` | Statut des agents du système | | `mqtt_send` | Publication sur un topic MQTT | | `mqtt_subscribe` | Souscription dynamique à un topic MQTT | diff --git a/skills/script.py b/skills/script.py index b4f828e..112307a 100644 --- a/skills/script.py +++ b/skills/script.py @@ -14,6 +14,7 @@ Usage LLM : SKILL:script ARGS:list SKILL:script ARGS:show SKILL:script ARGS:save | + SKILL:script ARGS:edit | SKILL:script ARGS:exec [args...] SKILL:script ARGS:run | SKILL:script ARGS:delete @@ -25,11 +26,12 @@ import subprocess import tempfile from datetime import datetime -DESCRIPTION = "Bibliothèque de scripts bash : sauvegarder, lister, afficher, exécuter" +DESCRIPTION = "Bibliothèque de scripts bash : sauvegarder, lister, afficher, éditer, exécuter" USAGE = ( "SKILL:script ARGS:list\n" "SKILL:script ARGS:show \n" "SKILL:script ARGS:save | \n" + "SKILL:script ARGS:edit | \n" "SKILL:script ARGS:exec [args]\n" "SKILL:script ARGS:run | \n" "SKILL:script ARGS:delete " @@ -53,9 +55,18 @@ def _ensure_dir(context) -> str: return d +_FORBIDDEN_EXTENSIONS = {".service", ".timer", ".socket", ".target", ".mount", ".conf", ".py", ".js"} + + def _safe_name(name: str) -> str: - """Empêche les traversées de répertoire.""" - return os.path.basename(name.strip().replace("/", "_")) + """Empêche les traversées de répertoire et normalise le nom.""" + n = os.path.basename(name.strip().replace("/", "_")) + # Retire toute extension connue pour obtenir le nom brut + root, ext = os.path.splitext(n) + while ext: + n = root + root, ext = os.path.splitext(n) + return n def _build_env(context, scripts_dir: str) -> dict: @@ -137,6 +148,20 @@ def run(args: str, context) -> str: name_raw, content = rest.split("|", 1) name = _safe_name(name_raw) content = content.strip().replace("\\n", "\n") + + if not name: + return "Nom de script invalide." + + # Vérifie extension interdite sur le nom brut + _, raw_ext = os.path.splitext(name_raw.strip()) + if raw_ext.lower() in _FORBIDDEN_EXTENSIONS: + return f"Extension '{raw_ext}' interdite. Utilise un nom sans extension (ex: mon_script)." + + # Vérifie que le contenu est substantiel (pas juste un shebang ou vide) + lines = [l.strip() for l in content.splitlines() if l.strip() and not l.strip().startswith("#")] + if len(lines) < 1: + return "Contenu du script vide ou incomplet. Fournis au moins une commande." + d = _ensure_dir(context) path = os.path.join(d, name + ".sh") existed = os.path.exists(path) @@ -148,6 +173,35 @@ def run(args: str, context) -> str: verb = "mis à jour" if existed else "créé" return f"Script '{name}' {verb} : {path}" + # ── edit ────────────────────────────────────────────────────────────── + if action == "edit": + # Format : edit | + if "|" not in rest: + return "Format : edit | \nEx: edit mon_script 3 | echo 'nouveau'" + head, new_line_content = rest.split("|", 1) + head_parts = head.strip().split(None, 1) + if len(head_parts) < 2: + return "Format : edit | " + name = _safe_name(head_parts[0]) + try: + line_no = int(head_parts[1].strip()) + except ValueError: + return "Le numéro de ligne doit être un entier." + if line_no < 1: + return "Le numéro de ligne doit être >= 1." + d = _ensure_dir(context) + path = os.path.join(d, name + ".sh") + if not os.path.exists(path): + return f"Script '{name}' introuvable dans {d}" + with open(path) as f: + lines = f.readlines() + if line_no > len(lines): + return f"Le script '{name}' n'a que {len(lines)} lignes." + lines[line_no - 1] = new_line_content.strip() + "\n" + with open(path, "w") as f: + f.writelines(lines) + return f"Ligne {line_no} du script '{name}' modifiée.\nNouveau contenu :\n{''.join(lines)}" + # ── exec ────────────────────────────────────────────────────────────── if action == "exec": parts2 = rest.split(None, 1)