85 lines
5.0 KiB
Markdown
85 lines
5.0 KiB
Markdown
# 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 1–3 de [MCP-NAS-SCRIPTING.md](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.
|