[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f-qKx9lPNRNmB52KsoeqVtqtIJ-oxdJT2dAa_u1n-Zho":3,"$fk1oB-e5O_BgwF2c-QwDgsx2Y7SMPhf77z9g2mrpoBuM":23},{"chapter":4,"prev":13,"next":18},{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10,"contentHtml":11,"contentHtmlEn":12},"email","07","Inbox hub & Atlas Inbox — deux pipelines email","Inbox hub & Atlas Inbox — two email pipelines","Décrit les deux pipelines IMAP→DB du harness (contact@ trié pour le hub, atlas@ déclencheur d'actions agentiques), la façade d'envoi sortant et les doctrines bloquantes associées (scan AV, zéro contact client direct).","Describes the two IMAP→DB pipelines of the harness (contact@ sorted for the hub, atlas@ triggering agentic actions), the outbound sending facade, and the associated blocking doctrines (AV scan, zero direct client contact).","\u003Ch2 id=\"inbox-vue-ensemble\">Vue d'ensemble — deux pipelines distincts\u003C\u002Fh2>\n\n\u003Cp>Le système héberge \u003Cstrong>deux\u003C\u002Fstrong> boîtes mail et \u003Cstrong>deux\u003C\u002Fstrong> espaces de stockage d'emails qui n'ont aucun lien de jointure entre eux. C'est la première source de confusion ; à retenir avant tout :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>\u003C\u002Fth>\n      \u003Cth>Pipeline « Inbox hub »\u003C\u002Fth>\n      \u003Cth>Pipeline « Atlas Inbox »\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Boîte IMAP\u003C\u002Ftd>\n      \u003Ctd>Boîte de contact principale (hébergeur mutualisé)\u003C\u002Ftd>\n      \u003Ctd>Boîte agentique dédiée (hébergeur mutualisé, boîte distincte)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Stockage cible\u003C\u002Ftd>\n      \u003Ctd>Table d'emails du hub\u003C\u002Ftd>\n      \u003Ctd>Table de messages bruts + table de workflow Atlas\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Collecteur actif\u003C\u002Ftd>\n      \u003Ctd>Script de collecte Python (actif depuis mai 2026)\u003C\u002Ftd>\n      \u003Ctd>Script de poll Python direct (sans façade intermédiaire)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Implémentation désactivée\u003C\u002Ftd>\n      \u003Ctd>Ancienne façade Nuxt \u003Cstrong>désactivée mai 2026\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>(aucune façade Nuxt — poll Python direct)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Classification\u003C\u002Ftd>\n      \u003Ctd>Heuristique statique (domaine expéditeur → client \u002F MRR \u002F spam)\u003C\u002Ftd>\n      \u003Ctd>LLM, intention parmi 6 valeurs possibles\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Finalité\u003C\u002Ftd>\n      \u003Ctd>Triage et affichage dans le hub (bugs clients, MRR, priorité)\u003C\u002Ftd>\n      \u003Ctd>Déclencher des actions : run, chantier, question, négociation, conseil\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Le schéma de flux général est le suivant :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>Boîte de contact principale (IMAP)\n              │\n              │ cron toutes les minutes — collecteur Python inbox hub\n              │ (lecture seule, fenêtre glissante 7 jours, dédup par UID)\n              ▼\n    Stockage inbox hub\n    (classification heuristique : client \u002F priorité \u002F MRR \u002F spam)\n              │\n              ▼  interface hub\n\n  [DÉSACTIVÉ mai 2026] ancienne façade Nuxt \u002F tâche planifiée Nitro\n  — saturait la boucle d'événements sur timeouts IMAP\n\nBoîte agentique Atlas (IMAP)\n              │ cron toutes les minutes — collecteur Python Atlas\n              │ (lecture des messages non lus)\n              ▼\n  Messages bruts ──1:1──▶ Entrées workflow Atlas (statut « reçu »)\n              │\n              ├─ pièces jointes présentes → analyse antivirus + heuristiques\n              │        └─ pièces jointes sûres : oui \u002F non\n              ▼\n  Classification LLM → intention détectée\n              │\n  ┌──────┬──────────┬──────────┬──────────────┐\n run  question  chantier  négociation  conseil \u002F bruit\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch2 id=\"inbox-hub-pipeline\">Pipeline « Inbox hub » — boîte de contact principale\u003C\u002Fh2>\n\n\u003Ch3>Un seul collecteur actif (depuis mai 2026)\u003C\u002Fh3>\n\n\u003Cp>Cette table d'emails n'a \u003Cstrong>qu'un seul\u003C\u002Fstrong> chemin d'alimentation câblé en production : le collecteur Python dédié à l'inbox hub. L'ancienne façade Nuxt est hors service.\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\n    \u003Cstrong>Collecteur Python actif\u003C\u002Fstrong> — effectue un poll IMAP direct de la boîte de contact principale via la façade de connexion inbox (mode lecture seule). Il interroge les messages des 7 derniers jours, traite les UIDs du plus récent au plus ancien, et insère chaque message avec une logique \u003Cem>upsert\u003C\u002Fem> : en cas de conflit sur l'identifiant IMAP, les champs déjà renseignés ne sont jamais écrasés. Il tourne en cron toutes les minutes. Ce collecteur \u003Cstrong>remplace\u003C\u002Fstrong> l'ancienne tâche planifiée Nitro.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Implémentation historique — désactivée\u003C\u002Fstrong> : l'ancienne façade Nuxt (endpoint de synchronisation → tâche Nitro), déclenchée par un script cron dédié, a été \u003Cstrong>désactivée en mai 2026\u003C\u002Fstrong>. Elle saturait la boucle d'événements du serveur Nuxt en raison de timeouts IMAP répétés qui bloquaient également la connexion à la base de données. La ligne cron correspondante est commentée dans le crontab.\n  \u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>Il n'existe donc \u003Cstrong>pas\u003C\u002Fstrong> deux chemins co-actifs : un chemin live (collecteur Python) et un chemin mort (ancienne façade Nuxt). Le skill de lecture d'urgence (fallback IMAP, lecture seule) précise explicitement qu'il ne touche jamais au stockage principal pour éviter toute double insertion.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Détection d'une vraie insertion\u003C\u002Fstrong> : la requête d'upsert utilise un mécanisme permettant de distinguer une vraie insertion d'un backfill sur doublon — ce qui permet de comptabiliser séparément les nouveaux messages et les mises à jour silencieuses.\u003C\u002Fp>\n\n\u003Ch3>Classification heuristique (sans LLM)\u003C\u002Fh3>\n\n\u003Cp>La classification de l'inbox hub repose sur une heuristique statique appliquée à \u003Cstrong>l'adresse expéditeur uniquement\u003C\u002Fstrong> (le sujet n'est pas utilisé) :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>L'adresse est comparée à un référentiel de domaines et d'emails clients connus → enrichit le message avec le nom du client, sa priorité et son MRR.\u003C\u002Fli>\n  \u003Cli>Sinon, des marqueurs de spam sont testés sur l'adresse (\u003Ccode>newsletter\u003C\u002Fcode>, \u003Ccode>noreply\u003C\u002Fcode>, \u003Ccode>no-reply\u003C\u002Fcode>, \u003Ccode>mailer-daemon\u003C\u002Fcode>, \u003Ccode>postmaster\u003C\u002Fcode>, \u003Ccode>notifications@\u003C\u002Fcode>, \u003Ccode>updates@\u003C\u002Fcode>, \u003Ccode>marketing@\u003C\u002Fcode>) → le message est marqué \u003Ccode>spam\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Le statut initial posé est \u003Ccode>spam\u003C\u002Fcode> ou \u003Ccode>new\u003C\u002Fcode>. Le corps du message est extrait en deux variantes (texte brut et HTML) de façon multipart, en ignorant explicitement les parties marquées comme pièces jointes. Les deux corps sont persistés intégralement, sans troncature. Sur ré-sync d'un message déjà connu, l'upsert ne backfille les corps \u003Cstrong>que s'ils sont vides\u003C\u002Fstrong> (via \u003Ccode>COALESCE\u003C\u002Fcode>) — la classification existante reste intacte.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Pièces jointes : non capturées par le flux actif.\u003C\u002Fstrong> Le collecteur Python inbox hub ne persiste aucune pièce jointe — les parties \u003Ccode>Content-Disposition: attachment\u003C\u002Fcode> sont explicitement ignorées lors de l'extraction du corps. Le stockage des pièces jointes (une ligne par fichier en données binaires) appartenait à l'ancienne façade désactivée ; en l'état actuel, la table de pièces jointes de l'inbox hub n'est plus alimentée par ce pipeline.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Gestion des credentials de connexion IMAP\u003C\u002Fh3>\n\n\u003Cp>Le collecteur actif ne lit pas les variables de connexion IMAP directement : il délègue à une façade de connexion qui résout les credentials de la boîte de contact. Ces valeurs résident \u003Cstrong>uniquement\u003C\u002Fstrong> dans le fichier d'environnement du serveur hôte, jamais dans le dépôt.\u003C\u002Fp>\n\n\u003Cp>Plusieurs composants du système (client d'envoi sortant, scripts utilitaires, collecteur principal) référencent le mot de passe IMAP sous des noms de variables légèrement différents selon leur origine. En cas de divergence, le fichier d'environnement hôte fait foi et définit le mapping réel.\u003C\u002Fp>\n\n\u003Ch3>Structure du stockage inbox hub\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Champ\u003C\u002Fth>\n      \u003Cth>Type\u003C\u002Fth>\n      \u003Cth>Rôle\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Identifiant interne\u003C\u002Ftd>\n      \u003Ctd>entier, clé primaire\u003C\u002Ftd>\n      \u003Ctd>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Identifiant IMAP\u003C\u002Ftd>\n      \u003Ctd>chaîne, unique\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Clé de déduplication\u003C\u002Fstrong> (upsert \u003Ccode>ON CONFLICT\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Expéditeur (adresse, nom), sujet\u003C\u002Ftd>\n      \u003Ctd>chaîne\u003C\u002Ftd>\n      \u003Ctd>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Date de réception\u003C\u002Ftd>\n      \u003Ctd>horodatage avec fuseau\u003C\u002Ftd>\n      \u003Ctd>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Nom client, priorité client, MRR\u003C\u002Ftd>\n      \u003Ctd>chaîne \u002F entier\u003C\u002Ftd>\n      \u003Ctd>Rempli par la classification heuristique\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Marqueur bug, mot-clé bug, sévérité IA, résumé IA\u003C\u002Ftd>\n      \u003Ctd>entier \u002F chaîne \u002F texte\u003C\u002Ftd>\n      \u003Ctd>Triage bug (posé à 0 à l'insertion initiale)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Chemin d'archive\u003C\u002Ftd>\n      \u003Ctd>chaîne\u003C\u002Ftd>\n      \u003Ctd>Référence au fichier .eml archivé (non écrit par le flux actif)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Statut\u003C\u002Ftd>\n      \u003Ctd>chaîne\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>new\u003C\u002Fcode>, \u003Ccode>spam\u003C\u002Fcode>, …\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Traité par, traité le, notes\u003C\u002Ftd>\n      \u003Ctd>divers\u003C\u002Ftd>\n      \u003Ctd>Annotations internes\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Lien vers chantier\u003C\u002Ftd>\n      \u003Ctd>entier, clé étrangère\u003C\u002Ftd>\n      \u003Ctd>Rattachement à un chantier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Corps texte brut, corps HTML\u003C\u002Ftd>\n      \u003Ctd>texte\u003C\u002Ftd>\n      \u003Ctd>Corps complet non tronqué\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Créé le, mis à jour le\u003C\u002Ftd>\n      \u003Ctd>horodatage\u003C\u002Ftd>\n      \u003Ctd>Écrits à chaque insertion ou mise à jour\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>L'interface hub expose plusieurs endpoints pour lister les messages, accéder au détail d'un message, le mettre à jour, consulter ses pièces jointes, le lier à un chantier, ou déclencher manuellement une synchronisation.\u003C\u002Fp>\n\u003Ch2 id=\"pipeline-atlas-inbox\">Pipeline de réception et classification des emails entrants\u003C\u002Fh2>\n\n\u003Cp>Ce pipeline traite en trois étapes automatiquement enchaînées les emails reçus sur la boîte de réception centrale de CodeMyShop : \u003Cstrong>collecte → analyse des pièces jointes → classification\u003C\u002Fstrong>.\u003C\u002Fp>\n\n\u003Ch3>Collecte des messages\u003C\u002Fh3>\n\n\u003Cp>Un planificateur déclenche le cycle de collecte toutes les minutes via un mécanisme de verrou non-bloquant : si un cycle est déjà en cours, le déclenchement suivant est silencieusement ignoré. Un chien de garde vérifie périodiquement (toutes les quinze minutes) les verrous orphelins et les libère si nécessaire.\u003C\u002Fp>\n\n\u003Cp>Le cycle de collecte suit les étapes suivantes :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Connexion IMAP via la façade obligatoire\u003C\u002Fstrong> — l'accès à la boîte mail passe exclusivement par la couche d'abstraction email de Synedre OS (voir la section sur le garde-fou de façade ci-dessous). Toute connexion IMAP directe est bloquée structurellement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Recherche des messages non lus\u003C\u002Fstrong> — la requête porte sur les messages marqués \u003Ccode>UNSEEN\u003C\u002Fcode>, avec une limite configurable (50 par défaut). Le fetch utilise le mode \u003Ccode>PEEK\u003C\u002Fcode> pour ne pas marquer les messages comme lus automatiquement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Persistance en base\u003C\u002Fstrong> — chaque message est inséré de façon idempotente : un conflit sur l'identifiant de message est ignoré silencieusement. Le corps du message est plafonné (60 000 caractères en texte brut, 120 000 en HTML) pour éviter des dépassements de capacité. Le marquage \u003Ccode>\\Seen\u003C\u002Fcode> n'est posé qu'\u003Cem>après\u003C\u002Fem> confirmation de l'insertion, garantissant que tout message non marqué sera retraité en cas de crash.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Enregistrement dans le registre Atlas\u003C\u002Fstrong> — une seconde insertion, également idempotente, crée l'entrée de suivi avec le statut initial \u003Ccode>received\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Enchaînement automatique\u003C\u002Fstrong> — si le message comporte des pièces jointes, l'analyse antivirus est déclenchée \u003Cem>avant\u003C\u002Fem> la classification (doctrine scan-first), puis la classification est lancée dans tous les cas.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>\u003Cstrong>Anti-boucle interne :\u003C\u002Fstrong> les emails dont l'expéditeur est la boîte Atlas elle-même, ou dont le sujet contient un identifiant interne de suivi, sont marqués comme lus et ignorés sans insertion, afin d'éviter les boucles d'auto-spawn. Les re-forwards manuels d'un expéditeur fondateur identifié sont en revanche traités normalement, car ils constituent un flux de travail légitime.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Audit :\u003C\u002Fstrong> chaque cycle est tracé dans un journal forensic append-only. Par conception, aucune donnée personnelle n'est enregistrée en clair : seul un condensé cryptographique tronqué de l'adresse expéditeur est conservé.\u003C\u002Fp>\n\n\u003Ch3>Analyse des pièces jointes\u003C\u002Fh3>\n\n\u003Cp>Lorsqu'un message comporte des pièces jointes, le module d'analyse est invoqué avant toute classification. Le mécanisme est le suivant :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>Récupération des métadonnées du message (identifiant IMAP, présence de pièces jointes) par jointure entre les tables de suivi.\u003C\u002Fli>\n  \u003Cli>Re-fetch du message en lecture seule par son identifiant IMAP.\u003C\u002Fli>\n  \u003Cli>Extraction des pièces jointes vers un répertoire temporaire isolé, avec protection anti-traversal de chemin sur les noms de fichiers.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Refus pré-scan\u003C\u002Fstrong> : les extensions exécutables connues (\u003Ccode>.exe\u003C\u002Fcode>, \u003Ccode>.scr\u003C\u002Fcode>, \u003Ccode>.bat\u003C\u002Fcode>, \u003Ccode>.cmd\u003C\u002Fcode>, \u003Ccode>.com\u003C\u002Fcode>, \u003Ccode>.vbs\u003C\u002Fcode>, \u003Ccode>.js\u003C\u002Fcode>, \u003Ccode>.jar\u003C\u002Fcode>, \u003Ccode>.msi\u003C\u002Fcode>, \u003Ccode>.dmg\u003C\u002Fcode>, \u003Ccode>.deb\u003C\u002Fcode>, \u003Ccode>.rpm\u003C\u002Fcode>, \u003Ccode>.ps1\u003C\u002Fcode>) et les fichiers dépassant 25 Mo sont immédiatement rejetés avec le verdict \u003Ccode>blocked\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Analyse antivirus de chaque pièce jointe via la façade de l'agent de sécurité (voir §5).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Verdict global\u003C\u002Fstrong> : \u003Ccode>attachments_safe = 1\u003C\u002Fcode> uniquement si au moins un fichier a été scanné \u003Cem>et\u003C\u002Fem> qu'aucun fichier suspect, infecté ou refusé n'a été détecté. Dans le cas contraire, les pièces jointes sont déplacées en quarantaine et le statut du message est basculé en \u003Ccode>failed\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Persistance du verdict détaillé (format structuré JSONB) et enregistrement de l'audit.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Classification par intelligence artificielle\u003C\u002Fh3>\n\n\u003Cp>La classification détermine la nature de chaque email entrant et déclenche la matérialisation des objets métier correspondants.\u003C\u002Fp>\n\n\u003Ch3>Garde-fous pré-classification\u003C\u002Fh3>\n\n\u003Cp>\u003Cstrong>Blocage sur pièces jointes non sûres :\u003C\u002Fstrong> si un message comporte des pièces jointes, que le verdict d'analyse est disponible, et que ce verdict est négatif (\u003Ccode>attachments_safe = 0\u003C\u002Fcode>), la classification est refusée et l'événement est audité. Tolérance : un verdict absent (message sans pièce jointe ou scan non encore effectué) laisse passer la classification.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Court-circuit pré-LLM — re-forward fondateur vers Atlas :\u003C\u002Fstrong> si l'expéditeur est le fondateur et que le sujet indique un re-forward d'un récapitulatif Atlas, l'intention est forcée à \u003Ccode>noise\u003C\u002Fcode> sans appel au modèle de langage. Ce court-circuit évite de déclencher un spawn coûteux sur un contenu déjà traité.\u003C\u002Fp>\n\n\u003Ch3>Protection contre l'injection de prompt\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>Le corps de l'email est encadré par des marqueurs explicites de début et de fin.\u003C\u002Fli>\n  \u003Cli>Un avertissement explicite précise au modèle que le contenu encadré est de la donnée email, non des instructions.\u003C\u002Fli>\n  \u003Cli>Le corps est tronqué à 8 000 caractères avant d'être soumis au modèle.\u003C\u002Fli>\n  \u003Cli>La sortie est contrainte par une énumération stricte d'intentions valides — toute hallucination hors-enum est rejetée par la validation.\u003C\u002Fli>\n  \u003Cli>Le périmètre et le destinataire ne sont jamais dérivés du contenu du message.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>OCR local — enrichissement du contexte\u003C\u002Fh3>\n\n\u003Cp>Après validation du verdict antivirus (pièces jointes sûres), si des pièces jointes sont présentes, un moteur OCR local (Tesseract) analyse les images pour en extraire le texte. Ce texte est ajouté au contexte du prompt entre marqueurs dédiés. Deux règles de doctrine s'appliquent :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Le texte OCR n'est \u003Cstrong>jamais persisté en base de données\u003C\u002Fstrong> (données personnelles potentielles — usage en mémoire vive uniquement).\u003C\u002Fli>\n  \u003Cli>L'OCR est déclenché \u003Cstrong>après\u003C\u002Fstrong> le garde-fou antivirus, afin de ne jamais lire le contenu d'une image non encore analysée.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Appel au modèle de langage\u003C\u002Fh3>\n\n\u003Cp>Si aucun court-circuit ne s'est déclenché, le système interroge un modèle de langage via la façade IA unifiée de Synedre OS. Le fournisseur et le modèle sont définis par la configuration de routage (par défaut : Mistral, modèle \u003Ccode>mistral-small-latest\u003C\u002Fcode>). La sortie est demandée en JSON structuré avec un délai d'expiration de 30 secondes.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Validation de la sortie :\u003C\u002Fstrong> l'intention doit appartenir à l'ensemble des valeurs valides et le score de confiance doit être compris entre 0 et 1. Un score inférieur à 0,7 déclenche un audit \u003Ccode>classification_uncertain\u003C\u002Fcode>. Un score inférieur à 0,5 entraîne un plancher automatique à \u003Ccode>noise\u003C\u002Fcode>, indépendamment du seuil d'audit.\u003C\u002Fp>\n\n\u003Ch3>Garde-fous post-LLM\u003C\u002Fh3>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Re-forward fondateur avec pièces jointes classifié à tort en noise :\u003C\u002Fstrong> si le modèle retourne \u003Ccode>noise\u003C\u002Fcode> alors que l'expéditeur est le fondateur et que le message comporte des pièces jointes, l'intention est requalifiée en \u003Ccode>question\u003C\u002Fcode>. Ce cas couvre les situations où le contenu actionnable se trouve dans une image que le modèle de petite taille ne peut pas analyser directement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Client existant rerouté :\u003C\u002Fstrong> si l'intention est \u003Ccode>negociation\u003C\u002Fcode> ou \u003Ccode>conseil\u003C\u002Fcode> mais que l'adresse expéditrice correspond à un tenant actif (vérification par domaine email dans les tables de référence client), l'intention est requalifiée en \u003Ccode>run\u003C\u002Fcode> scopé sur ce tenant. Les prospects restent en \u003Ccode>negociation\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Intents valides et matérialisation\u003C\u002Fh3>\n\n\u003Cp>Les six intentions valides sont : \u003Ccode>run\u003C\u002Fcode>, \u003Ccode>chantier\u003C\u002Fcode>, \u003Ccode>question\u003C\u002Fcode>, \u003Ccode>noise\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode>, \u003Ccode>conseil\u003C\u002Fcode>. Chaque intention matérialise au plus une entrée dans la table métier correspondante, de façon idempotente.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Intent\u003C\u002Fth>\n      \u003Cth>Action\u003C\u002Fth>\n      \u003Cth>Objet créé\u003C\u002Fth>\n      \u003Cth>Déduplication\u003C\u002Fth>\n      \u003Cth>Lien tracé\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>run\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>UPSERT\u003C\u002Ftd>\n      \u003Ctd>Travail (\u003Ccode>source='atlas-inbox'\u003C\u002Fcode>, \u003Ccode>trigger='email'\u003C\u002Fcode>, scope optionnel si rerouté)\u003C\u002Ftd>\n      \u003Ctd>Contrainte unique sur la référence source\u003C\u002Ftd>\n      \u003Ctd>Identifiant du travail lié\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>question\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>UPSERT\u003C\u002Ftd>\n      \u003Ctd>Question (\u003Ccode>status='pending'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Contrainte unique sur la référence source\u003C\u002Ftd>\n      \u003Ctd>Identifiant de la question liée\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>negociation\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>INSERT\u003C\u002Ftd>\n      \u003Ctd>Négociation (\u003Ccode>status='nouveau'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Garde sur lien déjà présent\u003C\u002Ftd>\n      \u003Ctd>Identifiant de la négociation liée\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>conseil\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>INSERT\u003C\u002Ftd>\n      \u003Ctd>Conseil (\u003Ccode>status='received'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Garde sur lien déjà présent\u003C\u002Ftd>\n      \u003Ctd>Identifiant du conseil lié\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>chantier\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Draft manuel (CLI)\u003C\u002Ftd>\n      \u003Ctd>Chantier (\u003Ccode>status='draft'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>Identifiant du chantier lié\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>noise\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No-op total\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Pour les intentions \u003Ccode>negociation\u003C\u002Fcode> et \u003Ccode>conseil\u003C\u002Fcode>, le nom de l'entreprise est dérivé du domaine expéditeur, sauf pour les domaines génériques (\u003Ccode>gmail.com\u003C\u002Fcode>, \u003Ccode>outlook.com\u003C\u002Fcode>, \u003Ccode>laposte.net\u003C\u002Fcode>, etc.).\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Cas particulier de l'intent \u003Ccode>chantier\u003C\u002Fcode> :\u003C\u002Fstrong> contrairement aux autres intentions, la création d'un chantier n'est pas automatique à la classification. Le message reste en statut \u003Ccode>classified\u003C\u002Fcode> avec l'intention \u003Ccode>chantier\u003C\u002Fcode>. La création du chantier en mode brouillon est déclenchée manuellement par le fondateur via l'option \u003Ccode>--link-chantier\u003C\u002Fcode> de l'interface CLI, conformément à la doctrine des procédures en sept étapes.\u003C\u002Fp>\n\n\u003Ch3>Interface en ligne de commande — classification\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode># Classifier un email par son identifiant\npython3 -m un composant interne --id-atlas-email 42\n\n# Classifier tous les emails en attente (reçus, pièces jointes sûres ou absentes)\npython3 -m un composant interne --all-pending\n\n# Reclassifier un email déjà traité\npython3 -m un composant interne --reclassify 42\n\n# Créer un chantier brouillon depuis un email classifié chantier\npython3 -m un composant interne --link-chantier 42\n\n# Rapport des classifications à faible confiance (&lt; 0.7)\npython3 -m un composant interne --report-uncertain\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Garde-fou de façade email\u003C\u002Fh3>\n\n\u003Cp>Ce garde-fou est un mécanisme technique, non une simple convention. Il s'agit d'un hook de pré-exécution branché sur les commandes shell lancées par les agents. Son rôle est d'intercepter toute tentative d'accès direct à la messagerie (envoi ou lecture IMAP\u002FSMTP en Python inline ou via un script non référencé) et de la bloquer avec un code d'erreur dur.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Fonctionnement :\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>Une liste blanche recense les modules email autorisés de Synedre OS (client email, synchronisation IMAP, accès direct). Toute commande faisant référence à l'un de ces modules est laissée passer.\u003C\u002Fli>\n  \u003Cli>Les commandes non listées sont analysées à la recherche de motifs d'accès direct : imports \u003Ccode>smtplib\u003C\u002Fcode>, \u003Ccode>imaplib\u003C\u002Fcode>, classes \u003Ccode>SMTP_SSL\u003C\u002Fcode>\u002F\u003Ccode>IMAP4_SSL\u003C\u002Fcode>, méthodes d'envoi de message MIME, ou appels à des façades parallèles bannies (\u003Ccode>send_immediate\u003C\u002Fcode>, \u003Ccode>send_email_quick\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>En cas de correspondance, la commande est bloquée et un message d'erreur explicatif est affiché à l'agent.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Ce garde-fou a été introduit suite à un incident de production où une façade non officielle contournait l'appui sur le dossier \u003Ccode>\u002FSent\u003C\u002Fcode> IMAP et corrompait l'encodage des caractères accentués lors de l'échappement shell. Le hook empêche structurellement toute rechute.\u003C\u002Fp>\n\n\u003Ch3>Schéma de la table de suivi Atlas\u003C\u002Fh3>\n\n\u003Cp>La table de suivi centrale stocke l'ensemble des informations de cycle de vie d'un email entrant :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Identité :\u003C\u002Fstrong> clé primaire de l'entrée Atlas, clé étrangère vers la table de messages email.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Classification :\u003C\u002Fstrong> intention classifiée, score de confiance (numérique), nom du modèle utilisé, justification textuelle.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Statut :\u003C\u002Fstrong> progression dans le pipeline (\u003Ccode>received\u003C\u002Fcode> → \u003Ccode>classified\u003C\u002Fcode> ou \u003Ccode>failed\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Pièces jointes :\u003C\u002Fstrong> verdict détaillé (JSONB), indicateur binaire de sécurité.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Liens vers les objets métier :\u003C\u002Fstrong> identifiants des chantiers, travaux, questions, négociations et conseils créés.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Bloc d'autorisation d'expédition :\u003C\u002Fstrong> jeton d'expédition, horodatages d'émission et de consommation, identifiant du consommateur, identifiant du message de réponse.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Provenance :\u003C\u002Fstrong> type de source et métadonnées associées (JSONB), permettant de distinguer un email collecté par poll IMAP d'un re-forward manuel. C'est cette information qui permet au court-circuit de re-forward fondateur de fonctionner sans gaspiller un spawn.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>\u003Cstrong>Journal d'audit forensic :\u003C\u002Fstrong> une table d'audit append-only enregistre chaque action significative (type d'action, acteur, détails structurés en JSONB, horodatage). Par conception, les détails ne contiennent jamais de données personnelles en clair : uniquement des condensés, identifiants, codes d'erreur et métriques.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note — Spawn et expédition sont deux pipelines distincts :\u003C\u002Fstrong>\u003C\u002Fp>\n  \u003Cul>\n    \u003Cli>Le \u003Cstrong>module de spawn\u003C\u002Fstrong> (planificateur toutes les 5 minutes) déclenche un agent en mode autonome pour les seules intentions \u003Ccode>chantier\u003C\u002Fcode> et \u003Ccode>question\u003C\u002Fcode>. Les intentions \u003Ccode>noise\u003C\u002Fcode> et \u003Ccode>run\u003C\u002Fcode> ne déclenchent jamais de spawn via ce planificateur.\u003C\u002Fli>\n    \u003Cli>Le \u003Cstrong>module d'expédition\u003C\u002Fstrong> gère le pipeline d'autorisation par email : génération d'un jeton d'expédition, envoi de l'email d'autorisation via la façade officielle, puis triple vérification anti-usurpation (liste blanche expéditeur + DKIM + correspondance du fil de discussion + jeton à usage unique atomique) avant d'exécuter la commande d'expédition.\u003C\u002Fli>\n  \u003C\u002Ful>\n  \u003Cp>Ces deux modules sont hors périmètre du présent chapitre.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"extraction-manuelle-pj\">Extraction manuelle de pièces jointes\u003C\u002Fh2>\n\n\u003Cp>Un outil en ligne de commande permet d'extraire les pièces jointes d'un email \u003Cstrong>par identifiant de message\u003C\u002Fstrong> (et non par identifiant de boîte). Il accepte un répertoire de sortie optionnel ; par défaut, les fichiers sont déposés dans un dossier temporaire dédié.\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Les identifiants de connexion à la boîte mail (serveur, utilisateur, mot de passe) sont lus depuis les fichiers d'environnement du dépôt — la valeur effective fait foi.\u003C\u002Fli>\n  \u003Cli>Le parser est multipart-aware et préserve les octets binaires à l'écriture.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cstrong>Doctrine :\u003C\u002Fstrong> les fichiers extraits ne doivent jamais être ouverts ni parsés avant que le scanner antivirus ait rendu un verdict \u003Ccode>clean\u003C\u002Fcode> (voir section suivante). La commande de recherche d'emails liste les pièces jointes \u003Cem>sans les extraire\u003C\u002Fem>, précisément pour respecter ce principe scan-first.\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"facade-email-sortant\">Façade d'envoi d'e-mails professionnels\u003C\u002Fh2>\n\n\u003Cp>Une \u003Cstrong>façade unique\u003C\u002Fstrong> centralise tous les envois d'e-mails professionnels. L'utilisation directe de la bibliothèque SMTP bas niveau est interdite hors de cette façade. C'est la règle absolue de ce périmètre.\u003C\u002Fp>\n\n\u003Ch3>Cycle brouillon → validation → envoi\u003C\u002Fh3>\n\n\u003Cp>Le flux de travail suit trois étapes distinctes :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Brouillon (\u003Ccode>--draft\u003C\u002Fcode>) :\u003C\u002Fstrong> écrit un fichier JSON horodaté dans le répertoire des brouillons avec le statut \u003Ccode>draft\u003C\u002Fcode>, et envoie \u003Cem>automatiquement\u003C\u002Fem> une copie de validation au responsable (jamais au client). Le message ne part pas au client à cette étape — un message console l'indique explicitement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Validation (\u003Ccode>--validate\u003C\u002Fcode>) :\u003C\u002Fstrong> renvoie la copie de validation au responsable pour relecture. La commande \u003Ccode>--list\u003C\u002Fcode> liste les brouillons en attente ; \u003Ccode>--preview\u003C\u002Fcode> affiche le contenu complet d'un brouillon identifié.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Envoi réel (\u003Ccode>--send --draft-id N\u003C\u002Fcode>) :\u003C\u002Fstrong> envoie le message au destinataire final et archive une copie dans le dossier « Envoyés » de la boîte IMAP. Seul ce déclenchement explicite, après validation humaine, livre le message au client.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>Les sous-commandes \u003Ccode>draft\u003C\u002Fcode> et \u003Ccode>send\u003C\u002Fcode> ne s'enchaînent \u003Cstrong>jamais\u003C\u002Fstrong> automatiquement : c'est l'implémentation de la doctrine \u003Cem>show before send\u003C\u002Fem>.\u003C\u002Fp>\n\n\u003Ch3>Arguments disponibles\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Flag\u003C\u002Fth>\n      \u003Cth>Effet\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--to\u003C\u002Fcode> \u002F \u003Ccode>--cc\u003C\u002Fcode> \u002F \u003Ccode>--subject\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Destinataires (répétable ou CSV) et objet du message\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--body\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Corps du message en ligne\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--body-file\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Corps lu depuis un fichier UTF-8 (recommandé, évite l'inline)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--markdown\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Convertit le corps Markdown en HTML avant envoi (recommandé pour tout e-mail client)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--html\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Corps déjà au format HTML\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--attachment\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Pièce jointe (répétable) — échoue immédiatement si le fichier est absent\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--draft-id\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Cible un brouillon existant pour envoi, prévisualisation ou validation\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>L'option \u003Ccode>--draft\u003C\u002Fcode> exige obligatoirement \u003Ccode>--to\u003C\u002Fcode>, \u003Ccode>--subject\u003C\u002Fcode> et un corps (\u003Ccode>--body\u003C\u002Fcode> ou \u003Ccode>--body-file\u003C\u002Fcode>).\u003C\u002Fp>\n\n\u003Ch3>Copie de validation automatique\u003C\u002Fh3>\n\n\u003Cp>À chaque création de brouillon, le système envoie une copie au responsable avec le sujet préfixé \u003Ccode>[À VALIDER → &lt;destinataire&gt;] &lt;objet&gt;\u003C\u002Fcode>. Un mécanisme anti-boucle empêche l'envoi de cette copie si le destinataire est déjà la boîte de validation elle-même.\u003C\u002Fp>\n\n\u003Cp>Avant l'affichage du brouillon, le système interroge la façade de gestion du ton rédactionnel pour rappeler le registre approprié selon le destinataire — opération best-effort, non bloquante.\u003C\u002Fp>\n\n\u003Ch3>Envoi réel et archivage\u003C\u002Fh3>\n\n\u003Cp>L'envoi réel passe par SMTP puis archive une copie dans le dossier « Envoyés » de la boîte IMAP. Plusieurs variantes de nommage de ce dossier sont supportées (conventions OVH, Gmail, encodage UTF-7). Si l'archivage IMAP échoue, l'e-mail est tout de même parti — c'est un avertissement non bloquant. La signature HTML est ajoutée automatiquement à l'envoi.\u003C\u002Fp>\n\n\u003Ch3>Configuration SMTP\u002FIMAP\u003C\u002Fh3>\n\n\u003Cp>Les paramètres de connexion (serveur SMTP, port, identifiants, boîte d'expédition, serveur IMAP) sont lus depuis les fichiers d'environnement du dépôt. Une vérification au démarrage liste toute variable manquante. L'utilisation du port 465 active le mode SMTP over SSL.\u003C\u002Fp>\n\n\u003Ch2 id=\"scan-av-obligatoire\">Scan antivirus obligatoire avant ouverture (P0)\u003C\u002Fh2>\n\n\u003Cp>La façade de scan antivirus est sous la responsabilité de l'agent \u003Cstrong>Mitnick\u003C\u002Fstrong>. \u003Cstrong>Aucune pièce jointe extraite n'est ouverte ni parsée avant un verdict \u003Ccode>clean\u003C\u002Fcode>.\u003C\u002Fstrong> Le recours à un service cloud externe (de type analyse en ligne) est exclu pour protéger les données des clients. Ce garde-fou est absolu et non contournable.\u003C\u002Fp>\n\n\u003Ch3>Couches d'analyse\u003C\u002Fh3>\n\n\u003Cp>Le scanner applique cinq couches successives :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Refus des exécutables :\u003C\u002Fstrong> toute extension exécutable reconnue (binaires Windows\u002FLinux, scripts shell, PowerShell, JavaScript, VBScript, archives Java…) déclenche immédiatement un verdict \u003Ccode>infected\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>ClamAV :\u003C\u002Fstrong> analyse par signature via le binaire système. Une correspondance positive déclenche \u003Ccode>infected\u003C\u002Fcode> en fail-fast. Si ClamAV n'est pas installé sur la machine, le scan renvoie une erreur explicite.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Heuristiques PDF :\u003C\u002Fstrong> détection par expressions régulières de constructions PDF dangereuses (JavaScript embarqué, actions automatiques, formulaires soumis, médias enrichis…) sur les premiers mégaoctets du fichier → verdict \u003Ccode>suspicious\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Macros Office :\u003C\u002Fstrong> pour les formats Office à macros activées, recherche de patterns d'exécution automatique et d'appels système via l'outil \u003Ccode>olevba\u003C\u002Fcode> (best-effort) → \u003Ccode>suspicious\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Archives :\u003C\u002Fstrong> aucune descente dans les archives ; verdict \u003Ccode>suspicious\u003C\u002Fcode> par défaut, revue manuelle requise.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Marqueur de verdict propre\u003C\u002Fh3>\n\n\u003Cp>Sur verdict \u003Ccode>clean\u003C\u002Fcode>, un fichier de marqueur JSON atomique (empreinte SHA-256 + horodatage) est écrit dans un sous-répertoire de la pièce jointe. Les processus en aval opèrent en \u003Cem>fail-close\u003C\u002Fem> : l'\u003Cstrong>absence\u003C\u002Fstrong> du marqueur signifie « pas encore scanné » et bloque le traitement. L'écriture est atomique (fichier temporaire + remplacement) ; si elle échoue, le verdict reste correct — seule la persistance du marqueur est optionnelle.\u003C\u002Fp>\n\n\u003Ch3>Tableau des verdicts\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Verdict\u003C\u002Fth>\n      \u003Cth>Signification\u003C\u002Fth>\n      \u003Cth>Code de sortie CLI\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>clean\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Aucun signal détecté — ouverture autorisée\u003C\u002Ftd>\n      \u003Ctd>0\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>suspicious\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Signaux statiques présents — revue manuelle ou sandbox\u003C\u002Ftd>\n      \u003Ctd>1\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>infected\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Correspondance ClamAV ou exécutable — NE PAS OUVRIR\u003C\u002Ftd>\n      \u003Ctd>2\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>error\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Scan impossible (ClamAV indisponible, fichier absent…)\u003C\u002Ftd>\n      \u003Ctd>3\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>L'interface en ligne de commande accepte un ou plusieurs chemins et renvoie le \u003Cstrong>pire verdict\u003C\u002Fstrong> parmi tous les fichiers analysés. Le pipeline de traitement des e-mails entrants consomme ce scanner et refuse de progresser si les pièces jointes ne sont pas déclarées sûres.\u003C\u002Fp>\n\n\u003Ch2 id=\"zero-communication-client\">Doctrine — zéro communication client directe (P0)\u003C\u002Fh2>\n\n\u003Cp>Le système ne parle \u003Cstrong>jamais\u003C\u002Fstrong> directement aux clients. Toute communication externe transite par le responsable humain.\u003C\u002Fp>\n\n\u003Cp>Plusieurs garde-fous techniques concrétisent cette règle dans ce périmètre :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>La façade d'envoi n'envoie jamais au client lors de la création d'un brouillon ; elle écrit le brouillon et envoie une copie de validation au responsable. Seul l'envoi explicite post-validation livre le message au client.\u003C\u002Fli>\n  \u003Cli>Un hook de prévention intercepte toute tentative d'utilisation directe des bibliothèques mail bas niveau hors de la liste blanche des façades autorisées. Ce garde-fou rend la façade non contournable.\u003C\u002Fli>\n  \u003Cli>Le moteur de classification des e-mails entrants ne dérive jamais le destinataire ni la portée d'une réponse depuis le corps du message (protection anti-injection). Les brouillons de réponse restent en statut \u003Ccode>pending\u003C\u002Fcode> jusqu'à validation humaine.\u003C\u002Fli>\n  \u003Cli>Le vouvoiement est systématique pour tout contact externe. Les rendez-vous se fixent via un lien de réservation en ligne uniquement — jamais de créneau proposé en dur dans un e-mail.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2 id=\"taches-planifiees\">Tâches planifiées actives\u003C\u002Fh2>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Fréquence\u003C\u002Fth>\n      \u003Cth>Rôle\u003C\u002Fth>\n      \u003Cth>Pipeline\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Chaque minute\u003C\u002Ftd>\n      \u003Ctd>Synchronisation de la boîte de réception\u003C\u002Ftd>\n      \u003Ctd>Collecteur IMAP → table de stockage des e-mails entrants (\u003Cstrong>seul collecteur live\u003C\u002Fstrong>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Chaque minute\u003C\u002Ftd>\n      \u003Ctd>Interrogation du pipeline de traitement\u003C\u002Ftd>\n      \u003Ctd>Détection des nouveaux e-mails → file de traitement\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Toutes les 15 minutes\u003C\u002Ftd>\n      \u003Ctd>Surveillance des verrous orphelins\u003C\u002Ftd>\n      \u003Ctd>Libération automatique des verrous bloquants du pipeline\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Toutes les 5 minutes (avec délai initial de 2 min)\u003C\u002Ftd>\n      \u003Ctd>Spawn agentique\u003C\u002Ftd>\n      \u003Ctd>Lancement d'instances headless pour les intents \u003Ccode>chantier\u003C\u002Fcode> et \u003Ccode>question\u003C\u002Fcode> uniquement (liste blanche fixe)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cstrong>Collecteur désactivé :\u003C\u002Fstrong> l'ancien collecteur qui appelait la façade applicative web via une route HTTP est \u003Cstrong>commenté\u003C\u002Fstrong> depuis mai 2026. Il causait une saturation de la boucle d'événements du serveur applicatif (timeouts IMAP\u002Fbase de données en cascade). Il est remplacé par le worker Python externe listé ci-dessus. Ne pas le réactiver sans corriger la cause racine.\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"pieges-connus\">Pièges connus\u003C\u002Fh2>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Deux tables distinctes, deux mondes étanches.\u003C\u002Fstrong> La table de stockage des e-mails du hub (indexée par identifiant IMAP) et les tables du pipeline de traitement (indexées par compte et identifiant de message) ne sont pas joignables entre elles.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Marquage « lu » posé après insertion :\u003C\u002Fstrong> un crash entre la récupération et l'insertion laisse l'e-mail non lu, donc retraité au cycle suivant. L'idempotence est assurée par une contrainte de conflit à l'insertion.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Récupération silencieuse obligatoire (PEEK) :\u003C\u002Fstrong> le fetch du pipeline de traitement doit utiliser le mode PEEK pour ne pas consommer le drapeau « non lu » prématurément.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>--markdown\u003C\u002Fcode> recommandé\u003C\u002Fstrong> pour tout e-mail client (sinon le corps apparaît brut, non formaté). Préférer toujours \u003Ccode>--body-file\u003C\u002Fcode> à \u003Ccode>--body\u003C\u002Fcode> inline.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Scan-first non négociable :\u003C\u002Fstrong> ouvrir une pièce jointe avant verdict \u003Ccode>clean\u003C\u002Fcode> constitue une dette d'architecture de niveau P0.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Décalage un composant interne :\u003C\u002Fstrong> la description interne du collecteur d'e-mails mentionne un intervalle de 5 minutes, mais il tourne effectivement chaque minute.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Anti-boucle e-mail :\u003C\u002Fstrong> la boîte de contact du fondateur transfère volontairement vers la boîte du pipeline. Seule cette dernière est filtrée comme expéditeur automatique. Ne pas ajouter la boîte du fondateur dans la liste des expéditeurs à ignorer.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Intent \u003Ccode>chantier\u003C\u002Fcode> :\u003C\u002Fstrong> le moteur de classification ne crée jamais de chantier automatiquement — c'est une action CLI explicite. Cela évite la création de chantiers orphelins sans lettre de mission.\u003C\u002Fli>\n\u003C\u002Ful>","\u003Ch2 id=\"inbox-vue-ensemble\">Overview — two distinct pipelines\u003C\u002Fh2>\n\n\u003Cp>The system hosts \u003Cstrong>two\u003C\u002Fstrong> mailboxes and \u003Cstrong>two\u003C\u002Fstrong> email storage spaces that share no join relationship whatsoever. This is the primary source of confusion; the following must be understood before anything else:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>\u003C\u002Fth>\n      \u003Cth>Pipeline \"Inbox hub\"\u003C\u002Fth>\n      \u003Cth>Pipeline \"Atlas Inbox\"\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>IMAP mailbox\u003C\u002Ftd>\n      \u003Ctd>Main contact mailbox (shared hosting)\u003C\u002Ftd>\n      \u003Ctd>Dedicated agentic mailbox (shared hosting, separate mailbox)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Target storage\u003C\u002Ftd>\n      \u003Ctd>Hub email table\u003C\u002Ftd>\n      \u003Ctd>Raw message table + Atlas workflow table\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Active collector\u003C\u002Ftd>\n      \u003Ctd>Python collection script (active since May 2026)\u003C\u002Ftd>\n      \u003Ctd>Direct Python poll script (no intermediate facade)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Disabled implementation\u003C\u002Ftd>\n      \u003Ctd>Legacy Nuxt facade \u003Cstrong>disabled May 2026\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>(no Nuxt facade — direct Python poll)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Classification\u003C\u002Ftd>\n      \u003Ctd>Static heuristic (sender domain → client \u002F MRR \u002F spam)\u003C\u002Ftd>\n      \u003Ctd>LLM, intent among 6 possible values\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Purpose\u003C\u002Ftd>\n      \u003Ctd>Triage and display in the hub (client bugs, MRR, priority)\u003C\u002Ftd>\n      \u003Ctd>Trigger actions: run, project, question, negotiation, advice\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>The general flow diagram is as follows:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>Main contact mailbox (IMAP)\n              │\n              │ cron every minute — Python inbox hub collector\n              │ (read-only, 7-day sliding window, dedup by UID)\n              ▼\n    Inbox hub storage\n    (heuristic classification: client \u002F priority \u002F MRR \u002F spam)\n              │\n              ▼  hub interface\n\n  [DISABLED May 2026] legacy Nuxt facade \u002F Nitro scheduled task\n  — was saturating the event loop on IMAP timeouts\n\nAtlas agentic mailbox (IMAP)\n              │ cron every minute — Atlas Python collector\n              │ (reads unread messages)\n              ▼\n  Raw messages ──1:1──▶ Atlas workflow entries (status \"received\")\n              │\n              ├─ attachments present → antivirus analysis + heuristics\n              │        └─ safe attachments: yes \u002F no\n              ▼\n  LLM classification → detected intent\n              │\n  ┌──────┬──────────┬──────────┬──────────────┐\n run  question  project  negotiation  advice \u002F noise\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch2 id=\"inbox-hub-pipeline\">Pipeline \"Inbox hub\" — main contact mailbox\u003C\u002Fh2>\n\n\u003Ch3>A single active collector (since May 2026)\u003C\u002Fh3>\n\n\u003Cp>This email table has \u003Cstrong>only one\u003C\u002Fstrong> active feed path wired in production: the Python collector dedicated to the inbox hub. The legacy Nuxt facade is out of service.\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\n    \u003Cstrong>Active Python collector\u003C\u002Fstrong> — performs a direct IMAP poll of the main contact mailbox via the inbox connection facade (read-only mode). It queries messages from the last 7 days, processes UIDs from most recent to oldest, and inserts each message using an \u003Cem>upsert\u003C\u002Fem> logic: on conflict with an existing IMAP identifier, already-populated fields are never overwritten. It runs as a cron job every minute. This collector \u003Cstrong>replaces\u003C\u002Fstrong> the legacy Nitro scheduled task.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Legacy implementation — disabled\u003C\u002Fstrong>: the old Nuxt facade (sync endpoint → Nitro task), triggered by a dedicated cron script, was \u003Cstrong>disabled in May 2026\u003C\u002Fstrong>. It was saturating the Nuxt server event loop due to repeated IMAP timeouts that also blocked the database connection. The corresponding cron line is commented out in the crontab.\n  \u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>There are therefore \u003Cstrong>not\u003C\u002Fstrong> two co-active paths: one live path (Python collector) and one dead path (legacy Nuxt facade). The emergency read skill (IMAP fallback, read-only) explicitly states that it never touches the main storage in order to avoid any double insertion.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Detection of a genuine insertion\u003C\u002Fstrong>: the upsert query uses a mechanism that distinguishes a genuine insertion from a backfill on a duplicate — which allows new messages and silent updates to be counted separately.\u003C\u002Fp>\n\n\u003Ch3>Heuristic classification (no LLM)\u003C\u002Fh3>\n\n\u003Cp>Inbox hub classification relies on a static heuristic applied to the \u003Cstrong>sender address only\u003C\u002Fstrong> (the subject is not used):\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>The address is compared against a reference set of known client domains and email addresses → enriches the message with the client name, priority, and MRR.\u003C\u002Fli>\n  \u003Cli>Otherwise, spam markers are tested against the address (\u003Ccode>newsletter\u003C\u002Fcode>, \u003Ccode>noreply\u003C\u002Fcode>, \u003Ccode>no-reply\u003C\u002Fcode>, \u003Ccode>mailer-daemon\u003C\u002Fcode>, \u003Ccode>postmaster\u003C\u002Fcode>, \u003Ccode>notifications@\u003C\u002Fcode>, \u003Ccode>updates@\u003C\u002Fcode>, \u003Ccode>marketing@\u003C\u002Fcode>) → the message is flagged \u003Ccode>spam\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>The initial status assigned is either \u003Ccode>spam\u003C\u002Fcode> or \u003Ccode>new\u003C\u002Fcode>. The message body is extracted in two variants (plain text and HTML) via multipart parsing, explicitly ignoring parts marked as attachments. Both bodies are persisted in full, without truncation. On re-sync of an already-known message, the upsert backfills the bodies \u003Cstrong>only if they are empty\u003C\u002Fstrong> (via \u003Ccode>COALESCE\u003C\u002Fcode>) — the existing classification remains intact.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Attachments: not captured by the active flow.\u003C\u002Fstrong> The Python inbox hub collector does not persist any attachments — \u003Ccode>Content-Disposition: attachment\u003C\u002Fcode> parts are explicitly ignored during body extraction. Attachment storage (one row per file in binary data) belonged to the now-disabled legacy facade; as of the current state, the inbox hub attachment table is no longer fed by this pipeline.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>IMAP connection credential management\u003C\u002Fh3>\n\n\u003Cp>The active collector does not read the IMAP connection variables directly: it delegates to a connection facade that resolves the credentials for the contact mailbox. These values reside \u003Cstrong>exclusively\u003C\u002Fstrong> in the host server environment file, never in the repository.\u003C\u002Fp>\n\n\u003Cp>Several system components (outbound mail client, utility scripts, main collector) reference the IMAP password under slightly different variable names depending on their origin. In the event of a discrepancy, the host environment file takes precedence and defines the actual mapping.\u003C\u002Fp>\n\n\u003Ch3>Inbox hub storage structure\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Field\u003C\u002Fth>\n      \u003Cth>Type\u003C\u002Fth>\n      \u003Cth>Role\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Internal identifier\u003C\u002Ftd>\n      \u003Ctd>integer, primary key\u003C\u002Ftd>\n      \u003Ctd>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>IMAP identifier\u003C\u002Ftd>\n      \u003Ctd>string, unique\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Deduplication key\u003C\u002Fstrong> (upsert \u003Ccode>ON CONFLICT\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Sender (address, name), subject\u003C\u002Ftd>\n      \u003Ctd>string\u003C\u002Ftd>\n      \u003Ctd>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Received date\u003C\u002Ftd>\n      \u003Ctd>timestamp with time zone\u003C\u002Ftd>\n      \u003Ctd>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Client name, client priority, MRR\u003C\u002Ftd>\n      \u003Ctd>string \u002F integer\u003C\u002Ftd>\n      \u003Ctd>Populated by heuristic classification\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Bug flag, bug keyword, AI severity, AI summary\u003C\u002Ftd>\n      \u003Ctd>integer \u002F string \u002F text\u003C\u002Ftd>\n      \u003Ctd>Bug triage (set to 0 at initial insertion)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Archive path\u003C\u002Ftd>\n      \u003Ctd>string\u003C\u002Ftd>\n      \u003Ctd>Reference to the archived .eml file (not written by the active flow)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Status\u003C\u002Ftd>\n      \u003Ctd>string\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>new\u003C\u002Fcode>, \u003Ccode>spam\u003C\u002Fcode>, …\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Processed by, processed on, notes\u003C\u002Ftd>\n      \u003Ctd>various\u003C\u002Ftd>\n      \u003Ctd>Internal annotations\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Link to project\u003C\u002Ftd>\n      \u003Ctd>integer, foreign key\u003C\u002Ftd>\n      \u003Ctd>Association with a project\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Plain text body, HTML body\u003C\u002Ftd>\n      \u003Ctd>text\u003C\u002Ftd>\n      \u003Ctd>Full untruncated body\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Created at, updated at\u003C\u002Ftd>\n      \u003Ctd>timestamp\u003C\u002Ftd>\n      \u003Ctd>Written on every insertion or update\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>The hub interface exposes several endpoints to list messages, access message details, update a message, view its attachments, link it to a project, or manually trigger a synchronisation.\u003C\u002Fp>\n\u003Ch2 id=\"pipeline-atlas-inbox\">Inbound Email Reception and Classification Pipeline\u003C\u002Fh2>\n\n\u003Cp>This pipeline processes emails received on CodeMyShop's central inbox through three automatically chained steps: \u003Cstrong>collection → attachment analysis → classification\u003C\u002Fstrong>.\u003C\u002Fp>\n\n\u003Ch3>Message Collection\u003C\u002Fh3>\n\n\u003Cp>A scheduler triggers the collection cycle every minute via a non-blocking lock mechanism: if a cycle is already running, the next trigger is silently ignored. A watchdog periodically checks (every fifteen minutes) for orphaned locks and releases them as needed.\u003C\u002Fp>\n\n\u003Cp>The collection cycle proceeds through the following steps:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>IMAP connection via the mandatory facade\u003C\u002Fstrong> — mailbox access is exclusively routed through the Synedre OS email abstraction layer (see the facade guardrail section below). Any direct IMAP connection is structurally blocked.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Unseen message query\u003C\u002Fstrong> — the request targets messages flagged \u003Ccode>UNSEEN\u003C\u002Fcode>, with a configurable limit (50 by default). The fetch uses \u003Ccode>PEEK\u003C\u002Fcode> mode to avoid automatically marking messages as read.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Database persistence\u003C\u002Fstrong> — each message is inserted idempotently: a conflict on the message identifier is silently ignored. The message body is capped (60,000 characters for plain text, 120,000 for HTML) to prevent capacity overflows. The \u003Ccode>\\Seen\u003C\u002Fcode> flag is set only \u003Cem>after\u003C\u002Fem> insertion is confirmed, guaranteeing that any unflagged message will be reprocessed in the event of a crash.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Atlas registry entry\u003C\u002Fstrong> — a second, equally idempotent insert creates the tracking entry with the initial status \u003Ccode>received\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Automatic chaining\u003C\u002Fstrong> — if the message contains attachments, antivirus analysis is triggered \u003Cem>before\u003C\u002Fem> classification (scan-first doctrine), after which classification is launched in all cases.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>\u003Cstrong>Internal anti-loop:\u003C\u002Fstrong> emails whose sender is the Atlas mailbox itself, or whose subject contains an internal tracking identifier, are marked as read and discarded without insertion, preventing self-spawn loops. Manual re-forwards from an identified founder sender are processed normally, as they constitute a legitimate workflow.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Audit:\u003C\u002Fstrong> each cycle is recorded in an append-only forensic log. By design, no personal data is stored in plaintext: only a truncated cryptographic digest of the sender address is retained.\u003C\u002Fp>\n\n\u003Ch3>Attachment Analysis\u003C\u002Fh3>\n\n\u003Cp>When a message contains attachments, the analysis module is invoked before any classification. The mechanism is as follows:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>Retrieval of message metadata (IMAP identifier, attachment presence) via a join between tracking tables.\u003C\u002Fli>\n  \u003Cli>Read-only re-fetch of the message by its IMAP identifier.\u003C\u002Fli>\n  \u003Cli>Extraction of attachments to an isolated temporary directory, with path-traversal protection applied to filenames.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Pre-scan rejection\u003C\u002Fstrong>: known executable extensions (\u003Ccode>.exe\u003C\u002Fcode>, \u003Ccode>.scr\u003C\u002Fcode>, \u003Ccode>.bat\u003C\u002Fcode>, \u003Ccode>.cmd\u003C\u002Fcode>, \u003Ccode>.com\u003C\u002Fcode>, \u003Ccode>.vbs\u003C\u002Fcode>, \u003Ccode>.js\u003C\u002Fcode>, \u003Ccode>.jar\u003C\u002Fcode>, \u003Ccode>.msi\u003C\u002Fcode>, \u003Ccode>.dmg\u003C\u002Fcode>, \u003Ccode>.deb\u003C\u002Fcode>, \u003Ccode>.rpm\u003C\u002Fcode>, \u003Ccode>.ps1\u003C\u002Fcode>) and files exceeding 25 MB are immediately rejected with the verdict \u003Ccode>blocked\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Antivirus analysis of each attachment via the security agent facade (see §5).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Global verdict\u003C\u002Fstrong>: \u003Ccode>attachments_safe = 1\u003C\u002Fcode> only if at least one file was scanned \u003Cem>and\u003C\u002Fem> no suspicious, infected, or rejected file was detected. Otherwise, attachments are moved to quarantine and the message status is switched to \u003Ccode>failed\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Persistence of the detailed verdict (JSONB structured format) and audit logging.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>AI-Based Classification\u003C\u002Fh3>\n\n\u003Cp>Classification determines the nature of each inbound email and triggers the materialisation of the corresponding business objects.\u003C\u002Fp>\n\n\u003Ch3>Pre-Classification Guardrails\u003C\u002Fh3>\n\n\u003Cp>\u003Cstrong>Block on unsafe attachments:\u003C\u002Fstrong> if a message contains attachments, the analysis verdict is available, and that verdict is negative (\u003Ccode>attachments_safe = 0\u003C\u002Fcode>), classification is refused and the event is audited. Tolerance: an absent verdict (message with no attachment or scan not yet performed) allows classification to proceed.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Pre-LLM short-circuit — founder re-forward to Atlas:\u003C\u002Fstrong> if the sender is the founder and the subject indicates a re-forward of an Atlas summary, the intent is forced to \u003Ccode>noise\u003C\u002Fcode> without invoking the language model. This short-circuit avoids triggering a costly spawn on already-processed content.\u003C\u002Fp>\n\n\u003Ch3>Prompt Injection Protection\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>The email body is enclosed by explicit start and end markers.\u003C\u002Fli>\n  \u003Cli>An explicit warning instructs the model that the enclosed content is email data, not instructions.\u003C\u002Fli>\n  \u003Cli>The body is truncated to 8,000 characters before being submitted to the model.\u003C\u002Fli>\n  \u003Cli>Output is constrained by a strict enumeration of valid intents — any out-of-enum hallucination is rejected by validation.\u003C\u002Fli>\n  \u003Cli>Scope and recipient are never derived from the message content.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Local OCR — Context Enrichment\u003C\u002Fh3>\n\n\u003Cp>After antivirus verdict validation (safe attachments), if attachments are present, a local OCR engine (Tesseract) analyses images to extract their text. This text is appended to the prompt context between dedicated markers. Two doctrine rules apply:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>OCR text is \u003Cstrong>never persisted to the database\u003C\u002Fstrong> (potentially personal data — in-memory use only).\u003C\u002Fli>\n  \u003Cli>OCR is triggered \u003Cstrong>after\u003C\u002Fstrong> the antivirus guardrail, so that the content of an unscanned image is never read.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Language Model Invocation\u003C\u002Fh3>\n\n\u003Cp>If no short-circuit has fired, the system queries a language model via the Synedre OS unified AI facade. The provider and model are defined by the routing configuration (default: Mistral, model \u003Ccode>mistral-small-latest\u003C\u002Fcode>). Output is requested as structured JSON with a 30-second timeout.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Output validation:\u003C\u002Fstrong> the intent must belong to the set of valid values and the confidence score must be between 0 and 1. A score below 0.7 triggers a \u003Ccode>classification_uncertain\u003C\u002Fcode> audit event. A score below 0.5 automatically floors the intent to \u003Ccode>noise\u003C\u002Fcode>, independently of the audit threshold.\u003C\u002Fp>\n\n\u003Ch3>Post-LLM Guardrails\u003C\u002Fh3>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Founder re-forward with attachments incorrectly classified as noise:\u003C\u002Fstrong> if the model returns \u003Ccode>noise\u003C\u002Fcode> while the sender is the founder and the message contains attachments, the intent is requalified to \u003Ccode>question\u003C\u002Fcode>. This case covers situations where actionable content is contained in an image that the small model cannot analyse directly.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Existing client rerouted:\u003C\u002Fstrong> if the intent is \u003Ccode>negociation\u003C\u002Fcode> or \u003Ccode>conseil\u003C\u002Fcode> but the sender address matches an active tenant (verified by email domain against client reference tables), the intent is requalified to \u003Ccode>run\u003C\u002Fcode> scoped to that tenant. Prospects remain in \u003Ccode>negociation\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Valid Intents and Materialisation\u003C\u002Fh3>\n\n\u003Cp>The six valid intents are: \u003Ccode>run\u003C\u002Fcode>, \u003Ccode>chantier\u003C\u002Fcode>, \u003Ccode>question\u003C\u002Fcode>, \u003Ccode>noise\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode>, \u003Ccode>conseil\u003C\u002Fcode>. Each intent materialises at most one entry in the corresponding business table, idempotently.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Intent\u003C\u002Fth>\n      \u003Cth>Action\u003C\u002Fth>\n      \u003Cth>Object created\u003C\u002Fth>\n      \u003Cth>Deduplication\u003C\u002Fth>\n      \u003Cth>Link recorded\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>run\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>UPSERT\u003C\u002Ftd>\n      \u003Ctd>Work item (\u003Ccode>source='atlas-inbox'\u003C\u002Fcode>, \u003Ccode>trigger='email'\u003C\u002Fcode>, optional scope if rerouted)\u003C\u002Ftd>\n      \u003Ctd>Unique constraint on source reference\u003C\u002Ftd>\n      \u003Ctd>Linked work item identifier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>question\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>UPSERT\u003C\u002Ftd>\n      \u003Ctd>Question (\u003Ccode>status='pending'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Unique constraint on source reference\u003C\u002Ftd>\n      \u003Ctd>Linked question identifier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>negociation\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>INSERT\u003C\u002Ftd>\n      \u003Ctd>Negotiation (\u003Ccode>status='nouveau'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Guard on existing link\u003C\u002Ftd>\n      \u003Ctd>Linked negotiation identifier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>conseil\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>INSERT\u003C\u002Ftd>\n      \u003Ctd>Advisory (\u003Ccode>status='received'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Guard on existing link\u003C\u002Ftd>\n      \u003Ctd>Linked advisory identifier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>chantier\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Manual draft (CLI)\u003C\u002Ftd>\n      \u003Ctd>Workstream (\u003Ccode>status='draft'\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>Linked workstream identifier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>noise\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Total no-op\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>For \u003Ccode>negociation\u003C\u002Fcode> and \u003Ccode>conseil\u003C\u002Fcode> intents, the company name is derived from the sender domain, except for generic domains (\u003Ccode>gmail.com\u003C\u002Fcode>, \u003Ccode>outlook.com\u003C\u002Fcode>, \u003Ccode>laposte.net\u003C\u002Fcode>, etc.).\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Special case for the \u003Ccode>chantier\u003C\u002Fcode> intent:\u003C\u002Fstrong> unlike other intents, workstream creation is not automatic upon classification. The message remains in \u003Ccode>classified\u003C\u002Fcode> status with the \u003Ccode>chantier\u003C\u002Fcode> intent. Workstream creation in draft mode is triggered manually by the founder via the \u003Ccode>--link-chantier\u003C\u002Fcode> option of the CLI, in accordance with the seven-step procedure doctrine.\u003C\u002Fp>\n\n\u003Ch3>Command-Line Interface — Classification\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode># Classify an email by its identifier\npython3 -m un composant interne --id-atlas-email 42\n\n# Classify all pending emails (received, safe or absent attachments)\npython3 -m un composant interne --all-pending\n\n# Reclassify an already-processed email\npython3 -m un composant interne --reclassify 42\n\n# Create a draft workstream from a chantier-classified email\npython3 -m un composant interne --link-chantier 42\n\n# Report low-confidence classifications (&lt; 0.7)\npython3 -m un composant interne --report-uncertain\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Email Facade Guardrail\u003C\u002Fh3>\n\n\u003Cp>This guardrail is a technical mechanism, not a mere convention. It is a pre-execution hook wired to shell commands launched by agents. Its role is to intercept any attempt at direct mailbox access (inline Python IMAP\u002FSMTP send or read, or via an unregistered script) and block it with a hard error code.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>How it works:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>An allowlist enumerates the authorised Synedre OS email modules (email client, IMAP sync, direct access). Any command referencing one of these modules is passed through.\u003C\u002Fli>\n  \u003Cli>Unlisted commands are analysed for direct-access patterns: \u003Ccode>smtplib\u003C\u002Fcode> and \u003Ccode>imaplib\u003C\u002Fcode> imports, \u003Ccode>SMTP_SSL\u003C\u002Fcode>\u002F\u003Ccode>IMAP4_SSL\u003C\u002Fcode> classes, MIME message send methods, or calls to banned parallel facades (\u003Ccode>send_immediate\u003C\u002Fcode>, \u003Ccode>send_email_quick\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>On a match, the command is blocked and an explanatory error message is displayed to the agent.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>This guardrail was introduced following a production incident in which an unofficial facade bypassed the IMAP \u003Ccode>\u002FSent\u003C\u002Fcode> folder and corrupted the encoding of accented characters during shell escaping. The hook structurally prevents any recurrence.\u003C\u002Fp>\n\n\u003Ch3>Atlas Tracking Table Schema\u003C\u002Fh3>\n\n\u003Cp>The central tracking table stores all lifecycle information for an inbound email:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Identity:\u003C\u002Fstrong> Atlas entry primary key, foreign key to the email messages table.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Classification:\u003C\u002Fstrong> classified intent, confidence score (numeric), model name used, textual justification.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Status:\u003C\u002Fstrong> progression through the pipeline (\u003Ccode>received\u003C\u002Fcode> → \u003Ccode>classified\u003C\u002Fcode> or \u003Ccode>failed\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Attachments:\u003C\u002Fstrong> detailed verdict (JSONB), binary safety indicator.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Links to business objects:\u003C\u002Fstrong> identifiers of created workstreams, work items, questions, negotiations, and advisories.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Dispatch authorisation block:\u003C\u002Fstrong> dispatch token, issuance and consumption timestamps, consumer identifier, reply message identifier.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Provenance:\u003C\u002Fstrong> source type and associated metadata (JSONB), allowing an IMAP-polled email to be distinguished from a manual re-forward. This information enables the founder re-forward short-circuit to function without wasting a spawn.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>\u003Cstrong>Forensic audit log:\u003C\u002Fstrong> an append-only audit table records each significant action (action type, actor, structured details in JSONB, timestamp). By design, details never contain personal data in plaintext: only digests, identifiers, error codes, and metrics.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note — Spawn and dispatch are two distinct pipelines:\u003C\u002Fstrong>\u003C\u002Fp>\n  \u003Cul>\n    \u003Cli>The \u003Cstrong>spawn module\u003C\u002Fstrong> (scheduler every 5 minutes) triggers an agent in autonomous mode exclusively for \u003Ccode>chantier\u003C\u002Fcode> and \u003Ccode>question\u003C\u002Fcode> intents. The \u003Ccode>noise\u003C\u002Fcode> and \u003Ccode>run\u003C\u002Fcode> intents never trigger a spawn via this scheduler.\u003C\u002Fli>\n    \u003Cli>The \u003Cstrong>dispatch module\u003C\u002Fstrong> manages the email-based authorisation pipeline: dispatch token generation, sending the authorisation email via the official facade, then triple anti-spoofing verification (sender allowlist + DKIM + thread matching + single-use atomic token) before executing the dispatch command.\u003C\u002Fli>\n  \u003C\u002Ful>\n  \u003Cp>Both modules are outside the scope of this chapter.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"extraction-manuelle-pj\">Manual Attachment Extraction\u003C\u002Fh2>\n\n\u003Cp>A command-line tool allows attachments to be extracted from an email \u003Cstrong>by message identifier\u003C\u002Fstrong> (not by mailbox identifier). It accepts an optional output directory; by default, files are placed in a dedicated temporary folder.\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Mailbox connection credentials (server, username, password) are read from the repository's environment files — the effective value takes precedence.\u003C\u002Fli>\n  \u003Cli>The parser is multipart-aware and preserves binary bytes on write.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cstrong>Doctrine:\u003C\u002Fstrong> extracted files must never be opened or parsed before the antivirus scanner has returned a \u003Ccode>clean\u003C\u002Fcode> verdict (see next section). The email search command lists attachments \u003Cem>without extracting them\u003C\u002Fem>, precisely to enforce this scan-first principle.\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"facade-email-sortant\">Outbound Professional Email Facade\u003C\u002Fh2>\n\n\u003Cp>A \u003Cstrong>single facade\u003C\u002Fstrong> centralises all outbound professional email sending. Direct use of the low-level SMTP library is prohibited outside this facade. This is the absolute rule for this scope.\u003C\u002Fp>\n\n\u003Ch3>Draft → Validation → Send Cycle\u003C\u002Fh3>\n\n\u003Cp>The workflow follows three distinct steps:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Draft (\u003Ccode>--draft\u003C\u002Fcode>):\u003C\u002Fstrong> writes a timestamped JSON file in the drafts directory with status \u003Ccode>draft\u003C\u002Fcode>, and \u003Cem>automatically\u003C\u002Fem> sends a validation copy to the manager (never to the client). The message is not delivered to the client at this stage — a console message states this explicitly.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Validation (\u003Ccode>--validate\u003C\u002Fcode>):\u003C\u002Fstrong> resends the validation copy to the manager for review. The \u003Ccode>--list\u003C\u002Fcode> command lists pending drafts; \u003Ccode>--preview\u003C\u002Fcode> displays the full content of an identified draft.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Actual Send (\u003Ccode>--send --draft-id N\u003C\u002Fcode>):\u003C\u002Fstrong> sends the message to the final recipient and archives a copy in the IMAP mailbox's Sent folder. Only this explicit trigger, after human validation, delivers the message to the client.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>The \u003Ccode>draft\u003C\u002Fcode> and \u003Ccode>send\u003C\u002Fcode> sub-commands are \u003Cstrong>never\u003C\u002Fstrong> chained automatically: this is the implementation of the \u003Cem>show before send\u003C\u002Fem> doctrine.\u003C\u002Fp>\n\n\u003Ch3>Available Arguments\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Flag\u003C\u002Fth>\n      \u003Cth>Effect\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--to\u003C\u002Fcode> \u002F \u003Ccode>--cc\u003C\u002Fcode> \u002F \u003Ccode>--subject\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Recipients (repeatable or CSV) and message subject\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--body\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Inline message body\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--body-file\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Body read from a UTF-8 file (recommended, avoids inline)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--markdown\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Converts the Markdown body to HTML before sending (recommended for all client emails)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--html\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Body already in HTML format\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--attachment\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Attachment (repeatable) — fails immediately if the file is missing\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--draft-id\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Targets an existing draft for sending, preview, or validation\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>The \u003Ccode>--draft\u003C\u002Fcode> option requires \u003Ccode>--to\u003C\u002Fcode>, \u003Ccode>--subject\u003C\u002Fcode>, and a body (\u003Ccode>--body\u003C\u002Fcode> or \u003Ccode>--body-file\u003C\u002Fcode>) to be provided.\u003C\u002Fp>\n\n\u003Ch3>Automatic Validation Copy\u003C\u002Fh3>\n\n\u003Cp>Each time a draft is created, the system sends a copy to the manager with the subject prefixed \u003Ccode>[TO VALIDATE → &lt;recipient&gt;] &lt;subject&gt;\u003C\u002Fcode>. An anti-loop mechanism prevents this copy from being sent if the recipient is already the validation mailbox itself.\u003C\u002Fp>\n\n\u003Cp>Before displaying the draft, the system queries the tone management facade to recall the appropriate register for the recipient — a best-effort, non-blocking operation.\u003C\u002Fp>\n\n\u003Ch3>Actual Send and Archiving\u003C\u002Fh3>\n\n\u003Cp>The actual send goes through SMTP and then archives a copy in the IMAP mailbox's Sent folder. Several naming variants for this folder are supported (OVH, Gmail conventions, UTF-7 encoding). If IMAP archiving fails, the email has still been sent — this is a non-blocking warning. The HTML signature is appended automatically at send time.\u003C\u002Fp>\n\n\u003Ch3>SMTP\u002FIMAP Configuration\u003C\u002Fh3>\n\n\u003Cp>Connection parameters (SMTP server, port, credentials, sending mailbox, IMAP server) are read from the repository's environment files. A startup check lists any missing variable. Using port 465 activates SMTP over SSL mode.\u003C\u002Fp>\n\n\u003Ch2 id=\"scan-av-obligatoire\">Mandatory Antivirus Scan Before Opening (P0)\u003C\u002Fh2>\n\n\u003Cp>The antivirus scan facade is under the responsibility of agent \u003Cstrong>Mitnick\u003C\u002Fstrong>. \u003Cstrong>No extracted attachment is opened or parsed before a \u003Ccode>clean\u003C\u002Fcode> verdict.\u003C\u002Fstrong> Use of an external cloud service (such as an online analysis service) is excluded to protect client data. This safeguard is absolute and cannot be bypassed.\u003C\u002Fp>\n\n\u003Ch3>Analysis Layers\u003C\u002Fh3>\n\n\u003Cp>The scanner applies five successive layers:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Executable rejection:\u003C\u002Fstrong> any recognised executable extension (Windows\u002FLinux binaries, shell scripts, PowerShell, JavaScript, VBScript, Java archives…) immediately triggers an \u003Ccode>infected\u003C\u002Fcode> verdict.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>ClamAV:\u003C\u002Fstrong> signature-based analysis via the system binary. A positive match triggers \u003Ccode>infected\u003C\u002Fcode> in fail-fast mode. If ClamAV is not installed on the machine, the scan returns an explicit error.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>PDF heuristics:\u003C\u002Fstrong> regex-based detection of dangerous PDF constructs (embedded JavaScript, automatic actions, submitted forms, rich media…) on the first megabytes of the file → \u003Ccode>suspicious\u003C\u002Fcode> verdict.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Office macros:\u003C\u002Fstrong> for macro-enabled Office formats, searches for auto-execution patterns and system calls via the \u003Ccode>olevba\u003C\u002Fcode> tool (best-effort) → \u003Ccode>suspicious\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Archives:\u003C\u002Fstrong> no descent into archives; \u003Ccode>suspicious\u003C\u002Fcode> verdict by default, manual review required.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Clean Verdict Marker\u003C\u002Fh3>\n\n\u003Cp>On a \u003Ccode>clean\u003C\u002Fcode> verdict, an atomic JSON marker file (SHA-256 hash + timestamp) is written to a subdirectory of the attachment. Downstream processes operate in \u003Cem>fail-close\u003C\u002Fem> mode: the \u003Cstrong>absence\u003C\u002Fstrong> of the marker means \"not yet scanned\" and blocks processing. The write is atomic (temporary file + replacement); if it fails, the verdict remains correct — marker persistence is optional only.\u003C\u002Fp>\n\n\u003Ch3>Verdict Table\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Verdict\u003C\u002Fth>\n      \u003Cth>Meaning\u003C\u002Fth>\n      \u003Cth>CLI Exit Code\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>clean\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No signal detected — opening authorised\u003C\u002Ftd>\n      \u003Ctd>0\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>suspicious\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Static signals present — manual review or sandbox\u003C\u002Ftd>\n      \u003Ctd>1\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>infected\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>ClamAV match or executable — DO NOT OPEN\u003C\u002Ftd>\n      \u003Ctd>2\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>error\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Scan impossible (ClamAV unavailable, file missing…)\u003C\u002Ftd>\n      \u003Ctd>3\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>The command-line interface accepts one or more paths and returns the \u003Cstrong>worst verdict\u003C\u002Fstrong> across all analysed files. The inbound email processing pipeline consumes this scanner and refuses to proceed if attachments have not been declared safe.\u003C\u002Fp>\n\n\u003Ch2 id=\"zero-communication-client\">Doctrine — Zero Direct Client Communication (P0)\u003C\u002Fh2>\n\n\u003Cp>The system \u003Cstrong>never\u003C\u002Fstrong> communicates directly with clients. All external communication goes through the human manager.\u003C\u002Fp>\n\n\u003Cp>Several technical safeguards enforce this rule within this scope:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>The sending facade never sends to the client when creating a draft; it writes the draft and sends a validation copy to the manager. Only the explicit post-validation send delivers the message to the client.\u003C\u002Fli>\n  \u003Cli>A prevention hook intercepts any attempt to directly use low-level mail libraries outside the whitelist of authorised facades. This safeguard makes the facade non-bypassable.\u003C\u002Fli>\n  \u003Cli>The inbound email classification engine never derives the recipient or the scope of a reply from the message body (anti-injection protection). Reply drafts remain in \u003Ccode>pending\u003C\u002Fcode> status until human validation.\u003C\u002Fli>\n  \u003Cli>Formal address (vouvoiement) is used systematically for all external contacts. Appointments are scheduled exclusively via an online booking link — a hard-coded time slot is never proposed in an email.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2 id=\"taches-planifiees\">Active Scheduled Tasks\u003C\u002Fh2>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Frequency\u003C\u002Fth>\n      \u003Cth>Role\u003C\u002Fth>\n      \u003Cth>Pipeline\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Every minute\u003C\u002Ftd>\n      \u003Ctd>Inbox synchronisation\u003C\u002Ftd>\n      \u003Ctd>IMAP collector → inbound email storage table (\u003Cstrong>sole live collector\u003C\u002Fstrong>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Every minute\u003C\u002Ftd>\n      \u003Ctd>Processing pipeline polling\u003C\u002Ftd>\n      \u003Ctd>New email detection → processing queue\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Every 15 minutes\u003C\u002Ftd>\n      \u003Ctd>Orphan lock monitoring\u003C\u002Ftd>\n      \u003Ctd>Automatic release of blocking pipeline locks\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Every 5 minutes (with a 2-min initial delay)\u003C\u002Ftd>\n      \u003Ctd>Agentic spawn\u003C\u002Ftd>\n      \u003Ctd>Launch of headless instances for \u003Ccode>chantier\u003C\u002Fcode> and \u003Ccode>question\u003C\u002Fcode> intents only (fixed whitelist)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cstrong>Disabled collector:\u003C\u002Fstrong> the former collector that called the web application facade via an HTTP route has been \u003Cstrong>commented out\u003C\u002Fstrong> since May 2026. It caused saturation of the application server's event loop (cascading IMAP\u002Fdatabase timeouts). It has been replaced by the external Python worker listed above. Do not reactivate it without fixing the root cause.\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"pieges-connus\">Known Pitfalls\u003C\u002Fh2>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Two distinct tables, two isolated worlds.\u003C\u002Fstrong> The hub email storage table (indexed by IMAP identifier) and the processing pipeline tables (indexed by account and message identifier) cannot be joined.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Read flag set after insertion:\u003C\u002Fstrong> a crash between fetch and insertion leaves the email unread, and therefore reprocessed on the next cycle. Idempotency is ensured by an insert conflict constraint.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Mandatory silent fetch (PEEK):\u003C\u002Fstrong> the processing pipeline fetch must use PEEK mode to avoid consuming the unread flag prematurely.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>--markdown\u003C\u002Fcode> recommended\u003C\u002Fstrong> for all client emails (otherwise the body appears raw, unformatted). Always prefer \u003Ccode>--body-file\u003C\u002Fcode> over inline \u003Ccode>--body\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Scan-first non-negotiable:\u003C\u002Fstrong> opening an attachment before a \u003Ccode>clean\u003C\u002Fcode> verdict constitutes a P0-level architectural debt.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>un composant interne drift:\u003C\u002Fstrong> the internal description of the email collector mentions a 5-minute interval, but it actually runs every minute.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Email anti-loop:\u003C\u002Fstrong> the founder's contact mailbox intentionally forwards to the pipeline mailbox. Only the latter is filtered as an automatic sender. Do not add the founder's mailbox to the list of senders to ignore.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>chantier\u003C\u002Fcode> intent:\u003C\u002Fstrong> the classification engine never creates a chantier automatically — this is an explicit CLI action. This prevents the creation of orphan chantiers without a mission brief.\u003C\u002Fli>\n\u003C\u002Ful>",{"slug":14,"chapterNum":15,"title":16,"titleEn":17},"hub","06","Le Hub (\u002Fhub\u002F*)","The Hub (\u002Fhub\u002F*)",{"slug":19,"chapterNum":20,"title":21,"titleEn":22},"memory","08","Mémoire & apprentissage — architecture à trois niveaux","Memory & Learning — Three-Level Architecture",{"chapters":24},[25,32,39,46,53,60,63,64,67,74,81],{"slug":26,"chapterNum":27,"title":28,"titleEn":29,"summary":30,"summaryEn":31},"overview","01","Vue d'ensemble du harness agentique","Agentic harness overview","Le harness transforme une demande (courriel, console, cron) en action déployée en enchaînant classification, spawn LLM via pseudo-TTY, validation qualité à deux niveaux et boucle d'apprentissage, le tout sans état métier hors base de données.","The harness transforms a request (email, console, cron) into a deployed action by chaining intent classification, pseudo-TTY LLM spawn, two-level quality validation, and a learning loop — with no business state stored outside the database.",{"slug":33,"chapterNum":34,"title":35,"titleEn":36,"summary":37,"summaryEn":38},"data-layer","02","La couche données","The Data Layer","Décrit l'architecture données de Synedre OS : une base PostgreSQL unique (un composant interne\u002Fun composant interne), les trois chemins d'accès (Nuxt\u002FNitro, Python agentique, Drizzle ORM DDL), les familles de tables par préfixe et les conventions de nommage associées.","Describes the Synedre OS data architecture: a single PostgreSQL database (un composant interne\u002Fun composant interne), the three access paths (Nuxt\u002FNitro, agentic Python, Drizzle ORM DDL), the table families by prefix, and the associated naming conventions.",{"slug":40,"chapterNum":41,"title":42,"titleEn":43,"summary":44,"summaryEn":45},"agentic-core","03","Le cœur agentique : Atlas et les agents","The Agentic Core: Atlas and the Agents","Décrit l'architecture du moteur agentique : classification LLM des emails entrants, spawn headless de Claude Code via pseudo-TTY, injection des personas agents et orchestration post-spawn (deploy, QA, récap).","Describes the architecture of the agentic engine: LLM classification of incoming emails, headless spawning of Claude Code via pseudo-TTY, injection of agent personas, and post-spawn orchestration (deploy, QA, recap).",{"slug":47,"chapterNum":48,"title":49,"titleEn":50,"summary":51,"summaryEn":52},"chantiers","04","Chantiers, travaux & tâches — modèle de données et API","Worksites, Jobs & Tasks — Data Model and API","Décrit la hiérarchie chantier\u002Ftravail\u002Ftâche de Synedre OS, le modèle de données DB, les statuts\u002Fscopes canoniques et l'API Python associée.","Describes the worksite\u002Fjob\u002Ftask hierarchy in Synedre OS, the DB data model, canonical statuses\u002Fscopes, and the associated Python API.",{"slug":54,"chapterNum":55,"title":56,"titleEn":57,"summary":58,"summaryEn":59},"automates","05","Automates, crons & runs","Automations, crons & runs","Décrit la couche d'exécution non-conversationnelle de Synedre OS : les 209 façades Python, leur enrobage cron, le registre un composant interne et la frontière entre doctrine run et unité agent.","Describes the non-conversational execution layer of Synedre OS: the 209 Python facades, their cron wrapper, the un composant interne registry, and the boundary between run doctrine and agent unit.",{"slug":14,"chapterNum":15,"title":16,"titleEn":17,"summary":61,"summaryEn":62},"Présentation de l'interface d'administration privée Synedre OS (mothership-app), son architecture en couches Nuxt, la carte de ses modules et pages, ainsi que le système d'authentification et de session.","Presentation of the Synedre OS private administration interface (mothership-app), its Nuxt layered architecture, the map of its modules and pages, as well as the authentication and session management system.",{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10},{"slug":19,"chapterNum":20,"title":21,"titleEn":22,"summary":65,"summaryEn":66},"Ce chapitre décrit les trois niveaux de mémoire du harness Synedre OS (réflexe, Zettelkasten, vectoriel pgvector), les automates d'indexation associés et la boucle de capitalisation des erreurs en règles réutilisables.","This chapter describes the three memory levels of the Synedre OS harness (reflex, Zettelkasten, vector pgvector), the associated indexing automata, and the loop for capitalizing errors into reusable rules.",{"slug":68,"chapterNum":69,"title":70,"titleEn":71,"summary":72,"summaryEn":73},"deploy","09","Déploiement & infrastructure","Deployment & infrastructure","Décrit le pipeline de mise en ligne Synedre OS : asymétrie entre .\u002Fdeploy (IA, preprod) et .\u002Fship (Alex, production), le dispatcher YAML, le pattern build-host sans build VPS, et les règles associées (secrets, commit-avant-deploy, drift, smoke).","Describes the Synedre OS release pipeline: asymmetry between .\u002Fdeploy (AI, preprod) and .\u002Fship (Alex, production), the YAML dispatcher, the build-host pattern without build VPS, and the associated rules (secrets, commit-before-deploy, drift, smoke).",{"slug":75,"chapterNum":76,"title":77,"titleEn":78,"summary":79,"summaryEn":80},"facades","10","catalogue des façades et points d'entrée du harness","harness facade and entry-point catalogue","comment le harness synedre organise ses centaines d'automates en familles fonctionnelles, avec les garde-fous en temps réel qui encadrent chaque action.","how the synedre harness organizes its hundreds of automations into functional families, with the real-time guard-rails that frame every action.",{"slug":82,"chapterNum":83,"title":84,"titleEn":85,"summary":86,"summaryEn":87},"skills-hooks","11","Skills, agents & hooks — harness agentique Synedre OS","Skills, agents & hooks — Synedre OS agentic harness","Décrit le câblage complet du harness Claude Code de synedre-os : skills disponibles, sous-agents délégables et hooks d'événements qui imposent la doctrine commit-en-flux, garde-fous prod\u002Femail et injection mémoire.","Describes the complete wiring of the synedre-os Claude Code harness: available skills, delegatable sub-agents, and event hooks that enforce the commit-in-flow doctrine, prod\u002Femail guardrails, and memory injection."]