nas-runbooks/common/mcp-nas-run-command-v2.md

5.0 KiB
Raw Blame History

MCP NAS — run_command v2 (P0) : heredocs, commandes longues, reset_session

Date : 2026-07-04 | Commit 30560984fa sur bolbol/linux-mcp-nas@main | Testé et validé le jour même.

Ce runbook remplace les RÈGLES 13 de MCP-NAS-SCRIPTING.md. Le chunking base64, la limite ~350 chars et l'interdiction de heredoc n'existent plus. La section « Services internes (172.17.0.1) » de l'ancien runbook reste valide.


Ce qui a changé

Avant, run_command envoyait la commande brute à tmux via send-keys : le parsing shell cassait sur les quotes, les heredocs déclenchaient un timeout tueur de session, et au-delà de ~350 caractères la commande était tronquée — d'où le workaround base64 en 2 appels.

Depuis le patch P0, le serveur écrit la commande dans un fichier script depuis Python (open(f"/tmp/mcp_{uid}.sh","w").write(command) — zéro parsing shell) et n'envoie au pane que :

. /tmp/mcp_XXX.sh > /tmp/mcp_XXX.out 2>&1; echo $? > /tmp/mcp_XXX.rc

via send-keys -l (mode littéral). Conséquences :

Avant (v1) Maintenant (v2)
Heredoc → timeout 120 s, session morte Heredocs natifs, y compris <<'EOF' avec quotes/$VAR/backticks
Limite ~350 chars, base64 obligatoire Testé 5 250 chars en un appel, intégrité parfaite
Quote simple ' → troncature silencieuse Quotes arbitraires OK (aucun parsing shell)
Prompt de continuation > empoisonnait la session Mécaniquement impossible
Empilement sur pane occupé (vim/ssh/pager) Busy-check : refus propre avec message d'options
Sessions poisonnées → new_session en chaîne reset_session : reprise sans destruction

Nouveaux comportements

  • Busy-check : si le pane exécute déjà quelque chose, run_command renvoie [SESSION OCCUPÉE] 'x' exécute actuellement 'sleep'… au lieu d'empiler. Options proposées : capture_pane, send_keys, reset_session, ou une autre session.
  • Timeout intelligent : à expiration, le serveur tente un Ctrl-C puis sonde le shell. S'il répond → session préservée (cwd/env conservés). Sinon → session recréée dans le même cwd.
  • reset_session(session) : envoie Ctrl-C, q (pager), Ctrl-U, vérifie que le shell répond. Reprend la main sur une session bloquée SANS la détruire. Pour vim : send_keys ':q!' d'abord.
  • Cleanup au démarrage : sessions tmux orphelines et fichiers /tmp/mcp_* purgés au boot du container (les 24 sessions zombies accumulées ont disparu au premier rebuild).

Tests de validation (2026-07-04)

  1. Heredoc avec quotes simples/doubles, $VAR, backticks, $(subshell), !#&*(){}[]|;<> → contenu intact, zéro expansion, exit code correct.
  2. Commande de 5 250 caractères (heredoc 50 lignes × 100 chars + vérifications) → 5 050 octets écrits, 50/50 lignes conformes au motif.
  3. Busy-check + reset_session : sleep 300 lancé via send_keys → run_command refusé proprement → reset_session → « shell réactif, cwd/env conservés (cwd: /tmp) ».

Bonnes pratiques v2

  • Écrire les scripts Python directement en heredoc : cat <<'EOF' > /tmp/s.py … EOF puis python3 /tmp/s.py.
  • Ne plus jamais utiliser le workaround base64 (2 appels) ni le découpage printf-append.
  • Session par défaut claude fiable ; reset_session avant de créer une session neuve.
  • Le rebuild du container se fait depuis le Mac (ssh nas), jamais depuis mcp-nas lui-même :
    ssh -p 22222 Best0f@192.168.100.33
    cd /volume1/docker/linux-mcp-nas
    sudo docker compose build && sudo docker compose up -d --force-recreate
    curl -s http://127.0.0.1:3042/health
    

P1 — denylist ancrée + nas.internal (2026-07-04, commit 7fb13c5, déployé et testé)

  • Denylist ancrée : les patterns ne sont plus cherchés n'importe où dans la chaîne brute. La commande est découpée en segments (; & | retours ligne), les préfixes neutres (sudo, env, VAR=…, chemin /sbin/…) sont retirés, et les patterns sont ancrés sur le début de chaque commande. Les corps de heredoc sont exclus de l'analyse.
    • Fini les faux positifs : grep reboot /var/log, echo "ne pas faire reboot", un heredoc de doc contenant « mkfs » → acceptés.
    • Toujours refusés : reboot, /sbin/reboot, echo ok && reboot, VAR=1 sudo shutdown, rm -rf /volume*|/opt|/mnt|/, mkfs.*, dd of=/dev/*, fork bomb, > /dev/sd*, docker rm -f, fdisk, parted, syno*, chmod -R 777 /. (30/30 tests unitaires.)
    • Changement assumé : rm -rf /tmp/x est désormais autorisé (avant, tout chemin absolu était bloqué) ; les racines critiques restent protégées.
  • nas.internal : extra_hosts: nas.internal:host-gateway dans le compose → depuis toute session, nas.internal = l'hôte NAS (172.17.0.1). Utiliser http://nas.internal:3232 (Gitea), :9000 (Portainer), :8787 (NyoraNotes)… au lieu de mémoriser 172.17.0.1.