Initial commit — Agent HAL v1.0

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