Handoff auto-suffisant pour reprise parallèle du sujet installateur Léa
(install silencieuse dashboard + MAJ AD/GPO + procédure), isolé des autres
sujets. Sans secrets (renvois reference_credentials). Réf commit 48b1fc280.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deux canaux de déploiement des clients Léa, en s'appuyant sur l'installeur EXE
v1.0.2 existant (per-user, upgrade-safe, mode /VERYSILENT /CONFIG=).
- Route dashboard `GET /api/fleet/installer` (web_dashboard/app.py, insertion pure,
125 lignes, 0 suppression) : sert un ZIP {EXE + install_silencieux.bat + enroll.txt
(SERVER_URL/token injectés via _resolve_public_server_url) + LISEZMOI} pour un poste
NEUF. L'EXE génère lui-même le machine_id → pas de pré-enregistrement (distinct de
la voie ZIP download_agent_package). Tri EXE en semver (1.0.10 > 1.0.2). Testée e2e.
- deploy/installer/silent/ : lanceur d'install silencieuse + gabarit enroll + notice.
- deploy/gpo/Deploy-LeaUpdate.ps1 : MAJ upgrade-safe via GPO **logon script user-context**
(install per-user invisible au contexte SYSTEM). Détection version = **DisplayVersion
du registre HKCU** (primaire pour install EXE ; le config.txt EXE ne porte PAS la
version — vérifié sur poste réel), fallback RPA_AGENT_VERSION (installs ZIP).
Comparaison semver, idempotent, journalisation best-effort silencieuse, non bloquant
à l'ouverture de session. Options -DryRun / -TargetVersionOverride / -LogShareOverride.
**Validé sur poste réel .11 (Windows PowerShell 5.1)** : branche « déjà à jour » et
branche « MAJ requise » (DryRun, 0 install), détection = 1.0.2.
- docs/PROCEDURE_DEPLOIEMENT_LEA_JULIEN.md : procédure (poste neuf / MAJ manuelle / GPO / rollback).
- EXE binaire jamais versionné (deploy/installer/releases/.gitignore).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Helper partagé materialize_anchor_from_b64 (généralisé à tous les needsAnchor) réutilisé par la route HTTP + le bridge R1 (dé-duplication) ; source canonique anchor_action_types.NEEDS_ANCHOR ; backfill idempotent tools/backfill_anchors_r1.py (--dry-run par défaut, NON appliqué). Revue adversariale MERGE-READY, 2 nits levés, 29 tests unit verts. Déploiement clinique + backfill --apply = décision Dom, NON inclus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Suite revue adversariale MERGE-READY (« non-bloquant ≠ oublié », Dom) :
1. Contrat crop CORROMPU figé par test : `materialize_anchor_from_b64` retire la
clé `_anchor_image_base64` de params AVANT le try (pop). Un crop inexploitable
(b64 invalide, PNG au magic cassé) → pas d'exception, anchor_id NULL, aucun
VisualAnchor, ET clé retirée (pas de donnée morte volumineuse dans les params).
3 cas paramétrés verrouillent ce comportement assumé. Docstring clarifiée.
2. `workflow_id` réintroduit dans le sidecar metadata.json (parité avec la route
d'origine avant refactor). Nouveau paramètre optionnel du helper, passé par les
deux appelants live (route HTTP + bridge R1). 2 tests : présent quand fourni,
absent sinon (pas de clé None parasite). Backfill inchangé (workflow_id optionnel).
TDD : +5 tests. 29 tests unit VERTS, 0 régression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les steps needsAnchor des workflows importés par le pont R1
(import_core_workflow_to_db) affichaient « Ancre requise » car R1 laissait
_anchor_image_base64 dans params sans créer le VisualAnchor ni poser anchor_id.
La route HTTP import_learned_workflow faisait ce travail mais en dupliquait ~50
lignes inline.
Fix GÉNÉRAL et réutilisable (mandat Dom), pas une rustine click_anchor :
- helper unique services/anchor_image_service.materialize_anchor_from_b64 :
crée VisualAnchor + fichier PNG + pose step.anchor_id + retire le b64 de params ;
ne commit pas (transaction gérée par l'appelant) ; tolérant (crop invalide →
anchor_id NULL, pas de crash) ; générique pour TOUS les action_type needsAnchor.
- branchement dans import_core_workflow_to_db (flux R1) ;
- route HTTP refactorée pour réutiliser le même helper (dé-duplication réelle,
-48 lignes) → source unique de vérité.
- source canonique needsAnchor côté backend : services/anchor_action_types.py
(miroir de frontend_v4/src/types.ts, garde-fou anti-divergence testé).
- script tools/backfill_anchors_r1.py (--dry-run par défaut / --apply) : filtre
source=learned_import ET action_type ∈ NEEDS_ANCHOR ET anchor_id NULL ET crop
présent ; DB résolue depuis la config runtime VWB (pas en dur) ; dry-run
affiche le chemin DB résolu + estime PNG/disque ; idempotent. NON exécuté en
--apply (backup + arbitrage Dom requis avant, jamais la DB clinique DGX).
TDD : tests rouges d'abord puis verts. 20 tests neufs (matérialisation 6 types
d'ancre, b64 retiré, sans-crop→NULL, R1 clic, ré-import idempotent, dé-dup route,
backfill apply/idempotent/dry-run/filtre manual+hors-needsAnchor, canonique). 24
tests unit VERTS, 0 régression (test_import_core_workflow_to_db intact).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ferme Gap C : _edge_to_normalized_actions produit désormais une action navigate
(handler serveur atteignable). Ajoute _coerce_action_coords : cast x_pct/y_pct en
float APRÈS résolution des templates, JAMAIS de fallback (0,0) — template non
résolu / valeur invalide → pause_for_human (safety_level=high). Non-régression
prouvée sur mouse_click classiques (idempotent sur floats).
⚠️ NE FERME PAS le write-only : Gap A (P1-B) non livré — aucun step click/type ne
déclare encore consommer navigate_login_coords. TestCompilerGapLiteralFloats
assert l'état ouvert. Boucle complète = chantier suivant (P1-B + test e2e edge→action).
21 tests verts, boot OK. Revue croisée Claude (GO jalon partiel).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sémantique R1 tranchée par Dom (02/07) : create-or-skip (la version validée
par revue humaine fait foi, un ré-apprentissage ne l'écrase pas). Aligne
docstring learned_workflow_bridge (disait à tort « pas branché au worker »
depuis c82829f2b) + spec F1-1 (barré create-or-update).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test_coords_consumption_gap.py documents 3 structural gaps where
NavigateCoords are written but never consumed. test_capture_io.py and
test_image_chat_cli.py cover capture and chat CLI paths.
Add import_core_workflow_to_db() — create-or-update par signature de
trajectoire (décision produit Dom 23/06). Les workflows source='manual'
sont exclus du filtre de fusion. Inclut test TDD idempotent (ré-import
2× → toujours 1 seul workflow).
Complete la voie 1 sur les 2 items de confort/securite du checklist upgrade :
- BACKUP : robocopy code+config vers <app>_backup HORS python-embed/sessions/
logs (leger, rapide) => filet de rollback manuel si la nouvelle version
deconne (l'install manuel n'a pas d'A/B auto).
- PURGE : suppression des captures accumulees (agent_v1/sessions) = donnees
d'apprentissage internes non exploitables cote clinique. Libere le disque ;
le fix capture JPEG evite la reprise de saturation. Logs conserves (180j).
Valide .11 (upgrade sur etat Emilie) : Lea tuee, identite+serveur preserves,
backup code/config sans embed, 40 PNG purges -> 0, exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Poste clinique (Emilie) = install existante + Lea vive + config reelle
(machine_id lea-4zbgwxty, vrai serveur). L'installeur regenerait config.txt +
machine_id a chaque install => l'upgrade ecrasait l'identite fleet et forcait
la resaisie du serveur/token (Gap 1), et ne fermait pas la Lea en cours =>
DLL python-embed verrouillees (Gap 2).
Voie 1 :
- FindExistingInstallDir + LoadExistingConfig : detecte l'install, pre-remplit
le wizard avec la VRAIE conf et memorise le machine_id.
- CurStepChanged(ssInstall) : preserve le machine_id existant (pas de regen).
- PrepareToInstall : tue Lea via le PID du lock avant la copie (libere les DLL).
Valide sur .11 (upgrade silencieux sur etat Emilie simule) : machine_id +
serveur preserves, fausse Lea tuee, lock retire, 4 fixes presents, exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le check de completude embed ne verifiait que httpx/httpcore/h11 ; anyio et
typing_extensions (requis par httpx 0.28.1 sous py<3.13) manquaient => import
httpx aurait pu casser a l'install malgre un build vert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 fixes TDD-verts (revue qualite 3 GO + httpx debloque par peuplement embed).
MAJ silencieuse embarquee flag OFF (dormante, quadruple gate). Cible EXE->Julien.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prerequis merge fixes client Lea : la MAJ silencieuse rapporte AGENT_VERSION
au serveur ; on fige explicitement 1.0.1 (defaut du code) cote config livree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Câble le type d'action 'extract_dossier' dans get_next_action (api_stream)
vers _handle_extract_dossier_action (replay_engine). La brique 3 (OCR ->
gate -> persist dossier VWB) était committée mais non atteignable au runtime
faute de dispatch. Import + elif dédié, timeout 180s, exécuteur non bloquant.
Note: le handler utilise encore l'ancienne chaîne (extract_grid + gate maison).
Le remplacement par l'extraction ancrée (map_roles/vlm_client) est une modif
séparée côté replay_engine (ma zone).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Factorise un client propre pour la lecture d'écran : downscale image (fenêtre
max_model_len), thinking off, post_fn injectable (testable sans vLLM). Sert de
vlm_client à extract_dossier_from_image dans le handler runtime. 4 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enchaine ocr_fn -> tokens_from_grid -> map_roles -> assess_quality. OCR et client
VLM injectables (testable hors-ligne, import OCR lazy = module reste pur). C'est la
brique que le handler runtime extract_dossier appellera. 4 tests (35 au total role_mapper).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_resolve_lea_zip_template() reste résolu à la volée (full buildé après démarrage OK) ;
ajout d'un WARNING explicite quand le full est absent et qu'on retombe sur le ZIP
léger non autoportant (plus de fallback silencieux). Fonction injectable pour tests.
4 tests + 32 non-régression verts. refs DETTE-024
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sanitize_log_entries (réutilise anonymize_text, mapping partagé = tokens cohérents),
branché dans POST /api/v1/agents/logs avant le store : message + logger tokenisés,
ts/level préservés. 7 tests TDD. Rempart PII central du push-log (couvre les postes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le VLM ne fournit que des value_ids ; la value est reconstruite côté Python
depuis l'OCR (le texte VLM est ignoré) -> 0 hallucination par construction.
9 tests unitaires : ancrage, ids hors plage, dédup ordonnée, value_ids vide,
confidence min, bbox englobante, anti-injection. Module pur, non branché runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
finalize_session appelle _maybe_import_to_vwb : si RPA_R1_AUTO_IMPORT (OFF par
défaut), le workflow appris est assaini (sanitize_workflow_dict) puis importé en
DB VWB rejouable via le pont idempotent (import_core_workflow_to_db), dans un
app-context VWB lazy mutualisé (vwb_db). NON bloquant : un échec n'interrompt
jamais la finalisation. Rend l'appris rejouable sans geste manuel (R1).
Tests : câblage du seam + gating du flag + non-régression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Résolution des chevauchements par priorité de détecteur + longueur : corrige le
FN où, sur 'Dossier/Patient NOM (NAISSANCE) Prénom', le nom de naissance fuyait. (Qwen)
- RE_GXD5_DIAG : tokenise le numéro de dossier ([DOSSIER_n]) ET le nom ([NOM_n]) dans
'GXD5 Diagnostics - <num> - NOM PRENOM' — 3 patients fuyaient en prod clinique, 0 FP. (Qwen)
- sanitize_workflow_dict : assainit les champs texte d'un workflow appris (by_text, noms)
avant import en DB VWB (canal apprentissage). Utilisé par R1. (Claude)
14 tests verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ExtractionJob -> ExtractedTable -> ExtractedField (SQLAlchemy, cascade), avec
preuve par cellule (bbox + confidence) réutilisant la sémantique VWBEvidence,
et statut dossier needs_review|complete. Brique 2 de la verticale extraction.
Documenté : ce canal conserve les données patient EN CLAIR (≠ canal
apprentissage anonymisé) — aucune anonymisation ne doit cibler ces colonnes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>