diff --git "a/MESSAGE=Mises à jour disponibles :\n" "b/MESSAGE=Mises à jour disponibles :\n" new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c57dce --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# agent_hal + +Agent de contrôle système complet et d'édition de code/fichiers. Remplace et étend `agent_debian` avec des capacités supplémentaires : édition avancée de fichiers, git, SSH distant, fetch web, todo list. Dispose également d'une surveillance proactive disque/RAM. + +## Rôle + +HAL gère **ce serveur** (et les machines distantes via SSH) pour toute tâche système, devops ou édition de code. Pour l'automatisation multi-serveurs, utiliser `agent_ansible`. Pour déployer de nouveaux agents, utiliser `agent_deploy`. + +## Installation + +```bash +cd /opt/agent_hal +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +systemctl enable --now agent_hal +``` + +## Skills disponibles + +| Skill | Description | +|-------|-------------| +| `sysinfo` | CPU, RAM, disque, uptime | +| `apt` | Gestion des paquets (install, remove, update, upgrade, search) | +| `systemd` | Contrôle des services (start, stop, restart, status, enable) | +| `cron` | Gestion des tâches cron (list, add, remove, clear) | +| `process` | Liste, kill, surveillance des processus | +| `network` | Interfaces, scan réseau, ping, traceroute, DNS, ports, firewall | +| `user` | Gestion des utilisateurs et groupes | +| `container` | Gestion Docker/LXC (ps, start, stop, logs, exec, stats) | +| `journal` | Consultation des logs systemd (journalctl) | +| `filesystem` | Lecture, écriture, édition, recherche de fichiers (`read`, `write`, `edit`, `multiedit`) | +| `git` | Opérations git (status, log, diff, add, commit, push, pull, clone, branch) | +| `ssh` | Exécution de commandes sur une machine distante + transfert de fichiers (`COPY`) | +| `shell` | Commandes bash directes (fallback) | +| `script` | Bibliothèque de scripts bash (save/list/show/edit/exec/run/delete) | +| `web_fetch` | Récupérer le contenu d'une URL HTTP/HTTPS | +| `todo` | Liste de tâches en mémoire (add, list, done, delete, clear) | +| `agents_status` | Statut des agents du système | +| `mqtt_send` | Publication sur un topic MQTT | +| `mqtt_subscribe` | Souscription dynamique à un topic MQTT | +| `muc_send` | Message dans le groupe XMPP | + +## Scan réseau + +``` +"Liste les machines connectées au réseau" +→ SKILL:network ARGS:scan 192.168.7.0/24 +``` + +Utilise nmap (ping scan) en priorité, sinon arp-scan, sinon table ARP. La détection du subnet est automatique si non précisé. + +## Bibliothèque de scripts + +Les scripts bash sont stockés dans `/opt/agent_hal/scripts/`. Toujours créés via `SKILL:script ARGS:save`, jamais via `filesystem write`. + +Variables disponibles dans les scripts : `$MQTT_BROKER`, `$MQTT_REPLY_TOPIC`, `$AGENT_ID` + +```bash +# Exemple d'envoi de résultat depuis un script +mosquitto_pub -h "$MQTT_BROKER" -t "$MQTT_REPLY_TOPIC" -m "résultat" +``` + +## Confirmations requises + +Les actions suivantes demandent confirmation avant exécution (requêtes XMPP directes uniquement) : + +- **cron** : `add`, `remove`, `clear` +- **systemd** : `start`, `stop`, `restart`, `enable`, `disable`, `mask`, `unmask`, `daemon-reload` + +## Surveillance proactive + +Toutes les 5 minutes : +- **Disque** : alerte si un volume dépasse 85% +- **RAM** : alerte si la mémoire utilisée dépasse 90% + +## Configuration + +`config/config.json` : +```json +{ + "agent_id": "hal", + "xmpp": { + "jid": "hal@xmpp.ovh", + "password": "...", + "admin_jid": "sylvain@xmpp.ovh", + "muc_room": "agents@muc.xmpp.ovh", + "use_omemo": true + }, + "mqtt": { "host": "localhost", "port": 1883 }, + "llm": { + "base_url": "http://192.168.7.119:11434", + "model": "qwen3:8b", + "temperature": 0.3 + }, + "llm_profiles": { + "local": "qwen3:8b", + "code": "qwen2.5-coder:7b", + "cloud": "gpt-oss:120b-cloud" + }, + "use_llm_coordinator": true +} +``` + +## Commandes + +``` +/report — Rapport système (uptime, RAM, disque, stats tâches) +/update — Git pull + redémarrage du service +/status — État de la queue de tâches +/pause — Pause du traitement des tâches +/resume — Reprise +/script — Gestion de la bibliothèque de scripts bash +``` + +## Fichiers + +``` +agent_hal.py — Point d'entrée +skills/ — 20 skills +scripts/ — Scripts bash persistants +config/ — Configuration et system prompt +agent_hal.service — Unit systemd +``` diff --git a/apt-get b/apt-get new file mode 100644 index 0000000..e69de29 diff --git a/config/config.json b/config/config.json index 47c3f12..c0a1f1e 100644 --- a/config/config.json +++ b/config/config.json @@ -16,15 +16,16 @@ }, "llm": { "base_url": "http://192.168.7.119:11434", - "model": "qwen3:8b", + "model": "gpt-oss:120b-cloud", "temperature": 0.3 }, "work_hours": "00:00-23:59", "queue_db": "/opt/agent_hal/data/queue.db", "system_prompt": "/opt/agent_hal/config/system_prompt.txt", "llm_profiles": { - "local": "qwen3:8b", - "code": "qwen2.5-coder:7b" + "local": "ministral-3:14b", + "code": "qwen2.5-coder:7b", + "cloud": "gpt-oss:120b-cloud" }, "use_llm_coordinator": true -} +} \ No newline at end of file diff --git a/config/system_prompt.txt b/config/system_prompt.txt index 452effb..405383f 100644 --- a/config/system_prompt.txt +++ b/config/system_prompt.txt @@ -9,7 +9,8 @@ Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP (directement). - **systemd** : gestion des services (start, stop, restart, status, logs, enable, disable) - **cron** : tâches planifiées (list, add, remove) - **process** : processus (list, kill, top, find, tree) -- **network** : réseau (ip, ping, traceroute, DNS, ports, firewall, bandwidth) +- **network** : réseau (scan hôtes, arp, ip, ping, traceroute, DNS, ports, firewall, bandwidth) + - `scan [subnet]` : découvrir les machines connectées (ex: `scan 192.168.7.0/24`) — **à utiliser obligatoirement** pour lister les hôtes du réseau, ne jamais inventer une liste - **user** : utilisateurs (add, delete, passwd, groups, sudo, ssh-key) - **container** : Docker et LXC (ps, start, stop, logs, exec, stats, images) - **journal** : logs système (tail, service, errors, since, grep, kernel) @@ -38,17 +39,52 @@ Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP (directement). 1. Utilise toujours le skill le plus spécifique disponible 2. Pour éditer du code, préfère `filesystem edit` ou `filesystem multiedit` plutôt que `shell sed` 3. Après chaque action importante (install, restart, delete, commit), vérifie le résultat -4. Pour les scripts complexes, utilise SKILL:script pour les créer puis les exécuter +4. **OBLIGATOIRE : tout script bash doit être créé via `SKILL:script ARGS:save | ` et jamais via `filesystem write`.** C'est la seule façon de le rendre accessible et exécutable dans la bibliothèque de scripts. N'utilise JAMAIS filesystem pour écrire un fichier .sh. 5. En cas d'erreur, diagnostique avant de réessayer 6. Réponds toujours en français 7. Sois concis dans tes réponses — l'essentiel, pas tout le stdout brut 8. Pour les opérations git, vérifie toujours le status avant de committer -## Communication MQTT +## Écriture de scripts bash — règles strictes + +Quand tu écris du contenu pour `SKILL:script ARGS:save`, respecte ces règles : + +### ❌ Interdit dans les scripts bash +- `muc_send`, `mqtt_send`, `shell`, et tous les autres noms de skills — ce ne sont PAS des commandes bash +- Les guillemets échappés : écris `"texte"` et non `\"texte\"` +- Les backslashes inutiles dans les chaînes + +### ✅ Pour envoyer un message depuis un script +Les variables d'environnement suivantes sont injectées automatiquement : +- `$MQTT_BROKER` — hôte du broker MQTT +- `$MQTT_REPLY_TOPIC` — topic de retour vers Nexus +- `$AGENT_ID` — identifiant de l'agent + +Envoyer un résultat à Nexus : +```bash +mosquitto_pub -h "$MQTT_BROKER" -t "$MQTT_REPLY_TOPIC" -m "mon résultat" +``` + +Envoyer une alerte vers le MUC via Nexus : +```bash +mosquitto_pub -h "$MQTT_BROKER" -t "agents/nexus/inbox" -m "ALERTE : quelque chose s'est passé" +``` + +### ✅ Bonnes pratiques +- Commence toujours par `#!/bin/bash` et `set -euo pipefail` +- Utilise des guillemets doubles autour des variables : `"$VAR"` +- Gère les cas d'erreur avec des messages explicites +- Pour les scans réseau longs, ajoute un timeout global ou parallélise avec `&` + `wait` + +### Exemple de script correct +```bash +#!/bin/bash +set -euo pipefail +RESULT=$(df -h / | tail -1) +mosquitto_pub -h "$MQTT_BROKER" -t "$MQTT_REPLY_TOPIC" -m "Disque : $RESULT" +``` + +## Communication MQTT (depuis le LLM, pas depuis un script) Tu peux envoyer des messages à d'autres agents : SKILL:mqtt_send ARGS:agents/nexus/inbox | {"type":"result","payload":"mon résultat"} - -Pour les scripts qui doivent retourner un résultat : - Les variables $MQTT_BROKER et $MQTT_REPLY_TOPIC sont disponibles dans l'environnement. - mosquitto_pub -h $MQTT_BROKER -t $MQTT_REPLY_TOPIC -m "résultat" diff --git a/scripts/monitor_disconnect.sh b/scripts/monitor_disconnect.sh new file mode 100755 index 0000000..b7ea451 --- /dev/null +++ b/scripts/monitor_disconnect.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +# Sous‑réseau à surveiller +SUBNET="192.168.7.0/24" + +# Fichier de cache contenant la liste des hôtes vus lors du dernier scan +CACHE="/tmp/monitor_disconnect_cache.txt" + +# Fonction de scan : ping‑scan avec nmap, on ne garde que les adresses IP actives +scan_hosts() { + nmap -sn "$SUBNET" -oG - | awk '/Up/{print $2}' +} + +# Première exécution → créer le cache et quitter +if [[ ! -f "$CACHE" ]]; then + scan_hosts > "$CACHE" + exit 0 +fi + +# Scan actuel +CURRENT="$(mktemp)" +scan_hosts > "$CURRENT" + +# Déterminer les hôtes qui ont disparu depuis le dernier scan +MISSING=$(comm -23 <(sort "$CACHE") <(sort "$CURRENT")) || true + +# Si des hôtes sont manquants, envoyer une alerte XMPP via MQTT (le broker relayera vers Nexus) +if [[ -n "$MISSING" ]]; then + while IFS= read -r ip; do + MSG="ALERTE : machine $ip déconnectée du réseau $SUBNET" + mosquitto_pub -h "$MQTT_BROKER" -t "agents/nexus/inbox" -m "$MSG" + done <<< "$MISSING" +fi + +# Mettre à jour le cache pour le prochain cycle +mv "$CURRENT" "$CACHE" diff --git a/scripts/monitor_updates_report.sh b/scripts/monitor_updates_report.sh new file mode 100755 index 0000000..cf97cb8 --- /dev/null +++ b/scripts/monitor_updates_report.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +# Met à jour la liste des paquets (silencieux) +apt-get update -qq + +# Récupère la liste des paquets pouvant être mis à jour (simulation) +UPDATES=$(apt-get -s upgrade 2>/dev/null | grep -E '^Inst' | awk '{print $2 " " $3 " -> " $4}' || true) + +if [[ -z "$UPDATES" ]]; then + MESSAGE="Aucune mise à jour disponible" +else + MESSAGE="Mises à jour disponibles :\ +$UPDATES" +fi + +# Envoie le résultat via MQTT +mosquitto_pub -h "$MQTT_BROKER" -t "$MQTT_REPLY_TOPIC" -m "$MESSAGE" diff --git a/skills/cron.py b/skills/cron.py index cf8d52a..9462947 100644 --- a/skills/cron.py +++ b/skills/cron.py @@ -35,6 +35,16 @@ def _run(cmd: str, timeout: int = 10) -> str: return str(e) +def _get_current_crontab() -> str: + """Retourne le crontab actuel, ou chaîne vide si inexistant.""" + result = subprocess.run( + "crontab -l", shell=True, text=True, capture_output=True + ) + if result.returncode != 0: + return "" + return result.stdout.strip() + + def run(args: str, context) -> str: parts = args.strip().split(None, 1) action = parts[0].lower() if parts else "list" @@ -54,26 +64,28 @@ def run(args: str, context) -> str: command = " ".join(words[5:]) entry = f"{cron_expr} {command}" - current = _run("crontab -l 2>/dev/null") + current = _get_current_crontab() if entry in current: return f"Cette entrée existe déjà : {entry}" def _do_add(): with tempfile.NamedTemporaryFile(mode="w", suffix=".cron", delete=False) as f: - if current and "no crontab" not in current.lower(): + if current: f.write(current + "\n") f.write(entry + "\n") tmpfile = f.name - out = _run(f"crontab {tmpfile}") + result = subprocess.run(f"crontab {tmpfile}", shell=True, text=True, capture_output=True) os.unlink(tmpfile) - return f"Entrée ajoutée : {entry}\n{out}" + if result.returncode != 0: + return f"❌ Erreur crontab : {(result.stdout + result.stderr).strip()}" + return f"✅ Entrée ajoutée : {entry}" return _confirm_or_execute(context, f"Ajouter cron : {entry}", _do_add) if action == "remove": if not rest: return "Précise le pattern à supprimer." - current = _run("crontab -l 2>/dev/null") + current = _get_current_crontab() lines = [l for l in current.splitlines() if rest not in l] removed_count = len(current.splitlines()) - len(lines) if removed_count == 0: @@ -84,9 +96,11 @@ def run(args: str, context) -> str: with tempfile.NamedTemporaryFile(mode="w", suffix=".cron", delete=False) as f: f.write(new_cron + "\n") tmpfile = f.name - out = _run(f"crontab {tmpfile}") + result = subprocess.run(f"crontab {tmpfile}", shell=True, text=True, capture_output=True) os.unlink(tmpfile) - return f"{removed_count} entrée(s) supprimée(s) contenant '{rest}'.\n{out}" + if result.returncode != 0: + return f"❌ Erreur crontab : {(result.stdout + result.stderr).strip()}" + return f"✅ {removed_count} entrée(s) supprimée(s) contenant '{rest}'." return _confirm_or_execute(context, f"Supprimer {removed_count} cron contenant '{rest}'", _do_remove) diff --git a/skills/network.py b/skills/network.py index 20ef2e4..98a1e63 100644 --- a/skills/network.py +++ b/skills/network.py @@ -2,6 +2,8 @@ Skill NETWORK — administration réseau. Usage LLM : + SKILL:network ARGS:scan [subnet] — découverte des hôtes (ex: scan 192.168.7.0/24) + SKILL:network ARGS:arp — table ARP locale (hôtes récents) SKILL:network ARGS:ip [show|route|link] SKILL:network ARGS:ping [count] SKILL:network ARGS:traceroute @@ -20,8 +22,8 @@ Usage LLM : """ import subprocess -DESCRIPTION = "Administration réseau : ip, ping, traceroute, DNS, ports, firewall ufw/iptables" -USAGE = "SKILL:network ARGS:ip | ping | traceroute | dns | ports | connections | firewall status|allow|deny|list | wget " +DESCRIPTION = "Administration réseau : scan réseau, table ARP, ip, ping, traceroute, DNS, ports, firewall ufw/iptables" +USAGE = "SKILL:network ARGS:scan [subnet] | arp | ip | ping | traceroute | dns | ports | connections | firewall status|allow|deny|list | wget " def _run(cmd: str, timeout: int = 20) -> str: @@ -167,6 +169,31 @@ def run(args: str, context) -> str: return "Précise l'URL." return _run(f"curl -sI {url} | head -10") + if action == "scan": + # Détermine le subnet à scanner + subnet = rest.strip() + if not subnet: + # Auto-détecte le subnet depuis l'IP locale + iface_out = _run("ip -br addr show | grep -v '^lo' | head -1") + # Extrait le CIDR (ex: 192.168.7.5/24 → 192.168.7.0/24) + import re + m = re.search(r'(\d+\.\d+\.\d+)\.\d+/(\d+)', iface_out) + if m: + subnet = f"{m.group(1)}.0/{m.group(2)}" + else: + subnet = "192.168.0.0/24" + # Préfère nmap si dispo, sinon ping sweep + nmap_check = _run("which nmap") + if nmap_check and "nmap" in nmap_check: + return _run(f"nmap -sn --host-timeout 3s {subnet} -oG - | grep 'Up$' | awk '{{print $2, $3}}'", timeout=60) + # Fallback : arp-scan + arpscan_check = _run("which arp-scan") + if arpscan_check and "arp-scan" in arpscan_check: + return _run(f"arp-scan {subnet}", timeout=30) + # Fallback : table ARP après ping broadcast + _run(f"ping -c 1 -b {subnet.rsplit('.',1)[0]}.255 2>/dev/null || true", timeout=5) + return _run("arp -n") + if action == "arp": return _run("arp -n") diff --git a/skills/script.py b/skills/script.py index 112307a..ce58ee8 100644 --- a/skills/script.py +++ b/skills/script.py @@ -147,7 +147,7 @@ def run(args: str, context) -> str: return "Format : save | " name_raw, content = rest.split("|", 1) name = _safe_name(name_raw) - content = content.strip().replace("\\n", "\n") + content = content.strip().replace("\\n", "\n").replace('\\"', '"').replace("\\'", "'") if not name: return "Nom de script invalide."