Files
rpa_vision_v3/docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md
2026-05-09 11:32:52 +02:00

860 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.