14 KiB
DESIGN — MAJ silencieuse du client Léa + déploiement CANARY (DETTE-022 v2)
Date : 2026-07-01
Branche : feat/push-log-dgx
Statut : premier draft fonctionnel — GATED OFF partout, aucun swap réel, revue supervisée Dom requise avant toute activation
⚠️ RIEN N'A ÉTÉ DÉPLOYÉ. Aucun SSH poste, aucune action fleet. Ce document + le code de la branche sont un livrable de conception/implémentation pour revue.
1. Problème
Pousser des correctifs au client Léa sur ~19 postes cliniques live (Wallerstein) sans patch manuel DSI et sans déranger les TIM en plein travail. Contrainte absolue : une MAJ ratée peut briquer toute la flotte. Le mécanisme doit donc être conservateur : canary lent + rollback béton plutôt que rapide et risqué.
2. État de départ (stub commit 813b33b47) — ce qui existait déjà
Le noyau était plus avancé qu'un simple squelette. Déjà présent et testé (vert) :
| Brique | Fichier | Rôle |
|---|---|---|
| Décision serveur PURE | agent_v0/server_v1/update_check.py |
parse_version/is_newer (semver correct : 1.0.2 < 1.0.10), decide_update(), build_download_url() |
| Endpoint serveur gated | agent_v0/server_v1/api_stream.py:7843+ |
GET /api/v1/agents/update/check — 503 si RPA_AUTO_UPDATE_SERVER_ENABLED OFF, Bearer requis |
| Noyau client PUR | agent_v0/agent_v1/network/updater.py |
auto_update_enabled() (flag RPA_AUTO_UPDATE_ENABLED, défaut OFF), should_update() (double garde anti-downgrade), download_update() (staging + SHA256, ne touche jamais les fichiers vivants) |
| Stubs dangereux (no-op) | updater.py:246+ |
apply_update() / write_boot_ok_marker() — réservés révision humaine (swap fichiers, édition Lea.bat, restart) |
| Version agent | agent_v0/agent_v1/config.py:30 |
AGENT_VERSION = os.environ.get("RPA_AGENT_VERSION", "1.0.1") (amorcé 105ade959) |
| Tests | tests/unit/test_update_check_server.py, tests/unit/test_agent_v1_updater.py, tests/integration/test_update_check_endpoint.py |
R2/R3 verts |
Ce qui MANQUAIT (comblé par ce draft)
- Aucune logique canary :
decide_updaterecevaitmachine_idmais l'ignorait pour choisir la version. La version cible était une seule var globaleRPA_AGENT_LATEST_VERSION→ une MAJ partait sur toute la flotte d'un coup. C'est le trou de sécurité n°1. - Le noyau client n'était pas wiré :
updater.pyn'était appelé nulle part.main.pyne l'importait pas. Aucun caller HTTP de/agents/update/check. - Pas d'orchestrateur reliant check → décide → download (staging) côté client.
3. Fleet / versioning existant (réutilisé, pas réinventé)
- Registre SQLite
enrolled_agents(agent_v0/server_v1/agent_registry.py:105) : colonneversion+last_seen_atparmachine_id. Le dashboard Fleet (web_dashboard/templates/index.html:2247) affiche déjà la version par poste. - Limite connue :
versionn'est écrite qu'à l'enroll(installateur), pas rafraîchie par le heartbeat runtime. Le serveur connaît donc la version installée, pas forcément la version vive. → inventaire de version = amélioration future (voir §8), non bloquante pour le canary (le canary est piloté par une allow-list demachine_id, pas par l'inventaire).
4. Design retenu (et pourquoi)
Aligné sur l'état de l'art self-update desktop 2025 (canary / blue-green / A-B swap + watchdog rollback + intégrité + version) — sources en fin de doc.
4.1 CANARY côté serveur — la keystone de sécurité (IMPLÉMENTÉ)
Nouveau module PUR agent_v0/server_v1/update_policy.py. Il résout la version cible
PAR MACHINE :
- poste dans l'allow-list canary →
canary_version(la nouvelle) ; - tous les autres postes →
stable_version(le floor, inchangé).
Piloté 100 % par variables d'environnement serveur (aucun rebuild, aucune DSI) :
RPA_AGENT_STABLE_VERSION # version servie à TOUTE la flotte (défaut 1.0.1)
RPA_AGENT_CANARY_VERSION # version servie AUX SEULS postes canary (optionnel)
RPA_AGENT_CANARY_MACHINES # allow-list CSV des machine_id canary
Garde-fous du résolveur (tous prudents par défaut) :
- machine_id absent / liste vide / pas de
canary_version→ stable ; canary_versiondoit être strictement plus récente questable(sinon on sert stable — jamais de recul) ;- ne lève jamais ; version illisible → retombe sur stable via le comparateur semver tolérant.
Wiring : _latest_agent_version(machine_id) dans api_stream.py appelle
resolve_target_version_from_env(machine_id). Rétrocompat : si l'ancienne
RPA_AGENT_LATEST_VERSION est positionnée, elle prime (pas de régression d'un
déploiement existant).
Effet : la 1.0.2 ne peut PAS fuiter hors de la liste canary. Blast radius =
la liste. On démarre la liste = lea-4zbgwxty (Émilie) seul.
Promotion = quand le canary est validé : RPA_AGENT_STABLE_VERSION=<canary>
- vider
RPA_AGENT_CANARY_MACHINES→ toute la flotte suit. Rollback canary = viderRPA_AGENT_CANARY_MACHINES/ remettre l'ancienneRPA_AGENT_CANARY_VERSION→ le prochain check ne propose plus rien.
4.2 Orchestrateur client (IMPLÉMENTÉ, GATED, sans swap)
updater.run_update_cycle(local_version, machine_id, staging_dir, checker?, downloader?) :
- GATE
auto_update_enabled()(RPA_AUTO_UPDATE_ENABLED, défaut OFF) — si OFF, ne fait strictement rien, aucun appel réseau ; checker(...)→ réponse serveur (défaut =_default_update_checker: GET vers l'endpoint gated, Bearer, 503→None, jamais d'exception) ;should_update(...)→ plan (double garde semver anti-downgrade) ;download_update(...)→ ZIP en staging + vérif SHA256 (fichiers vivants jamais touchés) ;apply_update(staged)= stub no-op → résultatapplied: False. Le swap réel n'est PAS fait par du code d'agent.
Statuts retournés (diagnostic/log) : disabled | check_failed | up_to_date | download_failed | staged. Best-effort total : aucune exception ne remonte (ne casse jamais Léa).
4.3 Wiring runtime (IMPLÉMENTÉ, GATED)
main.py : thread daemon _auto_update_loop, démarré uniquement si
AUTO_UPDATE_ENABLED, à côté des boucles permanentes existantes (même pattern
que le log shipper). Sécurité « au bon moment » : on ne stage PAS pendant un
enregistrement (self.session_id) ou un replay actif (self._replay_active) —
pas de perturbation du travail TIM. Intervalle RPA_AUTO_UPDATE_INTERVAL_S
(défaut 3600 s / 1 h : une MAJ n'est jamais urgente).
4.4 Intégrité + version
- Intégrité : SHA256 vérifié dans
download_update(déjà présent) ; mismatch → rejet + staging propre. - Version :
AGENT_VERSIONenvoyée à chaque check (current_version) ; le serveur choisit la cible par machine. - Signature (à ajouter, §8) : SHA256 seul protège de la corruption, pas de l'usurpation. Recommandation : signer le manifeste (le SHA256 vient d'un canal authentifié — l'endpoint Bearer — donc chaîne acceptable pour le POC ; signature détachée = durcissement futur).
4.5 Swap atomique + rollback (SPEC — réservé révision humaine, PAS codé par agent)
Le swap réel reste dans les stubs apply_update / write_boot_ok_marker et
dans Lea.bat. Un agent ne doit pas écrire de code qui écrase des binaires
vivants ni relance un process. Spec cible pour la revue humaine :
- A-B / staging : le ZIP est extrait dans
Lea_next\. Au prochain démarrage,Lea.bat(hors-process) : backupLea\→Lea_prev\, swapLea_next\→Lea\, lance la nouvelle version. - Watchdog rollback : la nouvelle version doit écrire un marker
boot_ok_<version>après ~60 s de heartbeat DGX sain + session OK. SiLea.batne trouve pas le marker au démarrage suivant (crash au boot), il restaureLea_prev\automatiquement. Cible « rollback latency » < 90 s (état de l'art). - Cas edge (documenté dans les stubs) : DGX down ≠ Léa N+1 buguée — le health-check doit distinguer les deux pour éviter un faux rollback.
5. Fichiers touchés (cette branche)
Ajouts
agent_v0/server_v1/update_policy.py— canary PUR (résolveur par machine + lecture env).tests/unit/test_update_policy_canary.py— TDD canary (résolveur + env).
Modifs
agent_v0/server_v1/api_stream.py—_latest_agent_version(machine_id)canary-aware (rétrocompat legacy) + docstring endpoint.agent_v0/agent_v1/network/updater.py—_default_update_checker()+run_update_cycle()(orchestrateur gated, sans swap).agent_v0/agent_v1/config.py—AUTO_UPDATE_INTERVAL_S,AUTO_UPDATE_STAGING_DIR.agent_v0/agent_v1/main.py— thread_auto_update_loopgated + import config.tests/unit/test_agent_v1_updater.py— TDDrun_update_cycle(gate off, up-to-date, staged, sha mismatch, checker raise).tests/integration/test_update_check_endpoint.py— TDD canary HTTP (poste canary vs hors-canary).deploy/lea_package/config.txt— flags client MAJ documentés (commentés, OFF).
Intacts (réservés révision humaine) : updater.apply_update, updater.write_boot_ok_marker, Lea.bat.
6. Matrice des flags (tout OFF par défaut)
| Flag | Côté | Défaut | Effet |
|---|---|---|---|
RPA_AUTO_UPDATE_SERVER_ENABLED |
serveur | OFF (503) | active l'endpoint de décision |
RPA_AGENT_STABLE_VERSION |
serveur | 1.0.1 |
version floor de toute la flotte |
RPA_AGENT_CANARY_VERSION |
serveur | — | nouvelle version, postes canary seulement |
RPA_AGENT_CANARY_MACHINES |
serveur | — | allow-list CSV canary |
RPA_AGENT_LATEST_VERSION (legacy) |
serveur | — | si set, prime sur le canary (rétrocompat) |
RPA_AUTO_UPDATE_ENABLED |
client | OFF | active la boucle de check + staging |
RPA_AUTO_UPDATE_INTERVAL_S |
client | 3600 |
intervalle de check |
7. Plan de déploiement CANARY (étapes + critères GO / ROLLBACK)
Prérequis avant TOUTE étape : la mécanique de swap réel (§4.5) doit avoir été implémentée et revue par un humain. Tant qu'elle est en stub, ce plan ne fait que stager un ZIP (aucun poste ne change réellement de version) — ce qui est déjà utile pour valider la chaîne check/download/intégrité à vide.
Étape 0 — Serveur seul (aucun poste touché)
- Action :
RPA_AUTO_UPDATE_SERVER_ENABLED=true,RPA_AGENT_STABLE_VERSION=1.0.1, PAS de canary encore. - GO si :
GET /agents/update/checkrépond 200 pour unmachine_idquelconque avecupdate_available:false. Aucun poste n'a la MAJ activée côté client. - ROLLBACK : repasser le flag serveur OFF.
Étape 1 — Canary Émilie, staging seul
- Action serveur :
RPA_AGENT_CANARY_VERSION=<nouvelle>,RPA_AGENT_CANARY_MACHINES=lea-4zbgwxty. - Action poste Émilie (config.txt) :
RPA_AUTO_UPDATE_ENABLED=true. - GO si : dans les logs d'Émilie (remontés par le push-log DGX),
[UPDATE] MAJ <v> téléchargée en staging (SHA256=True), ZIP présent dans le staging,applied:False, Léa continue de tourner normalement (session/replay non perturbés). Vérifier qu'AUCUN autre poste ne reçoitupdate_available:true. - ROLLBACK : vider
RPA_AGENT_CANARY_MACHINES(le check ne propose plus rien). Aucun impact : rien n'avait été appliqué.
Étape 2 — Canary Émilie, swap réel (après implémentation humaine du §4.5)
- GO si : après redémarrage, Émilie tourne la nouvelle version (
AGENT_VERSIONremontée), markerboot_okécrit, heartbeat DGX sain > 24 h, zéro régression fonctionnelle (enregistrement + replay OK). - ROLLBACK : automatique par watchdog
Lea.batsi pas deboot_okau boot ; manuel = restaurerLea_prev\+ vider la liste canary.
Étape 3 — Élargissement progressif (rings)
- Ajouter 2-3 postes à
RPA_AGENT_CANARY_MACHINES, attendre 48 h par palier. - GO/ROLLBACK : mêmes critères qu'étape 2, par palier.
Étape 4 — Promotion générale
RPA_AGENT_STABLE_VERSION=<nouvelle>+ viderRPA_AGENT_CANARY_MACHINES.- Toute la flotte converge au rythme de son intervalle de check.
- ROLLBACK flotte : remettre
RPA_AGENT_STABLE_VERSIONà l'ancienne (les postes ne redescendent pas seuls — le swap-down reste une opération supervisée ; les nouveaux checks ne proposeront plus la MAJ).
8. Améliorations futures (hors périmètre de ce draft)
- Swap réel + watchdog rollback (§4.5) — la brique manquante n°1, révision humaine.
- Inventaire de version vive : rafraîchir
enrolled_agents.versionau heartbeat (le serveur saurait exactement quelle version tourne où — utile pour piloter le canary depuis le dashboard). - Signature détachée du manifeste (durcissement au-delà du SHA256 sur canal Bearer).
- Endpoint de download versionné : aujourd'hui
/api/fleet/download/<machine_id>(dashboard) sert l'installateur complet et ignore?type=&version=; il faudra qu'il serve le vrai payloadcode-onlyincrémental attendu par le contrat d'URL. - Auto-report du résultat de swap (succès/rollback) au serveur pour un tableau de bord canary.
9. Sources (état de l'art self-update desktop / canary 2025)
- Rollback Strategies for Enterprise: 2025 Best Practices — sparkco.ai
- Canary Deployment with Auto-Rollback for AI Agents — antigravitylab.net
- awesome-agentic-patterns — canary rollout & automatic rollback
- What is Canary Testing — aqua-cloud.io
- Rollback Automation Best Practices for CI/CD — hokstadconsulting.com