fix(agent_v1): assainissement PII des logs client a la source (push-log-DGX, brique 4)

Remplace dans les logs/print le contenu utilisateur brut par un equivalent
PII-safe via core/log_safe : titres de fenetre -> _title_hash, reponses VLM ->
[len,has_target], metadonnees -> _sanitize_metadata, chemins -> _path_ext,
workflow_name -> _title_hash. 8 fichiers (executor, recovery, captor, streamer,
main, capture_server, activity_panel, window_info_crossplatform).

Audit Qwen complete : ~17 fuites de titre multi-lignes + 2e fuite VLM (print)
non listees ont ete traitees ; localisation par contenu (refs Qwen derivees).

Preserve volontairement : prompts de grounding VLM (vlm_description) ou le titre
est load-bearing (resolution 100% vision) -> ne PAS hasher.
Differe : window_focus_change (verdict apprentissage).
En attente arbitrage Dom : button_text (~11 captions), patterns, champs detail.

py_compile 8/8 OK, imports OK, helper 6/6 vert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-27 11:42:40 +02:00
parent 4a38000e74
commit 46ad5973d1
8 changed files with 72 additions and 60 deletions

View File

@@ -32,6 +32,7 @@ from pynput.keyboard import Key, KeyCode
# Importation relative pour rester dans le module v1
from ..vision.capturer import VisionCapturer
from ..vision.system_info import get_screen_metadata
from .log_safe import _sanitize_metadata
# from ..monitoring.system import SystemMonitor
logger = logging.getLogger(__name__)
@@ -676,7 +677,7 @@ class EventCaptorV1:
metadata = get_screen_metadata()
with self._screen_metadata_lock:
self._screen_metadata = metadata
logger.debug(f"Métadonnées système rafraîchies : {metadata}")
logger.debug(f"Métadonnées système rafraîchies : {_sanitize_metadata(metadata)}")
except Exception as e:
logger.error(f"Erreur refresh métadonnées système : {e}")

View File

@@ -26,6 +26,7 @@ from typing import Any, Dict, Optional
# DPI awareness est configure (SetProcessDpiAwareness(2) sur Windows).
# Sans cela, pynput et mss utilisent des coordonnees logiques (virtualisees).
from ..config import MACHINE_ID as _ # noqa: F401 — side-effect import
from .log_safe import _title_hash
import mss
from pynput.mouse import Button, Controller as MouseController
@@ -862,7 +863,7 @@ class ActionExecutorV1:
)
if handled:
logger.info(
f"[RUNTIME-DIALOG] '{current_title}' gere via serveur "
f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere via serveur "
f"fenetre -> bouton '{button_text}' "
f"[{resolved.get('method', 'server')}]"
)
@@ -890,7 +891,7 @@ class ActionExecutorV1:
)
if handled:
logger.info(
f"[RUNTIME-DIALOG] '{current_title}' gere localement "
f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere localement "
f"fenetre -> bouton '{button_text}' [dialog_window_text_template]"
)
return handled
@@ -917,7 +918,7 @@ class ActionExecutorV1:
)
if handled:
logger.info(
f"[RUNTIME-DIALOG] '{current_title}' gere par geometrie "
f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere par geometrie "
f"fenetre -> bouton '{button_text}'"
)
return handled
@@ -967,7 +968,7 @@ class ActionExecutorV1:
if not handled:
continue
logger.info(
f"[RUNTIME-DIALOG] '{current_title}' gere via serveur "
f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere via serveur "
f"-> bouton '{button_text}' [{resolved.get('method', 'server')}]"
)
return handled
@@ -992,13 +993,13 @@ class ActionExecutorV1:
if not handled:
continue
logger.info(
f"[RUNTIME-DIALOG] '{current_title}' gere localement "
f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere localement "
f"-> bouton '{button_text}' [dialog_text_template]"
)
return handled
logger.info(
f"[RUNTIME-DIALOG] Aucun bouton resolu pour '{current_title}'"
f"[RUNTIME-DIALOG] Aucun bouton resolu pour [title_hash={_title_hash(current_title)}]"
)
return None
@@ -1258,7 +1259,7 @@ class ActionExecutorV1:
if dialog_spec.get("skip_current_action_after_handle", False):
logger.info(
f"[RUNTIME-DIALOG] Dialogue '{current_title}' gere -> "
f"[RUNTIME-DIALOG] Dialogue [title_hash={_title_hash(current_title)}] gere -> "
f"action {action.get('action_id', 'unknown')} skippée"
)
return {
@@ -1587,7 +1588,7 @@ class ActionExecutorV1:
]
for pattern in popup_patterns:
if pattern in current_title:
logger.info(f"Observer : popup détectée par titre — '{current_title}'")
logger.info(f"Observer : popup détectée par titre — [title_hash={_title_hash(current_title)}]")
# On ne peut pas résoudre les coords juste par le titre
# → retourner popup sans coords, le caller fera handle_popup_vlm()
return {
@@ -1874,8 +1875,8 @@ class ActionExecutorV1:
)
else:
logger.warning(
f"[LEA] Fenêtre incorrecte : attendu '{expected_title}', "
f"actuel '{current_title}'"
f"[LEA] Fenêtre incorrecte : attendu [title_hash={_title_hash(expected_title)}], "
f"actuel [title_hash={_title_hash(current_title)}]"
)
auto_result = self._maybe_handle_runtime_dialog_before_pause(
action=action,
@@ -1888,8 +1889,8 @@ class ActionExecutorV1:
if auto_result is not None:
return auto_result
print(
f" [PRÉ-VÉRIF] Fenêtre '{current_title}'"
f"attendu '{expected_title}' → mode apprentissage"
f" [PRÉ-VÉRIF] Fenêtre [title_hash={_title_hash(current_title)}]"
f"attendu [title_hash={_title_hash(expected_title)}] → mode apprentissage"
)
try:
self.notifier.replay_learning_mode(
@@ -1936,8 +1937,8 @@ class ActionExecutorV1:
# des coordonnées devenues invalides.
result["success"] = False
result["error"] = (
f"Fenêtre incorrecte : attendu '{expected_title}', "
f"actuel '{current_title}'"
f"Fenêtre incorrecte : attendu [title_hash={_title_hash(expected_title)}], "
f"actuel [title_hash={_title_hash(current_title)}]"
)
result["warning"] = "wrong_window"
result["target_description"] = expected_title
@@ -1945,11 +1946,11 @@ class ActionExecutorV1:
result["screenshot"] = self._capture_screenshot_b64()
logger.warning(
f"[LEA] Wrong window sans correction → pause "
f"(attendu '{expected_title}', actuel '{current_title}')"
f"(attendu [title_hash={_title_hash(expected_title)}], actuel [title_hash={_title_hash(current_title)}])"
)
return result
else:
logger.info(f"[LEA] Pré-vérif OK : '{current_title}'")
logger.info(f"[LEA] Pré-vérif OK : [title_hash={_title_hash(current_title)}]")
# ── OBSERVER : pré-analyse écran avant résolution ──
# Détecte popups, dialogues, états inattendus AVANT de chercher la cible.
@@ -1964,8 +1965,8 @@ class ActionExecutorV1:
# Popup détectée AVANT la résolution — la fermer
popup_label = observation.get("popup_label", "popup")
popup_coords = observation.get("popup_coords")
print(f" [OBSERVER] Popup détectée : '{popup_label}' — fermeture")
logger.info(f"Observer : popup '{popup_label}' détectée avant résolution")
print(f" [OBSERVER] Popup détectée : [title_hash={_title_hash(popup_label)}] — fermeture")
logger.info(f"Observer : popup [title_hash={_title_hash(popup_label)}] détectée avant résolution")
# ── SÉCURITÉ : refuser de cliquer sur un dialogue système ──
# Avant de suivre les coordonnées du serveur (VLM-based,
@@ -2365,8 +2366,8 @@ class ActionExecutorV1:
recheck_title = recheck_info.get("title", "")
if not _matches_expected_window(recheck_title):
logger.warning(
f"P0.9 transition instable : matched '{post_title}' "
f"puis '{recheck_title}' à T+0.5s ≠ '{expected_after}'"
f"P0.9 transition instable : matched [title_hash={_title_hash(post_title)}] "
f"puis [title_hash={_title_hash(recheck_title)}] à T+0.5s ≠ [title_hash={_title_hash(expected_after)}]"
)
matched = False
post_title = recheck_title
@@ -2376,19 +2377,19 @@ class ActionExecutorV1:
result["runtime_dialog"] = runtime_dialog_handled
print(
f" [POST-VÉRIF] Dialogue runtime géré "
f"→ retour '{post_title}'"
f"→ retour [title_hash={_title_hash(post_title)}]"
)
logger.info(
"POST-VÉRIF runtime dialog géré : '%s' -> '%s'",
runtime_dialog_handled.get("dialog_title", ""),
post_title,
"POST-VÉRIF runtime dialog géré : [title_hash=%s] -> [title_hash=%s]",
_title_hash(runtime_dialog_handled.get("dialog_title", "")),
_title_hash(post_title),
)
else:
print(f" [POST-VÉRIF] OK en {elapsed_wait:.1f}s — '{post_title}'")
logger.info(f"POST-VÉRIF OK en {elapsed_wait:.1f}s : '{post_title}'")
print(f" [POST-VÉRIF] OK en {elapsed_wait:.1f}s — [title_hash={_title_hash(post_title)}]")
logger.info(f"POST-VÉRIF OK en {elapsed_wait:.1f}s : [title_hash={_title_hash(post_title)}]")
else:
print(f" [POST-VÉRIF] TIMEOUT {max_wait}s — '{post_title}''{expected_after}'")
logger.warning(f"POST-VÉRIF TIMEOUT : '{post_title}''{expected_after}'")
print(f" [POST-VÉRIF] TIMEOUT {max_wait}s — [title_hash={_title_hash(post_title)}] ≠ [title_hash={_title_hash(expected_after)}]")
logger.warning(f"POST-VÉRIF TIMEOUT : [title_hash={_title_hash(post_title)}] ≠ [title_hash={_title_hash(expected_after)}]")
if runtime_dialog_handled:
result["warning"] = (
f"runtime_dialog_handled_post_verify:{post_title}"
@@ -2396,9 +2397,9 @@ class ActionExecutorV1:
result["runtime_dialog"] = runtime_dialog_handled
logger.warning(
"POST-VÉRIF runtime dialog géré mais "
"fenêtre finale inattendue : '%s''%s'",
post_title,
expected_after,
"fenêtre finale inattendue : [title_hash=%s] ≠ [title_hash=%s]",
_title_hash(post_title),
_title_hash(expected_after),
)
# Contrôle strict : si success_strict, on STOP.
# On durcit aussi les vrais changements de fenêtre
@@ -2416,8 +2417,8 @@ class ActionExecutorV1:
if bool(action.get("success_strict")) or requires_transition:
result["success"] = False
result["error"] = (
f"Post-vérif échouée : fenêtre '{post_title}' "
f"au lieu de '{expected_after}'"
f"Post-vérif échouée : fenêtre [title_hash={_title_hash(post_title)}] "
f"au lieu de [title_hash={_title_hash(expected_after)}]"
)
result["warning"] = "wrong_window"
result["needs_human"] = True
@@ -2458,7 +2459,7 @@ class ActionExecutorV1:
# paste=True (opt-in via action.paste) → clipboard + Ctrl+V (non-Citrix)
self._type_text(text, paste=bool(action.get("paste", False)))
print(f" [TYPE] Termine.")
logger.info(f"Replay type : '{text[:30]}...' ({len(text)} chars, raw_keys={'oui' if raw_keys else 'non'})")
logger.info(f"Replay type : [{len(text)} chars] (raw_keys={'oui' if raw_keys else 'non'})")
elif action_type == "key_combo":
keys = action.get("keys", [])
@@ -2524,12 +2525,12 @@ class ActionExecutorV1:
if not self._window_title_matches_any(current_title, patterns):
logger.warning(
"[LEA] verify_screen garde KO : attendu un titre "
"contenant %s, actuel '%s'",
patterns, current_title,
"contenant %s, actuel [title_hash=%s]",
patterns, _title_hash(current_title),
)
print(
f" [VERIFY] Garde titre KO "
f"(patterns={patterns}, actuel='{current_title}') "
f"(patterns={patterns}, actuel=[title_hash={_title_hash(current_title)}]) "
"→ apprentissage humain"
)
try:
@@ -2557,15 +2558,15 @@ class ActionExecutorV1:
result["error"] = (
f"verify_screen titre fenêtre KO : attendu "
f"un titre contenant {patterns}, "
f"actuel '{current_title}'"
f"actuel [title_hash={_title_hash(current_title)}]"
)
result["warning"] = "setup_guard_window_mismatch"
result["needs_human"] = True
result["screenshot"] = self._capture_screenshot_b64()
return result
logger.info(
"[LEA] verify_screen garde OK : '%s' matche %s",
current_title, patterns,
"[LEA] verify_screen garde OK : [title_hash=%s] matche %s",
_title_hash(current_title), patterns,
)
print(f" [VERIFY] Termine (verification deferred au serveur).")
@@ -3736,8 +3737,8 @@ Example: x_pct=0.50, y_pct=0.30"""
real_x = int(x_pct * sw)
real_y = int(y_pct * sh)
label = server_result.get("matched_element", {}).get("label", "popup")
print(f" [POPUP-SERVER] Popup détectée ! Clic sur '{label}' → ({real_x}, {real_y})")
logger.info(f"[POPUP-SERVER] Clic popup '{label}' à ({real_x}, {real_y})")
print(f" [POPUP-SERVER] Popup détectée ! Clic sur [title_hash={_title_hash(label)}] → ({real_x}, {real_y})")
logger.info(f"[POPUP-SERVER] Clic popup [title_hash={_title_hash(label)}] à ({real_x}, {real_y})")
self._click((real_x, real_y), "left")
time.sleep(1.0)
return True
@@ -3856,8 +3857,8 @@ Example: x_pct=0.50, y_pct=0.30"""
raw_content = resp.json().get("message", {}).get("content", "")
full_response = prefill + raw_content
print(f" [POPUP-VLM] Réponse en {elapsed:.1f}s : {full_response.strip()}")
logger.info(f"[POPUP-VLM] Réponse VLM ({elapsed:.1f}s) : {full_response.strip()}")
print(f" [POPUP-VLM] Réponse en {elapsed:.1f}s : [len={len(full_response)}, has_target={'target' in full_response}]")
logger.info(f"[POPUP-VLM] Réponse VLM ({elapsed:.1f}s) : [len={len(full_response)}, has_target={'target' in full_response}]")
# Extraire le texte du bouton depuis la réponse
button_text = raw_content.strip().strip('"').strip("'").strip(".")
@@ -4172,7 +4173,7 @@ Example: x_pct=0.50, y_pct=0.30"""
try:
self.keyboard.type(char)
except Exception as e:
logger.debug(f"Impossible de taper '{char}': {e}")
logger.debug(f"Impossible de taper [1 char typed]: {e}")
# Délai humain entre les frappes (40-120ms)
time.sleep(random.uniform(0.04, 0.12))

View File

@@ -24,6 +24,8 @@ from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional
from .log_safe import _title_hash
logger = logging.getLogger(__name__)
@@ -168,8 +170,8 @@ class RecoveryEngine:
from ..window_info_crossplatform import get_active_window_info
active = get_active_window_info()
active_title = active.get("title", "")
logger.info(f"Recovery : Alt+F4 sur '{active_title}'")
print(f" [RECOVERY] Alt+F4 — fermeture de '{active_title}'")
logger.info(f"Recovery : Alt+F4 sur [title_hash={_title_hash(active_title)}]")
print(f" [RECOVERY] Alt+F4 — fermeture de [title_hash={_title_hash(active_title)}]")
except Exception:
logger.info("Recovery : Alt+F4 (fenêtre active inconnue)")
print(" [RECOVERY] Alt+F4 — fermeture fenêtre indésirable")
@@ -182,7 +184,7 @@ class RecoveryEngine:
return RecoveryResult(
action_taken=RecoveryAction.CLOSE_WINDOW,
success=True,
detail=f"Alt+F4 exécuté sur '{active_title if 'active_title' in dir() else '?'}'",
detail=f"Alt+F4 exécuté sur [title_hash={_title_hash(active_title) if 'active_title' in dir() else '?'}]",
)
elif strategy == RecoveryAction.CLICK_AWAY:

View File

@@ -29,6 +29,7 @@ from .ui.capture_server import CaptureServer
from .session.storage import SessionStorage
from .vision.capturer import VisionCapturer
from .finalize_contract import dispatch_finalize_result
from .core.log_safe import _title_hash
# Import optionnel du client serveur (pour le chat et les workflows)
# Deux chemins : relatif (depuis agent_v0.agent_v1) ou absolu (depuis C:\rpa_vision\agent_v1)
@@ -261,7 +262,7 @@ class AgentV1:
# Ne PAS en relancer une ici — deux threads poll simultanés causent
# une race condition où les actions sont consommées mais pas exécutées.
logger.info(f"Session {self.session_id} ({workflow_name}) sur machine {self.machine_id} en cours...")
logger.info(f"Session {self.session_id} [wf_hash={_title_hash(workflow_name)}] sur machine {self.machine_id} en cours...")
def _command_watchdog_loop(self):
"""Surveille un fichier de commande pour executer des ordres visuels (legacy)."""

View File

@@ -36,6 +36,7 @@ import requests
from PIL import Image
from ..config import API_TOKEN, BASE_DIR, STREAMING_ENDPOINT
from ..core.log_safe import _title_hash
from .persistent_buffer import MAX_ATTEMPTS, PersistentBuffer
@@ -138,7 +139,7 @@ class TraceStreamer:
target=self._buffer_drain_loop, daemon=True
)
self._drain_thread.start()
logger.info(f"Streamer pour {self.session_id} démarré")
logger.info(f"Streamer démarré")
def stop(self):
"""Arrêter le streaming et finaliser la session côté serveur.
@@ -166,7 +167,7 @@ class TraceStreamer:
self._drain_thread.join(timeout=2.0)
self._finalize_session()
logger.info(f"Streamer pour {self.session_id} arrêté")
logger.info(f"Streamer arrêté")
def push_event(self, event_data: dict):
"""Enfile un événement pour envoi immédiat.
@@ -632,7 +633,7 @@ class TraceStreamer:
self._check_redirect(resp, url)
if resp.ok:
result = resp.json()
logger.info(f"Session finalisée: {result}")
logger.info(f"Session finalisée [status={result.get('status')}, wf_hash={_title_hash(result.get('workflow_name',''))}]")
if self._on_finalize_result is not None:
try:
self._on_finalize_result(result)

View File

@@ -29,6 +29,8 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from ..core.log_safe import _title_hash
logger = logging.getLogger(__name__)
@@ -132,7 +134,7 @@ class ActivityPanel:
)
self._notifier_changement()
self._rafraichir_ui()
logger.info(f"[ACTIVITY] Workflow démarré : {nom} ({nb_etapes} étapes)")
logger.info(f"[ACTIVITY] Workflow démarré : [wf_hash={_title_hash(nom)}] ({nb_etapes} étapes)")
def mettre_a_jour(
self,

View File

@@ -27,6 +27,8 @@ import os
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from ..core.log_safe import _path_ext
logger = logging.getLogger(__name__)
CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006"))
@@ -312,7 +314,7 @@ class _FileActionHandlerLocal:
})
extensions[ext] = extensions.get(ext, 0) + 1
logger.info(f"Liste dossier '{path_str}' : {len(files)} fichiers")
logger.info(f"Liste dossier [ext={_path_ext(path_str)}] : {len(files)} fichiers")
return {"files": files, "count": len(files), "extensions": extensions, "path": path_str}
def _create_dir(self, params: dict) -> dict:
@@ -328,7 +330,7 @@ class _FileActionHandlerLocal:
target = _Path(path_str)
existed = target.exists()
target.mkdir(parents=True, exist_ok=True)
logger.info(f"Dossier '{path_str}' {'existait deja' if existed else 'cree'}")
logger.info(f"Dossier [ext={_path_ext(path_str)}] {'existait deja' if existed else 'cree'}")
return {"created": not existed, "path": path_str, "already_existed": existed}
def _move_file(self, params: dict) -> dict:
@@ -350,7 +352,7 @@ class _FileActionHandlerLocal:
_Path(dst).parent.mkdir(parents=True, exist_ok=True)
_shutil.move(src, dst)
logger.info(f"Fichier deplace : '{src}' -> '{dst}'")
logger.info(f"Fichier deplace : [ext={_path_ext(src)}] -> [ext={_path_ext(dst)}]")
return {"moved": True, "source": src, "destination": dst}
def _copy_file(self, params: dict) -> dict:
@@ -376,7 +378,7 @@ class _FileActionHandlerLocal:
_shutil.copytree(src, dst)
else:
_shutil.copy2(src, dst)
logger.info(f"Fichier copie : '{src}' -> '{dst}'")
logger.info(f"Fichier copie : [ext={_path_ext(src)}] -> [ext={_path_ext(dst)}]")
return {"copied": True, "source": src, "destination": dst}
def _sort_by_extension(self, params: dict) -> dict:
@@ -425,7 +427,7 @@ class _FileActionHandlerLocal:
extensions[ext] = extensions.get(ext, 0) + 1
logger.info(
f"Classement par extension '{source_dir_str}' : {len(moved)} fichiers"
f"Classement par extension [ext={_path_ext(source_dir_str)}] : {len(moved)} fichiers"
)
return {
"moved": moved,

View File

@@ -19,6 +19,8 @@ import platform
import subprocess
from typing import Any, Dict, Optional
from .core.log_safe import _title_hash
def _run_cmd(cmd: list[str]) -> Optional[str]:
"""Exécute une commande et renvoie la sortie texte (strippée), ou None en cas d'erreur."""
@@ -372,7 +374,7 @@ if __name__ == "__main__":
for i in range(5):
info = get_active_window_info()
rect = get_active_window_rect()
print(f"[{i+1}] App: {info['app_name']:20s} | Title: {info['title']}")
print(f"[{i+1}] App: {info['app_name']:20s} | Title: [title_hash={_title_hash(info['title'])}]")
if rect:
print(f" Rect: {rect['rect']} | Size: {rect['size']}")
else: