# AUDIT — Contrôles débranchés (serveur) Date : 2026-05-08 Branche : feature/qw-suite-mai HEAD : 56e869c46 Périmètre : agent_v0/server_v1/ + core/* importés. Client exclu. ## 1. Inventaire des fichiers audités | Fichier | Lignes | |---|---| | agent_v0/server_v1/__init__.py | 0 | | agent_v0/server_v1/visual_wait.py | 54 | | agent_v0/server_v1/monitor_router.py | 99 | | agent_v0/server_v1/replay_failure_logger.py | 143 | | agent_v0/server_v1/vm_controller.py | 143 | | agent_v0/server_v1/loop_detector.py | 154 | | agent_v0/server_v1/worker_stream.py | 172 | | agent_v0/server_v1/workflow_replay.py | 185 | | agent_v0/server_v1/safety_checks_provider.py | 195 | | agent_v0/server_v1/session_worker.py | 253 | | agent_v0/server_v1/agent_registry.py | 296 | | agent_v0/server_v1/replay_memory.py | 323 | | agent_v0/server_v1/execution_plan_runner.py | 373 | | agent_v0/server_v1/audit_trail.py | 393 | | agent_v0/server_v1/replay_learner.py | 395 | | agent_v0/server_v1/run_worker.py | 397 | | agent_v0/server_v1/live_session_manager.py | 464 | | agent_v0/server_v1/task_planner.py | 596 | | agent_v0/server_v1/chat_interface.py | 622 | | agent_v0/server_v1/replay_verifier.py | 632 | | agent_v0/server_v1/domain_context.py | 1020 | | agent_v0/server_v1/replay_engine.py | 1643 | | agent_v0/server_v1/resolve_engine.py | 2585 | | agent_v0/server_v1/stream_processor.py | 5137 | | agent_v0/server_v1/api_stream.py | 5445 | Total serveur : 21 719 lignes. Modules core/ effectivement importés par le serveur : - core.detection.omniparser_adapter (resolve_engine.py:272) - core.detection.ollama_client, core.detection.vlm_config (resolve_engine.py:502-503, api_stream.py:790) - core.detection.som_engine (resolve_engine.py:977) - core.embedding.clip_embedder (resolve_engine.py:1658) - core.anonymisation (api_stream.py:47) - core.auth.credential_vault, core.auth.auth_handler (api_stream.py:84-85) - core.llm.ocr_extractor (api_stream.py:824) - core.models.workflow_graph (api_stream.py:845) - core.workflow.shadow_observer, shadow_validator, execution_plan, execution_compiler, ir_builder (api_stream.py:1601-2656) - core.federation.learning_pack, faiss_global (api_stream.py:4647-4690) - core.learning.target_memory_store (replay_memory.py:62) ## 2. Findings par catégorie ### 2.1 Validations désactivées ou non consommées **F2.1.1 — Pré-check OCR sémantique (`_validate_text_at_position`) gardé par flag off-by-default** - `agent_v0/server_v1/api_stream.py:4519-4533` - Citation : ``` _text_precheck_enabled = os.environ.get( "RPA_ENABLE_TEXT_PRECHECK", "false" ).lower() in ("true", "1", "yes") if _text_precheck_enabled and result and result.get("resolved"): _by_text = (request.target_spec.get("by_text") or "").strip() if _by_text: from agent_v0.server_v1.resolve_engine import _validate_text_at_position _is_valid, _observed, _ocr_ms = _validate_text_at_position( tmp_path, float(result.get("x_pct", 0) or 0), float(result.get("y_pct", 0) or 0), _by_text, effective_w, effective_h, ) ``` - Statut : off-by-default — l'appel à `_validate_text_at_position` ne s'exécute QUE si `RPA_ENABLE_TEXT_PRECHECK=true`. La fonction reste définie en `resolve_engine.py:2239-2289` mais n'est jamais consommée en production tant que la variable env n'est pas positionnée. **F2.1.2 — `_validate_text_at_position` retourne `True` en cas d'échec OCR (politique permissive)** - `agent_v0/server_v1/resolve_engine.py:2253-2261, 2280, 2287-2289` - Citation : ``` Politique en cas d'échec OCR (lib absente, exception) : retourne (True, "", 0.0) pour ne pas bloquer le flux. Mieux vaut un faux positif rare qu'une régression bloquante introduite par la validation elle-même. """ reader = _get_validation_ocr_reader() if reader is None: return True, "", 0.0 if not expected_text or not expected_text.strip(): return True, "", 0.0 [...] if x2 - x1 < 10 or y2 - y1 < 10: return True, "", 0.0 [...] except Exception as e: logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e) return True, "", 0.0 ``` - Statut : actif (quand le flag global est on) mais résultat documenté comme intentionnellement permissif sur tout chemin d'erreur. Une erreur OCR = pas de blocage. **F2.1.3 — `_pre_check_screen_state` (CLIP) bascule `match=True` sur exception** - `agent_v0/server_v1/replay_engine.py:1374-1379` - Citation : ``` except Exception as e: # Ne jamais bloquer le replay en cas d'erreur du pre-check logger.error(f"Pre-check échoué (non bloquant): {e}") result["match"] = True # Fallback permissif result["reason"] = f"precheck_error: {e}" ``` - Statut : actif mais permissif explicitement (commentaire `# Fallback permissif`). Toute exception interne du pre-check CLIP renvoie `match=True` et l'action passe. **F2.1.4 — Vérification post-action (`verify_action`/`verify_with_critic`) skippée pour type/key_combo/wait et popup gérée** - `agent_v0/server_v1/api_stream.py:3394-3399` - Citation : ``` action_type_for_verify = (original_action or {}).get("type", "unknown") skip_verify = action_type_for_verify in ("type", "key_combo", "wait") # Skip aussi la vérification serveur si l'agent a déjà géré la popup skip_verify = skip_verify or agent_handled_popup verification = None if report.success and screenshot_after and not skip_verify: ``` - Statut : actif. La vérification visuelle post-action ne tourne que pour les click et seulement si `agent_handled_popup` est faux. **F2.1.5 — `_validate_match_context` consommé uniquement dans la branche template strict** - `agent_v0/server_v1/resolve_engine.py:201, 1864` - Le seul appel est `resolve_engine.py:1864` à l'intérieur du mode strict, pour le fallback template. Pas appelé dans `_resolve_with_precompiled_order` (V4) ni dans le mode classique. - Statut : actif sur un seul chemin de la cascade. ### 2.2 Garde-fous court-circuités (seuils, flags, conditions) **F2.2.1 — Drift > 0.20 ignoré quand `template_matching ≥ 0.95` ou `hybrid_text_direct ≥ 0.80`** - `agent_v0/server_v1/resolve_engine.py:2367-2390` - Citation : ``` if dx > _RESOLUTION_MAX_DRIFT or dy > _RESOLUTION_MAX_DRIFT: [...] _high_confidence_method = ( (method.startswith("template_matching") and score >= 0.95) or (method == "hybrid_text_direct" and score >= 0.80) ) if _high_confidence_method: logger.info( "[REPLAY] Drift (%.3f, %.3f) > %.2f IGNORÉ : score=%.3f " "sur %s — résultat visuel fiable, on l'utilise", dx, dy, _RESOLUTION_MAX_DRIFT, score, method, ) return result ``` - Statut : actif. Exemption introduite par 35b27ae49 (template ≥ 0.95) puis élargie par 40440f1ca à hybrid_text_direct ≥ 0.80. La garde de drift est neutralisée pour deux familles de méthodes. **F2.2.2 — Drift check inactif si fallback x/y_pct ressemblent à un placeholder 0.5/0.5 ou 0.0/0.0** - `agent_v0/server_v1/resolve_engine.py:2359-2363` - Citation : ``` _has_recorded_coords = ( fallback_x_pct > 0.001 and fallback_y_pct > 0.001 and not (abs(fallback_x_pct - 0.5) < 0.001 and abs(fallback_y_pct - 0.5) < 0.001) ) if _has_recorded_coords: ``` - Statut : actif. Sans coords enregistrées exploitables, la garde drift est inerte. **F2.2.3 — Self-healing Win+D au retry 1 désactivé (revert)** - `agent_v0/server_v1/replay_engine.py` (commit 22c0a2ba6, branche `next_retry == 2` conservée seule) - Citation post-revert : ``` if next_retry == 2: # Retry 2 : injecter un wait de 2s avant l'action wait_action = { "action_id": f"wait_retry_{uuid.uuid4().hex[:6]}", "type": "wait", "duration_ms": 2000, } actions_to_insert.append(wait_action) ``` - Statut : retiré. Aucune injection de gesture de récupération avant le retry 1 — boucle directe sur la même action. **F2.2.4 — Pre-check skip si heartbeat > 10s ou timeout > 500ms** - `agent_v0/server_v1/api_stream.py:999-1001, 3100-3130` - Citation : ``` _HEARTBEAT_MAX_AGE_SECONDS = 10.0 _PRECHECK_SIMILARITY_THRESHOLD = 0.85 [...] if age <= _HEARTBEAT_MAX_AGE_SECONDS: [...] precheck_result = await asyncio.wait_for( loop.run_in_executor(...), timeout=0.5, # Max 500ms pour le pre-check ) except asyncio.TimeoutError: logger.warning(...) precheck_result = None ``` - Statut : actif. Le pre-check CLIP est skip silencieusement si heartbeat trop ancien ou si l'embed prend > 500ms. **F2.2.5 — VLM Quick Find : confidence < 0.3 → ignoré (résultat valide perdu sous le seuil)** - `agent_v0/server_v1/resolve_engine.py:655-662` - Citation : ``` if x_pct is None or y_pct is None or confidence < 0.3: logger.info( "VLM Quick Find : élément non trouvé ou confiance trop basse " "(%.1fs, confidence=%.2f) pour '%s'", ``` - Statut : actif. Tout retour VLM avec confidence < 0.3 est dropé. **F2.2.6 — Image client tronquée → remplacement silencieux par dernier heartbeat** - `agent_v0/server_v1/api_stream.py:4422` - Citation : ``` if img.height < 800 or img.width < 1200: logger.warning( "[RESOLVE_TARGET] Image client tronquée %dx%d (declared %dx%d) — " "fallback heartbeat full screen", ``` - Statut : actif. Toute image reçue < 1200x800 est remplacée par un screenshot heartbeat (mémoire ou disque) avant cascade. Seuil élargi par 7233df2bb (était 400x200 avant). **F2.2.7 — CLIP mismatch < 0.75 retourne resolved=False mais ne bloque qu'en mode strict avec embedding fourni** - `agent_v0/server_v1/resolve_engine.py:1655-1691` - Citation : ``` clip_embedding = target_spec.get("clip_embedding") if clip_embedding: [...] if clip_sim < 0.75: logger.warning( f"CLIP MISMATCH : sim={clip_sim:.3f} < 0.75 — " f"écran actuel trop différent de l'enregistrement" ) return { "resolved": False, "method": "clip_mismatch", ``` - Statut : actif uniquement si `clip_embedding` fourni ET mode strict. Pour les workflows qui n'embarquent pas l'embedding, ce filet est inerte. ### 2.3 Flags d'environnement avec défaut permissif **F2.3.1 — `RPA_ENABLE_TEXT_PRECHECK`, défaut `"false"`** - `agent_v0/server_v1/api_stream.py:4519-4521` - Citation : ``` _text_precheck_enabled = os.environ.get( "RPA_ENABLE_TEXT_PRECHECK", "false" ).lower() in ("true", "1", "yes") ``` - Statut : off par défaut. Sans surcharge en environnement, le pré-check OCR ne s'exécute jamais. **F2.3.2 — `RPA_AUTH_DISABLED`, défaut absent (auth obligatoire) mais permet de tout débrayer** - `agent_v0/server_v1/api_stream.py:107-119` - Citation : ``` _AUTH_DISABLED = os.environ.get("RPA_AUTH_DISABLED", "").lower() in ( "1", "true", "yes", ) [...] if _AUTH_DISABLED: logger.warning( "[SÉCURITÉ] RPA_AUTH_DISABLED=true — authentification Bearer DÉSACTIVÉE. ..." ) API_TOKEN = _API_TOKEN_ENV or secrets.token_hex(32) ``` - Statut : par défaut auth obligatoire, mais flag explicite documenté pour la débrayer. **F2.3.3 — `RPA_LOOP_DETECTOR_ENABLED`, défaut `"1"` (activé)** - `agent_v0/server_v1/loop_detector.py:42-47, 78-79` - Citation : ``` def _env_bool_enabled(name: str) -> bool: val = os.environ.get(name, "1").strip().lower() return val not in ("0", "false", "no", "off", "") [...] if not _env_bool_enabled("RPA_LOOP_DETECTOR_ENABLED"): return LoopVerdict(detected=False) ``` - Statut : on par défaut, désactivable via `RPA_LOOP_DETECTOR_ENABLED=0`. **F2.3.4 — `RPA_SAFETY_CHECKS_LLM_ENABLED`, défaut `"1"` (activé)** - `agent_v0/server_v1/safety_checks_provider.py:42-44, 70` - Citation : ``` if safety_level == "medical_critical" and _env_bool_enabled("RPA_SAFETY_CHECKS_LLM_ENABLED"): ``` - Statut : on par défaut, mais ne tourne que si `safety_level == "medical_critical"` dans l'action. **F2.3.5 — `RPA_PII_BLUR_SERVER`, défaut `"true"` (activé)** - `agent_v0/server_v1/api_stream.py:1023` - Citation : ``` _PII_BLUR_ENABLED = os.environ.get("RPA_PII_BLUR_SERVER", "true").lower() in ("true", "1", "yes") ``` - Statut : on par défaut. ### 2.4 Étapes de cascade neutralisées **F2.4.1 — `_resolve_by_yolo` défini, importé, jamais appelé** - Définition : `agent_v0/server_v1/resolve_engine.py:293` - Import : `agent_v0/server_v1/api_stream.py:4363` - Recherche `_resolve_by_yolo(` dans le serveur entier : 0 site d'appel. - Statut : fonction morte. La détection OmniParser/YOLO n'est plus dans la cascade exécutée. **F2.4.2 — `_resolve_with_precompiled_order` (V4) appelé seulement si `target_spec["resolve_order"]` présent** - `agent_v0/server_v1/resolve_engine.py:1613-1635` - Citation : ``` resolve_order = target_spec.get("resolve_order") if resolve_order and isinstance(resolve_order, list): [...] result = _resolve_with_precompiled_order(...) if result and result.get("resolved"): return result [...] logger.info( "V4 resolve : toutes les méthodes pré-compilées ont échoué, " "fallback cascade legacy" ) ``` - Statut : actif quand un plan V4 est compilé ; sinon inerte. Fallback cascade legacy systématique en cas d'échec. **F2.4.3 — Étape grounding VLM directe conditionnée à `by_text_source ∈ {ocr, vlm}` ET `has_window`** - `agent_v0/server_v1/resolve_engine.py:1696-1715` - Citation : ``` by_text_source = target_spec.get("by_text_source", "") has_window = bool(target_spec.get("window_capture", {}).get("rect")) if by_text_strict and by_text_source in ("ocr", "vlm") and has_window: grounding_result = _resolve_by_grounding(...) ``` - Statut : actif sur ces deux conditions seulement. Si `by_text_source` est vide ou autre, ou si `window_capture.rect` absent, le grounding direct est sauté. **F2.4.4 — `_resolve_by_ocr_text` (hybrid_text_direct) reconnecté le 2026-05-06 dans la cascade strict (commit 1cbec2806)** - `agent_v0/server_v1/resolve_engine.py:1750-1790` - Citation du commit : ``` fix(resolve): rebrancher hybrid_text_direct dans _resolve_target_sync [...] la fonction _resolve_by_ocr_text (resolve_engine.py:1447) existait déjà mais [...] n'était appelée QUE depuis le runtime V4 [...] ``` - Statut : actif depuis 1cbec2806. Avant : étape OCR direct n'était pas dans la cascade strict pour les workflows non-V4. **F2.4.5 — Template matching mode strict : seuil 0.90 (étape 2 fallback)** - `agent_v0/server_v1/resolve_engine.py:1733, 1847-1875` - Citation : ``` result = _resolve_by_template_matching( [...] confidence_threshold=0.90, ) if result: score = result.get("score", 0) # Score >= 0.95 : match quasi-parfait, pas besoin de valider le contexte if score >= 0.95: [...] return result elif _validate_match_context(result, fallback_x_pct, fallback_y_pct, target_spec): [...] ``` - Statut : actif. `_validate_match_context` skippé si score ≥ 0.95. ### 2.5 Fonctions améliorantes définies mais non appelées **F2.5.1 — `_resolve_by_yolo` (résolution OmniParser+template, défini resolve_engine.py:293, jamais appelé)** - Voir F2.4.1. **F2.5.2 — `_fuzzy_match` importé dans api_stream.py mais jamais appelé** - Définition : `agent_v0/server_v1/resolve_engine.py:2086` - Import : `agent_v0/server_v1/api_stream.py:4372` - Recherche `_fuzzy_match(` dans api_stream.py : 0 appel hors la ligne d'import. - Statut : import mort. Le fuzzy match utilisé en runtime est `_text_match_fuzzy` (resolve_engine.py:2213), distinct. **F2.5.3 — `_get_omniparser`, `_build_target_description` importés dans api_stream.py mais non appelés directement** - `agent_v0/server_v1/api_stream.py:4362, 4365` - Statut : imports utilisés indirectement via `_resolve_target_sync` qui les appelle en interne. Pas un finding bloquant — pas de fonction améliorante hors usage interne. ### 2.6 Marqueurs de dette (TODO/FIXME/disabled/démo) dans le serveur **F2.6.1 — TODO `task_planner.py:400`** - Citation : `# Boucle : TODO — lister les éléments puis itérer` - Statut : commentaire de dette dans `task_planner.py`. **F2.6.2 — Commentaire « 8 mai 2026 : désactivé par défaut pour la démo GHT »** - `agent_v0/server_v1/api_stream.py:4512` - Citation : ``` # 8 mai 2026 : désactivé par défaut pour la démo GHT. Calibrage du # radius_px et min_token_ratio à finaliser post-démo (cf. rapport # docs/E2E_TEST_RUN_2026-05-08.md). Le pré-check était trop strict # sur les onglets à 2 tokens (Examens cliniques, Synthèse Urgences) # → faux rejets → cascade locale Léa V1 → clic au pif. Réactivable # via env RPA_ENABLE_TEXT_PRECHECK=true. Le code et les tests # restent en place pour reprise post-démo. ``` - Statut : marqueur démo explicite. **F2.6.3 — Mention « Fallback permissif » dans `_pre_check_screen_state`** - `agent_v0/server_v1/replay_engine.py:1377` — `result["match"] = True # Fallback permissif` **F2.6.4 — Ré-introduction explicite « non-bloquant » dans `_validate_text_at_position`** - `agent_v0/server_v1/resolve_engine.py:2288` — `logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e)` **F2.6.5 — Mode autonome → pause_for_human ignorée silencieusement** - `agent_v0/server_v1/api_stream.py:3011-3017` - Citation : ``` # Mode autonome sans safety_checks → skip (comportement legacy) logger.info( "pause_for_human ignorée (mode autonome) — replay %s continue", owning_replay["replay_id"] if owning_replay else "?" ) queue.pop(0) _replay_queues[session_id] = queue continue ``` - Statut : actif. La supervision n'est utilisée que si `execution_mode != "autonomous"` ou si `safety_level`/`safety_checks` déclarés. Ce câblage `execution_mode → supervised` a été corrigé par 7233df2bb. **F2.6.6 — Commentaire `# Fallback permissif`/`pas de blocage` cumulés** - Présents dans 3 fonctions de validation : `_pre_check_screen_state` (replay_engine.py:1377), `_validate_text_at_position` (resolve_engine.py:2288), et politique de `_get_validation_ocr_reader` (resolve_engine.py:2196). ## 3. Commits récents qui ont désactivé des contrôles (Sur les 20 derniers commits du dossier `agent_v0/server_v1/`) - `56e869c46` (8 mai) — `fix(replay): bug TypeError log + flag pré-check OCR off par défaut (démo GHT)` : ajoute `RPA_ENABLE_TEXT_PRECHECK` (default `"false"`) qui débraye intégralement l'appel à `_validate_text_at_position`. - `40440f1ca` (7 mai) — `fix(replay): cure régression b584bbabc — fallback recorded_coords aveugle` : restaure `resolved=False` sur drift trop grand (annule le fallback aveugle introduit par b584bbabc) ; étend l'exemption drift à `hybrid_text_direct ≥ 0.80` (resolve_engine.py:2380-2390). - `7233df2bb` (7 mai) — `fix(replay): câblage execution_mode supervised + seuil large fallback heartbeat` : élargit le seuil de détection image tronquée à `< 1200×800` (était `< 400×200`) → fallback heartbeat plus fréquent ; force `execution_mode='supervised'` par défaut quand non précisé. - `f62fda575` (7 mai) — `fix(stream): /resolve_target — fallback heartbeat full si image client tronquée` : introduit le remplacement silencieux de l'image client par un heartbeat disque/mémoire si tronquée. - `22c0a2ba6` (6 mai) — `revert: désactiver self-healing Win+D auto (cercle vicieux)` : retire l'injection automatique de Win+D au retry 1 sur `verification_failed`/`no_screen_change`. - `c969f93a2` (6 mai) — `fix(replay): self-healing Win+D auto au retry 1` : commit de la fonctionnalité, reverté par 22c0a2ba6. - `1cbec2806` (6 mai) — `fix(resolve): rebrancher hybrid_text_direct dans _resolve_target_sync` : reconnecte `_resolve_by_ocr_text` dans la cascade strict, qui auparavant n'était appelé que par le chemin V4 pré-compilé (non actif dans la majorité des workflows). - `b584bbabc` (1 mai) — `fix(stream): robustesse proxy VWB→streaming + ciblage textuel pour démo UHCD` : avait remplacé le rejet strict du drift par un `fallback_recorded_coords` (resolved=True). Reverté factuellement par 40440f1ca le 7 mai. - `35b27ae49` (2 mai) — `fix(stream+vwb): chaîne replay robuste — auth, anchor type_text, lock async, drift, prompt LLM` : introduit l'exemption drift pour `template_matching ≥ 0.95` (point d'entrée du finding F2.2.1). ## 4. Pistes prioritaires (P0 « pré-check OCR rejette systématiquement ») Findings directement reliés au P0 (motif factuel : « pré-check OCR rejette systématiquement, contrôles débranchés suite à checkout antérieur ») : 1. **F2.3.1 + F2.6.2** — `agent_v0/server_v1/api_stream.py:4519-4521` : flag `RPA_ENABLE_TEXT_PRECHECK` à défaut `"false"`. C'est le geste d'extinction explicite mentionné dans le commit `56e869c46` ; le pré-check OCR ne peut rejeter quoi que ce soit en l'état tant que la variable d'environnement n'est pas positionnée à `"true"` côté service rpa-streaming. Confirme directement la piste signalée. 2. **F2.1.1** — `agent_v0/server_v1/resolve_engine.py:2239-2289` (`_validate_text_at_position`) + `agent_v0/server_v1/api_stream.py:4525-4533` (point d'appel) : la fonction est conservée en place mais son point d'appel est conditionné par F2.3.1. Tant que le flag est off, la fonction est définie mais non consommée — état exact « contrôle débranché ». 3. **F2.6.5** — `agent_v0/server_v1/api_stream.py:3011-3017` : `pause_for_human ignorée (mode autonome) — replay continue`. Si `execution_mode` n'est pas propagé jusqu'au replay (cf. commit 7233df2bb qui corrige ce câblage), la pause supervisée censée intercepter un rejet OCR est skipée et la file d'actions continue. Combiné à F2.3.1, donne le motif observé : « rejets pré-check silencieux → cascade locale Léa → clic au pif » (extrait commit message `56e869c46`). ## 5. Findings côté CLIENT (`agent_v0/agent_v1/core/executor.py`) Fichier audité : `agent_v0/agent_v1/core/executor.py` (2893 lignes). Aucune occurrence de `RPA_ENABLE_*`, `RPA_DISABLE_*`, `if False:` ou `if 0:` dans ce fichier. ### 5.1 Validations désactivées ou non consommées Aucun finding (le client ne contient aucun bloc de validation neutralisé : la pré-vérif titre fenêtre, `_check_and_pause_on_system_dialog` et la cascade Observer→Policy sont toutes appelées en flux nominal). ### 5.2 Garde-fous court-circuités (seuils, flags, conditions) **F5.2.1 — `_check_and_pause_on_system_dialog` fail-closed sur exception (durcissement, pas une désactivation)** - `agent_v0/agent_v1/core/executor.py:2001-2043` - Citation : ``` except Exception as e: # Fix P0-D : fail-closed (principe "faux positif tolérable, # faux négatif catastrophique"). [...] self._system_dialog_pause = { "category": "unknown_check_failed", [...] } [...] return True ``` - Statut : actif. C'est un fail-closed (toute erreur de détection → pause supervisée), pas un fallback permissif. Listé pour traçabilité. **F5.2.2 — Seuil template-matching `_find_text_on_screen` durci à 0.75** - `agent_v0/agent_v1/core/executor.py:2367` - Citation : ``` threshold = 0.75 # Démo GHT 8 mai — éviter faux positifs (placeholders italiques, tabs voisins). En dessous, mieux vaut tomber en mode apprentissage humain qu'un clic au pif. ``` - Statut : actif. Seuil élevé en démo GHT (commit `7847a0e82`, 7 mai) — under threshold = pas de match retourné. Pas une désactivation, un durcissement. **F5.2.3 — Skip conditional_on_window : action acquittée success=True quand le dialogue n'est pas apparu** - `agent_v0/agent_v1/core/executor.py:567-592` - Citation : ``` if not match: [...] print( f" [SKIP] Dialogue '{cond_window}' absent → action skippée" ) result["success"] = True result["warning"] = "conditional_skipped" return result ``` - Statut : actif. Comportement attendu (skip explicite avec `warning=conditional_skipped`) mais l'action est rapportée `success=True` au serveur — pas une erreur côté replay engine. **F5.2.4 — `wrong_window_skipped` : action skippée silencieusement après timeout apprentissage** - `agent_v0/agent_v1/core/executor.py:754-764` - Citation : ``` else: # Timeout ou pas d'action → skipper cette action # L'état est peut-être déjà correct (ex: Ctrl+S # a sauvé sans dialogue → action de dialogue inutile) result["success"] = True result["warning"] = "wrong_window_skipped" logger.info( f"[LEA] Wrong window sans correction → skip " f"(l'état est peut-être déjà atteint)" ) ``` - Statut : actif. Si l'humain ne corrige pas dans les 120s (`_capture_human_correction(timeout_s=120)`), l'action est marquée success=True. Mêmes lignes pour `policy_skip` (`executor.py:993-996`). **F5.2.5 — Polling timeout REPLAY étendu à 30s pour démo GHT** - `agent_v0/agent_v1/core/executor.py:1786-1794` - Citation : ``` # 8 mai 2026 — démo GHT : 5s → 30s. Le serveur peut exécuter # extract_text (5-7s) PUIS dispatcher l'action suivante dans # la même réponse HTTP. À 5s, le client coupait avant la # réponse [...] timeout=30, ``` - Statut : actif. Marqueur démo, modification non commitée à HEAD (status `Not Committed Yet 2026-05-08`). Pas un contrôle débranché, contournement coûts d'IO. ### 5.3 Flags d'environnement avec défaut permissif **F5.3.1 — `RPA_OLLAMA_HOST`, défaut `"localhost"`** - `agent_v0/agent_v1/core/executor.py:2224` (et autres sites) - Citation : ``` ollama_host = os.environ.get("RPA_OLLAMA_HOST", "localhost") ``` - Statut : configuration uniquement, pas un garde-fou. Aucun autre flag environnemental dans le client : pas de `RPA_ENABLE_*`/`RPA_DISABLE_*` côté agent V1. ### 5.4 Étapes de cascade neutralisées **F5.4.1 — Self-healing désactivé côté client (revert miroir du serveur)** - Le client n'embarque pas de logique self-healing autonome — l'injection Win+D était purement serveur (cf. F2.2.3, commit `22c0a2ba6`). Côté client, la branche d'apprentissage humain (`_capture_human_correction`) reste l'unique recours en cas d'échec retry. - Statut : pas un finding spécifique au client. ### 5.5 Fonctions améliorantes définies mais non appelées **F5.5.1 — `_handle_possible_popup` (legacy clavier Enter/Escape/Tab+Enter) toujours définie** - `agent_v0/agent_v1/core/executor.py:2430-2472` - Citation : ``` def _handle_possible_popup(self) -> bool: """Tenter de gerer une popup imprevue. [...] Strategie simple (non bloquante, max ~3s) : 1. Essayer Enter (valide le bouton par defaut de la popup) 2. Si ca ne marche pas, essayer Escape (ferme la popup) 3. Si ca ne marche pas, essayer Tab + Enter [...] ``` - Recherche `_handle_possible_popup(` dans le client : 1 site (la définition). 0 site d'appel hors la définition. - Statut : fonction morte côté client. Le chemin actif est `_handle_popup_vlm` (executor.py:2102) + Observer/Policy. La version "clavier seul" est conservée mais non câblée. ### 5.6 Marqueurs de dette (TODO/FIXME/disabled/démo) dans le client **F5.6.1 — Marqueur explicite « démo GHT » multiple** - `agent_v0/agent_v1/core/executor.py:1786, 1813, 1835, 2367` : 4 commentaires « 8 mai 2026 — démo GHT » documentant des changements ciblés (timeout polling, plan B pause UX, threshold FIND-TEXT 0.75). **F5.6.2 — Plusieurs `except Exception: pass` silencieux** - `agent_v0/agent_v1/core/executor.py:455-456, 722-723, 958-959, 1017-1018, 1127-1128, 1244-1245, 1286-1287, 2619-2620` - Statut : 8 sites, tous sur des chemins best-effort (notification, snapshot UIA, log d'apprentissage). Aucun ne masque une décision de sécurité. ## 6. Autres fichiers .py modifiés < 14 jours (hors serveur/client déjà audités) Périmètre : 23 fichiers Python modifiés depuis 2026-04-24 hors `tests/`, `docs/`, `visual_workflow_builder/`, `web_dashboard/`, `agent_chat/`, `_archive/`, `tools/` et le dossier `agent_v0/server_v1/` (déjà audité §1-§4) et `agent_v0/agent_v1/core/executor.py` (déjà audité §5). ### 6.1 `core/execution/observe_reason_act.py` (2008 lignes) **F6.1.1 — Bloc `if False:` désactivant le pre-check VLM par-clic** - `core/execution/observe_reason_act.py:1704-1713` - Citation : ``` # --- Pas de pre-check VLM (le pipeline FAST→SMART→THINK a déjà validé) --- if False: try: pre_check = self._verify_pre_click(x, y, target_text, target_desc) if not pre_check: print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic") return False except Exception as e: print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}") ``` - Statut : désactivation explicite par `if False:`. Le commentaire justifie : « le pipeline FAST→SMART→THINK a déjà validé ». **F6.1.2 — `_verify_pre_click` : `return True` permissif sur erreur HTTP/exception** - `core/execution/observe_reason_act.py:1917, 1921` - Citation : ``` return True # En cas d'erreur HTTP, on laisse passer [...] return True # En cas d'erreur, on laisse passer ``` - Statut : fonction conservée mais devenue inerte (cf. F6.1.1). Si réactivée, retourne `True` (passe le clic) sur toute erreur Ollama/réseau. **F6.1.3 — `_act_type` : texte vide → `return True`** - `core/execution/observe_reason_act.py:1740-1742` - Citation : ``` if not decision.value: logger.warning("🎯 [ORA/type] Pas de texte à saisir") return True # Vide = rien à faire, pas un échec ``` - Statut : actif. Comportement documenté. **F6.1.4 — Cascade post-shortcut : timeout retourne `True` après ≥1 dialog géré** - `core/execution/observe_reason_act.py:1547-1550` - Citation : ``` if _elapsed() >= total_timeout: print(f"⏳ [ORA/post-shortcut] Timeout cascade ({total_timeout:.0f}s, " f"{dialogs_handled} dialog(s) géré(s))") return True # au moins un dialog traité → considéré OK ``` - Statut : actif. Politique permissive sur timeout cascade dialogues. **F6.1.5 — Flag `RPA_USE_FAST_PIPELINE`, défaut `"1"` (activé)** - `core/execution/observe_reason_act.py:1634` - Citation : ``` _use_fast = os.environ.get('RPA_USE_FAST_PIPELINE', '1') == '1' ``` - Statut : on par défaut. Désactivable via env. ### 6.2 `core/grounding/fast_pipeline.py` (216 lignes) **F6.2.1 — Expression mort-née `if False else screenshot_pil` dans appel arbiter** - `core/grounding/fast_pipeline.py:163` - Citation : ``` screenshot_pil=screenshot_pil or snapshot.elements[0] if False else screenshot_pil, ``` - Statut : à cause du `if False`, l'expression est équivalente à `screenshot_pil=screenshot_pil`. La branche `snapshot.elements[0]` n'est jamais évaluée. Probable reliquat d'expérimentation. ### 6.3 `core/grounding/title_verifier.py` (174 lignes) **F6.3.1 — `has_title_changed` retourne `True` si un seul titre est vide** - `core/grounding/title_verifier.py:73-74` - Citation : ``` if not title_before or not title_after: return True # Un des deux est vide = changement ``` - Statut : actif. Politique documentée — `not bloquante` (échec lecture titre = signal de changement). ### 6.4 `core/grounding/ui_tars_grounder.py` (288 lignes) **F6.4.1 — `available` toujours `True` sans vérifier le worker** - `core/grounding/ui_tars_grounder.py:135-137` - Citation : ``` @property def available(self) -> bool: return True # Toujours disponible — le script se lance à la demande ``` - Statut : actif. Pas de probe socket — la disponibilité est assumée et l'erreur réelle remonte au moment de `ground()`. ### 6.5 Fichiers sans finding Audit pattern par pattern (`if False`, `return True/False` suspects, `RPA_ENABLE_/RPA_DISABLE_`, `# disabled`, `# bypass`, `# TODO re-enable`, marqueurs démo, blocs `try` swallow exception sur fonction de validation) : | Fichier | Findings | |---|---| | `agent_v0/agent_v1/main.py` | aucun finding | | `agent_v0/agent_v1/network/feedback_bus.py` | aucun finding | | `agent_v0/agent_v1/ui/chat_window.py` | aucun finding (le marqueur `# démo GHT` ligne 846 documente uniquement un comportement UX) | | `agent_v0/agent_v1/ui/notifications.py` | aucun finding (idem ligne 143) | | `agent_v0/agent_v1/ui/paused_toast.py` | aucun finding | | `agent_v0/agent_v1/vision/capturer.py` | aucun finding | | `core/execution/input_handler.py` | aucun finding | | `core/grounding/dialog_handler.py` | aucun finding | | `core/grounding/element_signature.py` | aucun finding | | `core/grounding/fast_detector.py` | aucun finding | | `core/grounding/infigui_worker.py` | aucun finding | | `core/grounding/pipeline.py` | aucun finding | | `core/grounding/server.py` | aucun finding | | `core/grounding/shadow_learning_hook.py` | aucun finding | | `core/grounding/smart_matcher.py` | aucun finding | | `core/grounding/template_matcher.py` | aucun finding | | `core/grounding/think_arbiter.py` | aucun finding (`available = True` ligne 38 même pattern que F6.4.1, mais arbiter délègue au grounder qui détient F6.4.1) | | `core/knowledge/ui_patterns.py` | aucun finding | | `core/llm/ocr_extractor.py` | aucun finding | | `core/llm/t2a_decision.py` | aucun finding | ## 7. Datation git des findings (toutes sections confondues) | Finding | Fichier:ligne | Commit | Date | Message commit (raccourci) | |---|---|---|---|---| | F2.1.1 | api_stream.py:4519-4533 | `56e869c46` (gate) + `40440f1ca` (corps) | 2026-05-08 / 2026-05-07 | flag pré-check OCR off / cure régression b584bbabc | | F2.1.2 | resolve_engine.py:2253-2289 | `40440f1ca` | 2026-05-07 | cure régression b584bbabc | | F2.1.3 | replay_engine.py:1374-1379 | `4509038bf` | 2026-04-09 | refactor api_stream.py 6400→3350 | | F2.1.4 | api_stream.py:3394-3399 | `d5deac302` (corps) + `ae65be255` (3398) | 2026-03-26 / 2026-03-18 | feat replay visuel VLM-first | | F2.1.5 | resolve_engine.py:201, 1864 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.2.1 | resolve_engine.py:2367-2390 | `35b27ae49` (intro) → `40440f1ca` (élargi) | 2026-05-02 / 2026-05-07 | chaîne replay robuste / cure régression | | F2.2.2 | resolve_engine.py:2359-2363 | `a21f1ea9f` | 2026-04-11 | garde qualité résolution | | F2.2.3 | replay_engine.py (revert) | `c969f93a2` (intro) → `22c0a2ba6` (revert) | 2026-05-06 | self-healing Win+D / désactivation cercle vicieux | | F2.2.4 | api_stream.py:999-1001 | `d5deac302` | 2026-03-26 | feat replay visuel VLM-first | | F2.2.5 | resolve_engine.py:655-662 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.2.6 | api_stream.py:4422 | `7233df2bb` | 2026-05-07 | câblage execution_mode + seuil heartbeat élargi | | F2.2.7 | resolve_engine.py:1655-1691 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.3.1 | api_stream.py:4519-4521 | `56e869c46` | 2026-05-08 | flag pré-check OCR off par défaut (démo GHT) | | F2.3.2 | api_stream.py:107-119 | `93ef93e56` | 2026-04-14 | API streaming fail-closed | | F2.3.3 | loop_detector.py:42-47 | `2a51a844b` | 2026-05-05 | LoopDetector composite | | F2.3.4 | safety_checks_provider.py:42-44 | `7c6945171` | 2026-05-05 | SafetyChecksProvider hybride | | F2.3.5 | api_stream.py:1023 | `93ef93e56` | 2026-04-14 | API streaming fail-closed | | F2.4.1 | resolve_engine.py:293 (def) | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.4.2 | resolve_engine.py:1613-1635 | `f6ad5ff2b` | 2026-04-10 | runtime V4 honore resolve_order | | F2.4.3 | resolve_engine.py:1696-1715 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.4.4 | resolve_engine.py:1750-1790 | `1cbec2806` | 2026-05-06 | rebrancher hybrid_text_direct | | F2.4.5 | resolve_engine.py:1733, 1847-1875 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.5.1 | resolve_engine.py:293 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.5.2 | api_stream.py:4372 (import mort) | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.5.3 | api_stream.py:4362, 4365 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.6.1 | task_planner.py:400 | `99041f011` | 2026-04-09 | pipeline complet MACRO/MÉSO/MICRO | | F2.6.2 | api_stream.py:4512 | `56e869c46` | 2026-05-08 | flag pré-check OCR off par défaut | | F2.6.3 | replay_engine.py:1377 | `4509038bf` | 2026-04-09 | refactor api_stream.py | | F2.6.4 | resolve_engine.py:2288 | `40440f1ca` | 2026-05-07 | cure régression b584bbabc | | F2.6.5 | api_stream.py:3011-3017 | `964856ab3` (intro) → `35b27ae49` / `65da55731` (raffinements) | 2026-04-29 / 2026-05-02 / 2026-05-05 | extract_text serveur / chaîne replay robuste / SafetyChecksProvider | | F2.6.6 | replay_engine.py:1377 + resolve_engine.py:2196, 2288 | `4509038bf` + `40440f1ca` | 2026-04-09 / 2026-05-07 | refactor / cure régression | | F5.2.1 | executor.py:2001-2043 | (déjà fail-closed antérieur) | — | durcissement, pas désactivation | | F5.2.2 | executor.py:2367 | `7847a0e82` | 2026-05-07 | toast paused supervisée + threshold FIND-TEXT 0.75 | | F5.2.3 | executor.py:567-592 | (antérieur) | — | (chemin conditionnel intégré) | | F5.2.4 | executor.py:754-764 | (antérieur) | — | mode apprentissage humain | | F5.2.5 | executor.py:1786-1794 | non commité (workdir) | 2026-05-08 | démo GHT (uncommitted change) | | F5.5.1 | executor.py:2430-2472 | (antérieur) | — | legacy popup handler | | F5.6.1 | executor.py:1786, 1813, 1835, 2367 | `7847a0e82` + workdir | 2026-05-07 / 2026-05-08 | démo GHT | | F6.1.1 | observe_reason_act.py:1705 | `e2046837c` | 2026-04-25 | Phase 5 — pipeline FAST→SMART→THINK dans ORA | | F6.1.2 | observe_reason_act.py:1917, 1921 | `8903f3543` | 2026-04-22 | feat ORA — vérification pré-action VLM | | F6.1.3 | observe_reason_act.py:1742 | `0c5fffe95` | 2026-04-22 | boucle ORA observe→raisonne→agit | | F6.1.4 | observe_reason_act.py:1550 | `487bcb861` | 2026-04-26 | cascade post-raccourci DialogHandler/OCR | | F6.1.5 | observe_reason_act.py:1634 | `e2046837c` | 2026-04-25 | Phase 5 — FAST→SMART→THINK dans ORA | | F6.2.1 | fast_pipeline.py:163 | `b30d4b665` | 2026-04-25 | Phase 4 — Pipeline orchestré FAST→SMART→THINK | | F6.3.1 | title_verifier.py:73-74 | `343d6fbe9` | 2026-04-26 | EasyOCR remplace docTR (FastDetector + TitleVerifier) | | F6.4.1 | ui_tars_grounder.py:137 | `487bcb861` | 2026-04-26 | cascade post-raccourci DialogHandler/OCR | ## 8. Code original avant désactivation (quand récupérable) ### F2.1.1 / F2.3.1 / F2.6.2 — Pré-check OCR (`api_stream.py:4519-4533`) **Avant** (commit `40440f1ca`, 2026-05-07) — pré-check appelé inconditionnellement après résolution : ```python if result and result.get("resolved"): _by_text = (request.target_spec.get("by_text") or "").strip() if _by_text: from agent_v0.server_v1.resolve_engine import _validate_text_at_position _is_valid, _observed, _ocr_ms = _validate_text_at_position( tmp_path, float(result.get("x_pct", 0) or 0), float(result.get("y_pct", 0) or 0), _by_text, effective_w, effective_h, ) ``` **Après** (HEAD `56e869c46`, 2026-05-08) — pré-check gardé par flag off-by-default : ```python _text_precheck_enabled = os.environ.get( "RPA_ENABLE_TEXT_PRECHECK", "false" ).lower() in ("true", "1", "yes") if _text_precheck_enabled and result and result.get("resolved"): _by_text = (request.target_spec.get("by_text") or "").strip() if _by_text: from agent_v0.server_v1.resolve_engine import _validate_text_at_position _is_valid, _observed, _ocr_ms = _validate_text_at_position(...) ``` ### F2.2.3 — Self-healing Win+D au retry 1 (revert) **Avant** (commit `c969f93a2`, 2026-05-06) — code introduit (non récupéré ici via `git show` car branche restaurée par revert immédiat). **Après** (HEAD via `22c0a2ba6`, 2026-05-06) — branche `next_retry == 1` retirée, seule `next_retry == 2` (wait 2s) conservée : ```python if next_retry == 2: wait_action = { "action_id": f"wait_retry_{uuid.uuid4().hex[:6]}", "type": "wait", "duration_ms": 2000, } actions_to_insert.append(wait_action) ``` ### F2.2.6 — Seuil image tronquée **Avant** (`f62fda575`, 2026-05-07) — seuil minimal `< 400×200` (placeholders triviaux) : ```python if img.height < 200 or img.width < 400: [...] ``` **Après** (`7233df2bb`, 2026-05-07) — seuil élargi à `< 1200×800` : ```python if img.height < 800 or img.width < 1200: [...] ``` ### F2.4.4 — Reconnect `hybrid_text_direct` dans cascade strict **Avant** (avant `1cbec2806`, ≤ 2026-05-05) — `_resolve_by_ocr_text` n'était appelée QUE depuis le runtime V4 pré-compilé (extrait commit message). Code original non re-extrait ligne par ligne (commit message factuel suffit). **Après** (HEAD via `1cbec2806`, 2026-05-06) — appel ajouté dans `_resolve_target_sync` (cascade strict, resolve_engine.py:1750-1790). ### F6.1.1 — Désactivation pre-check VLM par-clic (`observe_reason_act.py:1704-1713`) **Avant** (commit `8903f3543`, 2026-04-22) : ```python # --- Vérification pré-action (skip si UI-TARS a déjà validé visuellement) --- if target_text and method_used not in ('template', 'ui_tars') and MSS_AVAILABLE and PIL_AVAILABLE: try: pre_check = self._verify_pre_click(x, y, target_text, target_desc) if not pre_check: print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic") return False except Exception as e: print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}") ``` **Après** (HEAD via `e2046837c`, 2026-04-25) : ```python # --- Pas de pre-check VLM (le pipeline FAST→SMART→THINK a déjà validé) --- if False: try: pre_check = self._verify_pre_click(x, y, target_text, target_desc) if not pre_check: print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic") return False except Exception as e: print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}") ``` ### Findings sans version « avant » récupérable (≤ 14 jours) Les findings suivants n'ont pas de version « activée » dans la fenêtre 14 jours (la désactivation est antérieure ou native dès l'introduction du code) : - F2.1.3, F2.1.4, F2.1.5 (commits `4509038bf` du 2026-04-09 et `d5deac302` du 2026-03-26) - F2.2.2, F2.2.4, F2.2.5, F2.2.7 (commits 2026-04-09 à 2026-04-11) - F2.3.2, F2.3.5 (commit `93ef93e56` du 2026-04-14) - F2.4.1, F2.4.3, F2.4.5, F2.5.1, F2.5.2, F2.5.3 (commit `4509038bf`) - F2.6.1 (commit `99041f011` du 2026-04-09) - F6.1.2, F6.1.3, F6.1.4 (commits 2026-04-22 à 2026-04-26 — `return True` permissif natif) - F6.2.1, F6.3.1, F6.4.1 (commits 2026-04-25 à 2026-04-26 — états natifs) - F5.2.1, F5.2.3, F5.2.4, F5.5.1 (chemins clients antérieurs, non touchés < 14 jours) Pour ces findings, le motif factuel est « introduction native du contrôle déjà à l'état permissif/désactivé », pas une bascule postérieure.