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:
@@ -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
@@ -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"
|
||||
)
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
@@ -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"
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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}'."
|
||||
@@ -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."
|
||||
@@ -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}."
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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}"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
@@ -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"
|
||||
@@ -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('&', '&').replace('<', '<').replace('>', '>') \
|
||||
.replace('"', '"').replace(''', "'").replace(' ', ' ')
|
||||
# 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"
|
||||
Reference in New Issue
Block a user