Initial commit — Agent HAL v1.0

Agent système complet remplaçant agent_debian :
- 20 skills : apt, systemd, cron, process, network, user, sysinfo,
  journal, container, shell, filesystem (enhanced), git, ssh,
  web_fetch, todo, script, mqtt_send, mqtt_subscribe, muc_send, agents_status
- filesystem : read avec numéros de lignes, edit, multiedit (style SHAI)
- git : status, log, diff, add, commit, push, pull, clone, branch, checkout
- ssh : exécution distante + SCP (password ou clé)
- web_fetch : GET/HEAD/POST avec nettoyage HTML
- todo : liste de tâches en mémoire
This commit is contained in:
2026-03-22 21:53:00 +00:00
commit ea1c67b33f
24 changed files with 2467 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
venv/
data/
__pycache__/
*.pyc
*.db
*.log
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""
Agent HAL — Contrôle système complet + édition de code/fichiers.
Remplace agent_debian avec des capacités étendues (git, ssh distant, web, todo).
"""
import os
import sys
import threading
import subprocess
import logging
from agents_core import BaseAgent, AgentContext, Message, MessageType
logger = logging.getLogger(__name__)
class AgentHal(BaseAgent):
AGENT_TYPE = "hal"
DESCRIPTION = (
"Contrôle système complet et édition de code/fichiers : "
"gestion des paquets (apt), services systemd, réseau, processus, "
"utilisateurs, conteneurs Docker/LXC, logs journald, cron, "
"édition avancée de fichiers (read/write/edit/multiedit), git, "
"exécution SSH distante, fetch web. "
"À utiliser pour toute tâche système, devops ou édition de code sur CE serveur ou en SSH."
)
DEFAULT_CONFIG_PATH = "/opt/agent_hal/config/config.json"
def get_skills_dir(self) -> str:
return os.path.join(os.path.dirname(__file__), "skills")
def on_start(self):
self.mqtt.send_to("nexus", f"Agent HAL ({self.agent_id}) en ligne.")
self._start_monitoring()
def setup_extra_subscriptions(self):
self.mqtt.subscribe(
f"agents/{self.agent_id}/control",
self._on_control_message,
)
def _on_control_message(self, msg, topic: str):
from agents_core.message_bus import Message as Msg
payload = msg.payload if isinstance(msg, Msg) else str(msg)
result = self._handle_system_command(payload)
if result and isinstance(msg, Msg):
self.mqtt.reply(msg, result)
def handle_custom_command(self, cmd: str, args: str, source_msg=None):
if cmd == "report":
return self._build_report()
if cmd == "update":
return self._self_update()
return f"Commande inconnue : /{cmd}"
def on_broadcast(self, msg: Message):
if "status" in str(msg.payload).lower():
self.mqtt.reply(msg, self._build_report())
def _build_report(self) -> str:
context = AgentContext(self)
lines = [f"── Rapport {self.agent_id} ──"]
stats = self.queue.daily_stats()
lines.append(
f"Tâches : {stats['total']} total / "
f"{stats['completed']} OK / {stats['failed']} erreurs / "
f"durée moy. {stats['avg_duration_s']}s"
)
try:
uptime = subprocess.check_output("uptime -p", shell=True, text=True).strip()
disk = subprocess.check_output(
"df -h / | tail -1 | awk '{print $3\"/\"$2\" (\"$5\" utilisé)\"}'",
shell=True, text=True
).strip()
mem = subprocess.check_output(
"free -h | awk '/^Mem:/{print $3\"/\"$2}'", shell=True, text=True
).strip()
lines.append(f"Uptime : {uptime} | RAM : {mem} | Disque / : {disk}")
except Exception:
pass
return "\n".join(lines)
def _self_update(self) -> str:
try:
out = subprocess.check_output(
"cd /opt/agent_hal && git pull",
shell=True, text=True, stderr=subprocess.STDOUT
)
subprocess.Popen(["systemctl", "restart", "agent_hal"])
return f"Mise à jour effectuée :\n{out}\nRedémarrage en cours..."
except subprocess.CalledProcessError as e:
return f"Erreur mise à jour : {e.output}"
def _start_monitoring(self):
t = threading.Thread(target=self._monitor_loop, daemon=True)
t.start()
def _monitor_loop(self):
import time
while self._running:
try:
self._check_disk_usage()
self._check_memory()
except Exception as e:
logger.debug(f"[Monitor] {e}")
time.sleep(300)
def _check_disk_usage(self):
result = subprocess.run(
"df -h | awk 'NR>1 && $5+0 > 85 {print $0}'",
shell=True, capture_output=True, text=True
)
if result.stdout.strip():
self.mqtt.alert(
f"Espace disque critique :\n{result.stdout.strip()}",
severity="critical"
)
def _check_memory(self):
result = subprocess.run(
"free | awk '/^Mem:/{if ($3/$2*100 > 90) print \"RAM utilisée à \"int($3/$2*100)\"%\"}'",
shell=True, capture_output=True, text=True
)
if result.stdout.strip():
self.mqtt.alert(result.stdout.strip(), severity="warning")
if __name__ == "__main__":
AgentHal().run()
+30
View File
@@ -0,0 +1,30 @@
{
"agent_id": "hal",
"xmpp": {
"jid": "hal@xmpp.ovh",
"password": "Matador3721",
"admin_jid": "sylvain@xmpp.ovh",
"muc_room": "agents@muc.xmpp.ovh",
"use_omemo": true
},
"mqtt": {
"host": "localhost",
"port": 1883,
"username": null,
"password": null,
"tls": false
},
"llm": {
"base_url": "http://192.168.7.119:11434",
"model": "qwen3:8b",
"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"
},
"use_llm_coordinator": true
}
+54
View File
@@ -0,0 +1,54 @@
Tu es HAL, un agent de contrôle système et d'édition de code. Tu contrôles pleinement ce serveur et peux agir sur des machines distantes via SSH.
Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP (directement).
## Tes skills disponibles et quand les utiliser
### Système
- **sysinfo** : informations système (CPU, RAM, disque, uptime, réseau)
- **apt** : gestion des paquets (install, remove, update, upgrade, search)
- **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)
- **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)
- **shell** : commande bash directe (fallback si aucun skill ne convient)
### Fichiers & Code
- **filesystem** : opérations fichiers (ls, cat, read, write, edit, multiedit, append, delete, mkdir, move, copy, find, grep, df, du, stat, chmod, chown)
- `read` : lire avec numéros de lignes
- `edit` : search & replace dans un fichier
- `multiedit` : plusieurs search & replace en une passe
- **git** : opérations git (status, log, diff, add, commit, push, pull, clone, branch, checkout, init)
### Distant
- **ssh** : exécuter des commandes sur une machine distante via SSH (password ou clé)
- supporte aussi `COPY` pour transférer des fichiers
### Web & Utilitaires
- **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)
- **script** : créer et exécuter des scripts bash persistants
- **mqtt_send** : envoyer un message à un agent ou topic MQTT
- **agents_status** : voir le statut de tous les agents en temps réel
## Règles importantes
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
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
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"
+28
View File
@@ -0,0 +1,28 @@
"""
Skill AGENTS_STATUS — afficher le statut en temps réel de tous les agents.
Usage LLM : SKILL:agents_status ARGS:
"""
DESCRIPTION = "Afficher le statut en temps réel de tous les agents (online/offline)"
USAGE = "SKILL:agents_status ARGS:(aucun argument)"
def run(args: str, context) -> str:
with context.agent._online_lock:
online = set(context.agent._online_agents)
all_caps = context.registry.all_agents()
if not all_caps:
return "Aucun agent connu dans le registre."
lines = ["── Statut des agents ──────────────────"]
for caps in sorted(all_caps, key=lambda c: c.agent_id):
if caps.agent_id == context.agent_id:
continue # Ne pas s'afficher soi-même
icon = "🟢" if caps.agent_id in online else "🔴"
label = "en ligne" if caps.agent_id in online else "hors ligne"
lines.append(f" {icon} {caps.agent_id} [{caps.agent_type}] — {label}")
lines.append(f" {caps.description}")
return "\n".join(lines) if len(lines) > 1 else "Aucun autre agent connu."
+107
View File
@@ -0,0 +1,107 @@
"""
Skill APT — gestion des paquets Debian.
Usage LLM :
SKILL:apt ARGS:update
SKILL:apt ARGS:upgrade
SKILL:apt ARGS:install <paquet1> [paquet2...]
SKILL:apt ARGS:remove <paquet>
SKILL:apt ARGS:purge <paquet>
SKILL:apt ARGS:search <terme>
SKILL:apt ARGS:show <paquet>
SKILL:apt ARGS:list-installed [filtre]
SKILL:apt ARGS:list-upgradable
SKILL:apt ARGS:autoremove
SKILL:apt ARGS:check-updates
"""
import subprocess
DESCRIPTION = "Gestion des paquets Debian via apt/dpkg"
USAGE = "SKILL:apt ARGS:install <paquet> | remove <paquet> | update | upgrade | search <terme> | show <paquet> | list-installed | list-upgradable | autoremove"
ENV = {"DEBIAN_FRONTEND": "noninteractive", "PATH": "/usr/bin:/bin:/usr/sbin:/sbin"}
def _run(cmd: str, timeout: int = 120) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout, env=ENV
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s) — commande trop longue."
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else ""
extra = parts[1] if len(parts) > 1 else ""
if action == "update":
return _run("apt-get update -q")
if action == "upgrade":
return _run("apt-get upgrade -y -q", timeout=300)
if action == "dist-upgrade":
return _run("apt-get dist-upgrade -y -q", timeout=300)
if action == "install":
if not extra:
return "Précise le(s) paquet(s) à installer."
return _run(f"apt-get install -y -q {extra}", timeout=180)
if action == "remove":
if not extra:
return "Précise le paquet à supprimer."
return _run(f"apt-get remove -y {extra}")
if action == "purge":
if not extra:
return "Précise le paquet à purger."
return _run(f"apt-get purge -y {extra}")
if action == "autoremove":
return _run("apt-get autoremove -y")
if action == "search":
if not extra:
return "Précise le terme de recherche."
return _run(f"apt-cache search {extra} | head -30")
if action == "show":
if not extra:
return "Précise le paquet."
return _run(f"apt-cache show {extra}")
if action in ("list-installed", "list"):
cmd = f"dpkg -l | grep '^ii' | awk '{{print $2\" \"$3}}'"
if extra:
cmd += f" | grep {extra}"
return _run(cmd)
if action == "list-upgradable":
return _run("apt list --upgradable 2>/dev/null")
if action == "check-updates":
out = _run("apt-get -s upgrade 2>/dev/null | grep '^[0-9]'")
return out or "Système à jour."
if action == "hold":
if not extra:
return "Précise le paquet."
return _run(f"apt-mark hold {extra}")
if action == "unhold":
if not extra:
return "Précise le paquet."
return _run(f"apt-mark unhold {extra}")
return (
"Action inconnue. Disponible : update, upgrade, install, remove, purge, "
"search, show, list-installed, list-upgradable, autoremove, check-updates, hold, unhold"
)
+160
View File
@@ -0,0 +1,160 @@
"""
Skill CONTAINER — gestion des conteneurs Docker et LXC/LXD.
Usage LLM :
SKILL:container ARGS:docker ps [all]
SKILL:container ARGS:docker start <nom>
SKILL:container ARGS:docker stop <nom>
SKILL:container ARGS:docker restart <nom>
SKILL:container ARGS:docker logs <nom> [N]
SKILL:container ARGS:docker stats
SKILL:container ARGS:docker images
SKILL:container ARGS:docker pull <image>
SKILL:container ARGS:docker rm <nom>
SKILL:container ARGS:docker rmi <image>
SKILL:container ARGS:docker exec <nom> <commande>
SKILL:container ARGS:docker inspect <nom>
SKILL:container ARGS:lxc list
SKILL:container ARGS:lxc start <nom>
SKILL:container ARGS:lxc stop <nom>
SKILL:container ARGS:lxc exec <nom> <commande>
SKILL:container ARGS:lxc info <nom>
"""
import subprocess
DESCRIPTION = "Gestion conteneurs Docker et LXC/LXD : start, stop, logs, exec, stats, images"
USAGE = "SKILL:container ARGS:docker ps|start|stop|restart|logs|exec|stats|images|pull | lxc list|start|stop|exec|info"
def _run(cmd: str, timeout: int = 30) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def _docker(action: str, args: list) -> str:
a = args[0] if args else ""
if action == "ps":
flag = "-a" if (a == "all" or a == "-a") else ""
return _run(f"docker ps {flag} --format 'table {{{{.Names}}}}\\t{{{{.Image}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}'")
if action == "start":
return _run(f"docker start {a}") if a else "Précise le nom du conteneur."
if action == "stop":
return _run(f"docker stop {a}") if a else "Précise le nom du conteneur."
if action == "restart":
return _run(f"docker restart {a}") if a else "Précise le nom du conteneur."
if action == "logs":
if not a:
return "Précise le nom du conteneur."
n = args[1] if len(args) > 1 else "50"
return _run(f"docker logs --tail {n} {a}")
if action == "stats":
return _run("docker stats --no-stream --format 'table {{{{.Name}}}}\\t{{{{.CPUPerc}}}}\\t{{{{.MemUsage}}}}'")
if action == "images":
return _run("docker images --format 'table {{{{.Repository}}}}\\t{{{{.Tag}}}}\\t{{{{.Size}}}}\\t{{{{.CreatedSince}}}}'")
if action == "pull":
return _run(f"docker pull {a}", timeout=120) if a else "Précise l'image."
if action == "rm":
return _run(f"docker rm {a}") if a else "Précise le nom du conteneur."
if action == "rm-stopped":
return _run("docker container prune -f")
if action == "rmi":
return _run(f"docker rmi {a}") if a else "Précise l'image."
if action == "exec":
if len(args) < 2:
return "Format : docker exec <conteneur> <commande>"
container = args[0]
cmd = " ".join(args[1:])
return _run(f"docker exec {container} {cmd}")
if action == "inspect":
return _run(f"docker inspect {a}") if a else "Précise le nom du conteneur."
if action == "network":
return _run("docker network ls")
if action == "volumes":
return _run("docker volume ls")
if action == "compose-up":
return _run("docker compose up -d", timeout=120) if not a else _run(f"docker compose -f {a} up -d", timeout=120)
if action == "compose-down":
return _run("docker compose down")
return f"Action docker inconnue : {action}"
def _lxc(action: str, args: list) -> str:
a = args[0] if args else ""
if action == "list":
return _run("lxc list --format table 2>/dev/null || lxc-ls -f 2>/dev/null")
if action == "start":
return _run(f"lxc start {a}") if a else "Précise le nom."
if action == "stop":
return _run(f"lxc stop {a}") if a else "Précise le nom."
if action == "restart":
return _run(f"lxc restart {a}") if a else "Précise le nom."
if action == "exec":
if len(args) < 2:
return "Format : lxc exec <conteneur> <commande>"
cmd = " ".join(args[1:])
return _run(f"lxc exec {a} -- {cmd}")
if action == "info":
return _run(f"lxc info {a}") if a else _run("lxc info")
if action == "snapshot":
return _run(f"lxc snapshot {a}") if a else "Précise le nom."
if action == "delete":
return _run(f"lxc delete {a} --force") if a else "Précise le nom."
return f"Action lxc inconnue : {action}"
def run(args: str, context) -> str:
parts = args.strip().split()
if not parts:
return "Précise : docker ou lxc suivi d'une action."
runtime = parts[0].lower()
action = parts[1].lower() if len(parts) > 1 else "ps"
rest = parts[2:] if len(parts) > 2 else []
if runtime == "docker":
return _docker(action, rest)
if runtime in ("lxc", "lxd"):
return _lxc(action, rest)
# Tentative de détection auto
if runtime in ("ps", "stats", "images", "logs", "start", "stop", "restart", "exec"):
return _docker(runtime, parts[1:])
return "Précise le runtime : docker ou lxc. Ex: SKILL:container ARGS:docker ps"
+109
View File
@@ -0,0 +1,109 @@
"""
Skill CRON — gestion des tâches cron.
Usage LLM :
SKILL:cron ARGS:list [utilisateur]
SKILL:cron ARGS:add <expression_cron> <commande>
SKILL:cron ARGS:remove <pattern>
SKILL:cron ARGS:system-list
"""
import subprocess
import tempfile
import os
DESCRIPTION = "Gestion des tâches cron (crontab)"
USAGE = "SKILL:cron ARGS:list | add <* * * * *> <commande> | remove <pattern> | system-list"
def _confirm_or_execute(context, description: str, action_fn) -> str:
"""Demande confirmation si requête XMPP directe, sinon exécute immédiatement."""
sender = getattr(context.agent, '_last_xmpp_sender', '')
if not sender:
return action_fn()
context.agent._pending_confirmations[sender] = {"description": description, "fn": action_fn}
return f"⚠️ Confirmation requise :\n{description}\n\nRéponds **oui** pour confirmer ou **non** pour annuler."
def _run(cmd: str, timeout: int = 10) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
return (result.stdout + result.stderr).strip() or "(aucune sortie)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else "list"
rest = parts[1] if len(parts) > 1 else ""
if action == "list":
user = rest.strip() or ""
flag = f"-u {user}" if user else ""
result = _run(f"crontab {flag} -l 2>/dev/null")
return result if result else "Crontab vide."
if action == "add":
words = rest.split()
if len(words) < 6:
return "Format : add <min> <heure> <jour> <mois> <jourSem> <commande>\nEx: add 0 3 * * * /usr/bin/apt-get update"
cron_expr = " ".join(words[:5])
command = " ".join(words[5:])
entry = f"{cron_expr} {command}"
current = _run("crontab -l 2>/dev/null")
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():
f.write(current + "\n")
f.write(entry + "\n")
tmpfile = f.name
out = _run(f"crontab {tmpfile}")
os.unlink(tmpfile)
return f"Entrée ajoutée : {entry}\n{out}"
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")
lines = [l for l in current.splitlines() if rest not in l]
removed_count = len(current.splitlines()) - len(lines)
if removed_count == 0:
return f"Aucune entrée contenant '{rest}' trouvée."
def _do_remove():
new_cron = "\n".join(lines)
with tempfile.NamedTemporaryFile(mode="w", suffix=".cron", delete=False) as f:
f.write(new_cron + "\n")
tmpfile = f.name
out = _run(f"crontab {tmpfile}")
os.unlink(tmpfile)
return f"{removed_count} entrée(s) supprimée(s) contenant '{rest}'.\n{out}"
return _confirm_or_execute(context, f"Supprimer {removed_count} cron contenant '{rest}'", _do_remove)
if action == "clear":
return _confirm_or_execute(
context,
"Effacer TOUT le crontab root (action irréversible)",
lambda: _run("crontab -r 2>/dev/null && echo 'Crontab effacé'")
)
if action == "system-list":
# Crons système dans /etc/cron.*
out = []
for d in ["/etc/cron.d", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly"]:
files = _run(f"ls {d} 2>/dev/null")
if files:
out.append(f"{d}:\n{files}")
return "\n\n".join(out) or "Aucun cron système trouvé."
return "Action inconnue. Disponible : list, add, remove, clear, system-list"
+254
View File
@@ -0,0 +1,254 @@
"""
Skill FILESYSTEM — opérations avancées sur le système de fichiers.
Usage LLM :
SKILL:filesystem ARGS:ls <chemin>
SKILL:filesystem ARGS:cat <fichier>
SKILL:filesystem ARGS:read <fichier> (avec numéros de lignes)
SKILL:filesystem ARGS:write <fichier> | <contenu>
SKILL:filesystem ARGS:edit <fichier> | <ancien> -> <nouveau>
SKILL:filesystem ARGS:multiedit <fichier> | <anc1> -> <nouv1> ;; <anc2> -> <nouv2>
SKILL:filesystem ARGS:append <fichier> | <contenu>
SKILL:filesystem ARGS:delete <chemin>
SKILL:filesystem ARGS:mkdir <chemin>
SKILL:filesystem ARGS:move <src> | <dst>
SKILL:filesystem ARGS:copy <src> | <dst>
SKILL:filesystem ARGS:chmod <mode> <chemin>
SKILL:filesystem ARGS:chown <owner> <chemin>
SKILL:filesystem ARGS:find <chemin> <pattern>
SKILL:filesystem ARGS:grep <pattern> <fichier>
SKILL:filesystem ARGS:df
SKILL:filesystem ARGS:du <chemin>
SKILL:filesystem ARGS:stat <chemin>
SKILL:filesystem ARGS:tail <fichier> [N]
SKILL:filesystem ARGS:head <fichier> [N]
"""
import os
import subprocess
DESCRIPTION = "Opérations filesystem avancées : ls, read, write, edit, multiedit, delete, find, grep, git-style édition"
USAGE = (
"SKILL:filesystem ARGS:ls <path> | read <file> | write <file>|<content> | "
"edit <file>|<old> -> <new> | multiedit <file>|<old1> -> <new1> ;; <old2> -> <new2> | "
"delete <path> | find <path> <pattern> | grep <pattern> <file> | df | du <path>"
)
FORBIDDEN = ["/proc", "/sys", "/dev", "/run/systemd"]
def _safe_path(path: str) -> bool:
path = os.path.realpath(path)
return not any(path.startswith(f) for f in FORBIDDEN)
def _run(cmd: str, timeout: int = 15) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else ""
rest = parts[1] if len(parts) > 1 else ""
if action == "ls":
path = rest or "."
return _run(f"ls -lah {path}")
if action == "cat":
if not rest:
return "Précise le fichier."
if not _safe_path(rest):
return f"Accès refusé : {rest}"
return _run(f"cat {rest}")
if action == "read":
# Lecture avec numéros de lignes (style SHAI)
if not rest:
return "Précise le fichier."
if not _safe_path(rest):
return f"Accès refusé : {rest}"
try:
with open(rest.strip(), "r", errors="replace") as f:
lines = f.readlines()
numbered = "".join(f"{i+1:4d} {line}" for i, line in enumerate(lines))
if len(numbered) > 6000:
numbered = numbered[:6000] + f"\n... (tronqué, {len(lines)} lignes total)"
return numbered or "(fichier vide)"
except Exception as e:
return str(e)
if action == "edit":
# Search & replace : edit <fichier> | <ancien> -> <nouveau>
if "|" not in rest:
return "Format : edit <fichier> | <ancien> -> <nouveau>"
filepath, change = rest.split("|", 1)
filepath = filepath.strip()
if not _safe_path(filepath):
return f"Accès refusé : {filepath}"
if " -> " not in change:
return "Format : edit <fichier> | <ancien> -> <nouveau>"
old_text, new_text = change.split(" -> ", 1)
old_text = old_text.strip()
new_text = new_text.strip()
try:
with open(filepath, "r", errors="replace") as f:
content = f.read()
if old_text not in content:
return f"Texte non trouvé dans {filepath} : {old_text[:80]!r}"
count = content.count(old_text)
new_content = content.replace(old_text, new_text)
with open(filepath, "w") as f:
f.write(new_content)
return f"Édition OK : {count} remplacement(s) dans {filepath}"
except Exception as e:
return str(e)
if action == "multiedit":
# Plusieurs search & replace : multiedit <fichier> | old1 -> new1 ;; old2 -> new2
if "|" not in rest:
return "Format : multiedit <fichier> | <anc1> -> <nouv1> ;; <anc2> -> <nouv2>"
filepath, changes_str = rest.split("|", 1)
filepath = filepath.strip()
if not _safe_path(filepath):
return f"Accès refusé : {filepath}"
try:
with open(filepath, "r", errors="replace") as f:
content = f.read()
results = []
for change in changes_str.split(";;"):
change = change.strip()
if " -> " not in change:
continue
old_text, new_text = change.split(" -> ", 1)
old_text = old_text.strip()
new_text = new_text.strip()
count = content.count(old_text)
if count == 0:
results.append(f"Non trouvé : {old_text[:60]!r}")
else:
content = content.replace(old_text, new_text)
results.append(f"{count} remplacement(s) : {old_text[:40]!r}")
with open(filepath, "w") as f:
f.write(content)
return f"Multiedit OK dans {filepath} :\n" + "\n".join(results)
except Exception as e:
return str(e)
if action == "tail":
parts2 = rest.split()
filepath = parts2[0] if parts2 else ""
n = parts2[1] if len(parts2) > 1 else "50"
return _run(f"tail -n {n} {filepath}")
if action == "head":
parts2 = rest.split()
filepath = parts2[0] if parts2 else ""
n = parts2[1] if len(parts2) > 1 else "30"
return _run(f"head -n {n} {filepath}")
if action == "write":
if "|" not in rest:
return "Format : write <fichier> | <contenu>"
filepath, content = rest.split("|", 1)
filepath = filepath.strip()
content = content.strip()
if not _safe_path(filepath):
return f"Accès refusé : {filepath}"
try:
os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
with open(filepath, "w") as f:
f.write(content)
return f"Fichier écrit : {filepath} ({len(content)} caractères)"
except Exception as e:
return str(e)
if action == "append":
if "|" not in rest:
return "Format : append <fichier> | <contenu>"
filepath, content = rest.split("|", 1)
filepath = filepath.strip()
if not _safe_path(filepath):
return f"Accès refusé : {filepath}"
try:
with open(filepath, "a") as f:
f.write(content.strip() + "\n")
return f"Contenu ajouté à {filepath}"
except Exception as e:
return str(e)
if action == "delete":
if not rest:
return "Précise le chemin."
if not _safe_path(rest):
return f"Accès refusé : {rest}"
if os.path.isdir(rest):
return _run(f"rm -rf {rest}")
return _run(f"rm -f {rest}")
if action == "mkdir":
if not rest:
return "Précise le chemin."
return _run(f"mkdir -p {rest}")
if action == "move":
if "|" not in rest:
return "Format : move <src> | <dst>"
src, dst = rest.split("|", 1)
return _run(f"mv {src.strip()} {dst.strip()}")
if action == "copy":
if "|" not in rest:
return "Format : copy <src> | <dst>"
src, dst = rest.split("|", 1)
return _run(f"cp -r {src.strip()} {dst.strip()}")
if action == "chmod":
parts2 = rest.split(None, 1)
if len(parts2) < 2:
return "Format : chmod <mode> <chemin>"
return _run(f"chmod {parts2[0]} {parts2[1]}")
if action == "chown":
parts2 = rest.split(None, 1)
if len(parts2) < 2:
return "Format : chown <owner:group> <chemin>"
return _run(f"chown -R {parts2[0]} {parts2[1]}")
if action == "find":
parts2 = rest.split(None, 1)
path = parts2[0] if parts2 else "."
pattern = parts2[1] if len(parts2) > 1 else "*"
return _run(f"find {path} -name '{pattern}' 2>/dev/null | head -50")
if action == "grep":
parts2 = rest.split(None, 1)
if len(parts2) < 2:
return "Format : grep <pattern> <fichier>"
return _run(f"grep -rn '{parts2[0]}' {parts2[1]} 2>/dev/null | head -50")
if action == "df":
return _run("df -h")
if action == "du":
path = rest or "."
return _run(f"du -sh {path}/* 2>/dev/null | sort -rh | head -20")
if action == "stat":
if not rest:
return "Précise le chemin."
return _run(f"stat {rest}")
return (
"Action inconnue. Disponible : ls, cat, read, tail, head, write, append, edit, multiedit, "
"delete, mkdir, move, copy, chmod, chown, find, grep, df, du, stat"
)
+160
View File
@@ -0,0 +1,160 @@
"""
Skill GIT — opérations git sur des dépôts locaux.
Usage LLM :
SKILL:git ARGS:status [chemin]
SKILL:git ARGS:log [chemin] [n]
SKILL:git ARGS:diff [chemin]
SKILL:git ARGS:add <chemin_repo> | <fichiers>
SKILL:git ARGS:commit <chemin_repo> | <message>
SKILL:git ARGS:push [chemin] [remote] [branche]
SKILL:git ARGS:pull [chemin] [remote] [branche]
SKILL:git ARGS:clone <url> [destination]
SKILL:git ARGS:branch [chemin]
SKILL:git ARGS:checkout <branche> [chemin]
SKILL:git ARGS:init <chemin>
SKILL:git ARGS:stash [chemin]
SKILL:git ARGS:tag <nom> [chemin]
"""
import os
import subprocess
DESCRIPTION = "Opérations git : status, log, diff, add, commit, push, pull, clone, branch, checkout, init"
USAGE = (
"SKILL:git ARGS:status [path] | log [path] [n] | diff [path] | "
"add <repo>|<files> | commit <repo>|<msg> | push [path] | pull [path] | "
"clone <url> [dest] | branch [path] | checkout <branch> [path] | init <path>"
)
def _git(cmd: str, cwd: str = None, timeout: int = 60) -> str:
try:
result = subprocess.run(
f"git {cmd}", shell=True, text=True,
capture_output=True, timeout=timeout,
cwd=cwd
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else f"(code retour : {result.returncode})"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def _find_git_root(path: str) -> str:
"""Remonte jusqu'à trouver un dépôt git."""
try:
result = subprocess.run(
"git rev-parse --show-toplevel", shell=True, text=True,
capture_output=True, cwd=path
)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return path
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else ""
rest = parts[1] if len(parts) > 1 else ""
if action == "status":
path = rest or "."
cwd = _find_git_root(path)
return _git("status -sb", cwd=cwd)
if action == "log":
parts2 = rest.split()
path = parts2[0] if parts2 else "."
n = parts2[1] if len(parts2) > 1 else "10"
cwd = _find_git_root(path)
return _git(f"log --oneline --graph -n {n}", cwd=cwd)
if action == "diff":
parts2 = rest.split(None, 1)
path = parts2[0] if parts2 else "."
extra = parts2[1] if len(parts2) > 1 else ""
cwd = _find_git_root(path)
return _git(f"diff {extra}", cwd=cwd)
if action == "add":
if "|" not in rest:
# Essaie d'interpréter comme "add <path>"
cwd = _find_git_root(rest or ".")
return _git("add -A", cwd=cwd)
repo, files = rest.split("|", 1)
cwd = _find_git_root(repo.strip())
return _git(f"add {files.strip()}", cwd=cwd)
if action == "commit":
if "|" not in rest:
return "Format : commit <chemin_repo> | <message>"
repo, message = rest.split("|", 1)
cwd = _find_git_root(repo.strip())
msg = message.strip().replace('"', '\\"')
return _git(f'commit -m "{msg}"', cwd=cwd)
if action == "push":
parts2 = rest.split()
path = parts2[0] if parts2 else "."
remote = parts2[1] if len(parts2) > 1 else "origin"
branch = parts2[2] if len(parts2) > 2 else ""
cwd = _find_git_root(path)
return _git(f"push {remote} {branch}", cwd=cwd, timeout=120)
if action == "pull":
parts2 = rest.split()
path = parts2[0] if parts2 else "."
remote = parts2[1] if len(parts2) > 1 else "origin"
branch = parts2[2] if len(parts2) > 2 else ""
cwd = _find_git_root(path)
return _git(f"pull {remote} {branch}", cwd=cwd, timeout=120)
if action == "clone":
parts2 = rest.split(None, 1)
if not parts2:
return "Format : clone <url> [destination]"
url = parts2[0]
dest = parts2[1] if len(parts2) > 1 else ""
return _git(f"clone {url} {dest}", timeout=180)
if action == "branch":
path = rest or "."
cwd = _find_git_root(path)
return _git("branch -a", cwd=cwd)
if action == "checkout":
parts2 = rest.split()
if not parts2:
return "Format : checkout <branche> [chemin]"
branch = parts2[0]
path = parts2[1] if len(parts2) > 1 else "."
cwd = _find_git_root(path)
return _git(f"checkout {branch}", cwd=cwd)
if action == "init":
path = rest or "."
os.makedirs(path, exist_ok=True)
return _git("init", cwd=path)
if action == "stash":
path = rest or "."
cwd = _find_git_root(path)
return _git("stash", cwd=cwd)
if action == "tag":
parts2 = rest.split()
if not parts2:
return "Format : tag <nom> [chemin]"
name = parts2[0]
path = parts2[1] if len(parts2) > 1 else "."
cwd = _find_git_root(path)
return _git(f"tag {name}", cwd=cwd)
return (
"Action inconnue. Disponible : status, log, diff, add, commit, push, pull, "
"clone, branch, checkout, init, stash, tag"
)
+108
View File
@@ -0,0 +1,108 @@
"""
Skill JOURNAL — consultation des logs système via journalctl.
Usage LLM :
SKILL:journal ARGS:tail [N]
SKILL:journal ARGS:service <service> [N]
SKILL:journal ARGS:boot [N]
SKILL:journal ARGS:errors [N]
SKILL:journal ARGS:since <durée> (ex: "1h", "30min", "yesterday")
SKILL:journal ARGS:grep <pattern> [service]
SKILL:journal ARGS:kernel [N]
SKILL:journal ARGS:disk-usage
"""
import subprocess
DESCRIPTION = "Consultation des logs système via journalctl et /var/log"
USAGE = "SKILL:journal ARGS:tail [N] | service <service> [N] | errors [N] | since <durée> | grep <pattern> | boot | kernel"
def _run(cmd: str, timeout: int = 15) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else "tail"
rest = parts[1] if len(parts) > 1 else ""
if action == "tail":
n = rest.strip() or "50"
return _run(f"journalctl -n {n} --no-pager -o short-iso")
if action == "service":
parts2 = rest.split()
service = parts2[0] if parts2 else ""
n = parts2[1] if len(parts2) > 1 else "50"
if not service:
return "Précise le service."
return _run(f"journalctl -u {service} -n {n} --no-pager -o short-iso")
if action == "boot":
n = rest.strip() or "50"
return _run(f"journalctl -b -n {n} --no-pager -o short-iso")
if action == "errors":
n = rest.strip() or "30"
return _run(f"journalctl -p err..emerg -n {n} --no-pager -o short-iso")
if action == "warnings":
n = rest.strip() or "30"
return _run(f"journalctl -p warning..emerg -n {n} --no-pager -o short-iso")
if action == "since":
if not rest:
return "Précise la durée (ex: '1h', '30min', 'yesterday', '2024-01-01')"
# Convertit "1h" → "1 hour ago", "30min" → "30 minutes ago"
since = rest.strip()
if since.endswith("h") and since[:-1].isdigit():
since = f"{since[:-1]} hours ago"
elif since.endswith("min") and since[:-3].isdigit():
since = f"{since[:-3]} minutes ago"
return _run(f"journalctl --since='{since}' --no-pager -o short-iso | tail -100")
if action == "grep":
parts2 = rest.split(None, 1)
pattern = parts2[0] if parts2 else ""
service = parts2[1].strip() if len(parts2) > 1 else ""
if not pattern:
return "Précise le pattern."
cmd = f"journalctl --no-pager -o short-iso"
if service:
cmd += f" -u {service}"
cmd += f" | grep -i '{pattern}' | tail -50"
return _run(cmd)
if action == "kernel":
n = rest.strip() or "30"
return _run(f"journalctl -k -n {n} --no-pager -o short-iso")
if action == "disk-usage":
return _run("journalctl --disk-usage")
if action == "vacuum":
# Nettoyage des vieux logs
size = rest.strip() or "500M"
return _run(f"journalctl --vacuum-size={size}")
if action == "file":
# Lire un fichier de log classique
filepath = rest.strip()
if not filepath:
return "Précise le fichier."
return _run(f"tail -100 {filepath}")
return (
"Action inconnue. Disponible : tail, service, boot, errors, warnings, "
"since, grep, kernel, disk-usage, vacuum, file"
)
+23
View File
@@ -0,0 +1,23 @@
"""
Skill MQTT_SEND — publier un message sur n'importe quel topic MQTT.
Permet à l'agent de communiquer proactivement avec d'autres agents.
Usage LLM : SKILL:mqtt_send ARGS:<topic> | <message>
"""
DESCRIPTION = "Publier un message sur un topic MQTT (communication inter-agents)"
USAGE = "SKILL:mqtt_send ARGS:<topic> | <message>"
def run(args: str, context) -> str:
if "|" not in args:
return "Format : SKILL:mqtt_send ARGS:<topic> | <message>"
topic, message = args.split("|", 1)
topic = topic.strip()
message = message.strip()
if not topic:
return "Topic vide."
context.mqtt.publish_raw(topic, message)
return f"Message publié sur '{topic}'."
+59
View File
@@ -0,0 +1,59 @@
"""
Skill MQTT_SUBSCRIBE — s'abonner dynamiquement à un topic MQTT.
Les messages reçus sont transmis via XMPP (admin) et loggés.
Usage LLM :
SKILL:mqtt_subscribe ARGS:subscribe | <topic>
SKILL:mqtt_subscribe ARGS:unsubscribe | <topic>
SKILL:mqtt_subscribe ARGS:list
"""
import logging
DESCRIPTION = "S'abonner / se désabonner dynamiquement d'un topic MQTT et recevoir les messages"
USAGE = "SKILL:mqtt_subscribe ARGS:subscribe|<topic> ou unsubscribe|<topic> ou list"
logger = logging.getLogger(__name__)
# Stockage des souscriptions dynamiques : {topic: callback}
_dynamic_subs: dict = {}
def run(args: str, context) -> str:
parts = [p.strip() for p in args.split("|", 1)]
action = parts[0].lower()
if action == "list":
if not _dynamic_subs:
return "Aucun topic MQTT surveillé."
return "Topics surveillés :\n" + "\n".join(f"{t}" for t in _dynamic_subs)
if len(parts) < 2 or not parts[1]:
return "Format : subscribe|<topic> ou unsubscribe|<topic> ou list"
topic = parts[1]
if action == "unsubscribe":
if topic in _dynamic_subs:
del _dynamic_subs[topic]
return f"Désabonné du topic '{topic}'."
return f"Pas abonné à '{topic}'."
if action == "subscribe":
if topic in _dynamic_subs:
return f"Déjà abonné à '{topic}'."
agent_id = context.agent_id
def _on_message(msg, t):
payload = msg.payload if hasattr(msg, "payload") else str(msg)
text = f"[MQTT:{t}] {payload}"
logger.info(f"[mqtt_subscribe] {text}")
if context.xmpp:
context.xmpp.send_to_all_admins(text)
_dynamic_subs[topic] = _on_message
context.mqtt.subscribe(topic, _on_message)
return f"Abonné au topic '{topic}'. Les messages seront transmis via XMPP."
return f"Action inconnue '{action}'. Utilise : subscribe, unsubscribe, list."
+24
View File
@@ -0,0 +1,24 @@
"""
Skill MUC_SEND — envoyer un message dans le groupe XMPP des agents.
Le groupe est agents@muc.xmpp.ovh (configuré dans config.json).
Usage LLM : SKILL:muc_send ARGS:<message>
"""
DESCRIPTION = "Envoyer un message dans le groupe XMPP des agents (MUC)"
USAGE = "SKILL:muc_send ARGS:<message à envoyer dans le groupe>"
def run(args: str, context) -> str:
message = args.strip()
if not message:
return "Message vide."
if not context.xmpp:
return "XMPP non configuré sur cet agent."
if not context.xmpp.muc_room:
return "Aucun groupe MUC configuré."
context.xmpp.send_to_group(message)
return f"Message envoyé dans le groupe {context.xmpp.muc_room}."
+179
View File
@@ -0,0 +1,179 @@
"""
Skill NETWORK — administration réseau.
Usage LLM :
SKILL:network ARGS:ip [show|route|link]
SKILL:network ARGS:ping <hôte> [count]
SKILL:network ARGS:traceroute <hôte>
SKILL:network ARGS:dns <nom>
SKILL:network ARGS:ports [tcp|udp]
SKILL:network ARGS:connections
SKILL:network ARGS:firewall status
SKILL:network ARGS:firewall allow <port/service>
SKILL:network ARGS:firewall deny <port/service>
SKILL:network ARGS:firewall delete <règle>
SKILL:network ARGS:firewall list
SKILL:network ARGS:bandwidth [interface]
SKILL:network ARGS:hosts [add|remove] <ip> <nom>
SKILL:network ARGS:wget <url>
SKILL:network ARGS:curl <url>
"""
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>"
def _run(cmd: str, timeout: int = 20) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else ""
rest = parts[1] if len(parts) > 1 else ""
if action == "ip":
sub = rest.strip().lower() or "show"
if sub == "show" or sub == "addr":
return _run("ip -br addr show")
if sub == "route":
return _run("ip route show")
if sub == "link":
return _run("ip -br link show")
if sub == "full":
return _run("ip addr show")
return _run(f"ip {rest}")
if action == "ping":
parts2 = rest.split()
host = parts2[0] if parts2 else ""
count = parts2[1] if len(parts2) > 1 else "4"
if not host:
return "Précise l'hôte."
return _run(f"ping -c {count} {host}", timeout=int(count) * 3 + 5)
if action == "traceroute":
host = rest.strip()
if not host:
return "Précise l'hôte."
return _run(f"traceroute -m 15 {host}", timeout=60)
if action == "dns":
host = rest.strip()
if not host:
return "Précise le nom à résoudre."
return _run(f"dig +short {host} && dig +short -x $(dig +short {host} | head -1) 2>/dev/null || nslookup {host}")
if action == "ports":
proto = rest.strip().lower()
if proto == "udp":
return _run("ss -ulnp")
return _run("ss -tlnp")
if action == "connections":
return _run("ss -tunp | head -50")
if action == "netstat":
return _run("ss -s")
if action == "firewall":
parts2 = rest.split(None, 1)
sub = parts2[0].lower() if parts2 else "status"
arg = parts2[1] if len(parts2) > 1 else ""
# Détecte ufw ou iptables
ufw_available = _run("which ufw") != ""
if sub == "status":
if ufw_available:
return _run("ufw status verbose")
return _run("iptables -L -n -v --line-numbers")
if sub == "allow":
if not arg:
return "Précise le port/service."
if ufw_available:
return _run(f"ufw allow {arg}")
return _run(f"iptables -A INPUT -p tcp --dport {arg} -j ACCEPT")
if sub == "deny":
if not arg:
return "Précise le port/service."
if ufw_available:
return _run(f"ufw deny {arg}")
return _run(f"iptables -A INPUT -p tcp --dport {arg} -j DROP")
if sub == "delete":
if not arg:
return "Précise la règle ou le numéro."
if ufw_available:
return _run(f"ufw delete {arg}")
return _run(f"iptables -D INPUT {arg}")
if sub == "list":
if ufw_available:
return _run("ufw status numbered")
return _run("iptables -L INPUT -n -v --line-numbers")
if sub == "enable":
return _run("ufw --force enable") if ufw_available else "ufw non disponible."
if sub == "disable":
return _run("ufw disable") if ufw_available else "ufw non disponible."
return f"Sous-commande firewall inconnue : {sub}"
if action == "bandwidth":
iface = rest.strip() or "eth0"
# ifstat ou /proc/net/dev si pas dispo
result = _run(f"cat /proc/net/dev | grep {iface}")
if not result:
return f"Interface {iface} introuvable."
return result
if action == "hosts":
parts2 = rest.split()
sub = parts2[0].lower() if parts2 else "list"
if sub == "list" or not parts2:
return _run("cat /etc/hosts")
if sub == "add" and len(parts2) >= 3:
ip, name = parts2[1], parts2[2]
return _run(f"echo '{ip} {name}' >> /etc/hosts && echo 'Ajouté : {ip} {name}'")
if sub == "remove" and len(parts2) >= 2:
name = parts2[1]
return _run(f"sed -i '/{name}/d' /etc/hosts && echo 'Supprimé : {name}'")
return "Usage : hosts list | add <ip> <nom> | remove <nom>"
if action == "wget":
url = rest.strip()
if not url:
return "Précise l'URL."
return _run(f"wget -q --spider {url} && echo 'URL accessible' || echo 'URL inaccessible'")
if action == "curl":
url = rest.strip()
if not url:
return "Précise l'URL."
return _run(f"curl -sI {url} | head -10")
if action == "arp":
return _run("arp -n")
if action == "hostname":
return _run("hostname -f")
return (
"Action inconnue. Disponible : ip, ping, traceroute, dns, ports, connections, "
"firewall, hosts, wget, curl, arp, hostname, netstat"
)
+99
View File
@@ -0,0 +1,99 @@
"""
Skill PROCESS — gestion des processus.
Usage LLM :
SKILL:process ARGS:list [filtre]
SKILL:process ARGS:top [N]
SKILL:process ARGS:kill <pid> [signal]
SKILL:process ARGS:killall <nom>
SKILL:process ARGS:nice <pid> <priorité>
SKILL:process ARGS:info <pid>
SKILL:process ARGS:tree
SKILL:process ARGS:find <nom>
"""
import subprocess
DESCRIPTION = "Gestion des processus : list, top, kill, killall, nice, find"
USAGE = "SKILL:process ARGS:list [filtre] | top [N] | kill <pid> [signal] | killall <nom> | info <pid> | tree | find <nom>"
def _run(cmd: str, timeout: int = 10) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else "list"
rest = parts[1] if len(parts) > 1 else ""
if action == "list":
cmd = "ps aux --sort=-%cpu | head -30"
if rest:
cmd = f"ps aux | grep -i {rest} | grep -v grep"
return _run(cmd)
if action == "top":
n = rest.strip() or "15"
return _run(
f"ps aux --sort=-%cpu | head -{n} | "
"awk 'NR==1{print} NR>1{printf \"%-10s %-6s %-5s %-5s %s\\n\",$1,$2,$3,$4,$11}'"
)
if action == "kill":
parts2 = rest.split()
if not parts2:
return "Précise le PID."
pid = parts2[0]
signal = parts2[1] if len(parts2) > 1 else "15"
return _run(f"kill -{signal} {pid}")
if action == "kill9":
if not rest:
return "Précise le PID."
return _run(f"kill -9 {rest.strip()}")
if action == "killall":
if not rest:
return "Précise le nom du processus."
return _run(f"killall {rest.strip()}")
if action == "nice":
parts2 = rest.split()
if len(parts2) < 2:
return "Format : nice <pid> <priorité (-20 à 19)>"
pid, prio = parts2[0], parts2[1]
return _run(f"renice {prio} -p {pid}")
if action == "info":
if not rest:
return "Précise le PID."
pid = rest.strip()
out = _run(f"ps -p {pid} -o pid,ppid,user,%cpu,%mem,vsz,rss,stat,start,time,comm --no-headers")
cmdline = _run(f"cat /proc/{pid}/cmdline 2>/dev/null | tr '\\0' ' '")
return f"Process {pid}:\n{out}\nCmdline: {cmdline}"
if action == "tree":
return _run("pstree -p | head -50")
if action == "find":
if not rest:
return "Précise le nom du processus."
return _run(f"pgrep -a -i {rest.strip()}")
if action == "lsof":
# Fichiers ouverts par un processus
if rest:
return _run(f"lsof -p {rest.strip()} | head -30")
return _run("lsof | wc -l && echo fichiers ouverts au total")
return "Action inconnue. Disponible : list, top, kill, kill9, killall, nice, info, tree, find, lsof"
+251
View File
@@ -0,0 +1,251 @@
"""
Skill SCRIPT — bibliothèque de scripts bash par agent.
Chaque agent dispose de son propre dossier scripts/ (configurable via
"scripts_dir" dans config.json, sinon /opt/<install_dir>/scripts).
L'environnement du script expose automatiquement :
MQTT_BROKER, MQTT_PORT, MQTT_REPLY_TOPIC, AGENT_ID, SCRIPTS_DIR
Ainsi un script peut publier son résultat directement :
mosquitto_pub -h $MQTT_BROKER -t $MQTT_REPLY_TOPIC -m "mon résultat"
Usage LLM :
SKILL:script ARGS:list
SKILL:script ARGS:show <nom>
SKILL:script ARGS:save <nom> | <contenu>
SKILL:script ARGS:edit <nom> <ligne> | <nouveau contenu de ligne>
SKILL:script ARGS:exec <nom> [args...]
SKILL:script ARGS:run | <contenu inline>
SKILL:script ARGS:delete <nom>
"""
import json
import os
import stat
import subprocess
import tempfile
from datetime import datetime
DESCRIPTION = "Bibliothèque de scripts bash : sauvegarder, lister, afficher, éditer, exécuter"
USAGE = (
"SKILL:script ARGS:list\n"
"SKILL:script ARGS:show <nom>\n"
"SKILL:script ARGS:save <nom> | <contenu>\n"
"SKILL:script ARGS:edit <nom> <ligne> | <nouveau contenu>\n"
"SKILL:script ARGS:exec <nom> [args]\n"
"SKILL:script ARGS:run | <contenu inline>\n"
"SKILL:script ARGS:delete <nom>"
)
def _scripts_dir(context) -> str:
"""Détermine le répertoire scripts de cet agent."""
if context.config.get("scripts_dir"):
return context.config["scripts_dir"]
queue_db = context.config.get("queue_db", "")
if queue_db:
install = os.path.dirname(os.path.dirname(queue_db))
return os.path.join(install, "scripts")
return f"/opt/{context.agent_id}/scripts"
def _ensure_dir(context) -> str:
d = _scripts_dir(context)
os.makedirs(d, exist_ok=True)
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 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:
env = os.environ.copy()
mc = context.config.get("mqtt", {})
env["MQTT_BROKER"] = mc.get("host", "localhost")
env["MQTT_PORT"] = str(mc.get("port", 1883))
env["MQTT_REPLY_TOPIC"] = "agents/nexus/inbox"
env["AGENT_ID"] = context.agent_id
env["SCRIPTS_DIR"] = scripts_dir
return env
def _notify(context, script_name: str, result: str):
"""Publie un événement d'exécution sur MQTT pour que Nexus notifie l'utilisateur."""
try:
context.mqtt.publish_raw("agents/scripts/execution", json.dumps({
"agent_id": context.agent_id,
"script": script_name,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"result": result[:1000],
}))
except Exception:
pass
def _run_script(cmd: str, env: dict, timeout: int = 120) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout,
env=env, executable="/bin/bash",
)
out = (result.stdout + result.stderr).strip()
if len(out) > 4000:
out = out[:4000] + "\n... [tronqué]"
return out or f"(code retour : {result.returncode})"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s dépassé)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else "list"
rest = parts[1] if len(parts) > 1 else ""
# ── list ──────────────────────────────────────────────────────────────
if action == "list":
d = _ensure_dir(context)
files = sorted(f for f in os.listdir(d) if f.endswith(".sh"))
if not files:
return f"Aucun script dans {d}"
lines = [f"Scripts disponibles ({d}) :"]
for f in files:
path = os.path.join(d, f)
size = os.path.getsize(path)
lines.append(f" {f[:-3]:30s} ({size} octets)")
return "\n".join(lines)
# ── show ──────────────────────────────────────────────────────────────
if action == "show":
name = _safe_name(rest)
if not name:
return "Précise le nom du script."
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:
content = f.read()
return f"── {name}.sh ──\n{content}"
# ── save ──────────────────────────────────────────────────────────────
if action == "save":
if "|" not in rest:
return "Format : save <nom> | <contenu du script>"
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)
with open(path, "w") as f:
if not content.startswith("#!"):
f.write("#!/bin/bash\n")
f.write(content + "\n")
os.chmod(path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH)
verb = "mis à jour" if existed else "créé"
return f"Script '{name}' {verb} : {path}"
# ── edit ──────────────────────────────────────────────────────────────
if action == "edit":
# Format : edit <nom> <numéro_ligne> | <nouveau contenu de ligne>
if "|" not in rest:
return "Format : edit <nom> <ligne> | <nouveau contenu>\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 <nom> <ligne> | <nouveau contenu>"
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)
name = _safe_name(parts2[0]) if parts2 else ""
sargs = parts2[1] if len(parts2) > 1 else ""
if not name:
return "Précise le nom du script."
d = _ensure_dir(context)
path = os.path.join(d, name + ".sh")
if not os.path.exists(path):
return f"Script '{name}' introuvable. Utilise 'list' pour voir les scripts disponibles."
env = _build_env(context, d)
out = _run_script(f'"{path}" {sargs}', env=env, timeout=120)
_notify(context, name, out)
return out
# ── run (inline) ──────────────────────────────────────────────────────
if action == "run":
if not rest:
return "Précise le contenu du script."
d = _ensure_dir(context)
content = rest.replace("\\n", "\n")
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False, dir="/tmp"
) as f:
f.write("#!/bin/bash\nset -e\n" + content)
tmpfile = f.name
os.chmod(tmpfile, stat.S_IRWXU)
env = _build_env(context, d)
out = _run_script(tmpfile, env=env, timeout=60)
os.unlink(tmpfile)
_notify(context, "<inline>", out)
return out
# ── delete ────────────────────────────────────────────────────────────
if action == "delete":
name = _safe_name(rest)
if not name:
return "Précise le nom du script."
d = _ensure_dir(context)
path = os.path.join(d, name + ".sh")
if not os.path.exists(path):
return f"Script '{name}' introuvable dans {d}"
os.unlink(path)
return f"Script '{name}' supprimé."
return "Action inconnue. Disponible : list, show, save, exec, run, delete"
+62
View File
@@ -0,0 +1,62 @@
"""
Skill SHELL — exécution de commandes shell arbitraires.
Skill de dernier recours quand aucun skill spécialisé ne convient.
Usage LLM : SKILL:shell ARGS:<commande bash>
"""
import subprocess
DESCRIPTION = "Exécution de commandes shell arbitraires (fallback général)"
USAGE = "SKILL:shell ARGS:<commande bash complète>"
# Commandes bloquées pour éviter les accidents critiques
BLOCKED = [
"rm -rf /",
"dd if=/dev/zero of=/dev/",
"mkfs",
"> /dev/sda",
":(){ :|:& };:", # fork bomb
]
def run(args: str, context) -> str:
cmd = args.strip()
if not cmd:
return "Commande vide."
# Vérification des commandes dangereuses
for blocked in BLOCKED:
if blocked in cmd:
return f"Commande bloquée pour sécurité : {blocked}"
try:
result = subprocess.run(
cmd,
shell=True,
text=True,
capture_output=True,
timeout=60,
executable="/bin/bash",
)
stdout = result.stdout.strip()
stderr = result.stderr.strip()
returncode = result.returncode
output = ""
if stdout:
output += stdout
if stderr:
output += ("\n" if output else "") + f"[stderr] {stderr}"
if not output:
output = f"(Commande exécutée, code retour : {returncode})"
# Tronqué à 4000 caractères
if len(output) > 4000:
output = output[:4000] + f"\n... [tronqué, {len(output)} caractères total]"
return output
except subprocess.TimeoutExpired:
return "Timeout (60s) — commande trop longue."
except Exception as e:
return f"Erreur : {e}"
+86
View File
@@ -0,0 +1,86 @@
"""
Skill SSH — exécuter des commandes sur une machine distante via SSH.
Usage LLM :
SKILL:ssh ARGS:<host> <user> password <mdp> | <commande>
SKILL:ssh ARGS:<host> <user> key <chemin_cle> | <commande>
SKILL:ssh ARGS:copy <host> <user> password <mdp> | <fichier_local> <chemin_distant>
"""
import subprocess
DESCRIPTION = "Exécuter des commandes SSH sur une machine distante"
USAGE = "SKILL:ssh ARGS:<host> <user> password <mdp> | <commande> ou key <chemin_cle> | <commande>"
def _ssh_cmd(host: str, user: str, auth: str, credential: str, cmd: str, timeout: int = 30) -> str:
if auth == "password":
full_cmd = (
f"sshpass -p '{credential}' ssh "
f"-o StrictHostKeyChecking=no "
f"-o ConnectTimeout=10 "
f"{user}@{host} '{cmd}'"
)
else:
full_cmd = (
f"ssh -i {credential} "
f"-o StrictHostKeyChecking=no "
f"-o ConnectTimeout=10 "
f"{user}@{host} '{cmd}'"
)
try:
result = subprocess.run(
full_cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else f"(code retour : {result.returncode})"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
if "|" not in args:
return "Format : <host> <user> password|key <credential> | <commande>"
left, command = args.split("|", 1)
command = command.strip()
parts = left.strip().split()
if len(parts) < 4:
return "Format : <host> <user> password|key <credential> | <commande>"
host, user, auth, credential = parts[0], parts[1], parts[2], parts[3]
if auth not in ("password", "key"):
return "Auth doit être 'password' ou 'key'"
# Sous-commande copy
if command.startswith("COPY "):
file_parts = command[5:].split()
if len(file_parts) < 2:
return "Format copy : COPY <fichier_local> <chemin_distant>"
local_file, remote_path = file_parts[0], file_parts[1]
if auth == "password":
scp_cmd = (
f"sshpass -p '{credential}' scp "
f"-o StrictHostKeyChecking=no "
f"{local_file} {user}@{host}:{remote_path}"
)
else:
scp_cmd = (
f"scp -i {credential} "
f"-o StrictHostKeyChecking=no "
f"{local_file} {user}@{host}:{remote_path}"
)
try:
result = subprocess.run(
scp_cmd, shell=True, text=True,
capture_output=True, timeout=60
)
return (result.stdout + result.stderr).strip() or f"Fichier copié vers {host}:{remote_path}"
except Exception as e:
return str(e)
return _ssh_cmd(host, user, auth, credential, command)
+63
View File
@@ -0,0 +1,63 @@
"""
Skill SYSINFO — informations système complètes.
Usage LLM : SKILL:sysinfo ARGS:all | cpu | mem | disk | uptime | load | net
"""
import subprocess
DESCRIPTION = "Informations système : CPU, RAM, disque, uptime, charge, réseau"
USAGE = "SKILL:sysinfo ARGS:all | cpu | mem | disk | uptime | load | net"
def _run(cmd: str) -> str:
try:
return subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.STDOUT).strip()
except subprocess.CalledProcessError as e:
return e.output.strip() or str(e)
def run(args: str, context) -> str:
what = args.strip().lower() or "all"
sections = []
if what in ("all", "uptime"):
uptime = _run("uptime -p")
since = _run("uptime -s")
sections.append(f"Uptime : {uptime} (depuis {since})")
if what in ("all", "load"):
load = _run("cat /proc/loadavg")
cpus = _run("nproc")
sections.append(f"Charge système : {load} ({cpus} CPU)")
if what in ("all", "cpu"):
cpu_info = _run("lscpu | grep -E 'Model name|CPU\\(s\\)|MHz'")
cpu_usage = _run(
"top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4\"%\"}'"
)
sections.append(f"CPU :\n{cpu_info}\nUtilisation : {cpu_usage}")
if what in ("all", "mem"):
mem = _run("free -h")
sections.append(f"Mémoire :\n{mem}")
if what in ("all", "disk"):
disk = _run("df -h --output=source,size,used,avail,pcent,target | column -t")
sections.append(f"Disques :\n{disk}")
if what in ("all", "net"):
ifaces = _run("ip -br addr show")
sections.append(f"Interfaces réseau :\n{ifaces}")
if what == "os":
osinfo = _run("cat /etc/os-release | grep -E '^(NAME|VERSION)='")
kernel = _run("uname -r")
sections.append(f"OS :\n{osinfo}\nKernel : {kernel}")
if not sections:
return (
"Option inconnue. Utilise : all, cpu, mem, disk, uptime, load, net, os"
)
return "\n\n".join(sections)
+166
View File
@@ -0,0 +1,166 @@
"""
Skill SYSTEMD — gestion des services systemd.
Usage LLM :
SKILL:systemd ARGS:status <service>
SKILL:systemd ARGS:start <service>
SKILL:systemd ARGS:stop <service>
SKILL:systemd ARGS:restart <service>
SKILL:systemd ARGS:reload <service>
SKILL:systemd ARGS:enable <service>
SKILL:systemd ARGS:disable <service>
SKILL:systemd ARGS:logs <service> [lignes]
SKILL:systemd ARGS:list [pattern]
SKILL:systemd ARGS:failed
SKILL:systemd ARGS:daemon-reload
"""
import subprocess
DESCRIPTION = "Gestion des services systemd (start/stop/restart/status/logs/enable/disable)"
USAGE = "SKILL:systemd ARGS:status <service> | start <service> | stop <service> | restart <service> | enable <service> | disable <service> | logs <service> [N] | list | failed"
# Actions destructives qui nécessitent confirmation (lecture seule = pas de confirmation)
_ACTIONS_REQUIRING_CONFIRMATION = {"start", "stop", "restart", "enable", "disable", "mask", "unmask", "daemon-reload"}
def _confirm_or_execute(context, description: str, action_fn) -> str:
"""Demande confirmation si requête XMPP directe, sinon exécute immédiatement."""
sender = getattr(context.agent, '_last_xmpp_sender', '')
if not sender:
return action_fn()
context.agent._pending_confirmations[sender] = {"description": description, "fn": action_fn}
return f"⚠️ Confirmation requise :\n{description}\n\nRéponds **oui** pour confirmer ou **non** pour annuler."
def _run(cmd: str, timeout: int = 30) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:4000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split()
action = parts[0].lower() if parts else ""
service = parts[1] if len(parts) > 1 else ""
if action == "status":
if not service:
return "Précise le service."
return _run(f"systemctl status {service} --no-pager -l")
if action == "start":
if not service:
return "Précise le service."
def _do():
out = _run(f"systemctl start {service}")
status = _run(f"systemctl is-active {service}")
return f"Démarrage de {service}... Statut : {status}\n{out}"
return _confirm_or_execute(context, f"Démarrer le service : {service}", _do)
if action == "stop":
if not service:
return "Précise le service."
def _do():
out = _run(f"systemctl stop {service}")
status = _run(f"systemctl is-active {service}")
return f"Arrêt de {service}... Statut : {status}\n{out}"
return _confirm_or_execute(context, f"Arrêter le service : {service}", _do)
if action == "restart":
if not service:
return "Précise le service."
def _do():
out = _run(f"systemctl restart {service}")
status = _run(f"systemctl is-active {service}")
return f"Redémarrage de {service}... Statut : {status}\n{out}"
return _confirm_or_execute(context, f"Redémarrer le service : {service}", _do)
if action == "reload":
if not service:
return "Précise le service."
return _run(f"systemctl reload {service}")
if action == "enable":
if not service:
return "Précise le service."
return _confirm_or_execute(
context, f"Activer au démarrage : {service}",
lambda: _run(f"systemctl enable {service}")
)
if action == "disable":
if not service:
return "Précise le service."
return _confirm_or_execute(
context, f"Désactiver au démarrage : {service}",
lambda: _run(f"systemctl disable {service}")
)
if action == "mask":
if not service:
return "Précise le service."
return _confirm_or_execute(
context, f"Masquer (bloquer) le service : {service}",
lambda: _run(f"systemctl mask {service}")
)
if action == "unmask":
if not service:
return "Précise le service."
return _confirm_or_execute(
context, f"Démasquer le service : {service}",
lambda: _run(f"systemctl unmask {service}")
)
if action == "logs":
if not service:
return "Précise le service."
n = parts[2] if len(parts) > 2 else "50"
try:
n = int(n)
except ValueError:
n = 50
return _run(f"journalctl -u {service} -n {n} --no-pager -o short-iso")
if action == "list":
pattern = parts[1] if len(parts) > 1 else ""
cmd = "systemctl list-units --type=service --no-pager"
if pattern:
cmd += f" | grep {pattern}"
return _run(cmd)
if action == "list-all":
return _run("systemctl list-units --type=service --all --no-pager")
if action == "failed":
return _run("systemctl list-units --state=failed --no-pager")
if action == "daemon-reload":
return _confirm_or_execute(
context, "Recharger la configuration systemd (daemon-reload)",
lambda: _run("systemctl daemon-reload && echo 'daemon-reload effectué'")
)
if action == "is-active":
if not service:
return "Précise le service."
return _run(f"systemctl is-active {service}")
if action == "is-enabled":
if not service:
return "Précise le service."
return _run(f"systemctl is-enabled {service}")
return (
"Action inconnue. Disponible : status, start, stop, restart, reload, "
"enable, disable, mask, unmask, logs, list, list-all, failed, "
"daemon-reload, is-active, is-enabled"
)
+74
View File
@@ -0,0 +1,74 @@
"""
Skill TODO — liste de tâches en mémoire pour la session courante.
Usage LLM :
SKILL:todo ARGS:add <texte>
SKILL:todo ARGS:list
SKILL:todo ARGS:done <id>
SKILL:todo ARGS:delete <id>
SKILL:todo ARGS:clear
"""
DESCRIPTION = "Liste de tâches en mémoire (session) : add, list, done, delete, clear"
USAGE = "SKILL:todo ARGS:add <texte> | list | done <id> | delete <id> | clear"
# Stockage partagé entre tous les appels (même processus)
_todos: list = []
_next_id: int = 1
def run(args: str, context) -> str:
global _todos, _next_id
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else ""
rest = parts[1].strip() if len(parts) > 1 else ""
if action == "add":
if not rest:
return "Précise le texte de la tâche."
_todos.append({"id": _next_id, "text": rest, "done": False})
_next_id += 1
return f"Tâche #{_next_id - 1} ajoutée : {rest}"
if action == "list":
if not _todos:
return "Aucune tâche."
lines = []
for t in _todos:
status = "" if t["done"] else ""
lines.append(f" {status} #{t['id']} {t['text']}")
return "Tâches :\n" + "\n".join(lines)
if action == "done":
if not rest:
return "Précise l'ID de la tâche."
try:
tid = int(rest)
except ValueError:
return f"ID invalide : {rest}"
for t in _todos:
if t["id"] == tid:
t["done"] = True
return f"Tâche #{tid} marquée comme terminée."
return f"Tâche #{tid} introuvable."
if action == "delete":
if not rest:
return "Précise l'ID de la tâche."
try:
tid = int(rest)
except ValueError:
return f"ID invalide : {rest}"
before = len(_todos)
_todos = [t for t in _todos if t["id"] != tid]
if len(_todos) < before:
return f"Tâche #{tid} supprimée."
return f"Tâche #{tid} introuvable."
if action == "clear":
_todos = []
_next_id = 1
return "Liste de tâches vidée."
return "Action inconnue. Disponible : add, list, done, delete, clear"
+127
View File
@@ -0,0 +1,127 @@
"""
Skill USER — gestion des utilisateurs et groupes.
Usage LLM :
SKILL:user ARGS:list
SKILL:user ARGS:add <nom> [--sudo]
SKILL:user ARGS:delete <nom>
SKILL:user ARGS:passwd <nom>
SKILL:user ARGS:info <nom>
SKILL:user ARGS:groups <nom>
SKILL:user ARGS:addgroup <nom> <groupe>
SKILL:user ARGS:removegroup <nom> <groupe>
SKILL:user ARGS:lock <nom>
SKILL:user ARGS:unlock <nom>
SKILL:user ARGS:whoami
SKILL:user ARGS:logged
SKILL:user ARGS:sudoers
"""
import subprocess
DESCRIPTION = "Gestion utilisateurs et groupes : add, delete, passwd, groups, lock/unlock, sudoers"
USAGE = "SKILL:user ARGS:list | add <nom> [--sudo] | delete <nom> | passwd <nom> | info <nom> | groups <nom> | addgroup <nom> <groupe> | lock <nom> | unlock <nom> | logged"
def _run(cmd: str, timeout: int = 15) -> str:
try:
result = subprocess.run(
cmd, shell=True, text=True,
capture_output=True, timeout=timeout
)
out = (result.stdout + result.stderr).strip()
return out[:3000] if out else "(aucune sortie)"
except subprocess.TimeoutExpired:
return f"Timeout ({timeout}s)"
except Exception as e:
return str(e)
def run(args: str, context) -> str:
parts = args.strip().split()
action = parts[0].lower() if parts else "list"
rest = parts[1:] if len(parts) > 1 else []
if action == "list":
return _run("getent passwd | awk -F: '$3>=1000 || $3==0 {print $1\" (uid=\"$3\", shell=\"$7\")\"}'")
if action == "add":
if not rest:
return "Précise le nom d'utilisateur."
name = rest[0]
sudo = "--sudo" in rest or "-s" in rest
cmds = [
f"adduser --gecos '' --disabled-password {name}",
]
if sudo:
cmds.append(f"usermod -aG sudo {name}")
return "\n".join(_run(c) for c in cmds)
if action == "delete":
if not rest:
return "Précise le nom d'utilisateur."
return _run(f"deluser --remove-home {rest[0]}")
if action == "passwd":
if not rest:
return "Précise le nom d'utilisateur."
# Génère un mot de passe aléatoire
pwd = _run("openssl rand -base64 12").strip()
out = _run(f"echo '{rest[0]}:{pwd}' | chpasswd")
return f"{out}\nNouveauMDP : {pwd}"
if action == "info":
if not rest:
return "Précise le nom d'utilisateur."
name = rest[0]
return _run(f"id {name} && getent passwd {name}")
if action == "groups":
if not rest:
return "Précise le nom d'utilisateur."
return _run(f"groups {rest[0]}")
if action == "addgroup":
if len(rest) < 2:
return "Format : addgroup <utilisateur> <groupe>"
return _run(f"usermod -aG {rest[1]} {rest[0]}")
if action == "removegroup":
if len(rest) < 2:
return "Format : removegroup <utilisateur> <groupe>"
return _run(f"gpasswd -d {rest[0]} {rest[1]}")
if action == "lock":
if not rest:
return "Précise le nom d'utilisateur."
return _run(f"usermod -L {rest[0]}")
if action == "unlock":
if not rest:
return "Précise le nom d'utilisateur."
return _run(f"usermod -U {rest[0]}")
if action == "whoami":
return _run("whoami && id")
if action == "logged":
return _run("who && echo '---' && last -n 10")
if action == "sudoers":
return _run("getent group sudo | cut -d: -f4")
if action == "ssh-key":
# Ajouter une clé SSH pour un utilisateur
if len(rest) < 2:
return "Format : ssh-key <utilisateur> <clé_publique>"
name = rest[0]
key = " ".join(rest[1:])
return _run(
f"mkdir -p /home/{name}/.ssh && "
f"echo '{key}' >> /home/{name}/.ssh/authorized_keys && "
f"chmod 700 /home/{name}/.ssh && "
f"chmod 600 /home/{name}/.ssh/authorized_keys && "
f"chown -R {name}:{name} /home/{name}/.ssh && "
f"echo 'Clé ajoutée pour {name}'"
)
return "Action inconnue. Disponible : list, add, delete, passwd, info, groups, addgroup, removegroup, lock, unlock, whoami, logged, sudoers, ssh-key"
+109
View File
@@ -0,0 +1,109 @@
"""
Skill WEB_FETCH — récupérer le contenu d'une URL HTTP/HTTPS.
Usage LLM :
SKILL:web_fetch ARGS:get <url>
SKILL:web_fetch ARGS:head <url>
SKILL:web_fetch ARGS:post <url> | <body_json>
"""
import urllib.request
import urllib.error
import urllib.parse
import json
import re
DESCRIPTION = "Récupérer le contenu d'une URL HTTP/HTTPS (GET, HEAD, POST)"
USAGE = "SKILL:web_fetch ARGS:get <url> | head <url> | post <url>|<body_json>"
MAX_SIZE = 8000
def _strip_html(html: str) -> str:
"""Supprime les balises HTML et nettoie le texte."""
# Supprime scripts et styles
html = re.sub(r'<(script|style)[^>]*>.*?</\1>', ' ', html, flags=re.DOTALL | re.IGNORECASE)
# Supprime les balises
html = re.sub(r'<[^>]+>', ' ', html)
# Décode les entités HTML basiques
html = html.replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>') \
.replace('&quot;', '"').replace('&#39;', "'").replace('&nbsp;', ' ')
# Nettoie les espaces multiples
html = re.sub(r'\s+', ' ', html).strip()
return html
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else "get"
rest = parts[1] if len(parts) > 1 else ""
if action == "get":
url = rest.strip()
if not url:
return "Précise une URL."
try:
req = urllib.request.Request(
url,
headers={
"User-Agent": "HAL-Agent/1.0 (compatible; Python urllib)",
"Accept": "text/html,text/plain,application/json,*/*"
}
)
with urllib.request.urlopen(req, timeout=15) as resp:
content_type = resp.headers.get("Content-Type", "")
raw = resp.read(MAX_SIZE * 3) # Lit plus pour avoir du contenu après stripping
charset = "utf-8"
if "charset=" in content_type:
charset = content_type.split("charset=")[-1].split(";")[0].strip()
text = raw.decode(charset, errors="replace")
# Si HTML, nettoie les balises
if "html" in content_type.lower():
text = _strip_html(text)
if len(text) > MAX_SIZE:
text = text[:MAX_SIZE] + f"\n... (tronqué à {MAX_SIZE} caractères)"
return f"[{resp.status} {url}]\n{text}"
except urllib.error.HTTPError as e:
return f"Erreur HTTP {e.code} : {e.reason}{url}"
except urllib.error.URLError as e:
return f"Erreur URL : {e.reason}{url}"
except Exception as e:
return f"Erreur : {e}"
if action == "head":
url = rest.strip()
if not url:
return "Précise une URL."
try:
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req, timeout=10) as resp:
headers = dict(resp.headers)
lines = [f"[{resp.status} {url}]"]
for k, v in headers.items():
lines.append(f" {k}: {v}")
return "\n".join(lines)
except Exception as e:
return f"Erreur : {e}"
if action == "post":
if "|" not in rest:
return "Format : post <url> | <body_json>"
url, body = rest.split("|", 1)
url = url.strip()
body = body.strip().encode("utf-8")
try:
req = urllib.request.Request(
url, data=body, method="POST",
headers={
"User-Agent": "HAL-Agent/1.0",
"Content-Type": "application/json"
}
)
with urllib.request.urlopen(req, timeout=15) as resp:
text = resp.read(MAX_SIZE).decode("utf-8", errors="replace")
return f"[{resp.status} {url}]\n{text}"
except Exception as e:
return f"Erreur : {e}"
return "Action inconnue. Disponible : get, head, post"