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.
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>
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>
- 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>
Nouvelle extract_grid_from_image() : reconstruit une grille List[List[cell]]
(lignes ET colonnes par clustering des centres y/x des tokens EasyOCR), en
conservant bbox + confiance + (row,col) par cellule. Contrairement à
extract_table_from_image (liste plate, coordonnée x jetée) — laissé intact.
Brique 1 de la verticale extraction dossier patient.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les workflows rechargés du disque retombaient sur machine_id='default' :
to_dict ne sérialisait pas l'attribut d'instance _machine_id et from_dict ne
le reposait pas (il dormait dans metadata['machine_id']). to_dict le sérialise
si présent (pas de 'default' parasite) ; from_dict le restaure depuis le champ
explicite ou metadata (rétrocompat des workflows déjà sur disque).
Test de non-régression round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Assainissement PII appliqué une seule fois à l'entrée de stream_event(),
avec un mapping de tokens par session (cohérence intra-session). Les chemins
de persistance et de traitement (jsonl, worker.process_event_direct,
shadow_observe_event, enrichissement SOM) consomment tous la copie assainie
au lieu de l'event brut — plus aucune PII patient en clair côté serveur.
Test de non-régression du câblage: stream_event ne doit jamais écrire de PII
brute (IPP/contenu saisi) dans live_events.jsonl ni la propager au worker/shadow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FN-1/2/3 : ajout RE_PRENOM_NOM (« Prénom NOM » inversé sans parens/crochets,
ex. « Alix DATTIN ») ; 2e mot tout-majuscules -> 0 FP sur « Mozilla Firefox ».
- FN-4 (majeur, 228 events) : sanitize_event scanne désormais les titres
RÉCURSIVEMENT (vision_info.window_capture.window_title et tout titre imbriqué),
au lieu de 3 clés top-level hardcodées.
2 correctifs issus de la revue croisée Qwen. 11 tests verts, 0 FP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sanitize_event(event, mapping) applique le principe « Léa apprend l'interface,
pas la donnée » (décision Dom 28/06) avant persistance :
- text_input -> contenu (text + raw_keys) remplacé par [SAISIE] (option b) :
résout la fuite la plus grave (contenu médical) SANS NER ni détection ;
- titres de fenêtre (active_window_title + window/to/from.title) : identité
patient tokenisée (anonymize_text), app/écran gardés ; cohérence par mapping.
Copie défensive (ne mute pas l'event d'origine). 4 tests (9 au total) verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pii_sanitizer.anonymize_text() remplace la PII par des tokens typés et
cohérents ([IPP_1], [AGE_1], [NOM_1]) : protège la donnée ET garde la structure
(type de champ) utile à l'apprentissage des variables. Sans modèle, déployable
partout. Filet regex (IPP/NIR/TEL/EMAIL/AGE, repris de anonymisation) + règles
structurelles cliniques (NOM (NAISSANCE) Prénom ; [Nom Prénom] PACS) + blacklist
logiciels anti-FP. 5 tests verts. Couche NER (noms libres) en complément ensuite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Module agent_v1/core/log_safe.py — 3 helpers purs pour assainir les logs
client à la source : _title_hash (SHA1[:8], corrélation sans révéler),
_sanitize_metadata (drop title/active_window/window_title), _path_ext
(extension seule). 6 tests unitaires verts. Module inerte (non encore wired) ;
le branchement dans le code runtime suit en étape supervisée.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setup_logging() branche un TimedRotatingFileHandler vers LOG_FILE (rotation
quotidienne + rétention 180j, Règlement IA Art.12) + console. Sous pythonw
(sans console), basicConfig->stderr était perdu => diagnostic terrain aveugle.
main.py appelle setup_logging au démarrage, avec fallback console si le fichier
est indisponible (ne jamais empêcher Léa de démarrer).
TDD: tests/unit/test_agent_v1_logging.py (3 tests RED->GREEN ; module chargé par
chemin pour éviter les imports lourds DETTE-011/013). py_compile main.py OK.
refs DETTE-021
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anonymisation déterministe de la cible par regex DÉDIÉES (email/date/tél/IPP →
tokens) avant hashing : deux sessions sur le même champ (patients/dates
différents) → même signature. Normalisation casse/accents/espaces (logique
action_executor._norm_text, redéfinie localement pour rester léger).
Choix QG Qwen (2026-06-25) : PAS de pii_blur (il protège les dates qu'on veut
neutraliser), PAS de NER (un hash d'identité doit être déterministe/portable
labo↔DGX). Noms propres sans titre non gérés (stratégie b ; gate = audit
agrégat by_text DGX avant prod). R2 fallback coords RETIRÉ (casserait F1).
R3 (machine_id hors hash) déjà conforme.
TDD: +4 tests (RED→GREEN, 9/9). Primitive non wirée (0 consommateur runtime)
→ changement de calcul sans impact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le champ by_role remontait la methode de detection (yolo/ocr/vlm), instable entre
sessions : deux apprentissages du meme parcours detectes differemment produisaient
deux signatures -> fusion (create-or-update) ratee. On sort by_role de la signature
et on s'appuie sur le texte semantique de la cible (by_text), independant du moteur
de grounding. Fallback quand by_text vide : titre de fenetre / description VLM.
Test TDD: test_signature_stable_despite_grounding_role_difference (RED->GREEN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extrait d'un workflow core (dict) la sequence ordonnee (action_type, target stable)
via traversee BFS depuis entry_nodes (comme le bridge d'import), en n'utilisant que
des champs stables (by_role/by_text/window) et en ignorant coords/IDs de noeuds.
Branche la primitive trajectory_signature sur de vrais workflows.
Test TDD: tests/unit/test_workflow_trajectory_signature.py (3 tests, RED->GREEN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Primitive partagee (SP-4/SP-2/competences) : hashe la sequence ordonnee
(action_type, target) d'un parcours en ignorant les champs session-specifiques
(node_id, timestamp, coordonnees) -> deux apprentissages du meme parcours = meme
signature = base du create-or-update (decision F1). Le target stable peut etre
compose avec screen_signature() existante.
Test TDD: tests/unit/test_trajectory_signature.py (5 tests, RED->GREEN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le score/confidence figés à 0.85 dans _resolve_by_grounding rendaient le
garde-seuil (_RESOLUTION_MIN_SCORES["grounding"]=0.60) inopérant (0.85>0.60
toujours accepté). Le grounding VLM n'a pas de confiance modèle native (prompt
{"x","y"}, pas de logprob de localisation — confirmé QG Qwen 2026-06-15). On
dérive une confiance SÉMANTIQUE : le texte cible est-il à la position trouvée ?
(_validate_text_at_position). Confirmé→0.90, absent→0.45 (<seuil→rejet),
non vérifiable→0.70. Confiance contextuelle documentée, PAS une proba modèle.
TDD : 5 tests (score varie / présent accepté / absent rejeté / score==confidence
/ sans by_text neutre), RED→GREEN. Non-régression : 24 tests resolve_engine +
câblage qwen3vl + legacy bbox verts. E2E panel inchangé (15/15). Pré-check OCR
non impacté. DETTE-018 (legacy non gardé) reste séparée.
refs DETTE-019
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ajoute AgentRegistry.verify_token(token) -> machine_id|None : compare le
SHA-256 du token aux token_hash des agents 'active' via hmac.compare_digest
(temps constant). Agent désinstallé/révoqué refusé ; rotation à l'enroll
invalide l'ancien token.
Inerte au runtime : méthode non branchée sur l'auth HTTP (le branchement
derrière flag RPA_FLEET_PER_AGENT_TOKEN sera le Patch 4). api_stream.py
intouché. TDD : 6 tests + non-régression WP-C/WP-B (53 verts). Voir
PLAN-WPC-TDD-EXECUTABLE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Génère un token unique (secrets.token_hex(32)) à chaque (ré)enrôlement,
persiste uniquement son empreinte SHA-256 dans token_hash, renseigne
token_issued_at, retourne le clair une seule fois dans le résultat de
enroll. Le clair n'est jamais journalisé ni persisté.
Inerte au runtime : api_stream.py intouché, l'endpoint /agents/enroll ne
propage ni le clair ni le hash (api_token global inchangé). Auth runtime
non modifiée. Aucun branchement _verify_token. TDD : 8 tests + non-régression
WP-B/WP-C (47 verts). Voir PLAN-WPC-TDD-EXECUTABLE / DETTE-015.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ajoute token_hash + token_issued_at à enrolled_agents via ALTER TABLE
idempotent (_init_db). Colonnes inertes : aucun branchement auth, runtime
inchangé (tests WP-B verts). Base du token par poste (WP-C, cf DETTE-015).
TDD: tests/unit/test_wpc_migration.py (présence, idempotence, préservation
des données d'une base existante). 3 tests + non-régression WP-B = 9 passed.
refs DETTE-015
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolve_device('auto') renvoyait 'cpu' sur le GB10 : le plafond max_total_gb=6
(pensé pour la RTX 12 Go dédiés) voyait used≈99 Go car la mémoire UNIFIÉE compte
la RAM système. Au-dessus de DEFAULT_LARGE_VRAM_GB=24 (grosse carte / mémoire
unifiée), le plafond n'est plus appliqué ; seul free >= min_free_gb décide.
RTX (<=24 Go) inchangée.
Détecté au bench GB10 2026-06-08 (auto->cpu, OCR 10x plus lent). +2 tests (17/17).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ferme le contournement "poste révoqué + nouveau machine_id + token global" :
quand RPA_FLEET_ENROLL_LOCKED=true, l'enrôlement d'un machine_id INCONNU est refusé
(FleetEnrollLockedError). Les machines déjà connues conservent leur comportement :
active -> AlreadyEnrolled, désinstallé non-revoke -> réactivable, admin_revoke -> Revoked.
- agent_registry.py : _fleet_enroll_locked() + FleetEnrollLockedError + gate avant INSERT
- tests/unit/test_fleet_enroll_lock_wpb.py : 6 tests (verts)
NB : le handler HTTP 403 (api_stream.py /api/v1/agents/enroll) reste dans le WIP de la
branche (api_stream déjà modifié par le préflight non committé) — sera embarqué au commit
de consolidation api_stream. La logique de sécurité (gate) est dans agent_registry, committée.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le dashboard refuse de démarrer si DASHBOARD_PASSWORD absent ET auth non
explicitement désactivée (DASHBOARD_AUTH_DISABLED). Supprime le mot de passe
par défaut hardcodé exploitable.
- web_dashboard/app.py : _require_dashboard_password() fail-closed (lève en prod
sans secret ; mode dev/test = DASHBOARD_AUTH_DISABLED=true)
- tests/unit/conftest.py : DASHBOARD_AUTH_DISABLED=true par défaut pour les tests
- tests/unit/test_dashboard_failclosed_wpa.py : 5 tests (fail-closed, anti-régression défaut)
- tests/unit/test_dashboard_auth_p0a.py : fixture _restore_module restaure un état neutre sûr
48 tests dashboard verts (WP-A + non-régression auth/routes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolve_device(auto/cuda/cpu) avec garde-fou VRAM et fallback CPU propre.
Bascule EasyOCR/SoM/docTR sur GPU si VRAM libre, rollback env sans toucher au code.
- core/gpu/device_policy.py (nouveau) : resolve_device + garde-fou VRAM (max_total_gb)
- core/detection/som_engine.py, core/llm/ocr_extractor.py,
agent_v0/server_v1/resolve_engine.py : câblage device auto (35 lignes)
- tests/unit/test_device_policy.py : 15 tests (verts venv réel)
Rollback sans toucher au code : RPA_VISION_DEVICE=cpu (force CPU global) / RPA_EASYOCR_GPU=0.
Bench GPU réel (latence) + activation large après verdict Qwen. QG Qwen deja valide sur le patch.
Mergé depuis worktree agent-a4f390f410e00ad7c (base 5b2afa362), 3 fichiers cibles non modifiés
dans le principal (zéro écrasement), dry-run apply propre.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Détecte les modèles VLM/grounding « aveugles » (capabilities sans vision, ex.
UI-TARS réimporté sans mmproj) pour éviter le HTTP 500 silencieux masqué par
la cascade de grounding.
- core/detection/model_health.py : has_vision_capability() (cache, fail-open)
+ smoke_check_models()
- core/execution/input_handler.py : gate vision dans _grounding_ui_tars
(skip propre vers niveau 3 si modèle aveugle, plus de 500 silencieux)
- tests/unit/test_model_health.py : 6 tests (vision/aveugle/fail-open/cache/smoke)
Incident 2026-06-08 : UI-TARS sans mmproj -> niveau 2 cascade en 500 silencieux,
non détecté (hors chemin runtime démo + échec avalé par fallback + zéro test).
NB : le smoke non bloquant au démarrage (api_stream.py startup) reste dans le WIP
de la branche, mélangé au préflight non committé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sans env RPA_VLM_MODEL/VLM_MODEL, get_vlm_model() tombait sur le default
gemma4:latest, qui peut etre absent du tunnel DGX (depull) -> 404 Ollama et
echec de tout le pipeline VLM avant un test Lea humain.
- core/detection/vlm_config.py : DEFAULT_VLM_MODEL gemma4:latest -> qwen2.5vl:7b-rpa
(confirme present DGX, deja default reasoning + fallback bbox grounding).
+ DGX_SAFE_VLM_MODELS allow-list documentee.
- tests/unit/test_vlm_default_dgx_safe.py : 5 tests (default != gemma4:latest,
default in allow-list, no-env -> DGX-safe, env garde priorite).
Logique de resolution inchangee, pas d'appel reseau a l'import.
gemma4:latest reste accessible via env explicite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adapter de benchmark isole (hors runtime Lea) ciblant un serveur
/v1/chat/completions a support vision (vLLM/SGLang/TGI), pour comparer
plus tard a Ollama via LeaBench. Ne controle jamais le desktop.
- core/evaluation/openai_compat_lea_bench_adapter.py : payload data-URL
image_url, parsing choices[0].message.content. Reutilise par import la
logique prompt/parse/normalisation de ollama_lea_bench_adapter (zero refactor).
- tools/lea_bench_openai_compat.py : wrapper CLI (--base-url defaut :8001).
- tests/unit/test_openai_compat_lea_bench_adapter.py : 6 tests mockes HTTP
(data URL, pas de fuite expectation/click_region, prediction valide,
abstain safe sur HTTP!=200 et reponse malformee, JSONL rechargeable).
Aucun runtime Lea modifie. Aucun service lance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>