Chapitres
Sur cette page
DOC-05 / Référence technique · Chapitre 05
Automates, crons & runs
La couche d'exécution non-conversationnelle : façades, registre, schedulers, runs et worker navigateur distribué.
Agent (pense) vs automate (exécute)
Le harness sépare deux registres d'exécution. L'agent pense, décide et délègue selon une boucle ReAct ; il est porté par une persona LLM et déclenché par un spawn, par l'outil Task ou par une sy_task_run. L'automate exécute une routine déterministe ; c'est un script Python synedre/ac_*.py déclenché par cron, par CLI ou par le scheduler Nitro.
| Agent | Automate | |
|---|---|---|
| Rôle | Pense, décide, délègue (ReAct) | Exécute une routine déterministe |
| Support | Persona LLM | Script Python synedre/ac_*.py |
| Table | ps_ac_agents / sy_agents | sy_automates |
| Déclenchement | Spawn, outil Task, sy_task_run | cron, CLI, scheduledTasks Nitro |
Un automate ne réfléchit pas : il peut appeler un LLM (génération de contenu, classification) mais son flux de contrôle est codé en dur. La pensée vit dans les personas et dans Atlas ; l'exécution répétable vit dans les automates. La table de liaison sy_automate_agents rattache chaque automate à l'agent propriétaire ; un agent peut, à l'inverse, engendrer un automate ou une sy_task_run.
Les façades synedre/*.py
Le dossier synedre/ rassemble près de deux cents fichiers .py. Ce ne sont pas tous des automates planifiés : la majorité sont des façades — un point d'entrée unique pour une capacité donnée — auxquelles s'ajoutent des librairies partagées, des outils manuels et des scripts uniques.
Le registre canonique de classification vit en base dans sy_automates, pas dans les fichiers. Deux axes le structurent :
kind— la nature technique :recurring(planifié, cron ou Nitro),oneshot(à la demande),tool(invoqué par un agent ou un skill),lib(librairie partagée non exécutable seule) etmeta(méta-outillage, comme le wrapper de cron).caste— le groupe d'agents propriétaire. Familles observées : Vigies (audits/QA), Scribes (rédaction), Oracles (veille/reporting), Horlogers (infra/session/backup), Bâtisseurs (build/provision), Tisserands (SEO/maillage).
Taxonomie fonctionnelle des familles ac_*
| Famille | Rôle |
|---|---|
| Atlas (orchestrateur) | Pipeline inbox → intent → spawn ; santé et monitoring d'Atlas. |
| Audits | Détection de drift et de findings. Un exit non-zéro signifie « findings », pas « crash ». |
| Backups | Dump PG et fichiers vers stockage objet, test de restauration mensuel. |
| Brainstorm | Jobs asynchrones (worker en boucle, filet cron en mode unitaire). |
| Blog / SEO | Génération et hygiène de contenu, SEO technique. |
| Email / inbox | Lecture et écriture courriel. L'envoi client passe exclusivement par la façade unique. |
| Browser | Automatisation navigateur (worker headful résidentiel). |
| Bank / invoicing | Synchronisation bancaire, facturation récurrente. |
| Brand / veille | Surveillance de marque, veille technologique, synchro des avis. |
| Mémoire / apprentissage | RAG vectoriel, consolidation, post-mortems. |
| SRE / garde-fous | Surveillance des coûts et du runaway, alerting, garde-fou d'écriture en production. |
| Libs / infra | Briques partagées (DB, logger, env, fournisseur IA, rotation de logs, wrapper de cron) — non planifiées. |
Le wrapper de cron — watchdog d'enrobage
Tout automate planifié via crontab est lancé à travers un wrapper unique (ac_cron_wrapper.py). Ce watchdog porte six responsabilités :
- Chargement de l'environnement — il charge les fichiers d'env dans le processus sans écraser les variables déjà présentes.
- Disjoncteur — il lit le compteur d'échecs consécutifs en base ; au-delà d'un seuil (dix échecs d'affilée) le script est désactivé, avec une seule ligne loggée au moment du seuil pour éviter le spam.
- Exécution — il lance le script en sous-processus, avec un timeout par défaut de 300 secondes, relevé jusqu'à 1800 secondes pour quelques scripts longs.
- Auto-réparation — il classe le stderr puis tente un correctif suivi d'un retry : import manquant injecté, redirection d'un log non inscriptible, création d'un dossier absent (sandboxé sous la racine), installation d'un module dans le venv, retry réseau avec backoff.
- Soft-fail — pour les scripts d'audit, un exit non-zéro vaut findings et non crash : le compteur est remis à zéro et le code de sortie est propagé aux consommateurs en aval.
- Logging DB — chaque crash est inséré dans une table append-only avec le type d'erreur, la trace, le correctif appliqué, le succès du retry et l'état de désactivation.
Le verrou par script est implicite : il n'y a pas de
flockdans le wrapper lui-même. Quelques crons ajoutentflock -nà la main, mais la majorité des entrées s'appuient sur le timeout et la fréquence pour éviter le recouvrement.
La connexion DB passe par la façade ac_db avec un search_path sur le schéma du vaisseau-mère ; d'anciennes constantes liées à un moteur SQL aujourd'hui retiré subsistent en code mort.
Le système de runs
sy_run — la doctrine « run »
Un run est une exécution scopée pilotée par Atlas sur un périmètre (vaisseau-mère ou tenant), le périmètre chargeant automatiquement le contexte — machine, client, interlocuteur, boîte mail — depuis la base. La table sy_run porte la source, le trigger, le scope, le titre, le statut et les horodatages.
| Trigger | Source | Porte d'entrée |
|---|---|---|
email | atlas-inbox | forward classifié par intent (run, question, chantier, bruit). |
chat | console | console scopée en tête de /hub/runs (sélecteur de scope + chat Atlas). |
Un troisième trigger planifié (cron) existe en doctrine mais n'est pas encore observé en usage.
sy_task_run — l'unité d'exécution agent
Une sy_task_run n'est pas un run. C'est l'unité d'exécution déléguée à un agent, rattachée à une tâche ou à un chantier. Un run peut engendrer une sy_task_run quand Atlas délègue. Les colonnes clés portent le codename, l'agent, le prompt, le template, le périmètre, les critères de sortie, le statut, le journal de sortie, le code de retour, les tokens consommés et le coût.
L'exécuteur est ac_task_worker.py (cron à la minute). Sa logique : choisir la tâche pending la plus ancienne, la passer en running, spawn selon le template, streamer la sortie standard vers le journal, puis marquer completed ou failed. En mode --live il spawn le CLI claude avec des permissions calées sur le template : lecture seule pour research, lecture plus rapport pour audit, écriture sandboxée au périmètre pour code.
sy_automate_logs — journal d'exécution
La table sy_automate_logs est le journal verbeux par exécution d'automate (durée, étapes, compteurs, erreurs, avertissements, contexte), à distinguer de la table d'erreurs cron qui ne consigne que les crashs du wrapper.
sy_run (Atlas, scopé)
└─ engendre ─► sy_task_run (agent) ──log──► output_log
sy_automates (registre) ──planifié──► sy_automate_logs (journal verbeux)
└─► table d'erreurs cron (crashs wrapper)
Ordonnancement : deux schedulers en parallèle
Nitro scheduledTasks
Le vaisseau-mère Nuxt/Nitro expose des scheduledTasks (tâches audit:* dans un layer dédié, fusionné par Nuxt). Chaque tâche est gardée par un guard d'environnement qui court-circuite l'exécution hors contexte interne.
| Tâche | Cron |
|---|---|
email:queue-process | */2 * * * * |
audit:uptime-monitor | */15 * * * * |
audit:dictionary-watch | */30 * * * * |
audit:deps-watch | 0 2 * * * |
audit:daily-meet | 0 8 * * * |
audit:ssl-watch | 0 9 * * * |
audit:brand-watch | 0 12 * * * |
D'autres fichiers d'audit sont présents mais volontairement non planifiés. La synchronisation IMAP et la synchro client sont désactivées (blocage d'event-loop côté fournisseur mail).
Filet crontab Linux
Le scheduler Nitro est mort depuis une régression de mai 2026 (toutes les scheduledTasks dark). Le filet de survie est le crontab Linux de l'utilisateur hôte, qui porte aujourd'hui l'essentiel de la charge. Un risque de build stale existe en parallèle : un ./deploy est requis pour que le container reflète les scheduledTasks du fichier de config.
Les familles d'entrées actives :
- Via wrapper : monitoring (toutes les 2 min), backup nocturne, audit des automates et des backups, veille hebdomadaire, synchro bancaire, facturation récurrente, poll inbox Atlas, spawn Atlas, synchro inbox, consolidation onirique nocturne.
- Hors wrapper (
python3 -m synedre.X) : indexation du second cerveau, worker de tâches, indexation des skills, notification au Fondateur, détecteur de déblocage, alertes SRE et coût, détecteur de runaway, extraction d'événements de négociation (avec flock), scan de leads, refresh des KPI cicatrices, synchro des avis. - Brainstorm worker : lancé en boucle au reboot, avec un filet qui le relance s'il a disparu et un mode unitaire de rattrapage.
- Scripts shell / Node : scans de flotte et de dépendances, dumps PG et fichiers vers stockage objet (avec fallback de notification d'échec), test de restauration mensuel, synchro du second cerveau, nettoyage des locks de chantier, watchdog des locks Atlas.
- Curl direct (court-circuit du Nitro mort) : drain de la file mail et synchro session-replay vers les endpoints locaux du vaisseau-mère.
- Consolidation mémoire : consolidation mensuelle (avec flock).
- Boucle d'auto-amélioration : indexation des sessions, monitoring et détection de propositions de skills, détecteur de patterns, résumé ReAct quotidien, métriques de mémoire, rotation des logs. Plusieurs tournent hors wrapper, donc sans logging ni auto-réparation.
- Quarantaine OSS + backups distants : balayage de quarantaine OSS et backups PG distants — un par tenant — avec fallback de notification d'échec.
La plupart des entrées sont historiquement commentées avec des tags de cutover ou de pause temporaire : le crontab est un journal d'archéologie. Sur plus de deux cents lignes, une large majorité est commentée et seule une cinquantaine est réellement active.
Réconciliation registre ↔ ordonnancement (à faire).
sy_automatesdéclare une soixantaine d'automatesrecurring, mais le crontab ne porte qu'une cinquantaine de lignes actives et Nitro sept tâches (mort, voir plus haut). Aucun mapping ne relie aujourd'hui chaque automaterecurringà sa ligne crontab ou à sa tâche Nitro : on ne peut pas dire combien sont réellement planifiés vs orphelins. C'est le gap opérationnel majeur de cette couche.
Le browser-worker : automate distribué hors-VPS
La famille Browser est le seul sous-système où l'exécution sort physiquement du VPS. Deux raisons distinctes de quitter le datacenter, donc deux topologies d'egress à ne pas confondre :
| Topologie | Où tourne le navigateur | Traite |
|---|---|---|
| SOCKS5 résidentiel | Chromium headless sur le VPS du vaisseau-mère | la réputation IP |
| Worker headful distant | Chrome headful sur une machine résidentielle | le fingerprint navigateur |
Pourquoi headful résidentiel et non headless VPS
Un proxy SOCKS5 fait sortir le trafic du VPS par une IP résidentielle via un tunnel SSH inverse depuis la maison. C'est suffisant pour les sites qui ne discriminent que sur la réputation IP datacenter. Un garde-fou refuse de lancer si le proxy est down ou si l'egress résolu correspond à l'IP du VPS — zéro egress accidentel par le datacenter.
Mais un challenge managé type Cloudflare Turnstile ne juge pas l'IP : il juge le fingerprint du navigateur (marqueurs navigator.webdriver, plugins, WebRTC, signatures headless). Un Chromium headless garde un fingerprint de bot même avec une IP propre. Doctrine qui en découle : classer la protection avant de coder le flow. Si c'est du challenge managé, le headful sur machine résidentielle est obligatoire ; le stealth headless ne passe pas. D'où le worker headful : un vrai Chrome, fenêtre visible, sur une machine résidentielle toujours allumée à IP résidentielle. Pas de proxy ni de garde-fou egress ici : on est déjà sur l'IP résidentielle.
File sy_browser_job + cycle de vie d'un job
La file vit en base (sy_browser_job) — pattern enqueue / claim / finish / get. Colonnes : kind, payload (jsonb), status (queued|running|done|failed), result, error, host, attempts et horodatages, avec index pour le claim ordonné.
L'inversion de contrôle est la clé : le VPS n'a aucun accès entrant vers la machine résidentielle (NAT, IP dynamique). C'est le worker distant qui poll le VPS via SSH sortant.
[VPS] [Machine résidentielle]
Atlas / skill worker --loop (daemon)
│ enqueue │ (1) auto-update à l'idle (git pull + re-exec)
▼ │ (2) SSH --claim ───────────────┐
ac_browser_job_cli --enqueue │ │
│ INSERT status=queued ▼ │
▼ FOR UPDATE SKIP LOCKED ── claim atomique running ◄─────┘
sy_browser_job │ (3) dispatch par kind (whitelist stricte)
│ Chrome headful
▼
sy_browser_job ◄── SSH --finish (status/result/error via STDIN JSON)
- Enqueue (VPS) — refuse tout
kindhors whitelist, valide le JSON, INSERT enqueuedavec un dollar-quoting randomisé (anti-injection). - Claim atomique — prend le plus vieux
queuedenFOR UPDATE SKIP LOCKED LIMIT 1, passerunning, set le host et incrémente les tentatives. Deux workers ne peuvent pas se voler un job. - Dispatch (worker, headful) — route par
kindvers le handler, jamais d'evalou d'execdu payload (paramètres métier seulement). La whitelist des kinds doit rester synchronisée des deux côtés : tout ajout = éditer les deux fichiers. - Second facteur — quand un flow le réclame, le code 2FA n'arrive jamais sur la machine résidentielle par email : le worker rappelle le VPS, qui lit la boîte mail côté VPS et renvoie le code. Le code n'est jamais loggué en clair.
- Finish — le worker transmet
{status, result, error}en un JSON unique sur STDIN (jamais en argument shell, pour éviter la rupture sur newline ou guillemet). - Auto-update — en boucle, à l'état idle uniquement, le worker fait un
git pull --ff-onlythrottlé et se ré-exécute si du code a changé.
Le gate SSH (défense si la clé fuit)
La clé SSH du worker est installée côté VPS avec command=…,no-pty,no-agent-forwarding,no-X11-forwarding,no-port-forwarding dans authorized_keys. Si la machine résidentielle est compromise, la clé n'ouvre pas un shell VPS libre.
Le gate lit la commande SSH originale, la parse en tokens via shlex.split (jamais bash -c), et n'exécute que si : le préfixe est exactement l'appel attendu de la façade CLI ; le sous-flag appartient à un ensemble fermé (--claim, --finish, --get, --enqueue, second facteur) ; et chaque flag secondaire matche un pattern strict. Tout token inconnu ou valeur non conforme entraîne un refus. C'est aussi pourquoi le worker n'utilise jamais de préfixe cd … && : le gate exige une commande commençant exactement par le préfixe et gère lui-même le répertoire de travail. Pire cas d'une clé volée : polluer la file, pas exécuter du code arbitraire.
PII & screenshots. Les résultats reviennent en
resultjsonb dans la base privée — jamais loggués en clair par le worker (seul un compteur l'est). Les screenshots de debug sont purgés en fin de run par le module métier. Un bug latent est noté : le timeout de job n'est pas appliqué, donc un job navigateur peut hang indéfiniment.
Garde-fous & opérations
- Disjoncteur : le wrapper de cron désactive un automate après dix crashs consécutifs.
- Audit quotidien : un automate relit chaque nuit la table d'erreurs cron et le journal d'exécution.
- Coût / runaway : alertes de coût, détecteur de runaway et alertes SRE à intervalles courts.
- Backups + test de restauration : dumps nocturnes vers stockage objet et test de restauration mensuel.
- Déblocage : un détecteur ré-tente les
sy_task_runbloquées (maximum quatre tentatives, avec kill-switch).
Dette signalée
- Cron cassé silencieux : une ligne quotidienne active pointe vers un script qui n'existe plus, et l'appel est en
python3direct (hors wrapper) — échec quotidien silencieux, sans logging ni auto-réparation. À supprimer ou recâbler vers la vraie façade via le wrapper. - Anti-leak P0 : certains automates héritent d'un modèle de chargement de configuration à moderniser ; le pattern propre (lecture par variable d'environnement) est déjà disponible.
- Scheduler Nitro mort → dépendance totale au crontab Linux, point de défaillance unique non supervisé hors du lancement au reboot.
- Incohérence de casse dans les colonnes de classification de
sy_automates. - Pas de lock générique dans le wrapper de cron ; recouvrement possible sur les jobs lents non protégés par
flock.