feat: amélioration scripts bash, scan réseau, fix cron, README

- system_prompt: section scripts bash (commandes interdites, mosquitto_pub, bonnes pratiques)
- script.py: nettoyage guillemets échappés à la sauvegarde
- network.py: nouvelle action scan (nmap/arp-scan/arp fallback), auto-détection subnet
- cron.py: _get_current_crontab() évite d'écrire "(aucune sortie)" dans le crontab
- README créé

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 19:10:59 +00:00
parent ea1c67b33f
commit fc5b3f3bdc
10 changed files with 278 additions and 21 deletions
View File
+124
View File
@@ -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
```
View File
+4 -3
View File
@@ -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
}
+43 -7
View File
@@ -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 <nom> | <contenu>` 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"
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
# Sousré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 : pingscan 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"
+18
View File
@@ -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"
+21 -7
View File
@@ -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)
+29 -2
View File
@@ -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 <hôte> [count]
SKILL:network ARGS:traceroute <hôte>
@@ -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 <host> | traceroute <host> | dns <host> | ports | connections | firewall status|allow|deny|list | wget <url>"
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 <host> | traceroute <host> | dns <host> | ports | connections | firewall status|allow|deny|list | wget <url>"
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")
+1 -1
View File
@@ -147,7 +147,7 @@ def run(args: str, context) -> str:
return "Format : save <nom> | <contenu du script>"
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."