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

190 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](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.33`**INACCESSIBLE 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**
```python
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**
```python
# 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**
```python
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**
```python
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**
```python
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