Initial commit — agent_debian v2.0

This commit is contained in:
2026-03-09 09:01:33 +00:00
commit 084787b106
18 changed files with 1669 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
venv/
__pycache__/
*.pyc
*.pyo
*.db
*.log
data/
*.egg-info/
.vault_pass
config/config.json
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Agent Debian — Administration système complète.
Contrôle apt, systemd, réseau, filesystem, processus, logs, conteneurs, utilisateurs.
"""
import os
import sys
import threading
import subprocess
import logging
sys.path.insert(0, "/opt")
from agents_core import BaseAgent, AgentContext, Message, MessageType
logger = logging.getLogger(__name__)
class AgentDebian(BaseAgent):
AGENT_TYPE = "debian"
DESCRIPTION = (
"Administration système Debian : paquets apt, services systemd, "
"réseau, filesystem, processus, logs, conteneurs Docker/LXC, utilisateurs"
)
DEFAULT_CONFIG_PATH = "/opt/agent_debian/config/config.json"
def get_skills_dir(self) -> str:
return os.path.join(os.path.dirname(__file__), "skills")
def on_start(self):
"""Au démarrage, signale à Nexus que l'agent est prêt."""
self.mqtt.send_to("nexus", f"Agent Debian ({self.agent_id}) en ligne.")
# Lance la surveillance proactive
self._start_monitoring()
def setup_extra_subscriptions(self):
"""Souscrit aussi aux commandes de contrôle."""
self.mqtt.subscribe(
f"agents/{self.agent_id}/control",
self._on_control_message,
)
def _on_control_message(self, msg, topic: str):
"""Messages de contrôle (pause, resume, report...)."""
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):
"""Commandes spécifiques à l'agent Debian."""
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):
"""Réagit aux broadcasts (ex: demande de statut globale)."""
if "status" in str(msg.payload).lower():
self.mqtt.reply(msg, self._build_report())
def _build_report(self) -> str:
"""Génère un rapport quotidien du système."""
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"
)
# Infos système rapides
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:
"""Git pull + redémarrage du service."""
try:
out = subprocess.check_output(
"cd /opt/agent_debian && git pull",
shell=True, text=True, stderr=subprocess.STDOUT
)
subprocess.Popen(["systemctl", "restart", self.agent_id])
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):
"""Lance la surveillance proactive en arrière-plan."""
t = threading.Thread(target=self._monitor_loop, daemon=True)
t.start()
def _monitor_loop(self):
"""Vérifie périodiquement les ressources critiques et alerte si nécessaire."""
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) # Toutes les 5 minutes
def _check_disk_usage(self):
"""Alerte si un disque dépasse 85%."""
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):
"""Alerte si la RAM disponible < 10%."""
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__":
AgentDebian().run()
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Agent Debian — Administration système
After=network.target mosquitto.service
Wants=mosquitto.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/agent_debian
ExecStart=/opt/agent_debian/venv/bin/python /opt/agent_debian/agent_debian.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=agent-debian
[Install]
WantedBy=multi-user.target
+38
View File
@@ -0,0 +1,38 @@
Tu es un agent d'administration système Debian. Tu contrôles pleinement ce serveur.
Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP (directement).
## Tes skills disponibles et quand les utiliser
- **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)
- **filesystem** : opérations fichiers (ls, cat, write, delete, find, grep, df, du)
- **network** : réseau (ip, ping, traceroute, DNS, ports, firewall)
- **process** : processus (list, kill, top, find)
- **journal** : logs système (tail, service, errors, since, grep)
- **user** : utilisateurs (add, delete, passwd, groups, sudo)
- **container** : Docker et LXC (ps, start, stop, logs, exec, stats)
- **cron** : tâches planifiées (list, add, remove)
- **script** : créer et exécuter des scripts bash
- **shell** : commande bash directe (fallback si aucun skill ne convient)
- **mqtt_send** : envoyer un message à un autre agent ou topic MQTT
## Règles importantes
1. Utilise toujours le skill le plus spécifique disponible
2. Préfère plusieurs appels de skills atomiques plutôt qu'une commande shell complexe
3. Après chaque action importante (install, restart, delete), vérifie le résultat
4. Si une tâche génère un script, utilise SKILL:script pour le créer et l'exécuter,
et le résultat sera automatiquement renvoyé via MQTT
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
## 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"
+2
View File
@@ -0,0 +1,2 @@
agents_core @ file:///opt/agents_core
requests>=2.28
+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"
+93
View File
@@ -0,0 +1,93 @@
"""
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 _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":
# Format attendu : "* * * * * commande"
# On split les 5 premiers champs (expression cron) + le reste (commande)
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}"
# Récupère le crontab actuel, ajoute la ligne
current = _run("crontab -l 2>/dev/null")
if entry in current:
return f"Cette entrée existe déjà : {entry}"
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}"
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]
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)
removed = len(current.splitlines()) - len(lines)
return f"{removed} entrée(s) supprimée(s) contenant '{rest}'.\n{out}"
if action == "clear":
return _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"
+177
View File
@@ -0,0 +1,177 @@
"""
Skill FILESYSTEM — opérations sur le système de fichiers.
Usage LLM :
SKILL:filesystem ARGS:ls <chemin>
SKILL:filesystem ARGS:cat <fichier>
SKILL:filesystem ARGS:write <fichier> | <contenu>
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 : ls, cat, write, delete, move, copy, chmod, find, grep, df, du"
USAGE = "SKILL:filesystem ARGS:ls <path> | cat <file> | write <file>|<content> | delete <path> | find <path> <pattern> | grep <pattern> <file> | df | du <path> | tail <file> [N]"
# Chemins interdits pour éviter les accidents
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:
# Sépare l'action du reste
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 == "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}"
# Confirmation implicite : on ne supprime pas récursivement sans -r explicite
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 -n '{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, tail, head, write, append, delete, "
"mkdir, move, copy, chmod, chown, find, grep, df, du, stat"
)
+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}'."
+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"
+132
View File
@@ -0,0 +1,132 @@
"""
Skill SCRIPT — créer et exécuter un script bash, avec renvoi du résultat via MQTT.
L'environnement du script expose automatiquement :
MQTT_BROKER, MQTT_REPLY_TOPIC, AGENT_ID
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:run | <contenu du script>
SKILL:script ARGS:save <nom> | <contenu>
SKILL:script ARGS:exec <nom> [args]
SKILL:script ARGS:list
SKILL:script ARGS:show <nom>
SKILL:script ARGS:delete <nom>
"""
import os
import subprocess
import stat
DESCRIPTION = "Créer/exécuter des scripts bash avec renvoi du résultat via MQTT"
USAGE = "SKILL:script ARGS:run|<contenu> | save <nom>|<contenu> | exec <nom> | list | show <nom>"
SCRIPTS_DIR = "/opt/agent_debian/scripts"
def _ensure_dir():
os.makedirs(SCRIPTS_DIR, exist_ok=True)
def _run(cmd: str, env: dict = None, timeout: int = 60) -> 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)"
except Exception as e:
return str(e)
def _build_env(context) -> dict:
"""Environnement injecté dans chaque script."""
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
return env
def run(args: str, context) -> str:
_ensure_dir()
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else "run"
rest = parts[1] if len(parts) > 1 else ""
if action == "run":
# Exécution directe d'un script inline
if not rest:
return "Précise le contenu du script."
content = rest.replace("\\n", "\n")
# Fichier temporaire
import tempfile
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)
out = _run(tmpfile, env=env, timeout=60)
os.unlink(tmpfile)
return out
if action == "save":
if "|" not in rest:
return "Format : save <nom> | <contenu du script>"
name, content = rest.split("|", 1)
name = name.strip().replace("/", "_") # Sécurité
content = content.strip().replace("\\n", "\n")
path = os.path.join(SCRIPTS_DIR, name + ".sh")
with open(path, "w") as f:
f.write("#!/bin/bash\n" + content)
os.chmod(path, stat.S_IRWXU)
return f"Script sauvegardé : {path}"
if action == "exec":
parts2 = rest.split(None, 1)
name = parts2[0] if parts2 else ""
sargs = parts2[1] if len(parts2) > 1 else ""
if not name:
return "Précise le nom du script."
path = os.path.join(SCRIPTS_DIR, name + ".sh")
if not os.path.exists(path):
return f"Script '{name}' introuvable dans {SCRIPTS_DIR}"
env = _build_env(context)
return _run(f"{path} {sargs}", env=env, timeout=120)
if action == "list":
files = [f for f in os.listdir(SCRIPTS_DIR) if f.endswith(".sh")]
return "\n".join(files) if files else "Aucun script sauvegardé."
if action == "show":
name = rest.strip()
if not name:
return "Précise le nom du script."
path = os.path.join(SCRIPTS_DIR, name + ".sh")
if not os.path.exists(path):
return f"Script '{name}' introuvable."
with open(path) as f:
return f.read()
if action == "delete":
name = rest.strip()
if not name:
return "Précise le nom du script."
path = os.path.join(SCRIPTS_DIR, name + ".sh")
if os.path.exists(path):
os.unlink(path)
return f"Script '{name}' supprimé."
return f"Script '{name}' introuvable."
return "Action inconnue. Disponible : run, save, exec, list, show, 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}"
+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)
+133
View File
@@ -0,0 +1,133 @@
"""
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"
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."
out = _run(f"systemctl start {service}")
status = _run(f"systemctl is-active {service}")
return f"Démarrage de {service}... Statut : {status}\n{out}"
if action == "stop":
if not service:
return "Précise le service."
out = _run(f"systemctl stop {service}")
status = _run(f"systemctl is-active {service}")
return f"Arrêt de {service}... Statut : {status}\n{out}"
if action == "restart":
if not service:
return "Précise le service."
out = _run(f"systemctl restart {service}")
status = _run(f"systemctl is-active {service}")
return f"Redémarrage de {service}... Statut : {status}\n{out}"
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 _run(f"systemctl enable {service}")
if action == "disable":
if not service:
return "Précise le service."
return _run(f"systemctl disable {service}")
if action == "mask":
if not service:
return "Précise le service."
return _run(f"systemctl mask {service}")
if action == "unmask":
if not service:
return "Précise le service."
return _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 _run("systemctl daemon-reload")
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"
)
+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"