feat(update): swap atomique + rollback (Lea.bat) + confirmation boot (main.py)

Implémente le SWAP réel de la MAJ silencieuse (DETTE-022), remplace les stubs :
- updater.apply_update : ARME le swap (extrait le ZIP -> agent_v1_new/ +
  marqueur UPDATE_READY, garde-fou zip-slip). N'écrase JAMAIS le vivant.
- updater.write_boot_ok_marker : désarme le rollback (retire PENDING_BOOT).
- Lea.bat (template + embed généré par configure_embed.ps1) : swap ATOMIQUE
  par renames (agent_v1 -> agent_v1_prev backup ; agent_v1_new -> agent_v1)
  + rollback auto si PENDING_BOOT persiste (boot précédent non confirmé).
- main.py : confirme le boot après 90 s de liveness locale OU quit propre
  (évite un faux rollback ; RPA_BOOT_CONFIRM_DELAY_S surchargeable pour les tests).

Testable (Python) : 45 tests verts. Le swap OS (renames Lea.bat) + le câblage
main.py seront validés par le test Win 11 (step 0 pré-canary, dont le rollback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-07-01 14:10:34 +02:00
parent 5d235e49f1
commit a210e5ee32
5 changed files with 313 additions and 80 deletions

View File

@@ -168,6 +168,22 @@ class AgentV1:
target=self._auto_update_loop, daemon=True, name="lea-auto-update"
).start()
# MAJ silencieuse — confirmation de boot post-swap. Si Lea.bat vient
# d'appliquer une MAJ (marqueur PENDING_BOOT), on désarme le rollback
# après ~90 s de tourne STABLE (liveness LOCALE, indépendante du DGX).
# Un quit propre avant 90 s confirme aussi (cf. main()). Seul un vrai
# crash laisse PENDING_BOOT → rollback au prochain lancement.
if _pending_boot_marker_exists():
def _boot_confirm():
import os as _os
import time as _time
_time.sleep(float(_os.environ.get("RPA_BOOT_CONFIRM_DELAY_S", "90")))
if self.running:
_confirm_boot_ok()
threading.Thread(
target=_boot_confirm, daemon=True, name="lea-boot-confirm"
).start()
# Mini-serveur HTTP pour captures a la demande (port 5006)
self._capture_server = CaptureServer()
self._capture_server.start()
@@ -718,6 +734,31 @@ def _agent_should_live(agent) -> bool:
return True
def _pending_boot_marker_exists() -> bool:
"""True si Lea.bat a posé PENDING_BOOT (boot post-MAJ à valider)."""
try:
from .network.updater import _resolve_app_dir
return (_resolve_app_dir(None) / "PENDING_BOOT").exists()
except Exception:
return False
def _confirm_boot_ok() -> None:
"""Confirme un boot post-MAJ : écrit boot_ok + retire PENDING_BOOT.
Désarme le rollback de Lea.bat. No-op si pas de PENDING_BOOT (boot normal).
Best-effort — ne doit jamais casser l'arrêt/la vie de Léa.
"""
try:
if not _pending_boot_marker_exists():
return
from .network import updater
updater.write_boot_ok_marker(AGENT_VERSION)
logger.info("[MAJ] Boot confirmé (v%s) — rollback désarmé", AGENT_VERSION)
except Exception as e: # noqa: BLE001
logger.debug("confirm_boot_ok: %s", e)
def main():
from .ui.session_watchdog import InteractiveSessionWatchdog
@@ -738,6 +779,10 @@ def main():
try:
watchdog.run()
# Sortie normale du watchdog = quit propre (tray / session) → le boot
# était sain : on confirme (couvre un quit AVANT les 90 s, évite un faux
# rollback). No-op si ce n'est pas un boot post-MAJ.
_confirm_boot_ok()
except KeyboardInterrupt:
logger.info("[MAIN] Interruption clavier — arret propre")
except Exception: