nas-runbooks/common/MCP-NAS-SCRIPTING.md

6.8 KiB
Raw Blame History

MCP NAS — Référence de scripting fiable

Date : 2026-06-23 | Session debug Veille/Bifrost (40+ tool calls observés)

⚠️ OBSOLÈTE EN PARTIE depuis le patch P0 du 2026-07-04 — voir mcp-nas-run-command-v2.md. Les RÈGLES 1, 2 et 3 ci-dessous (poisoning, heredoc interdit, limite 350 chars/base64) ne s'appliquent plus : heredocs, quotes et commandes longues fonctionnent nativement, et reset_session récupère une session bloquée. La section « RÈGLE 4 — Services internes (172.17.0.1) » reste valide.


Environnement du container

Propriété Valeur
Shell /bin/bash
User root uid=0
Python 3.11.15/usr/local/bin/python3
Modules Python requests, sqlite3, json, urllib, subprocess
Filesystem /mnt NAS Synology 11 To, R/W
Réseau host Docker 172.17.0.1
Réseau LAN NAS 192.168.100.33INACCESSIBLE depuis container

Binaires (vérifié 2026-06-23)

Outil Dispo Note
curl oui /usr/bin/curl — soumis limite 350 chars
base64 oui /usr/bin/base64
python3 oui 3.11 complet
git oui via subprocess Python
sqlite3 CLI non utiliser module Python import sqlite3
docker CLI non pas de socket monté — Portainer API
paramiko non pas de SSH natif Python

RÈGLE 1 — Sessions : poisoning

Après 3+ erreurs consécutives, la session refuse même echo hello.
Symptôme : {"error": "Error occurred during tool execution"} sur commande triviale.

Sessions mortes dans cette session : claude, db_update, verify.
Sessions stables : model_test, emergency, s1 (utilisées fraîchement).

Toujours spécifier session= dans run_command (sans = erreur systématique).
Après 2 erreurs d'affilée : changer de session immédiatement.
Si toutes poisonnées : new_session("fresh1").


RÈGLE 2 — Heredoc interdit

<< 'EOF' dans toute forme cause un timeout 120s qui tue la session.
Cela inclut cat > f << 'EOF', python3 << 'PYEOF', bash << 'EOF'.

Cause : l'injection tmux ne gère pas l'attente stdin que heredoc déclenche.


RÈGLE 3 — Limite 350 chars par commande

Au-delà de ~350 chars dans le champ command : timeout, troncature ou erreur silencieuse.

Méthode 1 — Code court (< 200 chars)

Deux appels séparés :

printf 'import json\nprint("ok")\n' > /tmp/s.py
python3 /tmp/s.py

Méthode 2 — Code moyen (200350 chars)

Appender en plusieurs appels :

printf 'import sqlite3\nconn=sqlite3.connect("/mnt/docker/bifrost/data/config.db")\n' > /tmp/s.py
printf 'cur=conn.cursor()\ncur.execute("SELECT * FROM config_keys")\n' >> /tmp/s.py
printf 'for r in cur.fetchall(): print(r)\n' >> /tmp/s.py
python3 /tmp/s.py

Méthode 3 — Code long ou avec quotes simples (> 350 chars)

Base64 encode côté Claude (bash_tool), decode sur NAS (2 run_command) :

# bash_tool : base64 -w0 fichier.py  →  obtenir la chaîne B64
# run_command 1 : printf '<B64>' | base64 -d > /tmp/s.py
# run_command 2 : python3 /tmp/s.py

Piège : une quote simple ' dans printf termine la chaîne → code tronqué sans erreur.
Dès qu'il y a des ' dans le code Python → base64 obligatoire.


RÈGLE 4 — Services internes (172.17.0.1)

Portainer — restart container

import urllib.request, json
# Auth
body = json.dumps({"username":"bestof","password":"<depuis .env>"}).encode()
req = urllib.request.Request("http://172.17.0.1:9000/api/auth", data=body,
      headers={"Content-Type":"application/json"})
jwt = json.loads(urllib.request.urlopen(req).read())["jwt"]
h = {"Authorization": "Bearer " + jwt}
# ID dynamique — change après recreate
req = urllib.request.Request(
    "http://172.17.0.1:9000/api/endpoints/2/docker/containers/json", headers=h)
cs = json.loads(urllib.request.urlopen(req).read())
cid = [c["Id"][:12] for c in cs if "bifrost" in str(c.get("Names","")).lower()
       and "proxy" not in str(c.get("Names","")).lower()][0]
# Stop + Start
for action in ["stop","start"]:
    req = urllib.request.Request(
        f"http://172.17.0.1:9000/api/endpoints/2/docker/containers/{cid}/{action}",
        data=b"", headers=h)
    urllib.request.urlopen(req)

n8n API — PUT workflow

# Uniquement ces 4 champs acceptés — autres -> 400
payload = {"name": wf["name"], "nodes": wf["nodes"],
           "connections": wf["connections"], "settings": wf.get("settings", {})}
# Header : X-N8N-API-KEY (pas Authorization Bearer)

Bifrost

Auth : header x-bf-vk: <VK>  — PAS Authorization Bearer (-> 401)
URL  : http://172.17.0.1:3085

Gitea push

import subprocess, os
env = open("/mnt/docker/hermes-platform/.env").read()
pwd = [l.split("=",1)[1].strip() for l in env.splitlines() if "PASSWORD_GITEA" in l][0]
os.chdir("/mnt/docker/nas-runbooks")
subprocess.run(["git","add","."])
subprocess.run(["git","commit","-m","message"])
subprocess.run(["git","push", f"http://bolbol:{pwd}@172.17.0.1:3232/bolbol/nas-runbooks.git"])

NyoraNotes

tok = open("/mnt/docker/nyora-notes/data/hermes-perso.token").read().strip()
note = {"title":"...","content":"...","tags":["infra","runbook","terminee"]}
body = json.dumps(note).encode()
req = urllib.request.Request("http://172.17.0.1:8787/notes", data=body,
      headers={"Authorization":"Bearer "+tok,"Content-Type":"application/json"})
urllib.request.urlopen(req)  # -> 201

SQLite Bifrost DB

import sqlite3, shutil
# Lecture : copier d'abord (WAL actif)
shutil.copy("/mnt/docker/bifrost/data/config.db", "/tmp/bifrost.db")
conn = sqlite3.connect("/tmp/bifrost.db")
# Écriture live (restart Bifrost après pour prise en compte)
conn = sqlite3.connect("/mnt/docker/bifrost/data/config.db")

Tableau de décision rapide

Besoin Faire
Code Python < 200 chars printf > /tmp + python3 (2 appels)
Code Python > 350 chars ou avec quotes base64 encode -> decode -> exec
Heredoc Jamais. Python open().write() en /tmp
Restart container Portainer API, ID dynamique
sqlite3 Python import sqlite3
Session instable list_sessions -> session fraîche
Gitea push subprocess git avec http://bolbol:pwd@

Améliorations suggérées linux-mcp-nas

  1. Monter docker.sock : -v /var/run/docker.sock:/var/run/docker.sock — Docker CLI direct, supprime Portainer API workaround
  2. Installer sqlite3 CLI : apt install -y sqlite3 dans l'image — inspection DB sans Python
  3. Augmenter timeout run_command : 120s trop court pour git sur gros repos
  4. Nommer sessions par rôle : git, db, api, test — rotation propre entre tâches
  5. Installer paramiko : pip install paramiko — SSH natif si besoin accès NAS direct