Files
nas-runbooks/common/PROTOCOL-INFRA.md
T

40 KiB

PROTOCOL-INFRA — Regles operationnelles infra

Protocole post-deployement (OBLIGATOIRE)

Apres tout deployement reussi ou probleme resolu, avant de cloturer la mission :

  1. Creer un runbook dans le repo Gitea bolbol/nas-runbooks

    • Ecrire UNIQUEMENT dans le dossier hermes-[identite]/
    • Utiliser le template /opt/data/RUNBOOK-TEMPLATE.md
    • La section "Ce qui NE fonctionne PAS" est OBLIGATOIRE
  2. Poster une note NyoraNotes

    • Tags : [infra, runbook]
    • Contenu : resume du probleme, solution, lien runbook Gitea
  3. Mettre a jour _INDEX.md dans nas-runbooks

    • Ajouter l entree dans le tableau de l instance
  4. git add . && git commit && git push vers Gitea

Regle de contribution

  • Ecriture : chaque instance ecrit UNIQUEMENT dans son dossier hermes-[nom]/
  • Lecture : tous les dossiers sont accessibles en lecture
  • Le runbook sans les echecs n a pas de valeur — toujours documenter les tentatives infructueuses

Acces Gitea nas-runbooks

URL clone : http://192.168.100.33:3232/bolbol/nas-runbooks.git Dossier local suggere : /opt/data/workspace/nas-runbooks/


Protocole de suivi de projet — checklist des operations validees

Pour tout projet a plusieurs etapes validees en discussion (surtout sur plusieurs sessions ou avec un budget forfait limite) : la liste ordonnee des operations validees devient une checklist markdown dans une NyoraNote dediee, plutot que d'etre reconstituee de memoire ou re-diagnostiquee a chaque session.

  • Creer la note des qu'un plan a plusieurs etapes est valide, taguee checklist-projet (+ tags du projet)
  • Cocher au fur et a mesure de l'execution reelle (pas de la simple discussion)
  • Ajouter une ligne des qu'une nouvelle etape est validee par Nabil en conversation -- jamais de reformulation retroactive de ce qui a ete decide
  • En debut de session sur un projet deja entame : chercher la note existante (tag checklist-projet) avant de re-diagnostiquer un etat deja connu

Reference complete de la tactique (pourquoi, comment) : NyoraNote "TACTIQUE -- Checklist des operations validees (suivi de projet)", id 75fd1181-3061-480d-ae60-c069ba942ebc.


REGISTRE DES SERVICES — Mise a jour obligatoire

Le fichier /volume1/docker/ports-registry.md est la source de verite de l'etat du NAS. Il contient deux sections distinctes a maintenir en permanence :

1. SERVICES ACTIFS (en tete du fichier)

Mise a jour OBLIGATOIRE a chaque installation OU desinstallation :

  • Installation : ajouter la ligne avec statut ACTIF, port, domaine, notes
  • Desinstallation : changer le statut en "DESINSTALLE JJ/MM/AAAA", liberer le port
  • Ne JAMAIS supprimer une ligne — les services desinstalles restent visibles avec leur date

2. Historique des modifications (fin du fichier)

Ajouter une ligne datee pour CHAQUE action :

  • Format : | AAAA-MM-JJ | ACTION | Port | Service |
  • Actions a documenter : Deploiement, DESINSTALLATION, Migration, Changement de port

Verification avant documentation

Avant de marquer un service comme DESINSTALLE, TOUJOURS verifier : curl --max-time 4 http://172.17.0.1: -o /dev/null -w "HTTP %{http_code}" Si le port repond → le service est encore actif → NE PAS marquer DESINSTALLE

Chemin du fichier

  • Copie locale NAS : /volume1/docker/ports-registry.md
  • Copie nas-runbooks : /volume1/docker/nas-runbooks/common/ports-registry.md
  • Les deux doivent rester synchronisees (copier l'un vers l'autre apres modification)
  • Push Gitea obligatoire apres toute modification

Skills transverses — deploiement automatique sur les 3 instances

Quand une lecon apprise doit etre appliquee automatiquement par l agent (pas juste documentee pour lecture humaine), la deployer comme skill actif plutot que de compter sur un rappel manuel a chaque fois :

  1. Creer data/skills//SKILL.md sur les 3 instances (hermes-tt, hermes-nyora, hermes-perso), avec un frontmatter description contenant les mots-cles qui doivent declencher le skill
  2. Supprimer le cache .skills_prompt_snapshot.json (racine data/ et data/.hermes/) sur les 3 instances pour forcer la regeneration de l index
  3. Redemarrer les 3 containers hermes-agent-* (via socket Docker /var/run/docker.sock depuis mcp-nas, ou Portainer)
  4. Documenter quand meme via le protocole standard (runbook common/, _INDEX.md, note NyoraNotes) — le skill rend la regle automatique, le runbook garde la tracabilite

Exemples deployes le 02/07/2026 : desactivation des skills concurrents a nyora-doc-api (docx/pdf/pptx/xlsx), et skill generation-contenu-volumineux (chunking obligatoire pour eviter la troncature DeepSeek V4 Flash sur les taches lourdes).


Verification obligatoire avant toute lecture/ecriture sur nas-runbooks depuis mcp-nas

Git est ABSENT du container mcp-nas (confirme le 02/07/2026). Toute ecriture sur bolbol/nas-runbooks depuis une session mcp-nas passe par l API Gitea (curl + token, PUT/POST /api/v1/repos/bolbol/nas-runbooks/contents/, avec le sha courant recupere par un GET prealable) — jamais par un dossier local suppose a jour.

Piege decouvert le 02/07/2026 : /mnt/docker/nas-runbooks etait un clone git reel mais sur branche locale master, alors que le remote n a qu une branche main. 14 commits locaux (15-23 juin, jamais pousses) contenaient du contenu reel absent du remote. Cause probable : le binaire git a disparu de mcp-nas avant l etape push. Fix applique : recuperation des 14 fichiers via l API vers main, _INDEX.md corrige, clone local renomme en nas-runbooks.STALE-clone-local-jamais-pousse-20260702 (conserve pour inspection, plus utilise comme source).

Regle : avant de lire ou modifier un fichier sous /mnt/docker/nas-runbooks (ou tout repo Gitea) depuis mcp-nas, verifier d abord via l API que le contenu local correspond au remote reel : GET /api/v1/repos/bolbol//branches GET /api/v1/repos/bolbol//contents/ Si un vrai clone git fonctionnel est necessaire (multi-fichiers, historique), le faire depuis le Mac ou git est present, chemin partage /Volumes/docker == /volume1/docker.

gstack (garrytan/gstack) — decision actee le 02/07/2026

Installation prevue sur le futur MacBook M5 Pro via Claude Code natif, PAS sur hermes-perso (mem_limit container 600m incompatible Chromium/Playwright, routing Sonnet/Opus incompatible regle DeepSeek exclusif Hermes, bug upstream #2015 AskUserQuestion/clarify non resolu sur --host hermes). Detail complet : common/GSTACK-EVALUATION-MACBOOK-VS-HERMES.md.

Piege .env vs docker-compose.yml — decouvert le 03/07/2026

Ajouter une variable au .env d un service NE SUFFIT PAS si le docker-compose.yml declare un bloc environment: qui whiteliste explicitement chaque variable transmise au container (cas d hermes-platform/docker-compose.yml). Une variable absente de ce bloc n atteint jamais le container meme si elle est correcte dans le .env.

Regle : pour toute nouvelle variable d integration (cle API, URL de service), toujours verifier ET modifier les deux fichiers :

  1. .env (definition de la valeur)
  2. docker-compose.yml, bloc environment: du/des service(s) concerne(s) (transmission de la valeur au container)

Verification post-fix : docker exec env | grep sur le container recree.

Cas reel : NYORA_DOC_API_KEY manquante des deux fichiers pour les 3 instances Hermes -> 401 sur nyora-doc-api. Detail complet : common/nyora-doc-api-hermes-guide.md, section "Panne corrigee le 03/07/2026".

RÈGLE CRITIQUE — Cron Hermes et publish n8n : une modif écrite sur disque n'est pas une modif live

Cron Hermes (jobs.json)

Le service cron de chaque agent Hermes (hermes-agent-tt / hermes-agent-nyora / hermes-agent-perso) charge jobs.json UNIQUEMENT au démarrage du container. Il ne relit jamais le fichier en cours de route. Modifier jobs.json (horaire, prompt, modèle) laisse le service tourner avec l'ancienne config en mémoire tant que le container n'est pas redémarré.

  • Après toute modification de jobs.json : redémarrer le container agent correspondant (nom réel : hermes-agent-tt / hermes-agent-nyora / hermes-agent-perso — PAS hermes-tt/hermes-nyora/hermes-perso, ce sont les containers hermes-workspace-*).
  • Restart via API Portainer : POST /api/endpoints/2/docker/containers/{id}/restart timeout systématiquement sur cette instance. Utiliser stop puis start explicites (2 appels), pas restart.
  • Après redémarrage, TOUJOURS vérifier next_run_at dans jobs.json correspond bien au nouveau schedule — sinon le corriger manuellement (le daemon ne recalcule pas toujours correctement au premier boot).
  • Portainer : endpoint local = id 2. Auth via 172.17.0.1:9000/api/auth (pas 192.168.100.33 depuis un container), credentials dans hermes-platform/.env.

Publish n8n (workflow actif modifié via l'outil update_workflow)

update_workflow sauvegarde un brouillon (nouveau versionId) mais n'active PAS automatiquement ce brouillon sur un workflow déjà publié — le workflow continue de servir l'ancienne activeVersion. publish_workflow peut échouer silencieusement avec "Version not found" sur un workflow dont la chaîne de versions est corrompue, y compris en réessayant depuis l'UI n8n elle-même (pas un bug de l'outil MCP).

  • Après toute modification d'un workflow n8n déjà actif : vérifier avec get_workflow_details que versionId == activeVersionId. Si différent, le changement n'est PAS en production.
  • Si publish_workflow échoue de façon persistante (même après unpublish_workflow puis republish) : ne pas insister, dupliquer le workflow (bouton Duplicate dans l'UI n8n) — la copie repart avec une chaîne de versions saine et se publie normalement. Archiver l'original cassé (archive_workflow) une fois la copie confirmée active.

Fuseau horaire

Le NAS et n8n tournent en UTC. Tunis = UTC+1 toute l'année (pas de changement d'heure). Pour un cron en heure de Tunis, toujours retirer 1h à l'expression cron (ex: 7h/12h/20h Tunis = "0 6,11,19 * * *" en UTC).

Leçon générale

Une réponse "success" d'un outil de modification (update_workflow, edit de jobs.json) ne garantit jamais que le changement est en production. Toujours vérifier l'état réel après coup : next_run_at recalculé pour un cron Hermes, activeVersion == draft pour un workflow n8n.

Git depuis mcp-nas — canal actuel (2026-07-10)

git n'est plus présent dans le container mcp-nas ni sur le NAS. Le canal standard pour toute écriture Gitea est l'API HTTP change-files :

  • Auth : GITEA_DEPLOY_TOKEN (scope write:repository, .env hermes-platform), header Authorization: token <TOK>. L'ancien password bolbol est périmé (401) — le runbook git-push-depuis-mcp-nas.md est obsolète sur ce point.
  • Endpoint : POST /api/v1/repos/bolbol/<repo>/contents avec files:[{operation:create|update, path, content(base64), [sha]}] → 1 commit multi-fichiers (Gitea 1.21.11).
  • Réseau : toujours 172.17.0.1:3232 depuis le container. Fichiers NAS lisibles/éditables via le mount /mnt/docker = /volume1/docker.
  • SSH Best0f par mot de passe : non fonctionnel depuis mcp-nas (pas de clé, SSHPASS ≠ mdp Best0f) — ne pas compter dessus.
  • Restart de containers sans docker CLI : API Portainer (creds bestof dans .env), POST /endpoints/{id}/docker/containers/{id}/restart.

2026-07-20 — Partition système /dev/md0 pleine (95%, 113 Mo restants) — résolu

Symptôme : df -h / sur l'hôte NAS à 95%. Piège : depuis le container mcp-nas, df -h / pointe sur /dev/mapper/cachedev_0 (volume data, 11 To) — toujours ssh nas-host pour voir la vraie partition système /dev/md0 (~2,3 Go, taille fixe DSM).

Méthode : du -sx / sans sudo (Best0f) sous-estime toujours l'usage réel — les dossiers root en permissions restrictives (souvent 0700) sont exclus silencieusement du total, pas juste illisibles en contenu. Comparer du -sx / 2>/dev/null au "Used" de df -h / ; un écart de plusieurs centaines de Mo pointe vers un dossier root oublié. Lister les coupables : du -sx / 2>&1 1>/dev/null | grep denied.

Cause ici : /usr/lib/python3.8/site-packages (0700 root:root) contenait une stack data-science/Streamlit complète (pandas, numpy, pyarrow, pillow, altair, jupyter) installée à la main début 2026 dans le Python système de base — pas un paquet Package Center, aucun process actif, aucun lien avec le stack actuel. Prototype abandonné.

Fix : sudo rm -rf /usr/lib/python3.8/site-packages/* /usr/etc/jupyter /usr/share/jupyter → 2,1 Go → 1,7 Go utilisés, 113 Mo → 529 Mo disponibles (95% → 77%). Diagnostic fait en lecture seule par Best0f (sans sudo), suppression exécutée par Nabil (le mot de passe sudo Best0f n'est jamais transmis à un agent). Runbook détaillé : common/partition-systeme-nas-du-vs-df-permissions.md.

REGLE CRITIQUE -- Anti-invention sur chiffres de recherche/benchmarks (2026-07-27)

Avant de presenter un chiffre (benchmark, prix, score) dans un rapport : le chiffre doit venir d'une page reellement lue en entier, pas d'un snippet de recherche. Si un fetch demande ne renvoie qu'une coquille vide (page JS, < 500 caracteres utiles) -> le dire explicitement, ne jamais compenser en silence par une recherche generique. Verifier le nom EXACT de la variante (Pro/Max/Flash/base) avant d'attribuer un chiffre -- deux benchmarks avec un score identique = signal d'hallucination a corriger avant envoi, jamais a ignorer. Detail complet : common/anti-hallucination-benchmarks-recherche-chiffree.md.

FIX -- auxiliary.vision mal route sur modeles text-only (2026-07-27)

auxiliary.vision.provider: auto resout vers le modele principal -- casse systematiquement sur deepseek-v4-flash (text-only). Toute instance avec un modele principal text-only DOIT router auxiliary.vision explicitement vers google/gemini-2.5-flash via bifrost-proxy (meme base_url/api_key que le modele principal). Applique sur hermes-perso et hermes-tt le 27/07/2026. Detail : common/vision-auxiliaire-routing-text-only-modeles.md.

FIX -- approvals.mode: smart + deny rules sur les 4 instances Hermes (2026-07-27)

Passage de manual/auto (valeur invalide sur tt) a smart sur hermes-perso, hermes-tt, hermes-nyora, hermes-nabil. cron_mode: deny inchange (un job planifie qui tombe sur une commande signalee reste bloque, jamais auto-approuve). Ajout de approvals.deny (patterns absolus, bloques avant meme le jugement du modele auxiliaire ou un /yolo) -- rm -rf /, dd vers /dev/, redirection vers .env/credentials/secrets, git push --force, docker volume rm/system prune, DROP TABLE/DATABASE ; + TRUNCATE et DELETE Baserow en plus sur hermes-tt. auxiliary.approval route explicitement vers deepseek-v4-flash (jamais mimo-v2.5, meme quand c'est le modele principal de l'instance) -- le juge de securite doit etre le modele le plus fiable des deux, pas le defaut. Exception : hermes-nabil (VPS) n'a aucune route deepseek disponible, laisse en auto (mimo-v2.5), ecart documente. Detail : common/smart-approvals-deny-rules-4-instances.md.

FIX -- root cause des tool_calls casses sur hermes-nyora : prompt du job veille, pas mimo-v2.5 (2026-07-27)

Les 24 incidents "Unrepairable tool_call arguments" venaient du prompt du cron veille-ia-nexum-4ee61494 qui ordonnait d'ecrire jusqu'a 20 analyses (champs sans limite de longueur) en UN SEUL heredoc -- exact anti-pattern du skill generation-contenu-volumineux, qui perd face a une instruction de prompt precise et contraire. Ce n'etait pas un probleme de fiabilite du modele. Fix : prompt reecrit pour une construction chunkee (1 fichier /tmp/veille_item_NN.json par opportunite, assemblage par script Python, jamais un nouvel appel LLM pour la fusion). Reflexe a generaliser : avant de blamer un modele sur des echecs de tool_call recurrents sur une tache precise, verifier D'ABORD si le prompt de cette tache contredit la regle de generation chunkee. Detail : common/veille-chunking-nyora-root-cause.md.

FIX -- watchdog telegram gateway autonome, sans acces DSM Task Scheduler (2026-07-27)

Incident hermes-perso (gateway Telegram bloque 2h17 en silence). Best0f n'a pas de sudo general (scope docker/rsync/journalctl), pas d'acces DSM Task Scheduler, pas de crontab utilisateur disponible -- ne jamais chercher a contourner ca en devinant un mot de passe ou en modifiant sudoers sans geste explicite de Nabil lui-meme. Solution retenue : container autonome hermes-watchdog-telegram (image docker:cli + python3, socket Docker monte, --restart unless-stopped) qui verifie toutes les 10 min les 3 instances NAS pour le motif exact de l'incident, redemarre automatiquement si detecte, et n'alerte (webhook veille-infra) que si un redemarrage automatique n'a pas suffi. Pattern reutilisable : toute tache planifiee cote NAS realisable via des commandes docker peut passer par un petit container autonome plutot que par le Planificateur DSM. Detail : common/watchdog-telegram-gateway-container-autonome.md.

FIX -- nyora-notes-tt : disk I/O error post-backup, moteur SQLite mixte sur WAL live (2026-08-02)

Tous les tools MCP RAG mail cassaient juste apres chaque backup n8n (toutes les 2h) : sqlean.dbapi2.OperationalError: disk I/O error des l'ouverture de connexion. Pas de piste materielle (dmesg propre, volume a 70%). Cause : la route /backup ouvrait une connexion avec le module sqlite3 stdlib, un moteur different de sqlean (utilise partout ailleurs dans l'app sur la meme DB WAL). Le checkpoint automatique au close() de cette connexion stdlib recree le -wal/-shm pendant que les connexions sqlean deja ouvertes tiennent encore les anciens fd -> index WAL desynchronise entre les deux moteurs -> disk I/O error. Absence de PRAGMA busy_timeout aggravait (echec dur au lieu d'attente). Reflexe a generaliser pour tout futur service Hermes/nyora-notes-* sur sqlean+WAL : jamais un deuxieme moteur/binding SQLite (stdlib, autre) sur une DB WAL live partagee avec un process long-vivant, meme pour une simple sauvegarde -- un seul moteur par DB, et PRAGMA busy_timeout systematique a l'ouverture de toute connexion. Detail : hermes-tt/nyora-notes-tt-backup-wal-moteur-mixte.md.

FIX -- cron n8n orphelin, boucle infinie sur statuts finaux (error_409/404), cause reelle instabilite hermes-mail-browser (2026-08-02)

Workflow n8n 'nyora-notes-tt -- Ingestion continue' actif depuis le 31/07, toutes les 10 min (292 executions), en boucle sur les memes dossiers error_409/error_404 -- statuts FINAUX (jamais retraitables avec succes), donc sa condition d'arret 'automatique quand tout est done' ne se declenchait jamais. neverError:true sur le noeud HTTP masquait le probleme (292/292 executions n8n 'success' malgre des 409 systematiques). Consequence : contention reguliere sur la page Playwright partagee de hermes-mail-browser, prise a tort pour de l'instabilite structurelle OWA. Desactive (unpublish_workflow). Verifie ensuite : hermes-mail-browser sain (RAM/CPU), timings /search et /message/attachments conformes aux baselines deja documentees une fois le bruit de fond supprime. Reflexe a generaliser : toute boucle n8n a condition d'arret basee sur un etat DB doit distinguer statut final vs statut retry-able, sinon elle ne se videra jamais ; et neverError:true cache ce genre de boucle inutile au monitoring standard n8n -- verifier le contenu des reponses, pas juste le statut d'execution. Avant de blamer un service externe pour de la lenteur persistante, chercher d'abord un consommateur interne recurrent via docker logs filtre par IP source. Detail : hermes-tt/n8n-cron-orphelin-hermes-mail-browser-instabilite.md.

REGLE -- id DOM d'un SPA (OWA/React) : jamais presume stable, tester sur plusieurs rendus + plusieurs echantillons (2026-08-08)

Un attribut id=/data-* scrape sur une ligne d'un SPA n'est PAS presume stable dans le temps. Cas nyora-notes-tt : elem_id (attribut id de ligne OWA), exploite comme message_id natif, s'est revele VOLATILE -- OWA/React regenere un GUID frais a chaque rendu (test rigoureux : 0/19 messages stables entre deux appels /search identiques, ensembles d'id disjoints, meme index -> id different ; l'ordre de liste est stable mais pas l'id, aria-posinset non peuple). Un premier test a 1 seul message toujours en position 0 sur appels rapproches l'avait faussement valide (le DOM n'etait pas re-rendu entre les appels -> meme id volatile reutilise). Reflexe : pour juger la stabilite d'un id DOM, faire >= 2 rendus REELS distincts (appels espaces OU requetes forcant un re-render), sur PLUSIEURS elements, apparier par une identite externe stable (ex. texte de la ligne) et verifier le RECOUVREMENT D'ENSEMBLES -- jamais un seul echantillon en position fixe. Seul data-convid (ConversationId Exchange, niveau THREAD) est stable cote ligne OWA ; aucun id natif par-message n'est expose par le scraping DOM. Detail : hermes-tt/nyora-notes-tt-message-id-elem-id-volatile-08-08-2026.md.

FIX -- /search hermes-mail-browser : count:0 transitoire sur appels rapproches (2026-08-08)

Deux appels /search a < ~10 s d'ecart : le 2e renvoie parfois count:0 (liste virtualisee OWA pas encore stabilisee cote Playwright). Fix cote client (hermes_mail_client.search_folder) : si items est vide de facon inattendue, retry UNIQUE apres time.sleep(4) avant d'abandonner (simple, pas de backoff). Piege de parsing associe : attrs (string pipe-delimitee) reinjecte aria-label en fin de chaine (texte libre non echappe, | et sauts de ligne possibles) -> NE JAMAIS split('|') global ; extraire par regex ciblee (data-convid=([^|]+), base64 sans |). Detail : meme runbook ci-dessus.

REGLE -- OWA : un glyphe d'icone (zone privee Unicode) ou un emoji casse tout matching de texte naif (2026-08-10)

Le texte rendu par OWA est fait pour l'oeil, pas pour servir de structure de donnees. OWA insere des glyphes d'icone issus de la ZONE PRIVEE Unicode (lu/non-lu, importance, piece jointe) directement dans le texte scrape -- et les libelles de dossiers portent des emoji. Ces caracteres ne sont PAS des espaces : .strip() ne les retire pas et ils survivent a un filtre if l.strip().

Le meme phenomene a maintenant casse TROIS surfaces differentes :

  1. Parsing d'une ligne de message (_parse_item, nyora-notes-tt) : un glyphe seul sur la ligne 0 de raw_text, present sur une partie seulement des messages, decale le decoupage positionnel d'un cran -- expediteur, objet ET date faux ENSEMBLE. Resultat mesure : 295/399 notes avec un received_date qui n'est pas une date (74%), dont 90 contenant un glyphe.
  2. Clic sur le volet dossiers (resolve_folder_treeitem, hermes-mail-browser) : inner_text() peut renvoyer vide ou reduit au seul glyphe d'icone -> aucun dossier ne matche.
  3. Resolution de chemin de dossier (deja documente le 30-31/07/2026).

PLAGE A RETENIR : la PUA COMPLETE U+E000-U+F8FF. La plage "Fluent UI" habituelle (U+E700-U+ECFF) est INSUFFISANTE -- U+E1B7 a ete observe en production hors de cette plage, a cote de U+E73E.

REFLEXES :

  • Lire des ATTRIBUTS STRUCTURES (data-folder-name, data-convid, aria-*) plutot que du texte rendu.
  • Nettoyer la plage PUA AVANT tout decoupage, jamais champ par champ en aval (c'est un decalage d'indice unique qui fausse tous les champs d'un coup).
  • Ne jamais lire une donnee par POSITION dans du texte rendu : chercher par MOTIF, et prevoir une sentinelle explicite si rien ne matche -- ne jamais retomber sur une ligne arbitraire. Detail : hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md.

REGLE -- une valeur par defaut cote serveur non transmise = troncature silencieuse (2026-08-10)

hermes_mail_client.search_folder() n'a jamais transmis limit a /search : il l'appliquait en tranche cliente ([:limit]) sur une reponse DEJA tronquee. hermes-mail-browser plafonne /search a 20 items par defaut, SANS erreur ni avertissement -- le dossier passe ensuite en statut done comme s'il avait ete traite entierement. Temoin 00 2026|Veille : 14 notes + 6 doublons = exactement 20 en base pour 37 messages reels.

REFLEXE : tout plafond cote serveur doit etre TRANSMIS EXPLICITEMENT, jamais laisse a sa valeur par defaut. Une troncature silencieuse est indetectable en aval -- corollaire : quand un compteur colle pile a une valeur ronde (20, 50, 100), suspecter un plafond avant de conclure que la source est vide. Detail : meme runbook ci-dessus.

REGLE -- verifier la PORTEE d'un resultat scope, pas seulement son code HTTP (2026-08-10)

GET /search?folder_path=X de hermes-mail-browser renvoie parfois le contenu de la BOITE DE RECEPTION au lieu du dossier demande -- en HTTP 200, sans aucune erreur. Deux chemins differents renvoyaient 77 items dont 63 communs, toutes les lignes portant le libelle "Boite de reception". Deterministe et lie au chemin (un dossier connu-bon reteste apres un connu-mauvais redonne le bon resultat) : ce n'est ni une derive d'etat ni de la contention. Cause probable : le segment est trouve dans l'arbre mais le clic ne change pas reellement de dossier, et la recherche s'execute alors dans la portee heritee -- go_to_folder_path ne leve une 404 que si un segment est INTROUVABLE, jamais si la navigation echoue silencieusement apres un clic accepte.

REFLEXE : un 200 ne prouve pas que la portee demandee a ete respectee. Quand une API navigue avant de lire, verifier dans la REPONSE une marque de la portee obtenue. Ici c'est gratuit : la derniere ligne de raw_text porte le libelle du dossier du message -- s'il ne correspond pas au dossier demande, la navigation a echoue. Corollaire : un audit de completude bati sur une source non verifiee produit des chiffres qui ont l'air precis et ne veulent rien dire. Detail : hermes-tt/nyora-notes-tt-parsing-owa-pua-10-08-2026.md (bug D).

REGLE -- relever un plafond de pagination change le profil temporel : verifier les timeouts en meme temps (2026-08-10)

Effet de bord constate juste apres le fix du plafond /search (20 -> 200) sur nyora-notes-tt. hermes-mail-browser accumule ses resultats par SCROLL CLAVIER PROGRESSIF dans une liste virtualisee : la duree de l'appel croit avec le nombre d'items demande. Les appels sont passes de ~30 s a 40-240 s selon le dossier, alors que search_folder gardait timeout=60 code en dur -- dimensionne pour l'ancien plafond. Resultat : les gros dossiers echouaient en timeout et le pipeline les aurait tous marques error. Corrige par SEARCH_TIMEOUT_S = 300.

REFLEXE : un plafond de pagination et un timeout sont couples. Relever l'un sans revoir l'autre transforme une troncature silencieuse en echec generalise -- on remplace un bug par un autre. Verifier aussi les timeouts en AVAL (retry, cron n8n, healthcheck) qui peuvent etre calibres sur l'ancienne duree.

REGLE -- UI React/Fluent : le succes d'un clic ne prouve PAS son effet (2026-08-10, soir)

Cause de fond du bug D ci-dessus, trouvee et corrigee : go_to_folder_path confondait DEPLIER et SELECTIONNER. Sur l'arborescence OWA, le chevron d'expansion et la ligne qui le porte sont deux cibles distinctes aux effets distincts. Le code ne cliquait la ligne (seul geste qui selectionne) qu'en repli, faute de bouton d'expansion : un dossier FEUILLE etait donc correctement selectionne, un dossier A ENFANTS ne l'etait jamais -- soit seul son chevron etait clique, soit, s'il etait deja aria-expanded="true" (etat persistant d'un appel a l'autre), aucun clic n'avait lieu. La portee d'une recherche OWA etant le dossier COURANT, la recherche s'executait dans la Boite de reception recharge par goto_owa_root(). 63 des 336 dossiers du compte etaient concernes.

REFLEXE : apres toute navigation dont depend la suite du traitement, confirmer l'etat atteint par un ATTRIBUT STRUCTURE (aria-selected), jamais par le fait que le clic n'a pas leve d'exception. Et en l'absence de confirmation, echouer explicitement : un 200 qui repond sur le mauvais perimetre coute infiniment plus cher qu'une erreur franche. Corollaire de diagnostic : quand un bug touche certains elements et pas d'autres, chercher d'abord ce qui les distingue STRUCTURELLEMENT (ici : avoir des enfants) avant d'incriminer le contenu (ici : les emojis, faussement suspectes pendant des jours). Detail : hermes-tt/mail-o365-bug-D-deplier-nest-pas-selectionner.md

REGLE -- un garde-fou qui "ne sait pas conclure" doit etre mesure, pas presume inoffensif (2026-08-10, soir)

Le controle de portee ajoute pour mitiger le bug D bloquait sur preuve positive et LAISSAIT PASSER les listes heterogenes, au motif qu'un faux positif serait pire. Les tests du correctif ont montre que cette branche "je ne sais pas" recouvrait en fait un SECOND mode de corruption (bug E) : quand un dossier ne contient aucun message DIRECT (ses messages sont dans ses sous-dossiers), OWA elargit de lui-meme la recherche a toute la boite aux lettres et renvoie un melange en HTTP 200. Mesure sur un dossier reel : 40 items, libelle majoritaire a 10/40, stable sur 3 appels. Les deux effets ont ete isoles en interrogeant /inbox (qui ne recherche pas) : reponse "liste introuvable" = dossier vide, donc navigation correcte et recherche fautive.

REFLEXE : avant de classer un cas comme non concluant, verifier empiriquement ce qu'il contient REELLEMENT en production. Ici l'uniformite s'est revelee etre la norme d'une portee correcte, meme sur les dossiers a enfants (27/27, 19/19) -- donc son absence etait une preuve suffisante pour bloquer, et la politique permissive choisie par prudence etait precisement ce qui laissait passer la corruption.

PIEGE -- batteries de tests Playwright : toujours sous flock, une seule serie a la fois (2026-08-10, soir)

Une premiere serie de tests a tourne en double : un job nohup chaine derriere un build avait survecu a sa session SSH. Les deux series tapant le meme navigateur, et le serveur SERIALISANT les appels (@serialize_owa), leurs resultats se sont entrelaces -- un test renvoyait la portee du test precedent, ce qui imitait parfaitement un echec du correctif en cours de validation.

REFLEXE : lancer ces batteries sous flock -n, verifier ps aux avant de conclure, et se mefier d'un resultat qui "pointe vers le dossier d'a cote" plutot que vers un dossier arbitraire.

REGLE -- un identifiant de deduplication ne doit jamais dependre d'un champ derive de l'instant de collecte (2026-08-11)

nyora-notes-tt deduplique sur un hash incluant received_date. Or normaliser_date_owa renvoie date.today() pour un message affiche en heure seule ("09:01") : pour tout message recent, la date stockee est une fonction du JOUR DE SCRAPE, pas du message. Le meme mail re-scrape deux jours de suite produisait donc deux identifiants -- 4 notes creees la ou il en fallait 0 lors du re-scan du lot pilote. Deux tentatives successives de fiabiliser le PARSING de la date avaient echoue : le defaut n'etait pas dans le parsing, il etait dans le choix du champ.

REFLEXE : avant d'inclure un champ dans une cle d'identite, se demander non pas "est-il correct ?" mais "vaudra-t-il la meme chose si je recollecte la meme donnee dans six mois ?". Tout champ dont la valeur depend de l'instant, du rendu ou du contexte de lecture est disqualifie, meme si son extraction est parfaite. Corollaire : une extraction par POSITION contamine les champs voisins -- ici l'objet etait derive de l'indice de la date, donc indirectement instable lui aussi. Extraire par MOTIF, jamais par indice, quand la structure n'est pas garantie.

REGLE -- quand aucune cle ne peut separer deux elements, compter plutot qu'identifier (2026-08-11)

Corollaire du precedent. Certains mails "gabarit" (accuses de reception, newsletters, relances d'un meme fil) sont STRICTEMENT indiscernables au niveau de la ligne de liste OWA : meme expediteur, meme objet, meme debut de corps -- seule la date les separe, et elle est inutilisable. Toute cle calculee sur ces champs fusionne donc des messages reels, ce qui est une PERTE SILENCIEUSE, bien pire qu'un doublon visible sur une archive. La solution n'a pas ete une meilleure cle, mais un changement de question : comparer le NOMBRE d'elements portant la cle dans la source (N) au nombre deja en base (M), et ingerer la difference.

REFLEXE : quand une identification exacte exige d'ouvrir la ressource complete (ici : ouvrir chaque mail), verifier d'abord si un comptage sur une cle approximative ne suffit pas. Ici cela a transforme un re-scan de 83 messages de ~50 min (une ouverture par message) en ~110 s, tout en supprimant le risque de perte. Une cle qui admet des collisions est acceptable DES LORS que la decision ne repose pas sur son unicite.

PIEGE -- la troncature de previsualisation d'OWA n'est pas stable, et un test sur donnees anciennes ne le voit pas (2026-08-11)

Mesure initiale sur un dossier de 35 messages, deux passes entrecoupees d'une navigation : texte identique a 100 %, conclusion "troncature stable par message". Faux. Le test ne portait que sur des messages ANCIENS. Sur un message du jour, les deux rendus du meme mail differaient par le seul marqueur de troncature : '... Extrait de commentaires Web :…' puis '... Extrait de commentaires Web :' Un caractere suffit a changer un hash, donc a creer un doublon.

REFLEXE : un test de stabilite temporelle doit inclure des donnees FRAICHES -- c'est sur elles que le rendu bouge, precisement parce que l'interface les affiche differemment selon leur age. Et se premunir en normalisant les marqueurs de troncature ET en bornant le texte hache en deca de la zone qui varie.

PIEGE -- un test dont l'echec produit le meme chiffre que le succes ne valide rien (2026-08-11)

Le script de validation du correctif de dedup verifiait processed_new == 0 apres re-scan. Un dossier tombe en erreur (navigateur sature, HTTP 500) renvoie lui aussi processed_new == 0, faute d'avoir rien lu : le premier rapport a donc affiche "[OK]" sur une panne complete du navigateur.

REFLEXE : verifier que le critere de succes est DISCRIMINANT vis-a-vis des modes d'echec connus. Ici il a fallu exiger status == 'done' en plus du compteur, et prevoir un verdict distinct "NON CONCLUANT" -- ni succes, ni anomalie -- pour ce qui n'a pas ete mesure.

REGLE -- Discipline "seam" pour toute nouvelle capacite Hermes (2026-08-17)

Avant d'ajouter une nouvelle capacite a une instance Hermes (nouvel outil, nouveau skill qui appelle un service externe, nouvelle integration), definir explicitement trois choses avant d'ecrire le premier fichier : (1) le service -- quelle fonction precise est rendue disponible et par quel contrat (entree/sortie) ; (2) le fournisseur -- quel composant l'implemente reellement (API externe, script local, autre instance Hermes) et ou vit sa configuration/ses credentials ; (3) le consommateur -- quelle(s) instance(s)/quel(s) skill(s) l'utilisent, et via quel mecanisme d'appel. Objectif : eviter l'ajout ad hoc ou service/fournisseur/consommateur sont meles dans un seul script sans frontiere claire. Inspire de l'architecture DeepSeek Harness (dsh, evalue le 15/08/2026, non adopte tel quel car encore en developer preview) sans en dependre. S'applique a toute nouvelle capacite a partir du 17/08/2026 ; pas de reprise retroactive des skills existants. Detail : common/seam-discipline-hermes.md.

REGLE -- Ne jamais inferer au-dela de la source, generation documentaire Zone Sud (2026-08-17)

Regle distincte de l'anti-invention benchmarks du 27/07/2026 (qui couvre la veille/recherche). Ici, toute valeur inseree dans un document officiel genere pour la Direction Zone Sud (DCO, courrier, evaluation AO, notification) doit venir textuellement d'une source verifiee pour ce dossier precis, jamais d'une estimation, d'un arrondi de convention, ou d'une valeur dupliquee d'un autre dossier par plausibilite. Champ absent ou ambigu -> le signaler a Nabil et marquer [A VERIFIER] dans le document plutot que le remplir a l'estime. Deploye le 17/08/2026 dans dco-generation-tt, gsd-ao-evaluation, courriers-officiels-tt (skills hermes-tt, non versionnes sur Gitea -- data/ gitignore, persistance via volume Docker uniquement). Reste a faire, avec Nabil : peupler references/exemples-valides/ de chaque skill avec 2-3 documents deja valides, pour un cadrage few-shot. Detail : common/regle-source-generation-documentaire-zone-sud.md.

FIX -- baserow-schema-mcp : 4 outils manquants (create/delete table/field) ajoutes en service compagnon, pas en extension du MCP natif (2026-08-17)

Le MCP natif de Baserow (image officielle, non patchable) ne couvre que les lignes (create_rows/delete_rows/list_tables/...), pas le schema (tables/champs). Nouveau service independant baserow-schema-mcp (port 3101, meme famille que le sidecar OAuth baserow-oauth-stub) proxyant 4 outils vers l'API REST native de Baserow. Trois pieges a retenir pour tout futur service MCP maison : (1) ces 4 routes de schema exigent un JWT utilisateur, le Database API Token ne fonctionne pas dessus (confirme via /api/redoc/) -- re-login proactif + retry sur 401 pour eviter de reproduire l'incident "rotation Baserow" deja vu sur dashboard-terrain et gsparc-mezzouna-api ; (2) l'API bas niveau mcp.server.sse.SseServerTransport a change entre la version figee par context-hub et mcp==1.29.0 (methodes disparues) -- preferer l'API haut niveau FastMCP (@mcp.tool() + mcp.sse_app()), plus stable dans le temps ; (3) mcp>=1.29 protege par defaut contre le DNS rebinding avec allowed_hosts=[], ce qui rejette TOUT Host header tant qu'on ne le configure pas explicitement. Reste a faire (manuel, hors perimetre agent) : reverse-proxy DSM baserow-schema.bolbol.tn -> 3101, puis ajout du connecteur cote Claude.ai. Detail complet : common/baserow-schema-mcp.md.

FIX -- baserow-schema-mcp : flux SSE bloque via le reverse-proxy DSM, resolu en HTTP 1.0 backend (2026-08-17, resolu)

Une fois la regle DSM baserow-schema.bolbol.tn -> 3101 creee (cert Let's Encrypt auto-provisionne OK, TLS OK), le endpoint /mcp/<token>/sse restait silencieusement bloque (0 octet recu avant timeout), reproduit avec plusieurs clients. Resolu par Nabil : choisir HTTP 1.0 (pas 1.1) comme version backend dans le formulaire DSM du reverse-proxy. Reteste apres coup avec succes sur curl (ALPN h2 par defaut) ET httpx pur HTTP/1.1 -- pas de regression cote client malgre le libelle "HTTP 1.0" trompeur (mode de proxying DSM->backend moins strict sur le chunked/keep-alive, pas une vraie retrogradation cote client). A appliquer directement sur toute future regle DSM devant un service SSE/MCP si le meme blocage silencieux apparait. Detail : common/baserow-schema-mcp.md.

RÈGLE -- Interdiction absolue d'utiliser Nabil-Key (bfk-0cd1fb...) dans les agents Hermes et services NAS, clés virtuelles dédiées obligatoires (2026-09-04)

  1. Périmètre et cause racine :

    • La clé Nabil-Key (bfk-0cd1fba7d440ca2eefd30b28a7349b7d28422dcbc49e982d) est STRICTEMENT réservée à l'usage personnel direct de Nabil et dispose d'un forfait plafonné à 8 $/mois (sur OpenCode Go).
    • Toute utilisation de cette clé par un agent automatique ou un conteneur draine le quota personnel de Nabil et bloque l'inférence.
    • INTERDICTION ABSOLUE de configurer ou d'injecter cette clé dans les conteneurs Hermes (model.api_key, auxiliary.*.api_key), les services NAS, les scripts de veille ou les cron jobs.
  2. Clés virtuelles dédiées par instance : Chaque instance Hermes et chaque consommateur doit impérativement utiliser sa propre clé virtuelle isolée :

    • hermes-agent-perso : vk-hermes-perso (sk-bf-bb062ad2-438d-4176-9f85-b703168b6df0)
    • hermes-agent-nyora : vk-hermes-nyora (sk-bf-90d6a517-3a46-4221-8df9-c035b63872e6)
    • hermes-agent-tt : variable d'environnement OPENCODE_GO_API_KEY (avec api_key: '' dans config.yaml)
    • context-hub : bfk-e5832bfe en .env (BIFROST_VK) -- valeur absente de la table governance_virtual_keys au 04/09/2026, a verifier avant de la citer comme reference fiable (cle stale ou rotee ?)
    • nyora-convert-api : vk-nyora-notes-tt (sk-bf-2685...)
  3. Piège de patch partiel (Ticket infra-2026-09-014) :

    • Lors d'une migration ou d'un changement de clé dans Hermes, veiller à auditer TOUS les blocs de configuration : model.api_key ET les sous-blocs auxiliary.vision.api_key, auxiliary.embeddings.api_key, etc.
    • Un remplacement restreint à model.api_key laisse la vision ou les tâches annexes débiter l'ancienne clé silencieusement.
  4. Routage Bifrost VPS via bifrost-proxy :

    • Les requêtes OpenCode Go doivent transiter par bifrost-proxy:3086, qui assure l'injection du header requis x-bf-eh-x-opencode-session vers Bifrost VPS (bifrost.vps.bolbol.tn:8080).

Détail complet : common/piege-virtual-key-nabil-key-bifrost-hermes-20260904.md.