Files
nas-runbooks/common/reglement-definitif-fix-mcp.md

149 lines
10 KiB
Markdown
Raw Permalink 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.
# reglement-definitif-api — Fix WORKER TIMEOUT/OOM + serveur MCP + intégration hermes-tt
**Instance auteur** : hermes-nyora
**Date** : 2026-08-23
**Tags** : [infra, mcp, docker, flask, gunicorn, weasyprint, tunisie-telecom, gemini]
**Statut** : valide (durci le 23/08 après-midi — voir section Durcissement en bas)
---
## DURCISSEMENT SÉCURITÉ (23/08/2026 après-midi — exécution Gemini, vérification Hermes)
Deuxième passage AntiGravity-Gemini sur le même projet (commit 4586d5d@v0) :
- **Token MCP roté** : `dev-token-please-change` → token 43 chars dans `.env` (chmod 600), plus aucun fallback. Config hermes-tt mise à jour symétriquement. Vérifié : ancien token → 401, nouveau → 200.
- **Comptes** : admin générique désactivé (is_active=0, hash écrasé) ; mot de passe Nabil.Derouiche roté (l'ancien TT@2026! était EN CLAIR dans init_db poussé sur Gitea). Refactor init_db() : création du superadmin uniquement si table vide ET via env ADMIN_USER/ADMIN_INITIAL_PASSWORD. Pas de réécriture d'historique git (rotation rend les valeurs mortes).
- **Rate limiting /login** : 5 req/min/IP fenêtre glissante mémoire, 6e → 429 + Retry-After:60. Piège évité : le healthcheck Docker tapait sur /login toutes les 30s — basculé sur la nouvelle route `/health` (exemptée du limiteur) dans le MÊME déploiement, sinon boucle unhealthy.
- **Sessions** : 8h, HTTPONLY, Samesite Lax, SECURE=True (accès réel via reverse proxy HTTPS reglement-definitif.bolbol.tn — PAS HTTP direct comme supposé initialement).
- **ProxyFix + PREFERRED_URL_SCHEME=https** : le backend Flask voit du HTTP derrière le proxy DSM ; sans ça, redirects en http://.
- **MCP évolutions** : paramètre seuil borné [10-100] sur reglement_consommation + 9e outil reglement_recherche_ligne (recherche textuelle dans Detail_PO, testé réel : query "cable" → résultats avec prix DT formatés).
Leçon de cadrage : la question SESSION_COOKIE_SECURE posée par l'exécuteur (« HTTP direct ou reverse proxy ? ») a été mal répondue au premier GO (HTTP direct) puis corrigée par Nabil (reverse proxy HTTPS confirmé par test curl 200 sur le domaine). Toujours vérifier soi-même un fait d'infrastructure avant d'en faire une condition de GO.
---
## Probleme
L'application Flask « Règlement Définitif » (Tunisie Telecom, DCZS) tournait en boucle de crash depuis fin juillet 2026 :
```
[CRITICAL] WORKER TIMEOUT (pid:7)
[ERROR] Worker (pid:7) was sent SIGKILL! Perhaps out of memory?
```
Le master gunicorn démarrait, les 2 workers bootaient, puis timeout à ~30s (défaut gunicorn) → SIGKILL → reboot workers → re-timeout, en boucle. Dernier incident avant fix : 22/08/2026.
Cause racine confirmée : imports top-level lourds (`from weasyprint import HTML, CSS` dans `routes/reports.py` et `services/pdf_generator.py`) exécutés à chaque boot de worker via `create_app()`. Sur le Celeron J4125 du NAS DS920+, 2 workers concurrents chargeant WeasyPrint/CFFI + google-api-client dépassaient les 30s sans heartbeat → kill du master. Facteur aggravant RAM : WeasyPrint ~150-200 Mo/worker sur un NAS contraint.
Deuxième objectif du projet : exposer les données de l'app comme outils MCP pour hermes-agent-tt (l'app n'a aucune API REST, que du web UI derrière Flask-Login).
---
## Contexte et contraintes
- Conteneur d'origine créé SANS Dockerfile visible, SANS volume (code cuit dans l'image), SANS labels compose → rebuild impossible tel quel, données non persistées.
- `credentials.json` (Google Service Account `excel-sync-483316`) et `users.db` n'existaient QUE dans le conteneur → extraction obligatoire AVANT suppression (perte définitive sinon).
- Réseau Docker partagé : `n8n` (external), où vivent aussi `hermes-agent-tt` et le reste de la stack.
- Ports host : api `5099:5055`, MCP `5098:8000` (5098 libre vérifié au préalable).
- Le projet est piloté par un workflow AntiGravity-Gemini (exécution complète, accès NAS via `ssh nas`) avec Hermes-Nyora en vérification/cadrage uniquement.
---
## Ce qui NE fonctionne PAS
| Tentative | Erreur obtenue | Raison de l'échec |
|-----------|----------------|-------------------|
| Gunicorn défaut (2 workers sync, timeout 30s) | `[CRITICAL] WORKER TIMEOUT` + SIGKILL OOM en boucle | Imports WeasyPrint top-level > 30s au boot ×2 workers concurrents |
| Repo Gitea `reglement-definitif` (état antérieur au projet) | Runbook 04/07 affirmait « source + Dockerfile sur Gitea » | FAUX — vérifié par API Gitea : aucun repo existant. Le conteneur était orphelin de toute source |
| Appel `tools/list` MCP direct sans session | `{"error":{"code":-32600,"message":"Bad Request: Missing session ID"}}` | StreamableHTTP exige initialize d'abord puis header `mcp-session-id` sur chaque appel |
| Endpoint `/mcp` sans Bearer | HTTP 401 | Normal — auth active (testé et validé) |
---
## Solution validée
### Architecture retenue (décisions Gemini, validées Hermes)
1. **Fix app** (aucun changement métier) :
- Lazy import WeasyPrint (déplacé dans les fonctions de rendu) → boot < 0,5s
- `cachetools.TTLCache(maxsize=256, ttl=300)` remplace le dict global non borné
- Résilience credentials : classe `GoogleCredentialsError` + `check_credentials_status()` — l'app démarre même sans credentials.json (erreur claire sur les écrans Sheets, pas de crash)
- Gunicorn : `--workers 1 --timeout 120 --graceful-timeout 30`, PAS de `--preload` (WeasyPrint/CFFI + fork = risque deadlock)
- Dockerfile `python:3.11-slim` + libs système Pango/Cairo/GdkPixbuf + fonts-dejavu + healthcheck `python -c urllib` (wget absent des images slim)
2. **Serveur MCP séparé** (`reglement-mcp`) : FastMCP StreamableHTTP sur `/mcp`, port 5098:8000, auth Bearer `MCP_SECRET_TOKEN`. Importe directement `app/config.py` + `app/services/` (gsheets, cache) — ces fichiers n'importent PAS Flask (vérifié). Image slim sans WeasyPrint.
3. **8 outils MCP agrégés** : list_marches, get_marche, po_stats, detail_po, appels_commande, consommation (+alertes >90%), instance_validation, dashboard_summary. Format tunisien DT, pas de dump brut.
### Ordre d'exécution (respecté)
```bash
# 1. SAUVEGARDE avant toute suppression (critique)
docker cp reglement-definitif-api:/app/credentials.json /volume1/docker/reglement-definitif/credentials.json
docker cp reglement-definitif-api:/app/data/users.db /volume1/docker/reglement-definitif/data/users.db.bak
# 2. Déployer l'API (sur le NAS, piège PATH Synology)
ssh nas "export PATH=/usr/local/bin:\$PATH; cd /volume1/docker/reglement-definitif && docker compose build --no-cache && docker compose up -d"
ssh nas "export PATH=/usr/local/bin:\$PATH; docker update --restart unless-stopped reglement-definitif-api"
# 3. Déployer le MCP
ssh nas "export PATH=/usr/local/bin:\$PATH; cd /volume1/docker/reglement-definitif/reglement-mcp && docker compose build --no-cache && docker compose up -d"
# 4. Config hermes-tt (/opt/data/config.yaml du conteneur hermes-agent-tt)
# mcp_servers.reglement-definitif: url http://reglement-mcp:8000/mcp + Bearer token
```
---
## Verification
Vérifié DEUX FOIS : preuves Gemini + tests indépendants Hermes-Nyora depuis le réseau n8n (23/08/2026).
```bash
# Conteneurs healthy
docker ps --filter name=reglement
# reglement-mpc / reglement-definitif-api : Up (healthy) tous les deux
# API web
curl -I http://127.0.0.1:5099/login # → HTTP 200
# MCP health
curl -s http://reglement-mcp:8000/health # → {"status":"healthy",...,"endpoint":"/mcp"}
# Auth active
curl -o /dev/null -w "%{http_code}" http://reglement-mcp:8000/mcp # sans token → 401
# Flux MCP complet (StreamableHTTP = initialize d'abord pour obtenir la session)
curl -D - -X POST http://reglement-mcp:8000/mcp \
-H "Authorization: Bearer $MCP_SECRET_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
# → récupérer le header mcp-session-id, puis tools/list AVEC ce header → 8 tools
# → tools/call reglement_list_marches → {"status":"success","total_marches":8,"nb_fournisseurs":4}
# Découverte côté hermes-tt (preuve Gemini)
docker exec hermes-agent-tt hermes mcp test reglement-definitif
# → Connected (281ms), Tools discovered: 8
```
Dépôt code : `bolbol/reglement-definitif` branche `v0` (commits efbe03b, 80ed7bc). Repo privé — invisible en API anonyme (vérifié : 404 anonyme = normal pour un repo privé).
---
## Pieges specifiques DSM / NAS
1. **Code cuit sans volume** : l'ancien déploiement n'avait NI volume NI Dockerfile. Toute app historique dans cet état est une bombe : credentials et SQLite perdus au premier recreate. Toujours extraire AVANT (`docker cp`).
2. **StreamableHTTP ≠ SSE simple** : chaque appel après initialize exige le header `mcp-session-id` retourné par initialize. Un curl nu renvoie `-32600 Missing session ID`.
3. **Piège PATH SSH Synology** : `export PATH=/usr/local/bin:$PATH` obligatoire dans chaque commande ssh nas non-interactive, sinon `docker: command not found`.
4. **Repo privé Gitea** : une recherche API anonyme ne voit PAS les repos privés — « not found » ne prouve pas l'inexistence. Vérifier authentifié avant de conclure (erreur commise pendant la vérification, corrigée).
5. **Healthcheck slim** : wget/curl absents de python:3.11-slim → healthcheck en `python -c urllib.request`.
6. **Credentials manquants ≠ crash** : l'app et le MCP démarrent verts sans credentials.json ; seuls les outils Sheets renvoient l'erreur explicite. Ne pas croire un statut healthy = données OK.
---
## References
- Repo code : `bolbol/reglement-definitif@v0` (Gitea)
- Prompt mission + recadrages : `/opt/data/workspace/prompts/antigravity-reglement-definitif.md`, `GO-reglement-definitif.md`, `RECADRAGE-GO-execution-gemini.md`
- Suivi projet : `/opt/data/workspace/prompts/SUIVI-reglement-definitif.md`
- Code extrait du conteneur d'origine : `/opt/data/workspace/apis/reglement-definitif/`
- Ports registry : à mettre à jour (ligne reglement-mcp 5098)