Files
rpa_vision_v3/docs/DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md

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/check503 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)

  1. Aucune logique canary : decide_update recevait machine_id mais l'ignorait pour choisir la version. La version cible était une seule var globale RPA_AGENT_LATEST_VERSION → une MAJ partait sur toute la flotte d'un coup. C'est le trou de sécurité n°1.
  2. Le noyau client n'était pas wiré : updater.py n'était appelé nulle part. main.py ne l'importait pas. Aucun caller HTTP de /agents/update/check.
  3. 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) : colonne version + last_seen_at par machine_id. Le dashboard Fleet (web_dashboard/templates/index.html:2247) affiche déjà la version par poste.
  • Limite connue : version n'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 de machine_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_versionstable ;
  • canary_version doit être strictement plus récente que stable (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 = vider RPA_AGENT_CANARY_MACHINES / remettre l'ancienne RPA_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?) :

  1. GATE auto_update_enabled() (RPA_AUTO_UPDATE_ENABLED, défaut OFF) — si OFF, ne fait strictement rien, aucun appel réseau ;
  2. checker(...) → réponse serveur (défaut = _default_update_checker : GET vers l'endpoint gated, Bearer, 503→None, jamais d'exception) ;
  3. should_update(...) → plan (double garde semver anti-downgrade) ;
  4. download_update(...) → ZIP en staging + vérif SHA256 (fichiers vivants jamais touchés) ;
  5. apply_update(staged) = stub no-op → résultat applied: 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_VERSION envoyé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) : backup Lea\Lea_prev\, swap Lea_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. Si Lea.bat ne trouve pas le marker au démarrage suivant (crash au boot), il restaure Lea_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.pyAUTO_UPDATE_INTERVAL_S, AUTO_UPDATE_STAGING_DIR.
  • agent_v0/agent_v1/main.py — thread _auto_update_loop gated + import config.
  • tests/unit/test_agent_v1_updater.py — TDD run_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/check répond 200 pour un machine_id quelconque avec update_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çoit update_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_VERSION remontée), marker boot_ok écrit, heartbeat DGX sain > 24 h, zéro régression fonctionnelle (enregistrement + replay OK).
  • ROLLBACK : automatique par watchdog Lea.bat si pas de boot_ok au boot ; manuel = restaurer Lea_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> + vider RPA_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)

  1. Swap réel + watchdog rollback (§4.5) — la brique manquante n°1, révision humaine.
  2. Inventaire de version vive : rafraîchir enrolled_agents.version au heartbeat (le serveur saurait exactement quelle version tourne où — utile pour piloter le canary depuis le dashboard).
  3. Signature détachée du manifeste (durcissement au-delà du SHA256 sur canal Bearer).
  4. 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 payload code-only incrémental attendu par le contrat d'URL.
  5. 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)