149 lines
10 KiB
Markdown
149 lines
10 KiB
Markdown
# 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) |