8 Commits

Author SHA1 Message Date
Dom
c371c9775f chore(bench): résultats bruts bench OCR (docTR 139 + EasyOCR 54 items, référencé par doc bench)
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m50s
tests / Tests unitaires (sans GPU) (push) Failing after 1m56s
tests / Tests sécurité (critique) (push) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:49:41 +02:00
Dom
931cf13217 feat(navigate): jalon partiel D1 — compile navigate + coercion coords sûre
Ferme Gap C : _edge_to_normalized_actions produit désormais une action navigate
(handler serveur atteignable). Ajoute _coerce_action_coords : cast x_pct/y_pct en
float APRÈS résolution des templates, JAMAIS de fallback (0,0) — template non
résolu / valeur invalide → pause_for_human (safety_level=high). Non-régression
prouvée sur mouse_click classiques (idempotent sur floats).

⚠️ NE FERME PAS le write-only : Gap A (P1-B) non livré — aucun step click/type ne
déclare encore consommer navigate_login_coords. TestCompilerGapLiteralFloats
assert l'état ouvert. Boucle complète = chantier suivant (P1-B + test e2e edge→action).

21 tests verts, boot OK. Revue croisée Claude (GO jalon partiel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:49:32 +02:00
Dom
fd9efdbbf5 docs(bench): PP-OCRv5 vs docTR vs EasyOCR CPU — PP-OCRv5 BLOCKED, docTR reste roi
Bench candidat PP-OCRv5 (veille OCR 02/07) : CPU BLOCKED (bug upstream
paddlepaddle 3.3.1 PIR/OneDNN, non contournable). docTR CPU = meilleur
rapport qualité/latence (0.7s, 10/11, word-level bboxes). PaddleOCR venv =
confirmé ORPHAN. Bench GPU = action séparée si on veut ré-évaluer PP-OCRv5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:45:36 +02:00
Dom
19187e633e docs(carto): carte de référence wiring code branché/non branché
Carto « existing-first » prouvée (chaîne imports depuis points d'entrée actifs,
fichier:ligne). Découvertes : self-healing = façade morte (enable_healing
fantôme), navigation write-only (avant D1), autonomous_planner inerte, YOLO/
CLIP/V4/api_core morts. Corrige 4 erreurs de nos cartos antérieures. Cascade
résolution = VLM-first prouvé (≠ README OCR→template→YOLO→VLM).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:44:47 +02:00
Dom
9a34ecded6 docs(R1): acte create-or-skip + corrige docstring wiring périmé
Sémantique R1 tranchée par Dom (02/07) : create-or-skip (la version validée
par revue humaine fait foi, un ré-apprentissage ne l'écrase pas). Aligne
docstring learned_workflow_bridge (disait à tort « pas branché au worker »
depuis c82829f2b) + spec F1-1 (barré create-or-update).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:44:47 +02:00
Dom
bd1c9d2c8a feat(deploy+bench+ops): DGX vm scripts, Windows RDP launcher, bench cases, agent_chat enable script
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m50s
tests / Tests unitaires (sans GPU) (push) Failing after 1m53s
tests / Tests sécurité (critique) (push) Has been skipped
2026-07-02 13:32:36 +02:00
Dom
6907ecc82f docs: track design docs, plans, audits, coordination infrastructure, handoffs
- 21 docs/*.md: audits, design notes, deployment plans, checklists, memos
- Coordination: ROLES, runbooks (DGX reboot, Lea live), patches, registre, syntheses, systemd, QG template
- Handoffs: 6 Codex handoff documents + README + template
2026-07-02 13:29:58 +02:00
Dom
7dd5c872df chore(gitignore): untrack ephemeral state + ignore large local artifacts
- git rm --cached: .inbox_baseline.txt, .loop_log.txt (coordination ephemeral)
- Add: .agents/, .codex/, agent_chat/state/, graphify/, graphify-out/ (local state/tool)
- Add: webbrowser (11M PostScript artifact), deploy/installer/lea_python_embed_working.tgz (37M)
- Add: benchmarks/computer_use/predictions/ (generated), **/instance/*.db.bak* (runtime backup)
2026-07-02 13:29:21 +02:00
66 changed files with 11756 additions and 474 deletions

20
.gitignore vendored
View File

@@ -144,3 +144,23 @@ deploy/installer/python-3.12.8-embed-amd64.zip
# Artefacts de build installateur (EXE compilés + staging) — non versionnés
deploy/releases/*.exe
deploy/build/
# Embed tgz working (37M, local build artifact)
deploy/installer/lea_python_embed_working.tgz
# Agent/Codex state (local, session-specific)
.agents/
.codex/
agent_chat/state/
# Graphify tool + generated output (1.2G)
graphify/
graphify-out/
# Local PostScript artifact (webbrowser = 11M DSC)
webbrowser
# Bench predictions (generated, not source)
benchmarks/computer_use/predictions/
# DB backups (instance level, runtime artifact)
**/instance/*.db.bak*

View File

@@ -420,6 +420,7 @@ from .replay_engine import (
_edge_to_normalized_actions,
_substitute_variables,
_resolve_runtime_vars,
_coerce_action_coords,
_SERVER_SIDE_ACTION_TYPES,
_handle_extract_text_action,
_handle_extract_table_action,
@@ -4334,6 +4335,9 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
if runtime_vars:
action = _resolve_runtime_vars(action, runtime_vars)
# Coercion coords: cast x_pct/y_pct en float après resolver
action = _coerce_action_coords(action)
type_ = action.get("type")
# pause_for_human : pause supervisée si safety_level/safety_checks ou mode supervised,

View File

@@ -1948,6 +1948,21 @@ def _edge_to_normalized_actions(edge, params: Dict[str, Any]) -> List[Dict[str,
normalized["parameters"]["temperature"] = action_params.get("temperature")
return [normalized]
elif action_type == "navigate":
normalized["type"] = "navigate"
normalized["parameters"] = {
"action": action_params.get("action", "login"),
"login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"),
"password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"),
"submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"),
}
login_config_keys = ("login_field", "password_field", "submit_button",
"success_elements", "context")
for key in login_config_keys:
if action_params.get(key) is not None:
normalized["parameters"][key] = action_params[key]
return [normalized]
else:
logger.warning(f"Type d'action inconnu : {action_type}")
return []
@@ -2045,6 +2060,38 @@ def _resolve_runtime_vars(value: Any, variables: Dict[str, Any]) -> Any:
return value
def _coerce_action_coords(action: dict) -> dict:
"""Cast x_pct/y_pct en float après template resolution par _resolve_runtime_vars.
Politique : si string non convertible ou template encore present → skip + pause_for_human.
Idempotent sur les actions qui ont déjà des floats (mouse_click existant).
Jamais fallback 0.0/0.0 — un clic sur coords (0,0) = top-left = potentiellement dangereux.
Appelé APRÈS _resolve_runtime_vars dans la boucle dispatch (api_stream.py).
"""
for key in ("x_pct", "y_pct"):
val = action.get(key)
if val is None:
continue
if isinstance(val, float):
continue # déjà float, idempotent
if isinstance(val, str):
# Template encore présent = non résolu par _resolve_runtime_vars
if val.startswith("{{") and val.endswith("}}"):
action["_skip_reason"] = f"coords_var non résolu: {key}={val}"
action["type"] = "pause_for_human"
action["safety_level"] = "high"
return action
try:
action[key] = float(val)
except (ValueError, TypeError):
action["_skip_reason"] = f"coords invalide: {key}={val}"
action["type"] = "pause_for_human"
action["safety_level"] = "high"
return action
return action
# =========================================================================
# Handlers pour les actions exécutées côté serveur (extract_text, t2a_decision)
# =========================================================================

View File

@@ -0,0 +1,21 @@
{"case_id": "easily_rec_shot_0001_72_538", "screenshot_path": "/tmp/easily_session/shots/shot_0001_full.png", "task": {"intent": "cliquer sur « 25003362 »", "target_text": "25003362", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003362 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.3362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003362", "ocr_dist": 0.0067, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0002_380_919", "screenshot_path": "/tmp/easily_session/shots/shot_0002_full.png", "task": {"intent": "cliquer sur « iméicamentset-substancs »", "target_text": "iméicamentset-substancs", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « iméicamentset-substancs » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1484, "y_pct": 0.5744, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "iméicamentset-substancs", "ocr_dist": 0.0107, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0003_388_380", "screenshot_path": "/tmp/easily_session/shots/shot_0003_full.png", "task": {"intent": "cliquer sur « cliniques »", "target_text": "cliniques", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « cliniques » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1516, "y_pct": 0.2375, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "cliniques", "ocr_dist": 0.0075, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0004_552_381", "screenshot_path": "/tmp/easily_session/shots/shot_0004_full.png", "task": {"intent": "cliquer sur « Imagerie »", "target_text": "Imagerie", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Imagerie » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2156, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Imagerie", "ocr_dist": 0.009, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0005_685_385", "screenshot_path": "/tmp/easily_session/shots/shot_0005_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2676, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0127, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0007_947_381", "screenshot_path": "/tmp/easily_session/shots/shot_0007_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3699, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0057, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0011_72_288", "screenshot_path": "/tmp/easily_session/shots/shot_0011_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.18, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0109, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0012_95_613", "screenshot_path": "/tmp/easily_session/shots/shot_0012_full.png", "task": {"intent": "cliquer sur « 25003451 »", "target_text": "25003451", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003451 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0371, "y_pct": 0.3831, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003451", "ocr_dist": 0.0111, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0014_910_378", "screenshot_path": "/tmp/easily_session/shots/shot_0014_full.png", "task": {"intent": "cliquer sur « Synthèse »", "target_text": "Synthèse", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Synthèse » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3555, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Synthèse", "ocr_dist": 0.0172, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0015_638_381", "screenshot_path": "/tmp/easily_session/shots/shot_0015_full.png", "task": {"intent": "cliquer sur « Notes »", "target_text": "Notes", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Notes » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2492, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Notes", "ocr_dist": 0.0051, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0016_970_393", "screenshot_path": "/tmp/easily_session/shots/shot_0016_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3789, "y_pct": 0.2456, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.008, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0020_695_379", "screenshot_path": "/tmp/easily_session/shots/shot_0020_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2715, "y_pct": 0.2369, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0057, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0021_127_395", "screenshot_path": "/tmp/easily_session/shots/shot_0021_full.png", "task": {"intent": "cliquer sur « d'admission »", "target_text": "d'admission", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « d'admission » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0496, "y_pct": 0.2469, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "d'admission", "ocr_dist": 0.0089, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0022_73_304", "screenshot_path": "/tmp/easily_session/shots/shot_0022_full.png", "task": {"intent": "cliquer sur « IPP: »", "target_text": "IPP:", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP: » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "IPP:", "ocr_dist": 0.0147, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0024_84_269", "screenshot_path": "/tmp/easily_session/shots/shot_0024_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0328, "y_pct": 0.1681, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0018, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0025_67_790", "screenshot_path": "/tmp/easily_session/shots/shot_0025_full.png", "task": {"intent": "cliquer sur « 25012257 »", "target_text": "25012257", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25012257 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0262, "y_pct": 0.4938, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25012257", "ocr_dist": 0.0053, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0028_770_385", "screenshot_path": "/tmp/easily_session/shots/shot_0028_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3008, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0166, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0029_766_378", "screenshot_path": "/tmp/easily_session/shots/shot_0029_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2992, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0196, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0032_954_375", "screenshot_path": "/tmp/easily_session/shots/shot_0032_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3727, "y_pct": 0.2344, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0095, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0037_2028_1290", "screenshot_path": "/tmp/easily_session/shots/shot_0037_full.png", "task": {"intent": "cliquer sur « terminé »", "target_text": "terminé", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « terminé » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7922, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "terminé", "ocr_dist": 0.0395, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0041_2010_1013", "screenshot_path": "/tmp/easily_session/shots/shot_0041_full.png", "task": {"intent": "cliquer sur « mémorisées »", "target_text": "mémorisées", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « mémorisées » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7852, "y_pct": 0.6331, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "mémorisées", "ocr_dist": 0.0454, "needs_human_check": false}}

View File

@@ -0,0 +1,15 @@
{"case_id": "easily_shot_0001_72_538", "screenshot_path": "/tmp/easily_session/shots/shot_0001_full.png", "task": {"intent": "cliquer sur « 25003362 »", "target_text": "25003362", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003362 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.3362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0003_388_380", "screenshot_path": "/tmp/easily_session/shots/shot_0003_full.png", "task": {"intent": "cliquer sur « cliniques »", "target_text": "cliniques", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « cliniques » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1516, "y_pct": 0.2375, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0004_552_381", "screenshot_path": "/tmp/easily_session/shots/shot_0004_full.png", "task": {"intent": "cliquer sur « Imagerie »", "target_text": "Imagerie", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Imagerie » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2156, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0005_685_385", "screenshot_path": "/tmp/easily_session/shots/shot_0005_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2676, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0012_95_613", "screenshot_path": "/tmp/easily_session/shots/shot_0012_full.png", "task": {"intent": "cliquer sur « 25003451 »", "target_text": "25003451", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003451 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0371, "y_pct": 0.3831, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0013_2393_1215", "screenshot_path": "/tmp/easily_session/shots/shot_0013_full.png", "task": {"intent": "cliquer sur « Oui »", "target_text": "Oui", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Oui » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9348, "y_pct": 0.7594, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0014_910_378", "screenshot_path": "/tmp/easily_session/shots/shot_0014_full.png", "task": {"intent": "cliquer sur « Synthèse »", "target_text": "Synthèse", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Synthèse » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3555, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0015_638_381", "screenshot_path": "/tmp/easily_session/shots/shot_0015_full.png", "task": {"intent": "cliquer sur « Notes »", "target_text": "Notes", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Notes » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2492, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0020_695_379", "screenshot_path": "/tmp/easily_session/shots/shot_0020_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2715, "y_pct": 0.2369, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0021_127_395", "screenshot_path": "/tmp/easily_session/shots/shot_0021_full.png", "task": {"intent": "cliquer sur « d'admission »", "target_text": "d'admission", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « d'admission » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0496, "y_pct": 0.2469, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0022_73_304", "screenshot_path": "/tmp/easily_session/shots/shot_0022_full.png", "task": {"intent": "cliquer sur « IPP: »", "target_text": "IPP:", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP: » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": false, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0023_73_304", "screenshot_path": "/tmp/easily_session/shots/shot_0023_full.png", "task": {"intent": "cliquer sur « IPP »", "target_text": "IPP", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": false, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0025_67_790", "screenshot_path": "/tmp/easily_session/shots/shot_0025_full.png", "task": {"intent": "cliquer sur « 25012257 »", "target_text": "25012257", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25012257 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0262, "y_pct": 0.4938, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0028_770_385", "screenshot_path": "/tmp/easily_session/shots/shot_0028_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3008, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
{"case_id": "easily_shot_0037_2028_1290", "screenshot_path": "/tmp/easily_session/shots/shot_0037_full.png", "task": {"intent": "cliquer sur « terminé »", "target_text": "terminé", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « terminé » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7922, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": false, "ocr_occurrences": 1}}

View File

@@ -0,0 +1,41 @@
{"case_id": "easily_rec_shot_0001_72_538", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0001_full.png", "task": {"intent": "cliquer sur « 25003362 »", "target_text": "25003362", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003362 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.3362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003362", "ocr_dist": 0.0067, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0002_380_919", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0002_full.png", "task": {"intent": "cliquer sur « iméicamentset-substancs »", "target_text": "iméicamentset-substancs", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « iméicamentset-substancs » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1484, "y_pct": 0.5744, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "iméicamentset-substancs", "ocr_dist": 0.0107, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0003_388_380", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0003_full.png", "task": {"intent": "cliquer sur « cliniques »", "target_text": "cliniques", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « cliniques » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1516, "y_pct": 0.2375, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "cliniques", "ocr_dist": 0.0075, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0004_552_381", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0004_full.png", "task": {"intent": "cliquer sur « Imagerie »", "target_text": "Imagerie", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Imagerie » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2156, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Imagerie", "ocr_dist": 0.009, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0005_685_385", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0005_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2676, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0127, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0006_2547_962", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0006_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9949, "y_pct": 0.6012, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0007_947_381", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0007_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3699, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0057, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0008_903_552", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0008_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3527, "y_pct": 0.345, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0009_903_552", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0009_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3527, "y_pct": 0.345, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0010_2546_1042", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0010_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9945, "y_pct": 0.6512, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0011_72_288", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0011_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.18, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0109, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0012_95_613", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0012_full.png", "task": {"intent": "cliquer sur « 25003451 »", "target_text": "25003451", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003451 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0371, "y_pct": 0.3831, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003451", "ocr_dist": 0.0111, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0013_2393_1215", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0013_full.png", "task": {"intent": "cliquer sur « Oui »", "target_text": "Oui", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Oui » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9348, "y_pct": 0.7594, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Oui", "ocr_dist": 0.0036, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0014_910_378", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0014_full.png", "task": {"intent": "cliquer sur « Synthèse »", "target_text": "Synthèse", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Synthèse » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3555, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Synthèse", "ocr_dist": 0.0172, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0015_638_381", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0015_full.png", "task": {"intent": "cliquer sur « Notes »", "target_text": "Notes", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Notes » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2492, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Notes", "ocr_dist": 0.0051, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0016_970_393", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0016_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3789, "y_pct": 0.2456, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.008, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0017_2506_1205", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0017_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9789, "y_pct": 0.7531, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0018_2549_1203", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0018_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9957, "y_pct": 0.7519, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0019_2557_244", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0019_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9988, "y_pct": 0.1525, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0020_695_379", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0020_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2715, "y_pct": 0.2369, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0057, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0021_127_395", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0021_full.png", "task": {"intent": "cliquer sur « d'admission »", "target_text": "d'admission", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « d'admission » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0496, "y_pct": 0.2469, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "d'admission", "ocr_dist": 0.0089, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0022_73_304", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0022_full.png", "task": {"intent": "cliquer sur « IPP: »", "target_text": "IPP:", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP: » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "IPP:", "ocr_dist": 0.0147, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0023_73_304", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0023_full.png", "task": {"intent": "cliquer sur « IPP »", "target_text": "IPP", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "IPP", "ocr_dist": 0.0163, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0024_84_269", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0024_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0328, "y_pct": 0.1681, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0018, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0025_67_790", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0025_full.png", "task": {"intent": "cliquer sur « 25012257 »", "target_text": "25012257", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25012257 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0262, "y_pct": 0.4938, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25012257", "ocr_dist": 0.0053, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0026_2545_1356", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0026_full.png", "task": {"intent": "cliquer sur « 95 »", "target_text": "95", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 95 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9941, "y_pct": 0.8475, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "95", "ocr_dist": 0.0291, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0027_2541_284", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0027_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9926, "y_pct": 0.1775, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0028_770_385", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0028_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3008, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0166, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0029_766_378", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0029_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2992, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0196, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0030_2546_595", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0030_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9945, "y_pct": 0.3719, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0031_2558_1415", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0031_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9992, "y_pct": 0.8844, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0032_954_375", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0032_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3727, "y_pct": 0.2344, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0095, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0033_2544_704", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0033_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9938, "y_pct": 0.44, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
{"case_id": "easily_rec_shot_0034_2188_1570", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0034_full.png", "task": {"intent": "cliquer sur « a »", "target_text": "a", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « a » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8547, "y_pct": 0.9812, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "a", "ocr_dist": 0.0391, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0035_2166_1296", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0035_full.png", "task": {"intent": "cliquer sur « 0 »", "target_text": "0", "current_window": "Fenêtre de dépassement de capacité de la barre détat système.", "expected_next_window": "", "question": "L'élément « 0 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8461, "y_pct": 0.81, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "0", "ocr_dist": 0.0133, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0036_2196_1285", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0036_full.png", "task": {"intent": "cliquer sur « - »", "target_text": "-", "current_window": "Fenêtre de dépassement de capacité de la barre détat système.", "expected_next_window": "", "question": "L'élément « - » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8578, "y_pct": 0.8031, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "-", "ocr_dist": 0.0239, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0037_2028_1290", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0037_full.png", "task": {"intent": "cliquer sur « terminé »", "target_text": "terminé", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « terminé » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7922, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "terminé", "ocr_dist": 0.0395, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0038_2031_1283", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0038_full.png", "task": {"intent": "cliquer sur « 0 »", "target_text": "0", "current_window": "Fenêtre de dépassement de capacité de la barre détat système.", "expected_next_window": "", "question": "L'élément « 0 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7934, "y_pct": 0.8019, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "0", "ocr_dist": 0.0407, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0039_2192_1298", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0039_full.png", "task": {"intent": "cliquer sur « 0 »", "target_text": "0", "current_window": "Fenêtre de dépassement de capacité de la barre détat système.", "expected_next_window": "", "question": "L'élément « 0 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8562, "y_pct": 0.8113, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "0", "ocr_dist": 0.0235, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0040_2131_1290", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0040_full.png", "task": {"intent": "cliquer sur « 9 0 - »", "target_text": "9 0 -", "current_window": "Fenêtre de dépassement de capacité de la barre détat système.", "expected_next_window": "", "question": "L'élément « 9 0 - » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8324, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "9 0 -", "ocr_dist": 0.0125, "needs_human_check": false}}
{"case_id": "easily_rec_shot_0041_2010_1013", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0041_full.png", "task": {"intent": "cliquer sur « mémorisées »", "target_text": "mémorisées", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « mémorisées » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7852, "y_pct": 0.6331, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "mémorisées", "ocr_dist": 0.0454, "needs_human_check": false}}

60
deploy/dgx/vm_launch.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Persistent VM launch — starts swtpm first, waits for socket, then QEMU
# VNC only, no SPICE (POC configuration)
VMROOT=/home/aivanov/quickemu-win11-arm-lea
SWTPM_SOCK="$VMROOT/windows-11-arm-lea.swtpm-sock"
/usr/bin/swtpm socket \
--ctrl type=unixio,path="$SWTPM_SOCK" \
--terminate \
--tpmstate dir="$VMROOT" \
--tpm2 &
# Wait for swtpm socket (up to 10s)
for _i in $(seq 1 100); do
if [ -S "$SWTPM_SOCK" ]; then break; fi
sleep 0.1
done
if [ ! -S "$SWTPM_SOCK" ]; then
echo "ERROR: swtpm socket not ready after 10s"
exit 1
fi
exec /usr/bin/qemu-system-aarch64 \
-name windows-11-arm-lea \
-machine virt,highmem=on,pflash0=rom,pflash1=efivars,accel=kvm \
-global kvm-pit.lost_tick_policy=discard \
-cpu host \
-smp cores=8,threads=1,sockets=1 \
-m 8G \
-device virtio-balloon \
-pidfile "$VMROOT/windows-11-arm-lea.pid" \
-rtc base=utc,clock=host \
-device ramfb \
-vga none \
-device virtio-gpu-pci,id=video0,xres=1280,yres=800 \
-display none \
-vnc 127.0.0.1:2,password=on \
-device virtio-serial-pci \
-chardev socket,id=agent0,path="$VMROOT/windows-11-arm-lea-agent.sock",server=on,wait=off \
-device virtserialport,chardev=agent0,name=org.qemu.guest_agent.0 \
-device virtio-rng-pci,rng=rng0 \
-object rng-random,id=rng0,filename=/dev/urandom \
-device qemu-xhci,id=input \
-device usb-kbd,bus=input.0 \
-k fr \
-device usb-tablet,bus=input.0 \
-device virtio-net-pci,netdev=nic \
-netdev user,hostname=windows-11-arm-lea,hostfwd=tcp::22220-:22,id=nic \
-blockdev node-name=rom,driver=file,filename=/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd,read-only=true \
-blockdev node-name=efivars,driver=file,filename="$VMROOT/OVMF_VARS.fd" \
-device virtio-scsi-pci,id=scsi0 \
-device scsi-hd,drive=SystemDisk,bus=scsi0.0,bootindex=2 \
-drive id=SystemDisk,if=none,format=qcow2,file="$VMROOT/disk.qcow2",discard=unmap,detect-zeroes=unmap,cache=writeback,aio=threads \
-chardev socket,id=chrtpm,path="$SWTPM_SOCK" \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis-device,tpmdev=tpm0 \
-monitor unix:"$VMROOT/windows-11-arm-lea-monitor.socket",server,nowait \
-serial unix:"$VMROOT/windows-11-arm-lea-serial.socket",server,nowait \
2>"$VMROOT/qemu.log"

50
deploy/dgx/vm_stop.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Persistent VM stop — ACPI poweroff via QEMU monitor, then SIGTERM, then SIGKILL
# Kill swtpm after QEMU exits. Cleanup PID/sockets.
VMROOT=/home/aivanov/quickemu-win11-arm-lea
MONITOR_SOCKET="$VMROOT/windows-11-arm-lea-monitor.socket"
PIDFILE="$VMROOT/windows-11-arm-lea.pid"
# Step 1: Send ACPI poweroff via QEMU monitor
if [ -S "$MONITOR_SOCKET" ]; then
echo "system_powerdown" | socat - UNIX-CONNECT:"$MONITOR_SOCKET" - > /dev/null 2>&1
echo "ACPI poweroff sent, waiting 30s..."
for i in $(seq 1 30); do
if [ ! -f "$PIDFILE" ] || ! ps -p "$(cat "$PIDFILE" 2>/dev/null)" > /dev/null 2>&1; then
echo "QEMU exited gracefully"
exit 0
fi
sleep 1
done
fi
# Step 2: SIGTERM (10s more)
QEMU_PID=$(cat "$PIDFILE" 2>/dev/null)
if [ -n "$QEMU_PID" ] && ps -p "$QEMU_PID" > /dev/null 2>&1; then
echo "Still running, sending SIGTERM..."
kill "$QEMU_PID" 2>/dev/null
for i in $(seq 1 10); do
if ! ps -p "$QEMU_PID" > /dev/null 2>&1; then
echo "QEMU exited after SIGTERM"
break
fi
sleep 1
done
fi
# Step 3: SIGKILL
QEMU_PID=$(cat "$PIDFILE" 2>/dev/null)
if [ -n "$QEMU_PID" ] && ps -p "$QEMU_PID" > /dev/null 2>&1; then
echo "Still running, SIGKILL..."
kill -9 "$QEMU_PID" 2>/dev/null
sleep 2
fi
# Step 4: Kill swtpm
pkill -f "swtpm.*windows-11-arm-lea" 2>/dev/null
sleep 2
# Step 5: Cleanup
rm -f "$PIDFILE" "$VMROOT"/*.sock "$VMROOT"/*.socket 2>/dev/null
echo "VM stop complete"

View File

@@ -0,0 +1,44 @@
@echo off
chcp 65001 >nul
title Connexion VM Lea (via DGX)
REM ============================================================
REM Connexion Bureau a distance a la VM Windows (Lea) du DGX.
REM Ouvre un tunnel SSH, lance le RDP (presse-papier actif),
REM puis referme le tunnel quand la session RDP est fermee.
REM ============================================================
REM --- Parametres (ajuste si besoin) ---
set "DGX_USER=aivanov"
set "DGX_HOST=192.168.1.45"
REM En deplacement (WireGuard, plus tard) : mettre DGX_HOST=10.10.0.1
set "LOCAL_PORT=13389"
set "RDP_FILE=%~dp0VM-Lea.rdp"
echo.
echo [1/3] Ouverture du tunnel SSH vers %DGX_USER%@%DGX_HOST% ...
echo (si un mot de passe est demande, saisis-le dans la fenetre "Tunnel")
start "Tunnel-DGX-VMLea" ssh -o StrictHostKeyChecking=accept-new -o ExitOnForwardFailure=yes -N -L %LOCAL_PORT%:127.0.0.1:3390 %DGX_USER%@%DGX_HOST%
echo [2/3] Attente de l'etablissement du tunnel (max ~30s)...
set /a tries=0
:wait
timeout /t 1 /nobreak >nul
powershell -NoProfile -Command "try{(New-Object Net.Sockets.TcpClient).Connect('127.0.0.1',%LOCAL_PORT%);exit 0}catch{exit 1}" >nul 2>&1
if not errorlevel 1 goto ready
set /a tries+=1
if %tries% lss 30 goto wait
echo ! Tunnel non etabli. Verifie l'acces SSH au DGX (mot de passe / reseau).
pause
goto cleanup
:ready
echo [3/3] Connexion Bureau a distance (localhost:%LOCAL_PORT%) ...
mstsc "%RDP_FILE%"
:cleanup
echo.
echo Fermeture du tunnel SSH...
taskkill /FI "WINDOWTITLE eq Tunnel-DGX-VMLea*" /T /F >nul 2>&1
echo Termine.
timeout /t 2 /nobreak >nul

View File

@@ -0,0 +1,35 @@
CONNEXION BUREAU A DISTANCE - VM Lea (DGX)
==========================================
CONTENU
- Connexion-VM-Lea.cmd : le lanceur (double-clic)
- VM-Lea.rdp : le profil de connexion RDP (presse-papier active)
INSTALLATION (sur ton laptop Windows)
1. Copie les DEUX fichiers dans le MEME dossier (ex: le Bureau).
2. (Optionnel) clic droit sur Connexion-VM-Lea.cmd > Envoyer vers > Bureau
(creer un raccourci), pour un acces rapide.
UTILISATION
- Double-clic sur "Connexion-VM-Lea.cmd".
- Une fenetre "Tunnel" s'ouvre : si un mot de passe SSH est demande,
saisis le mot de passe du compte aivanov du DGX.
- Le Bureau a distance s'ouvre ensuite : saisis ton identifiant + mot de
passe WINDOWS de la VM.
- Copier-coller (texte ET fichiers) fonctionne dans les deux sens.
- Ferme la fenetre RDP pour finir : le tunnel se referme automatiquement.
PRE-REQUIS
- Etre sur le reseau du labo (meme WiFi) pour joindre 192.168.1.45.
- OpenSSH client (inclus dans Windows 10/11).
- Le Bureau a distance doit etre active dans la VM (deja fait).
EN DEPLACEMENT (plus tard)
- Quand WireGuard sera en place, edite Connexion-VM-Lea.cmd et remplace
DGX_HOST=192.168.1.45 par DGX_HOST=10.10.0.1
- Tout le reste est identique. L'adresse RDP reste localhost:13389.
CONFORT (optionnel, recommande)
- Pour ne plus saisir le mot de passe SSH a chaque fois : on signe la cle
SSH de ton laptop avec la CA (acces par certificat). Demande-le moi et
envoie-moi la cle publique de ton laptop.

View File

@@ -0,0 +1,18 @@
full address:s:localhost:13389
prompt for credentials:i:1
redirectclipboard:i:1
redirectdrives:i:1
drivestoredirect:s:*
redirectprinters:i:0
redirectsmartcards:i:0
audiomode:i:2
authentication level:i:0
negotiate security layer:i:1
enablecredsspsupport:i:1
screen mode id:i:2
dynamic resolution:i:1
desktopwidth:i:1280
desktopheight:i:800
session bpp:i:32
compression:i:1
username:s:

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# RDP vers la VM Windows (Lea) du DGX, depuis ce serveur Linux (.40).
# Ouvre un tunnel SSH (par certificat) puis lance xfreerdp.
# Presse-papier + dossier $HOME partage. Tunnel ferme a la sortie.
#
# Usage:
# ./connexion-vm-lea.sh # labo (DGX = 192.168.1.45)
# ./connexion-vm-lea.sh 10.10.0.1 # en deplacement (via WireGuard)
# ./connexion-vm-lea.sh 192.168.1.45 /u:MonUserWindows
set -euo pipefail
DGX_HOST="${1:-192.168.1.45}"
[ $# -gt 0 ] && shift || true
LOCAL_PORT=13389
CTL="$(mktemp -u /tmp/rdp-vmlea-ctl.XXXXXX)"
cleanup(){ ssh -S "$CTL" -O exit "aivanov@${DGX_HOST}" >/dev/null 2>&1 || true; }
trap cleanup EXIT INT TERM
echo "[1/3] Tunnel SSH (cert) vers aivanov@${DGX_HOST} ..."
ssh -o ExitOnForwardFailure=yes -fN -M -S "$CTL" -L "${LOCAL_PORT}:127.0.0.1:3390" "aivanov@${DGX_HOST}"
echo "[2/3] Attente du tunnel ..."
for _i in $(seq 1 40); do
ss -tlnp 2>/dev/null | grep -q "127.0.0.1:${LOCAL_PORT} " && break
sleep 0.25
done
echo "[3/3] Connexion RDP (localhost:${LOCAL_PORT}) — presse-papier + dossier $HOME ..."
xfreerdp /v:localhost:${LOCAL_PORT} /cert:ignore /clipboard /dynamic-resolution /drive:home,"$HOME" "$@" || true
echo "Session RDP terminee, fermeture du tunnel."

View File

@@ -0,0 +1,59 @@
# Unblock NoMachine on Windows 11 — run as Administrator
# Adds firewall rules for port 4000 (TCP+UDP) and verifies NoMachine service
Write-Host "=== Unblock NoMachine ===" -ForegroundColor Cyan
# 1. Add firewall inbound rules for NoMachine (port 4000 TCP + UDP)
$ruleName = "NoMachine Server (Port 4000)"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "Firewall rule '$ruleName' already exists — enabling it" -ForegroundColor Yellow
Enable-NetFirewallRule -DisplayName $ruleName
} else {
Write-Host "Creating firewall rule '$ruleName' for port 4000 TCP+UDP" -ForegroundColor Green
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Protocol TCP -LocalPort 4000 -Action Allow -Profile Any -Enabled True -Description "Allow NoMachine remote desktop connections"
New-NetFirewallRule -DisplayName "$ruleName (UDP)" -Direction Inbound -Protocol UDP -LocalPort 4000 -Action Allow -Profile Any -Enabled True -Description "Allow NoMachine UDP discovery"
}
# 2. Check NoMachine service is running
$svc = Get-Service -Name "nxsrv" -ErrorAction SilentlyContinue
if (-not $svc) {
$svc = Get-Service -Name "NoMachine Server" -ErrorAction SilentlyContinue
if (-not $svc) {
$svc = Get-Service | Where-Object { $_.DisplayName -like "*NoMachine*" -and $_.DisplayName -like "*Server*" } | Select-Object -First 1
}
}
if ($svc) {
Write-Host "NoMachine service: $($svc.Name) — Status: $($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') {'Green'} else {'Red'})
if ($svc.Status -ne 'Running') {
Write-Host "Starting NoMachine service..." -ForegroundColor Yellow
Start-Service -Name $svc.Name -ErrorAction SilentlyContinue
$svc = Get-Service -Name $svc.Name
Write-Host "After start: $($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') {'Green'} else {'Red'})
}
} else {
Write-Host "WARNING: NoMachine server service not found!" -ForegroundColor Red
}
# 3. Verify port 4000 is listening
Write-Host ""
Write-Host "Checking port 4000..." -ForegroundColor Cyan
$port4000 = Get-NetTCPConnection -LocalPort 4000 -ErrorAction SilentlyContinue
if ($port4000) {
Write-Host "Port 4000 is LISTENING on $($port4000.LocalAddress):$($port4000.LocalPort) — State: $($port4000.State)" -ForegroundColor Green
} else {
Write-Host "WARNING: Port 4000 NOT listening — NoMachine server may not be active" -ForegroundColor Red
Write-Host "Try: restart NoMachine from the Start Menu or Services app" -ForegroundColor Yellow
}
# 4. Show this machine's IP for remote connection
$ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notlike '*Loopback*' -and $_.IPAddress -notlike '127.*' -and $_.IPAddress -match '192\.168' } | Select-Object -First 1).IPAddress
if ($ip) {
Write-Host ""
Write-Host "Laptop IP on LAN: $ip" -ForegroundColor Green
Write-Host "From workstation: connect NoMachine to $ip" -ForegroundColor Green
}
Write-Host ""
Write-Host "=== Done ===" -ForegroundColor Cyan

View File

@@ -0,0 +1,174 @@
# Audit — Ce qui manque pour une appli 100% fonctionnelle
- **Date** : 2026-06-10
- **Demandeur** : Dom
- **Auteur** : Claude (audit read-only par 4 sous-agents d'exploration + contre-vérifications manuelles)
- **Périmètre** : agent client Windows (Léa), chaîne d'apprentissage, capacité de replay, maturité produit/fleet
- **Statut findings** : les fichier:ligne proviennent d'agents d'exploration. Les 3 contradictions majeures ont été re-vérifiées à la main (voir annexe). **Avant d'engager du code sur un item, revalider le point au cas par cas** (méthode habituelle).
- **Avis croisé Qwen** : reçu 2026-06-10 23:30 (`inbox_claude/2026-06-10_2330_qwen-to-claude_AVIS-GAPS-APPLI-100PCT.md`) — intégré en **Addendum (§7)**.
---
## 1. Diagnostic central
L'appli aujourd'hui, honnêtement qualifiée : **un record-and-replay supervisé robuste, avec une couche sémantique réelle au grounding, mais dont la boucle d'apprentissage n'est pas fermée et dont les filets de sécurité sont écrits mais débranchés.**
Trois promesses produit non tenues dans le code qui tourne :
1. **La boucle d'apprentissage est ouverte** — Shadow observe et construit des workflows, mais ils n'arrivent jamais dans VWB (import jamais déclenché).
2. **L'exécution ne se vérifie pas elle-même** — verify, healing, recovery : tout existe, tout est désactivé ou non branché.
3. **Pas de généralisation** — un workflow appris ne survit pas à un changement de poste/résolution ; FAISS est construit au training mais jamais consulté au replay.
Point structurel transverse : **deux chemins d'exécution aux capacités différentes** :
- `visual_workflow_builder/backend/api_v3/execute.py` (exécution locale VWB, Legacy + ORA)
- `agent_v0/server_v1/replay_engine.py` → agent Windows Léa (chemin POC)
Certains manques n'existent que sur l'un des deux. `t2a_decision` et le templating profond `{{var.field.sub}}` sont **implémentés sur le chemin Léa** (replay_engine.py:1922 et :2017) mais absents du chemin local. Cette asymétrie a piégé jusqu'aux agents d'audit eux-mêmes — c'est un coût de maintenance et un risque d'erreur permanent.
---
## 2. Axe agent client Windows (Léa) — ~80% fonctionnel, 1 bug critique
### Ce qui marche (vérifié wired)
- Capture complète : clics/double-clics, clavier+buffer texte, scroll, multi-écrans, DPI awareness, floutage sensible, dédup pHash.
- Résilience réseau : buffer SQLite persistant, retry 3×, backoff 1→30s, heartbeat 5s.
- Purge captures après ACK serveur (`PURGE_AFTER_ACK=1` défaut).
- Enroll Bearer token + machine_id ; détection dialogues système UAC/CredUI/SmartScreen **fail-closed** (pause supervisée).
- Rétention logs 180 j (conforme Règlement IA art. 12).
### Gaps
| # | Gap | Sévérité | Preuve | Type |
|---|-----|----------|--------|------|
| A1 | **Timeout HTTP client 5s** : étape serveur > 5s (extract_text, t2a) → client coupe, action déjà sortie de la queue → **perdue silencieusement**. Incident documenté 8 mai (4 actions perdues, pause step 18). | 🔴 BLOQUANT POC | `agent_v1/core/executor.py:1786` ; `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` | Bug |
| A2 | **Watchdog `_retry_pending` absent** côté serveur : actions perdues jamais republiées. | 🔴 BLOQUANT POC | `network/streamer.py:99-105` (constat) | Non implémenté |
| A3 | Écran verrouillé non détecté : agent capture noir, tape dans le vide. | 🟠 Important | — | Non implémenté |
| A4 | RecoveryEngine client : code complet, jamais appelé. | 🟡 Nice-to-have | `agent_v1/core/recovery.py` | Écrit non branché |
| A5 | Long-polling HTTP fragile par construction (vs SSE/WebSocket). | 🟡 Post-POC | `main.py:287-366` | Architecture |
Note : la suspicion « appel Ollama de commentaire d'action orphelin côté client » **ne se confirme pas** côté agent_v1 (`OLLAMA_HOST` défini dans config mais aucun appel client). Le commentaire d'action est côté serveur.
---
## 3. Axe chaîne d'apprentissage — marche jusqu'au dernier mètre, puis s'arrête
### Ce qui marche (vérifié wired)
- Chaîne Shadow complète : streaming → LiveSessionManager → `_worker_queue.txt``run_worker.py` → ScreenAnalyzer → CLIP → FAISS indexation → GraphBuilder DBSCAN → workflow JSON dans `data/training/workflows/{machine_id}/`.
- Apprentissage post-replay : ReplayLearner (JSONL `data/learning/replay_results/`) + TargetMemoryStore (SQLite `data/learning/target_memory.db`), consultés avant résolution, alimentés après succès/échec.
### Gaps
| # | Gap | Sévérité | Preuve | Type |
|---|-----|----------|--------|------|
| L1 | **Workflows Shadow jamais importés dans VWB** : `import_core_workflow()` existe (`visual_workflow_builder/backend/api/workflows.py:622`) mais aucun appel automatique post-finalize. La boucle d'apprentissage produit des fichiers que personne ne voit. | 🔴 BLOQUANT promesse produit | grep `import_core_workflow` depuis server_v1 = 0 hit | Écrit non branché |
| L2 | ShadowLearningHook jamais importé (état avril 2026 inchangé). | 🟠 Important | `core/grounding/shadow_learning_hook.py` | Écrit non branché |
| L3 | **FAISS construit mais jamais interrogé au replay** — la mémoire sémantique ne sert pas à la résolution. | 🟠 Important | `core/embedding/faiss_manager.py` | Écrit non branché |
| L4 | Pas de généralisation cross-résolution/cross-poste : workflows cloisonnés par machine_id, ancres dépendantes du poste source. | 🟠 Important | `core/models/workflow_graph.py` | Non implémenté |
| L5 | **Copilot : inexistant** (aucun moteur de suggestion). **Autonomous : AutonomousPlanner isolé du replay engine.** Le cycle Shadow→Copilot→Autonomous est aujourd'hui Shadow→(rien)→exécution déclenchée manuellement. | 🔴 BLOQUANT promesse produit | `agent_chat/autonomous_planner.py`, `agent_v0/server_v1/execution_plan_runner.py` | Squelettes |
| L6 | Recapture anchor VWB : pas de revalidation/régénération PNG post-modification (bug connu mai 2026). | 🟠 Important | `visual_workflow_builder/backend/services/anchor_image_service.py` | Non implémenté |
---
## 4. Axe replay/exécution — ça clique bien, mais ça ne sait pas si ça a marché
### Ce qui marche (vérifié wired)
- Cascade de résolution active : template matching → CLIP → OCR/UI-TARS → VLM.
- DialogHandler branché (détection popups pré-step).
- Pause supervisée avec choix utilisateur (skip/static/coords, timeout 120s).
- Chemin Léa : `t2a_decision` (replay_engine.py:1922, handlers :2045+), templating profond `{{var.field.sub}}` (`path.split('.')` :2017), extract_text.
### Gaps
| # | Gap | Sévérité | Preuve | Type |
|---|-----|----------|--------|------|
| R1 | **`verify_level='none'` en dur** : aucune vérification post-action que le clic a produit l'effet attendu (seul pHash d'attente d'écran). Contraire au principe « vérif avant + après chaque clic ». | 🟠 Important (🔴 avant clinique) | `execute.py:1545` | Branché désactivé |
| R2 | **VLM pre-check `if False:` en dur** : pas de validation que l'élément trouvé = l'élément attendu. | 🟠 Important | `core/execution/observe_reason_act.py:1707` | Branché désactivé |
| R3 | Healing engine implémenté, jamais appelé. | 🟠 Important | `core/healing/healing_engine.py:21-150` | Écrit non branché |
| R4 | **Aucune reprise après crash** : crash au step N → redémarrage à 0, pas de checkpoint. | 🔴 BLOQUANT clinique | `execute.py:1732` (thread daemon sans checkpoint) | Non implémenté |
| R5 | **OCR-DIRECT « centre de ligne »** : substring d'une ligne docTR → coordonnées du centre de la ligne entière. Sur une barre d'onglets, Imagerie/Notes/Synthèse ≈ mêmes coords. Latent et sournois. | 🟠 Important | `agent_v0/server_v1/resolve_engine.py:1447-1527` | Bug |
| R6 | Timeout VWB→serveur 30s vs étapes longues (t2a/Ollama lent) → 504. | 🟠 Important | `server_client.py:207` | Bug config |
| R7 | Reporting d'exécution pauvre : méthode de grounding utilisée et raison d'échec non tracées. | 🟠 Important | ExecutionStep DB | Incomplet |
| R8 | Popup détecté mais gestion échouée → continue (log seul), pas de pause. | 🟡 Nice-to-have | `execute.py:283` | Incomplet |
| R9 | 3 systèmes de grounding morts (code zombie) : `fast_detector`, `smart_matcher`, `template_matcher` standalone. | 🟡 Ménage | `core/grounding/` | Poids mort |
| R10 | TitleVerifier aveugle en VM (crop EasyOCR 45px illisible). | 🟡 Connu | `core/grounding/title_verifier.py:34-67` | Limitation |
---
## 5. Axe maturité produit / fleet — OK pour 5 TIM supervisés, pas au-delà
### Ce qui marche (vérifié)
- Fleet : enroll/uninstall/revoke SQLite (`agent_registry.py`), `_guard_agent_registry_access`, last_seen heartbeat.
- Sessions concurrentes thread-safe ; uploads images rate-limités sans sérialisation.
- Healthcheck multi-composants + timer systemd (API, dashboard, worker heartbeat, disque).
- Export ZIP workflows ; dashboard HTTP Basic fail-closed.
### Gaps
| # | Gap | Sévérité | Preuve | Type |
|---|-----|----------|--------|------|
| P1 | **DETTE-006/010 — grounding Qwen3-VL instable** (`smart_resize` non déterministe, config checkpoint factor 28 vs 32). LE risque technique du calendrier POC. | 🔴 BLOQUANT POC | `docs/MIGRATION_VLM_PLAN_2026-05-09.md`, DETTE_TECHNIQUE.md | En cours |
| P2 | **1 seul replay simultané** (verrou global `_replay_lock`). Acceptable POC séquentiel, bloquant au-delà. | 🟠 Important post-POC | `api_stream.py` | Limitation |
| P3 | Opérabilité non-dev : pas d'onglet « état systèmes », pas de monitoring GPU/Ollama, erreurs JSON brut. Acceptable seulement avec Dom en SSH derrière. | 🟠 Important | `web_dashboard/app.py` | Incomplet |
| P4 | Export ZIP sans **restore** en masse ; backup exporte les JSON, pas la DB (DETTE-015 : symlink tient pour le POC). | 🟠 Important | `core/system/backup_exporter.py:58-160` | Incomplet |
| P5 | Multi-users/RBAC : 1 user statique. Accepté POC (lié DETTE-016). | 🟡 Post-POC | `web_dashboard/app.py:67` | Accepté |
| P6 | Pas de rotation des logs serveurs (`logs/*.log`) — artifact_retention ne couvre que les données. | 🟡 Nice-to-have | `core/system/artifact_retention.py` | Incomplet |
| P7 | DETTE-013 : tests unit non exécutables sans `RPA_API_TOKEN` (sys.exit à l'import). | 🟠 Important dev | `agent_v0/server_v1/api_stream.py:135` | Bug |
| P8 | Ménage pré-POC (~9-10 j-h, `MENAGE_PRE_POC_2026-05-29.md`) non engagé ; ~21 modules core/ orphelins documentés. | 🟡 Planifié | docs/POC/ | Dette |
---
## 6. Priorisation proposée
### Horizon 1 — avant le M2 live (jours) : fiabiliser la chaîne qui existe
1. **A1** Timeout client 5s → 30s (1 constante) + **A2** watchdog `_retry_pending` serveur — le duo qui a tué la session du 8 mai.
2. **P1** Trancher DETTE-006/010 (calibration grounding Qwen3-VL sur DGX) avant le bench J+6.
3. **A3** Détection écran verrouillé.
### Horizon 2 — avant la clinique (semaines) : les filets de sécurité
4. **R1/R2** Réactiver verify (au moins post-condition légère) + pre-check.
5. **R5** Fix OCR centre-de-ligne (span du substring, pas centre de ligne).
6. **R4** Reprise sur crash (checkpoint step) + **R7** tracer la méthode de résolution.
7. **R6** Timeout VWB 30s → adapté aux étapes longues (ou polling asynchrone).
### Horizon 3 — la promesse produit (post-POC) : fermer la boucle
8. **L1** Pont auto Shadow→VWB (`import_core_workflow` post-finalize) — LA pièce qui transforme l'outil en produit apprenant.
9. **L3** FAISS consulté au replay + **L4** début de généralisation cross-poste.
10. **L5** Copilot (moteur de suggestion) puis branchement AutonomousPlanner.
11. Unifier les deux chemins d'exécution (execute.py local vs replay_engine.py Léa).
12. **P2** Replays parallèles, **P3** opérabilité TIM, **P4** restore, RBAC.
---
## 7. Addendum — Avis croisé Qwen (historien/QG, 2026-06-10 23:30)
### Convergences avec l'audit code
- **DETTE-006/010 = les deux vrais risques démo** (« si le grounding dérive, les clics ratent. Démo morte. ») — aligné avec P1/Horizon 1.
- **Monitoring/alerting productif absent** (P3) : « si un worker crashe à 3h du matin sur un TIM, personne ne le saura ».
- Écarts doc vs réalité confirmés par son registre : ContinuousLearner/Shadow hook orphelins (L2), cascade YOLO et OmniParser neutralisées (DETTE-004), ~1900 lignes de code mort jamais câblé (autonomous_planner, seeclick…) — cohérent avec L5/R9.
### Apports nouveaux de Qwen (absents de l'audit code)
1. **Multi-TIM jamais testé > 1 agent simultané** : le Fleet existe, mais routage session, isolation mémoire et contention GPU sous charge réelle sont **inconnus**. Mon audit avait noté le verrou replay global (P2) ; Qwen élargit : c'est toute la concurrence multi-agents qui n'a aucune preuve d'exécution.
2. **Le pipeline complet record → replay → compétence n'a jamais tourné en conditions réelles** : M2 live n'a pas encore eu lieu. « Le premier vrai test sera devant le client » si on ne fait pas M2 avant.
3. **Incidents récurrents de son registre** : worker zombie 5 jours (résolu par watchdog N3), tunnel Ollama instable (stabilisé systemd), UI-TARS 500 non détecté (toujours 0 test dédié), OOM VRAM GB10 (fixé).
4. **DETTE-015 jugée fragile** : le symlink a déjà cassé une fois (P0-1) ; peut resurgir si cwd change.
### Point de tension à arbitrer en M2 (pas tranché)
Qwen affirme : « si le serveur redémarre, les agents Windows tombent — pas de reconnexion automatique ». Mon audit client a trouvé buffer SQLite persistant + retry + backoff + health-check 30s (`streamer.py`). Les deux peuvent être vrais : le **transport** se reconnecte, mais la **session/replay en cours** ne reprend probablement pas après un restart serveur. À vérifier explicitement pendant M2 (test : restart serveur en cours de session).
### Verdict Qwen
- **1 TIM en démo contrôlée : prêt** (sous réserve DETTE-006/010).
- **5 TIM réels en clinique : pas prêt** — le gap n'est pas dans le code métier (OCR, VLM, grounding) mais dans l'**infra multi-utilisateur** : sessions, isolation, monitoring, résilience.
Ce verdict est compatible avec la priorisation §6 et la renforce : l'Horizon 2 doit inclure un **test de charge multi-agents** (2-3 agents simultanés minimum) avant la clinique, en plus des filets de sécurité.
---
## Annexe — Contradictions inter-agents résolues (contre-vérifiées à la main)
| Affirmation agent d'audit | Verdict après vérification |
|---|---|
| « t2a_decision non implémenté » | **FAUX sur le chemin Léa** : implémenté `agent_v0/server_v1/replay_engine.py:1922` + handlers :2045+. Vrai uniquement pour le chemin local execute.py. |
| « `{{var.field.sub}}` ne marche pas » | **FAUX sur le chemin Léa** : `path.split('.')` replay_engine.py:2017. Vrai uniquement chemin local. |
| « Le chemin replay vers Léa est démis / Léa n'existe plus » | **FAUX** : pont `learned_workflow_bridge.py` côté VWB + polling `/replay/next` côté client actifs. Les deux chemins coexistent — c'est l'asymétrie connue. |
Leçon : tout audit ou modification doit d'abord identifier **sur quel chemin d'exécution** il porte.

View File

@@ -0,0 +1,170 @@
# Benchmark OCR PP-OCRv5 CPU — 02/07/2026
> **Label**: baseline CPU, non verdict GPU
> **Machine**: Ryzen 9 9950X 32 threads, 123GB RAM, RTX 5070 12GB VRAM, CUDA driver 580.159.03/13.0
> **Image**: `shot_0172_full.png` (2560×1600, 721K, RGB) — capture écran Windows Léa
> **PaddleOCR**: 3.4.0, paddlepaddle 3.3.1 CPU-only (non compilé CUDA)
---
## 1. Résultats synthèse
| Engine | Cold (s) | Warm (s) | Detections | Mem init (MB) | Mem peak (MB) | Statut |
|--------|----------|----------|------------|---------------|---------------|--------|
| **docTR CPU** | 0.776 | 0.717 | 139 | 263.2 | 263.2 | ✅ OK |
| **EasyOCR CPU** | 4.878 | 4.856 | 54 | 0.6 | 156.9 | ✅ OK |
| **PP-OCRv5 CPU** | — | — | — | — | — | ❌ BLOCKED |
---
## 2. PP-OCRv5 CPU — VERDICT: BLOCKED
### Crash récurrent
Toute inference PaddleOCR sur paddlepaddle 3.3.1 CPU-only crash systématiquement :
```
(Unimplemented) ConvertPirAttribute2RuntimeAttribute not support
[pir::ArrayAttribute<pir::DoubleAttribute>]
(at /paddle/paddle/fluid/framework/new_executor/instruction/onednn/onednn_instruction.cc:116)
```
### Root cause
Bug dans le **PIR new executor** de paddlepaddle 3.3.1 CPU-only : l'instruction OneDNN
tente de convertir un `ArrayAttribute<DoubleAttribute>` en runtime attribute, opération
non implémentée. Ce bug est :
- **NON model-spécifique** : v3_mobile_det, v4_mobile_det, v5_mobile_det crashent tous
- **NON version-spécifique** : PP-OCRv3, v4 (fr absent), v5 crashent tous
- **NON API-spécifique** : `ocr()` (deprecated) et `predict()` crashent identiquement
- **NON contournable** par flags : `FLAGS_use_mkldnn=0`, `FLAGS_use_pir_api=0` n'ont aucun effet
### 7 approches testées — TOUTES FAILED
| # | Approche | Résultat |
|---|----------|----------|
| 1 | `FLAGS_use_mkldnn=0` via `os.environ` | Same crash |
| 2 | `det='PP-OCRv5_mobile_det'` param | ValueError "Unknown argument: det" (PaddleOCR 3.4.0 rejette ce param) |
| 3 | `FLAGS_use_mkldnn=0` shell-level avant Python | Same crash |
| 4 | `text_detection_model_name='PP-OCRv5_mobile_det'` | mobile_det DL OK → inference crash (same OneDNN) |
| 5 | `ocr_version='PP-OCRv4', lang='fr'` | ValueError "No models available for language 'fr' and PP-OCRv4" |
| 6 | PP-OCRv3 + `ocr(img, cls=True)` legacy | DeprecationWarning → TypeError sur `cls` kwarg → predict() → same crash |
| 7 | `FLAGS_use_pir_api=0` shell + os level | Same crash |
### PaddleOCR 3.4.0 __init__ params inspectés
28 paramètres au total. **Pas** de `enable_mkldnn`, `use_pir`, ou `det`. Param de détection
remplacé par `text_detection_model_name`. API v3.4.0 : `use_angle_cls` deprecated
`use_textline_orientation=True`, `show_log` supprimé (ValueError si utilisé).
### Incompatibilité downgrade
paddlepaddle 2.6.2 existe mais **incompatible** avec PaddleOCR 3.4.0 (requires ≥3.x).
PaddleOCR 2.x serait compatible avec paddlepaddle 2.6.2 mais API/outils complètement
différents — non évalué dans ce bench.
### Conclusion
**PP-OCRv5 CPU = BLOCKED**. Bug upstream dans paddlepaddle CPU-only binary, aucune
workaround applicative possible. Seules alternatives :
1. **paddlepaddle GPU binary** (RTX 5070 + CUDA 13.0 compatible) → bench GPU séparé
2. **Fix upstream** paddlepaddle (PR PIR executor OneDNN)
3. **Downgrade PaddleOCR 2.x + paddlepaddle 2.6.2** (API legacy, non testé)
---
## 3. docTR CPU — Résultats détaillés
- **Cold latency**: 0.776s (incl. model loading)
- **Warm latency**: 0.717s
- **Detections**: 139 (mot-level, agressif — fragmente "Dites", "Sortie", "de", "veille")
- **Mémoire**: 263.2MB stable (init = peak)
- **Qualité**: haute sur mots courts, fragmente les phrases longues
- **Confiance**: variable (0.26→0.99), nombreux tokens <0.7
### Observations docTR
- Word-level detection = 139 items → beaucoup de fragments 1-2 lettres
- Bonne qualité sur labels UI ("Mode", "veille", "RPA", "VWB", "Python", "proxmox")
- Fragmente les phrases ("Sortie de veille de l'accès vocal ou appuyez..." → 12 mots isolés)
- IP correctement détecté : "192.168.1.40:3002" (conf 0.90)
- Faux positifs : "0", "E03", "E", "€" isolés avec conf <0.4
---
## 4. EasyOCR CPU — Résultats détaillés
- **Cold latency**: 4.878s (heavy model loading)
- **Warm latency**: 4.856s
- **Detections**: 54 (line-level, plus conservatif)
- **Mémoire**: 0.6MB init → 156.9MB peak
- **Qualité**: bonne sur lignes complètes, plus robuste sur phrases
### Observations EasyOCR
- Line-level detection = 54 items → phrases plus cohérentes
- Cold start très lent (5x docTR) mais warm identique
- Meilleur sur textes longs, moins de fragmentation
- Peak mémoire plus élevé que docTR (156.9 vs 263.2 MB init docTR)
---
## 5. Comparaison avec baselines Mai 2026
> Bench Mai 2026 — image `landing_wide.png`, critère 11 items de référence
| Engine | Score Mai (11 ref) | Score Juillet (detections) | Latency warm | Commentaire |
|--------|-------------------|---------------------------|--------------|-------------|
| Tesseract | **11/11** | — (non re-benché) | — | Référence May, non retesté |
| EasyOCR brut | 8/11 | 54 det (shot_0172) | 4.856s | Fragmente moins, score < Tesseract |
| EasyOCR preproc | 9/11 | — | — | +1 vs brut May |
| docTR CPU | 10/11 | 139 det (shot_0172) | 0.717s | **Meilleur rapport qualité/latence** |
| PP-OCRv5 CPU | non testé May | BLOCKED | — | Bug PIR/OneDNN, 0 inference possible |
### Hierarchie CPU confirmée
```
docTR CPU (0.7s, 10/11) > EasyOCR preproc (4.9s, 9/11) > EasyOCR brut (4.9s, 8/11) > PP-OCRv5 CPU (BLOCKED)
```
docTR reste le **meilleur moteur OCR CPU** pour Léa en termes de latence + qualité.
Tesseract reste le plus précis (11/11) mais sans bounding boxes exploitables.
---
## 6. Recommandations
1. **docTR = moteur OCR CPU de production** — latence <1s, qualité 10/11, word-level bboxes
2. **PP-OCRv5 GPU bench = action séparée** — requiere paddlepaddle GPU binary sur RTX 5070
3. **PaddleOCR 3.4.0 = ORPHAN** — 0 imports dans le projet, pas dans requirements.txt,
CPU-only install sans CUDA → retirer du venv si cleanup D2 (C-MORT)
4. **Ne pas dépendre de PaddleOCR** pour POC T1 — docTR suffisant
5. **Bug report upstream** — paddlepaddle PIR executor OneDNN, repro: any model + CPU binary
---
## 7. Annexes
### A. Script bench
`scripts/bench_ppocrv5_cpu.py` — compare PP-OCRv5, docTR, EasyOCR sur shot_0172_full.png.
PP-OCRv5 crash → résultats JSON avec error field.
### B. Résultats JSON
`scripts/bench_ppocrv5_results.json` — 4522 lignes, contient tous texts + bboxes pour
docTR (139 items) et EasyOCR (54 items). PP-OCRv5 = error only.
### C. Machine specs
- CPU: Ryzen 9 9950X, 32 threads
- RAM: 123 GB
- GPU: RTX 5070 12GB VRAM (non utilisé — bench CPU)
- CUDA driver: 580.159.03 / runtime 13.0
- OS: Linux (Ubuntu)
- paddlepaddle: 3.3.1 CPU-only (pip install)
- PaddleOCR: 3.4.0
- docTR: (version installée dans venv)
- EasyOCR: (version installée dans venv)

View File

@@ -0,0 +1,104 @@
# Cartographie — Chaîne d'apprentissage & mise en commun des connaissances (2026-06-16)
> Question Dom : *« Comment le savoir appris sur chaque poste est-il mutualisé dans un fonds commun unique partagé par tous les postes, et exploité vers l'autonomie ? »*
> Méthode : graphify (graphe code 58k nodes) + 3 agents Explore + vérif code directe. Cross-checks Codex (pipeline server_v1) & Qwen (fédération/FAISS) **en cours** — ce doc sera enrichi.
> ⚠️ Verdicts = état runtime constaté ce jour (`poc-dgx` @ `2b1743c20`), pas la doc d'intention.
## TL;DR (réponse directe)
Le « fonds commun → autonomie » est **partiellement construit, mais les maillons clés sont soit silotés par machine, soit dormants** :
1.**Ce qui EST déjà commun** : les **compétences YAML** (`core/competences/`) et les **embeddings** (CLIP→FAISS) — partagés serveur, tous postes.
2.**Ce qui est SILOTÉ par machine (codé en dur)** : le **stockage** workflows/sessions (`{machine_id}/`) et surtout le **cross-session learning** qui **refuse de matcher entre machines** (`if workflow_machine != machine_id: continue`). C'est l'anti-pattern direct vs la vision.
3. 🌙 **Ce qui est DORMANT** : la **fédération** (`core/federation` : LearningPack, GlobalFAISSIndex) — le vrai mécanisme de fonds commun — est **bien conçue, globale et anonymisée** (Qwen confirmé : zéro machine_id, clé `pack_source_hash`), mais **doublement inerte au runtime** : (a) alimentée **seulement** par l'endpoint import **manuel** (jamais auto-déclenché) ; (b) son **`search()` n'est JAMAIS appelé** (`faiss_global.py:199` défini, zéro consommateur actif) → **index write-only, jamais consulté** → contribue **zéro** au comportement/à l'autonomie aujourd'hui.
4. 🚧 **Ce qui manque pour l'autonomie** : la **couche graphe** (WorkflowGraph) **EST construite en live** (`finalize_session``GraphBuilder`, import lazy `stream_processor.py:3017-3022`, DBSCAN) — **correction d'un faux "orphelin"****mais le graphe est siloté par machine** (persisté sous `workflows/{machine_id}/`) et le merge cross-session est machine-filtré. La progression **Shadow→Copilot→Autonomous** reste **du design, pas du runtime** (Shadow observe+log).
> ⚠️ **Caveat méthodo** : ce code utilise massivement des **imports lazy dans les handlers/méthodes**. Les verdicts "orphelin" basés sur grep d'imports top-level sont **non fiables** (federation ET GraphBuilder étaient ainsi faussement classés orphelins, puis confirmés WIRED). Tout "orphelin" ci-dessous non vérifié par lazy-import est à recontrôler.
→ Aujourd'hui, par défaut, **chaque poste est un silo cognitif** : il n'apprend pas des autres, sauf pour les compétences YAML et les embeddings centralisés.
## Tableau de synthèse (WIRED ? · COMMUN/SILOTÉ)
### A. Capture → construction → stockage (agent Explore #1)
| Composant | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|---|---|---|---|
| TraceStreamer (tag machine_id) | `agent_v1/network/streamer.py:91` | ✅ (main.py:227) | tague machine_id sur chaque POST |
| register/stream/finalize | `server_v1/api_stream.py:1748/1801/2336` | ✅ endpoints | session taguée machine_id |
| `_persist_workflow` | `server_v1/stream_processor.py:4417` (appelé 3066) | ✅ | **SILOTÉ** : écrit `data/training/workflows/{machine_id}/` + tag `_machine_id` |
| Store disque sessions | `data/training/live_sessions/{machine_id}/` | ✅ | **SILOTÉ** (arbo 1:1 par machine) |
| `_run_cross_session_learning` / `_find_best_cross_session_match` | `stream_processor.py:3149 / 3273` | ✅ (via finalize) | **SILOTÉ CODÉ** : `if workflow_machine != machine_id: continue` (L3284-3286) → un poste n'apprend jamais d'un autre |
| Listing `list_workflows` | `api_stream.py:2799` + `stream_processor.py:4518` | ✅ | **BIMODAL** : `machine_id=None` → tous ; sinon filtré |
| Client `list_workflows()` | `lea_ui/server_client.py:228` | ✅ (smart_tray:802) | **COMMUN** : n'envoie PAS machine_id → reçoit tous |
| Dashboard list_sessions | `web_dashboard/app.py:2289` | ✅ | filtre disque par machine_id (optionnel) |
| Replay ciblage | `api_stream.py:3064` + `replay_engine.py:1559` | ✅ | machine_id = **ROUTE** l'exécution vers le bon poste (légitime) |
### B. Apprentissage / cognition / compétences (agent Explore #2)
| Module | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|---|---|---|---|
| `target_memory_store` (mémoire cibles, phase 1) | `core/learning/target_memory_store.py:77` | ✅ hot-path `resolve_engine.py:1869` | **SILOTÉ par machine** (`data/learning/target_memory.db` local, sauf `RPA_LEARNING_DIR` partagé) |
| `continuous_learner` | `core/learning/continuous_learner.py` | ✅ (`stream_processor.py:3145`) | **SILOTÉ par session** |
| `replay_learner` | `server_v1/replay_learner.py:90` | ✅ (`api_stream.py:2436`) | **SILOTÉ par session** |
| `learning_manager` (états workflow VWB) | `core/learning/learning_manager.py:37` | ✅ singleton VWB | **COMMUN** |
| **Compétences YAML** (catalog/replay/persist/verdicts/promotions) | `core/competences/*` | ✅ endpoints `api_stream` + dashboard | ✅ **COMMUN** (tous lisent `data/competences/`) — **c'est le vrai fonds commun qui marche** |
| `observe_reason_act` (ORALoop) | `core/execution/observe_reason_act.py:145` | ✅ (VWB `api_v3/execute.py`) | siloté par exécution |
| `feedback_processor`, `versioned_store`, `core/cognition/*`, `core/knowledge`, `core/coaching`, `core/healing`, `core/supervision` | — | ⚠️ **ORPHELINS** (tests seuls) | — |
### C. Graphe / embeddings / autonomie (agent Explore #3)
| Composant | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|---|---|---|---|
| ScreenState (perception) | `core/pipeline/workflow_pipeline.py` | ✅ serveur | COMMUN |
| StateEmbedding + Builder (fusion 512d) | `core/models/state_embedding.py:44` / `core/embedding/state_embedding_builder.py:25` | ✅ `stream_processor` startup | **COMMUN** (vecteurs `data/training/embeddings/*.npy` centralisés) |
| CLIP (OpenCLIP ViT-B-32) | `core/embedding/clip_embedder.py` | ✅ | COMMUN |
| FAISSManager (similarité) | `core/embedding/faiss_manager.py:40` | ✅ `stream_processor` | **COMMUN serveur** mais **index per-session en mémoire** (pas un index global persistant) |
| **WorkflowGraph builder (couche 4)** | `core/graph/graph_builder.py:148` | ✅ **WIRED** (import lazy `stream_processor.py:3017`, instancié `:3022` dans `finalize_session`, DBSCAN) — *corrigé : Agent #3 l'avait dit orphelin à tort* | **SILOTÉ** : graphe construit par session, persisté `workflows/{machine_id}/` ; merge cross-session machine-filtré |
| WorkflowNode/Edge (modèles couche 4) | `core/models/workflow_graph.py:384` | ✅ utilisés par GraphBuilder | siloté (idem) |
| **Shadow** observer | `core/workflow/shadow_observer.py:25` | ⚠️ partiel (`api_stream:2700` observe + LOG seulement) | pas d'apprentissage collectif |
| **Copilot / Autonomous** | `core/learning/learning_engine.py` | ❌ **design papier**, pas runtime | — |
| `audit_trail.execution_mode` (shadow/assisted/autonomous) | `server_v1/audit_trail.py:50` | ✅ enregistré | mais **pas exploité** pour décider |
### D. Fédération = LE fonds commun par design (vérif directe + Qwen en cours)
| Composant | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|---|---|---|---|
| `LearningPackExporter` | `core/federation/learning_pack.py` | 🌙 **endpoint manuel** `GET /api/v1/traces/stream/learning-pack/export` (`api_stream.py:6278`, import lazy L6292) | conçu commun |
| `LearningPack` + `GlobalFAISSIndex` | `core/federation/learning_pack.py:294` / `faiss_global.py:51` | 🌙 **endpoint manuel** `POST .../learning-pack/import` (`api_stream.py:6323`, L6334-6353 `GlobalFAISSIndex()` ré-instancié) | conçu commun |
| Déclenchement automatique | — | ❌ **AUCUN** : rien dans le flux capture→learn n'appelle export/import | → fonds commun **dormant** |
**Verdict fédération (Claude + Qwen)** :
-**Bien architecturé** (Qwen) : `LearningPack` anonymise (machine_id blacklisté `_SENSITIVE_METADATA_KEYS:64`, `source_hash` SHA-256), `GlobalFAISSIndex` est **global** (clé `pack_source_hash`+`workflow_skeleton_id`+`node_name`+`app_name`, **aucun machine_id**), persistant (`.faiss`+`.meta.json`, `save/load` L245/277). Export prend **tous** les workflows (`processor._workflows.values()` L6305) sans filtre machine.
-**Mais doublement inerte** : (a) **pas d'auto-déclenchement** (rien dans capture→learn n'appelle export/import) ; (b) **`search()` jamais appelé** — `_global_faiss_index` n'est référencé QU'aux lignes `api_stream.py:6351-6372` (instanciation + `add_pack` à l'import). Aucun chemin de résolution/apprentissage/replay ne **lit** l'index global. → **write-only, jamais consulté** : contribue 0 au runtime.
Conclusion : le fonds commun fédéré est **codé correctement mais débranché du runtime** — c'est exactement le maillon à activer pour la vision.
## Où `machine_id` cloisonne vs route
- **ROUTE (légitime)** : ciblage d'un replay/d'une session vers le bon poste (`replay_engine.py:1559`, `start_replay`).
- **CLOISONNE (à corriger vs vision)** : (1) dossiers de stockage `{machine_id}/` ; (2) **filtre dur du cross-session learning** (`stream_processor.py:3284`) ; (3) `target_memory.db` local par machine. + machine_id **instable** (nouvel ID à chaque relance) qui fragmente même au sein d'un poste.
## Gap vs vision « fonds commun → autonomie » (constats, pas décisions)
Pour réaliser la vision, il manque le câblage de :
1. **Dé-siloter le savoir workflows/sessions** : retirer le filtre machine_id du cross-session learning + stockage commun (ou index commun), en gardant machine_id pour le seul routing.
2. **Activer la fédération en continu** (auto-export/import ou store partagé) au lieu du manuel dormant — c'est l'endroit conçu pour ça.
3. **Câbler la couche graphe (4)** en live (aujourd'hui orpheline) pour un knowledge graph commun.
4. **Implémenter Shadow→Copilot→Autonomous** (aujourd'hui observe+log / design) consommant ce fonds commun.
5. **Stabiliser machine_id** (persisté) pour ne pas fragmenter.
## Ce qui marche DÉJÀ comme fonds commun (à capitaliser)
- **Compétences YAML** (`core/competences/`) : micro-workflows réutilisables, états supervisés, **lus par tous les postes**. C'est le modèle commun qui fonctionne → piste à étendre.
- **Embeddings centralisés** (`data/training/embeddings/`) : matière première commune déjà là.
## E. Pipeline server_v1 (cross-check Codex — intégré)
- Pipeline **WIRED** : capture → `_worker_queue.txt``run_worker.py``StreamProcessor.reprocess_session()` → workflow JSON → replay → apprentissage. `api_stream.py` importe+instancie `ReplayLearner`/`StreamProcessor`/`StreamWorker` (`:32/40/41`, `:562-563`, startup `:1626-1629`).
- **`ReplayLearner` = commun mais faiblement effectif** : `ActionOutcome` sans `machine_id`, stockage global `data/learning/replay_results/` ; MAIS `query_similar()` ne lit que le cache mémoire `_recent` (`:273-304`) et `build_replay_from_raw_events()` crée une **nouvelle instance** (`stream_processor.py:2379-2382`) → **l'historique JSONL global n'est pas exploité** après restart/hors instance globale.
- **Risque ambiguïté** : la queue worker ne porte que `session_id`, pas `machine_id` (`api_stream.py:734-760`) → résolution disque ambiguë si 2 machines ont le même `session_id`.
- `machine_id` **route** légitimement : `/replay`, `/replay/next` refusent les actions d'une autre machine (`:3978-3982`, `:4033-4054`), `_find_active_agent_session` filtre `machine_id`/`bg_<machine_id>` (`replay_engine.py:1559-1588`).
## Pistes de correction CONVERGENTES (constats Codex+Claude — NON décidées, mapping seulement)
1. **Cantonner `machine_id` au routing/fleet/replay-target/audit** — pas au stockage ni au matching du savoir.
2. **Dé-siloter les workflows appris** : sortir du stockage logique `workflows/{machine_id}/` (ou neutraliser ce marqueur en lecture/matching).
3. **Retirer/rendre optionnel le filtre machine** dans `_run_cross_session_learning()` / `_find_best_cross_session_match()` (`stream_processor.py:3193-3203`, `3283-3286`) → apprendre sur tous les workflows compatibles.
4. **Brancher le fonds commun fédéré** : (a) alimenter le `GlobalFAISSIndex` en continu (auto export/import ou store partagé) ; (b) **appeler son `search()`** dans le hot-path résolution/apprentissage (aujourd'hui jamais lu).
5. **Rendre `ReplayLearner` durable** : charger/interroger l'historique JSONL global, réutiliser l'instance globale (pas une neuve par session).
6. **Stabiliser `machine_id`** (persisté) pour ne pas fragmenter intra-poste.
7. **Étendre le modèle "compétences communes"** (`core/competences/`, déjà commun + supervisé) comme colonne vertébrale du fonds commun.
---
*Sources : graphify-out (58k nodes) ; agents Explore #1/#2/#3 (⚠ verdicts "orphelin" sur imports top-level corrigés par lazy-import) ; cross-checks **Codex** (server_v1, msg 16:43) & **Qwen** (federation/FAISS, msg 16:50) intégrés ; vérifs directes `api_stream.py:6271-6372`, `faiss_global.py:199`, `stream_processor.py:3017-3022`.*

View File

@@ -0,0 +1,186 @@
# CARTO CODE NON BRANCHÉ — carte de référence wiring (2026-07-02)
> **But** : carte « existing-first » de référence. AVANT tout chantier/bench/proposition,
> consulter ce doc pour savoir si une brique existe et si elle est **réellement branchée au
> runtime**. Recadrage Dom 02/07 : « vérifier ce qui existe et non branché, c'est le BABA ».
>
> **Méthode** : verdict prouvé par chaîne d'imports depuis un point d'entrée actif
> (fichier:ligne), imports lazy inclus, gates de config citées. Jamais de conclusion sur un
> grep seul. Sources fusionnées : agent Claude « intelligence » + carto Qwen (volet 1
> détection) + `AUDIT_CODE_MORT_2026-07-02.md` (Qwen) + vérifs ponctuelles Claude.
>
> **Légende** : **WIRED** (chaîne prouvée) · **GATED** (branché mais derrière flag, défaut
> cité) · **ORPHELIN** (0 appelant runtime, recherche exhaustive) · **INCERTAIN** (non tranché,
> raison donnée).
>
> **Points d'entrée actifs runtime** : `api_stream.py` (streaming 5005, `rpa-streaming`) ·
> `run_worker.py` (worker VLM 5099) · VWB `app.py` (5002) · `web_dashboard/app.py` (5001) ·
> `agent_chat/app.py` (5004) · `server/api_upload.py` (8000).
---
## 0. Résumé exécutif — les découvertes qui changent une décision
1. **Self-healing = façade morte, malgré doc « wired ».** Chaîne d'import réelle (VWB →
`core/healing`), routes REST répondent, MAIS déclenchement **impossible** : le code teste
`hasattr(healing_integration, 'enable_healing')` et cette méthode **n'existe nulle part**
(`execution_integration.py:421`). `handle_execution_failure` = 0 appelant d'exécution.
Preuve d'inertie : `logs/healing/recovery.log` = **0 octet, mtime déc. 2025**. Le pont
manquant tient à **une méthode**, pas un module. → `PLAN_MENAGE_CODE_MORT` le classait
« wired — NE PAS TOUCHER » : **doc fausse**.
2. **`core/navigation` (commit du matin `f9a053132`) = write-only.** Le handler résout le
login et écrit `navigate_login_coords` dans `replay_state["variables"]`, mais **aucun
consommateur** : le compilateur `_edge_to_normalized_actions` n'a pas de branche `navigate`
et produit des coords littérales, jamais de templates `{{navigate_login_coords.x_pct}}`.
Détail : `docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md`. Décision D1 en attente Dom.
3. **AutonomousPlanner : coût sans usage.** Instancié au boot d'`agent_chat` (charge LLM +
OWL detector via `autonomous_planner.py:36`), mais **aucune route n'appelle une méthode de
planification** — seuls des setters. Type même du « code écrit jamais invoqué ».
4. **PaddleOCR installé, jamais importé.** `paddleocr 3.4.0` + `paddlepaddle 3.3.1` (CPU)
présents dans `.venv`, **0 `import paddle` dans le code**, 0 requirements, 0 deploy. Piste
bench en cours (Qwen), pas un composant actif.
5. **YOLO cascade de résolution = mort.** `_resolve_by_yolo` défini
(`resolve_engine.py:458`) + importé (`api_stream.py:6114`) mais **jamais appelé** ; aucune
branche `yolo` dans la cascade compilée. ⚠ À NE PAS confondre avec le YOLO de `som_engine`
(OmniParser SoM), lui **WIRED**.
6. **`server/api_core.py`** : blueprint Flask complet (capture/detect/embed/faiss) **jamais
enregistré** — orphelin absent du plan ménage.
7. **Nos propres cartos avaient 4 erreurs** (cf. §4). Re-prouver était justifié.
---
## 1. Chaîne détection / grounding / résolution
| Module | Verdict | Preuve (fichier:ligne) | Remarque |
|--------|---------|------------------------|----------|
| `core/detection/som_engine.py` | **WIRED** | resolve_engine.py:1192 (replay) · stream_processor.py:643 (recording) · api_stream.py:1958 (temps réel) | 3 chemins indépendants, singleton thread-safe. Tire YOLO weights direct. |
| `core/detection/omniparser_adapter.py` | **B-DORMANT** (branché lazy, fallback vide) | phase25_analyzer.py:388 · resolve_engine.py:437 · désactivé côté VWB (`_omniparser_available=False`) | Import lazy try/except, singleton. 7 zones cartographiées (§ audit Qwen). |
| `core/detection/owl_detector.py` | **WIRED (via AutonomousPlanner) — mais planner inerte** | autonomous_planner.py:36 | Chargé au boot agent_chat pour rien (cf. §0.3). 4 méthodes internes C-MORT. |
| `core/detection/ollama_client.py` | **WIRED partiel** | `classify_element_complete()` actif ; 5 vieilles méthodes + `check_ollama_available()` standalone = C-MORT | Duplicat VWB (D2). |
| `_resolve_by_yolo` (resolve_engine.py:458) | **ORPHELIN** | importé api_stream.py:6114, **0 appel réel**, 0 branche cascade | ≠ YOLO de som_engine (wired). |
| `core/grounding/bbox_parser.py` | **WIRED** | resolve_engine.py:29 | |
| `core/grounding/smart_resize.py` | **ORPHELIN (C-MORT)** | 0 appelant prod, DETTE-007 triple impl (2 autres existent) | |
| `core/grounding/server.py` | **WIRED** | service HTTP Flask port 8200 standalone | Upgrade C→A (Qwen). |
| `visual_workflow_builder/.../api/ui_detection.py` | **WIRED** | VWB app.py:310 (blueprint) · fast_detector.py:117 | UI-DETR-1 du recording, modèle rfdetr RFDETRMedium, 5 endpoints `/api/ui-detection`. |
| `core/semantic/phase25_analyzer.py` | **WIRED** | api_stream.py:7690 (route `lea_competence_persist:7435`) | |
| `core/extraction/{field_extractor,vlm_client,role_mapper}` | **WIRED-transitif** | field_extractor ← input_handler.py:121/504/722 (lazy) · vlm_client+role_mapper ← core/navigation/__init__.py:69, action_resolver.py:109 | Le plan ménage 23/06 (« 4/5 morts ») précède navigation. |
| `core/llm/` (ocr_extractor, extract_grid) | **WIRED** | api_stream.py:1766 · replay_engine.py:2115-2403 · resolve_engine.py:2597 | |
| `core/navigation/` | **WIRED (boot) / write-only (fonctionnel)** | api_stream.py:440 top-level NON gardé · handler résout mais 0 consommateur coords | cf. §0.2. ⚠ import non gardé → si casse, 5005 ne boote pas (garde-fou test_navigate_wiring.py). |
| PaddleOCR (venv) | **ORPHELIN** | 0 import, 0 requirements, 0 deploy | cf. §0.4. |
---
## 2. Modules « intelligence »
| Module | Verdict | Preuve (fichier:ligne) | Remarque |
|--------|---------|------------------------|----------|
| `core/healing/` | **ORPHELIN de fait** (importé, indéclenchable) | chaîne VWB app.py:217 → api/self_healing.py → services/self_healing_integration.py, MAIS `enable_healing` inexistant (execution_integration.py:421) ; `handle_execution_failure` 0 appelant | cf. §0.1. `logs/healing/recovery.log` vide depuis déc. 2025. |
| `core/coaching/` | **WIRED** | VWB app.py:284-285 (blueprint) → api/coaching_sessions.py:17,22 · exec : execution_integration.py:869 · front WebSocket | REST blueprint peut-être non consommé par l'UI (front = socket.io). |
| `core/cognition/working_memory` | **WIRED-transitif** | observe_reason_act.py:30,506 · ORALoop ← VWB execute.py:1542,2075 | Les 4 autres sous-modules cognition = MORTS (tests only). |
| `core/learning/` (4/5) | **WIRED** | target_memory_store: resolve_engine.py:1865 + api_stream.py:5132 · continuous_learner: stream_processor.py:3147 · learning_manager: VWB learning_integration.py:36, api/workflows.py:696 · feedback_processor: execution_loop.py:317 | `versioned_store` ORPHELIN. `record_observation` = **0 appelant** (learning_manager.py:54). |
| `core/execution/` | **WIRED massif** | observe_reason_act ← execute.py:1542 · input_handler ← execute.py:69 · dag_executor+llm_actions ← dag_execute.py:33,40 · action_executor/target_resolver/error_handler/execution_loop ← agent_chat app.py:328-340 · +transitifs | Morts : spatial_index, target_memory, workflow_runner (⚠ encore exporté par `__init__.py:10`). |
| `core/auth/` | **GATED — défaut OFF** | api_stream.py:278-286 : import lazy SSI `RPA_AUTH_VAULT_PATH` **et** `RPA_AUTH_VAULT_PASSWORD` définis (absents par défaut). Seul lieu qui les définit : CI `.gitea/workflows/tests.yml:35` | Vault inactif en prod. TOTP dans la même chaîne gated. |
| `core/federation/` | **WIRED manuel, write-only** | routes actives non gated : GET learning-pack/export api_stream.py:6431 · POST import :6476 | `GlobalFAISSIndex.search()` = **0 appelant**. Aucun auto-déclenchement. |
| `core/gpu/` (2/6) | **WIRED** | device_policy ← resolve_engine.py:1750 (hot-path) · gpu_resource_manager ← agent_chat app.py:53,266 | clip_manager, ollama_manager, vram_monitor, preflight = morts. |
| `core/embedding/` | **WIRED (lazy)** | construction CLIP/FAISS ← stream_processor.py:2560 `_ensure_initialized` (appelé process_screenshot:2804 + finalize_session:2969) · lecture web_dashboard app.py:309+ | Se déclenche au 1er screenshot / finalisation, pas au boot. |
| `agent_chat/autonomous_planner` | **INSTANCIÉ mais INERTE** | import app.py:48, instancié :358, mais seuls appels = setters :362,367 ; 0 route de planification | cf. §0.3. Tire owl_detector pour rien. |
| `agent_chat/urgences_orchestrator` | **WIRED** | import lazy app.py:2740, routes `/api/urgences/*` | |
| `agent_chat/gesture_catalog` | **WIRED ×2** | agent_chat app.py:377,955 · **api_stream.py:269,3598** (hot-path replay `optimize_replay_actions`) | Pas seulement le chat. |
| `core/validation/` | **GATED — défaut OFF** | flag `RPA_VALIDATOR_V2_ENABLED` défaut OFF (api_stream.py:91), consommé report_action_result:4924 | |
**WIRED confirmés (survol)** : capture, models, competences, corrections, data, graph,
knowledge, monitoring, persistence, pipeline, system, workflow, visual, config.py,
anonymisation (PII), matching/training (transitifs).
**ORPHELINS confirmés** : variants, precision, supervision, interfaces (0 importeur non-test) ·
`core/evaluation/` (consommé seulement par `tools/lea_bench*.py`, outillage CLI) ·
`server/api_core.py` (blueprint jamais enregistré).
---
## 3. Zones GATED (flags + défaut) — activation supervisée
| Flag | Défaut | Effet si ON | Preuve |
|------|--------|-------------|--------|
| `RPA_AUTH_VAULT_PATH` + `RPA_AUTH_VAULT_PASSWORD` | absents | active `core/auth` (vault Fernet + TOTP) | api_stream.py:278-286 |
| `RPA_VALIDATOR_V2_ENABLED` | OFF | active validation V2 (report_action_result) | api_stream.py:91 |
| `RPA_R1_AUTO_IMPORT` | OFF | active import auto core→DB VWB (R1) | api_stream.py:~4480 (revue en cours) |
| `RPA_AUTO_UPDATE_ENABLED` | OFF | MAJ silencieuse client (DETTE-022) | agent_v1/config.py:103 |
| `RPA_GROUNDING_ENGINE=qwen3vl_vllm` | legacy Qwen2.5-VL | grounder Qwen3-VL (override DGX runtime) | resolve_engine.py:1001-1007 |
---
## 4. Divergences corrigées avec les docs existants
1. **`core/healing` : doc `PLAN_MENAGE` = « wired, NE PAS TOUCHER » → FAUX.** Indéclenchable
(`enable_healing` fantôme, log vide déc. 2025). Le pont est à une méthode près.
2. **`feedback_processor` : CARTO 16/06 = ORPHELIN → FAUX.** Instancié à chaque ExecutionLoop
(execution_loop.py:317).
3. **`core/cognition` : CARTO 16/06 = tout orphelin → FAUX pour working_memory** (vivant au
runtime VWB via observe_reason_act).
4. **`core/extraction` : plan ménage « 4/5 morts » → périmé.** vlm_client + role_mapper
branchés via `core/navigation` (postérieur au doc).
Upgrades C→A/B confirmés par Qwen : autonomous_planner (C→A, mais inerte cf. §0.3),
seeclick_adapter (C→B), grounding/server.py (C→A), get_grounding_profile (C→A).
---
## 5. Code mort candidat suppression → voir `AUDIT_CODE_MORT_2026-07-02.md`
Résumé : **8 C-MORT** (~843 lignes, ex. deploy_windows.py, smart_resize.py, 7 config classes
dépréciées, agent_chat 410 endpoints) · **5 B-ORPHELIN** (à conserver, projections) · **4
duplicats** (décision Dom). Suppression = GO Dom par lot, worktree isolé + tests après chaque
lot. ⚠ Prudence renforcée vu les 4 erreurs de doc du §4 : re-prouver chaque item avant
suppression.
---
## 6. Cascade de résolution UI (`resolve_engine.py`) — ordre RÉEL prouvé
Point d'entrée unique au replay : le client Léa (`executor.py:2847`, **`strict_mode=True` hardcodé**
:2870) → route `resolve_target` (`api_stream.py:6131`) → `_resolve_target_sync`
(`resolve_engine.py:1804`). `replay_engine.py` ne résout pas (il construit le target_spec).
**Ordre réel au replay (mode strict VLM-first, `resolve_engine.py:1957`)** :
```
0. Mémoire persistante (replay_memory.memory_lookup:1869) — hit → skip toute la cascade
0c. dialog_button → OCR seul (1920-1952)
── strict VLM-first (1957) ──
S0a. Grounding VLM (_resolve_by_grounding:2019) si by_text_source ∈ {ocr, vlm}
S0b. Template matching icônes (2057) sinon
S0.5 OCR direct (_resolve_by_ocr_text:2105) si by_text
S1. VLM Quick Find (_vlm_quick_find:2158)
S1.5 SoM + VLM (_resolve_by_som:2207)
S2. Template matching fallback (2238)
S3. STOP replay resolved=False (2283)
```
Note : grounding VLM (S0a) et VLM Quick Find (S1) sont **deux appels VLM distincts**.
**Statut resolvers** : `_resolve_by_grounding`, `_resolve_by_template_matching`,
`_resolve_by_ocr_text`, `_vlm_quick_find`, `_resolve_by_som`, `replay_memory` = **WIRED**
(preuves lignes ci-dessus). Grounder Qwen3-VL : bascule dans `_resolve_by_grounding:1006`
(flag `RPA_GROUNDING_ENGINE=qwen3vl_vllm`, sinon legacy Qwen2.5-VL) — change modèle/endpoint/
prompt/parser, pas le flux.
**3 branches MORTES dans la cascade** :
- `_resolve_by_yolo` (:458) — importé api_stream.py:6114, **0 appel réel**. ORPHELIN.
- **Vérification CLIP** (:1972-2008) — **dead gate** : lit `target_spec["clip_embedding"]`
qui n'est **jamais peuplé** dans tout `agent_v0/` → branche jamais exécutée.
- **V4 pré-compilé** (`_resolve_with_precompiled_order:1601`, ordre figé `["ocr","template",
"vlm"]`) — **WIRED mais dormant en replay normal** : alimenté uniquement par l'endpoint
`/replay/plan` (`execution_plan_runner.py:173`), jamais par le flux VWB→Léa.
**Verdict README « OCR→template→YOLO→VLM » = FAUX** : (1) YOLO mort, (2) l'ordre est
VLM-first, (3) la séquence `ocr,template,vlm` n'existe que dans le V4 dormant.
## 7. Zones restantes non re-vérifiées (honnêteté)
- `core/analytics/` : ~13 sous-modules orphelins non re-vérifiés un par un (conforme doc).
- Reste couvert : chaîne détection/grounding/résolution + intelligence = prouvés. **Carto
considérée complète sur le périmètre runtime actif.**

View File

@@ -0,0 +1,145 @@
# CHECKLIST DGX — Contrôle avant installation clinique
- `Auteur`: Qwen
- `Date`: 2026-06-19
- `Version`: v1 — à vérifier point par point avant déploiement site clinique
---
## 1. SERVICES — Tous démarrent au reboot
| # | Service | Port | Statut attendu | Check |
|---|---|---|---|---|
| 1.1 | rpa-streaming | 5005 | `health=200` | `curl http://127.0.0.1:5005/health` |
| 1.2 | rpa-vision-v3-dashboard | 5001 | `401 sans creds, 200 avec creds` | `curl -u lea:<password> http://127.0.0.1:5001/api/system/status` |
| 1.3 | rpa-vision-v3-vwb-backend | 5002 | `401 LAN, 200 loopback` | `curl http://127.0.0.1:5002/health` puis `curl http://192.168.x.x:5002/health` |
| 1.4 | rpa-agent-chat | 5004 | `200` | `curl http://127.0.0.1:5004/api/status` |
| 1.5 | rpa-vision-v3-api | 8000 | `fermé LAN` | `curl http://192.168.x.x:8000` → timeout/refused |
| 1.6 | rpa-vision-v3-vwb-frontend | 3002 | `200` | `curl http://127.0.0.1:3002` |
| 1.7 | rpa-vision-v3-stream-worker | 5099 | `running` | `systemctl status rpa-vision-v3-stream-worker` |
| 1.8 | rpa-vision-v3-worker | — | `running` | `systemctl status rpa-vision-v3-worker` |
| 1.9 | rpa-firewall | — | `active (exited)` | `systemctl status rpa-firewall` |
| 1.10 | Dashboard systemd | 5001 | **service system ACTIF** (pas fallback user) | ✅ **VALIDÉ reboot 20/06** — system service active, fallback user masked |
**Check reboot** : `systemctl list-units --type=service | grep rpa` → tous `active running` ou `active exited`
---
## 2. RÉSEAU — Ports sensibles fermés LAN
| # | Port | Risque | Statut attendu | Check |
|---|---|---|---|---|
| 2.1 | 5900 (VNC GNOME) | Remote desktop | **LAN fermé, loopback OK** | `nmap 192.168.x.x -p 5900` → filtered/closed |
| 2.2 | 5902 (VNC VM Windows) | Remote desktop VM | **LAN fermé, tunnel SSH only** | `nmap 192.168.x.x -p 5902` → filtered/closed |
| 2.3 | 3389 (RDP/xrdp) | Remote desktop | **LAN fermé** | `nmap 192.168.x.x -p 3389` → filtered/closed |
| 2.4 | 22220 (SSH VM Windows) | Shell VM | **LAN fermé** | `nmap 192.168.x.x -p 22220` → filtered/closed |
| 2.5 | 8000 (API upload) | API non protégé | **LAN fermé** | `nmap 192.168.x.x -p 8000` → filtered/closed |
| 2.6 | 11434 (Ollama) | Modèles IA | **LAN fermé** | `nmap 192.168.x.x -p 11434` → filtered/closed |
| 2.7 | 5002 (VWB backend) | Données workflows | **LAN : auth requise (401)** | `curl http://192.168.x.x:5002/api/workflows/` → 401 |
| 2.8 | 5004 (Agent chat) | Chat interface | **À arbitrer** — ouvert ou fermé ? | Décision Dom |
| 2.9 | 3002 (VWB frontend) | Interface web | **À arbitre** — ouvert ou fermé ? | Décision Dom |
---
## 3. SÉCURITÉ — Authentification + accès
| # | Item | Statut attendu | Check |
|---|---|---|---|
| 3.1 | Dashboard Basic Auth | `401 sans creds` | `curl http://192.168.x.x:5001/api/system/status` → 401 |
| 3.2 | VWB Basic Auth | `401 LAN, 200 loopback` | Vérifié ✅ (commit cf81ce4c7) |
| 3.3 | Streaming Bearer Auth | `401 sans token` | `curl http://127.0.0.1:5005/api/v1/...` → 401 |
| 3.4 | SSH clé uniquement | Pas de password login | `grep PasswordAuthentication /etc/ssh/sshd_config` → no |
| 3.5 | Firewall persistant reboot | Ports fermés après reboot | ✅ **VALIDÉ reboot 20/06** — ports sensibles filtrés, services ouverts OK |
| 3.6 | RPA_SIGNING_KEY défini | FAISS metadata valide | ⚠️ **À FIXER** — HMAC mismatch, Option A en attente |
---
## 4. VM WINDOWS — Autostart + stabilité
| # | Item | Statut attendu | Check |
|---|---|---|---|
| 4.1 | VM boot auto au reboot DGX | Service systemd user `aivanov` | ✅ **VALIDÉ reboot 20/06**`win11-arm-lea.service` auto-démarre, linger=yes |
| 4.2 | VM accessible VNC | Tunnel SSH `localhost:5902` | Vérifié ✅ |
| 4.3 | VM ne pas libvirt en parallèle | Pas de conflit disk.qcow2 owner | ⚠️ **À DOCUMENTER** — ne pas lancer libvirt VM |
| 4.4 | disk.qcow2 owner = aivanov | Pas libvirt-qemu | `ls -la disk.qcow2` → aivanov:aivanov |
| 4.5 | swtpm lancé par script | Pas manuel | Script standalone gère swtpm ✅ |
| 4.6 | Léa config.txt pointe DGX | Pas cloud URL | `cat config.txt` → DGX IP |
---
## 5. DONNÉES — Persistence + integrity
| # | Item | Risque | Statut attendu | Check |
|---|---|---|---|---|
| 5.1 | workflows.db | 24 workflows live | `curl -u lea:<pw> http://127.0.0.1:5001/api/workflows | jq '.total'` → 24 |
| 5.2 | FAISS index | 13666 vectors | `curl ... /api/knowledge-base/stats | jq '.vectors_indexed'` → 13666 |
| 5.3 | FAISS metadata HMAC | Test endpoint 200 | ⚠️ **À FIXER** — Option A (resigner) |
| 5.4 | Sessions training | Non trackées git → safe au reset | `ls data/training/sessions/` |
| 5.5 | Git aligné | HEAD = dernier commit P0 | `git log -1` → cf81ce4c7 |
| 5.6 | workflows.db préservé au git reset | Backup avant reset | ⚠️ **Procédure à respecter** |
---
## 6. STABILITÉ — Test reboot (✅ exécuté en réel le 2026-06-20)
| # | Item | Check | Résultat | Verdict |
|---|---|---|---|---|
| 6.1 | Reboot DGX | Coupure secteur 02:07 | 9 services reviennent | ✅ PASS |
| 6.2 | VM Windows auto-start | `win11-arm-lea.service` | VM auto-démarre | ✅ PASS |
| 6.3 | Firewall persisté | Ports après reboot | Sensibles filtrés, services ouverts | ✅ PASS |
| 6.4 | Dashboard systemd | Après reboot | System service actif, user fallback masked | ✅ PASS |
| 6.5 | Worker healthy | Après reboot | PID 2267 actif, last_cycle continu | ✅ PASS |
| 6.6 | **IP DHCP dérive** | `.45``.46` | IP statique `.45` appliquée (Dom) | ⚠️ **G1 — IP statique obligatoire clinique** |
| 6.7 | **OVMF corruption VM** | Coupure brutale | OVMF corrompu, récupération manuelle (Codex) | ⚠️ **G2 — auto-réparation OVMF à implémenter** |
| 6.8 | **Léa guest reconnecte** | config.txt | CONFIGURE_ME, pas DGX | ⚠️ **G4 — config.txt à renseigner** |
---
## 7. PRÉ-REQUIS DSI (envoyés à Nicolas PORQUET)
| # | Item | Statut | Check |
|---|---|---|---|
| 7.1 | Proxy HTTPS | À installer clinique | Architecture validée |
| 7.2 | Docker | À installer | — |
| 7.3 | VLAN isolation | À configurer | — |
| 7.4 | SSH clé uniquement | ✅ Configuré DGX | `PasswordAuthentication no` |
| 7.5 | 100% on-premise | ✅ Aucune cloud call | Vérifier config Léa |
| 7.6 | Pas de secrets exposés | ✅ .env.local permissions | `ls -la .env.local` → 600 |
---
## ⚠️ ITEMS À FIXER AVANT CLINIQUE
1. **Dashboard fallback user** → ✅ **FIXÉ 20/06** (mask persistant, system service actif)
2. **Auto-start VM** → ✅ **VALIDÉ 20/06** (reboot réel prouvé)
3. **FAISS Option A** → ✅ **FIXÉ 19/06** (metadata resigné, 13666 vectors, test success=true)
4. **Git DGX aligné** : DGX sur ec1fb81, cible cf81ce4c7 → aligner avec backup workflows.db
5. **Test reboot** → ✅ **exécuté en réel 20/06** (5 PASS, 3 gaps identifiés)
6. **G1 Dérive IP DHCP** : IP statique labo `.45` OK ; clinique = Ethernet `.178` obligatoire
7. **G2 Auto-réparation OVMF** : snapshot sain au boot + restauration auto si TianoCode loop → **À IMPLÉMENTER**
8. **G4 Léa reprise auto** : config.txt persistant DGX + token + auto-login → **À RENSEIGNER**
---
## Commandes smoke rapide (à lancer sur DGX)
```bash
# Services
systemctl list-units --type=service | grep rpa
# Health endpoints
curl -s http://127.0.0.1:5002/health
curl -s http://127.0.0.1:5005/health
curl -s -u lea:v_zhmqOpGYcR-t7xJFKZyW-LjpvBuOOKss0ZleyH4jQ http://127.0.0.1:5001/api/system/status | jq '{workflows_count,status}'
curl -s -H "Authorization: Bearer o3_LHqV_7_Gc6OVPHndhsBbvG6HJ5PCgl8yIBhGUIz8" http://127.0.0.1:5005/api/v1/traces/stream/processing/status | jq '{status,processing_ready}'
# Firewall LAN
nmap 192.168.1.45 -p 5900,5902,3389,22220,8000,11434
# VM
virsh -c qemu:///system list # doit être VIDE (standalone, pas libvirt)
ps aux | grep qemu-system-aarch64 | grep win11
# Git
cd ~/ai/rpa_vision_v3 && git log -1 --oneline
```

View File

@@ -0,0 +1,87 @@
# Décisions produit en attente — à trancher à tête reposée
- `Date`: 2026-06-23
- `Auteur`: Claude (questions) / Dom (réponses)
- `Usage`: Dom remplit la ligne **Réponse Dom** quand il a la tête au calme. Chaque décision débloque un ou plusieurs chantiers du plan `PLAN_ACTION_SUITE_2026-06-23.md`. Segmentation par **fonctionnalité (F#)**.
> Contexte : H3 (cœur produit) traité **en premier**, décisions d'abord puis exécution séquencée (validé Dom 23/06). On ne touche pas l'archi rejeu pendant la livraison clinique du jour.
---
## ✅ Déjà tranché (23/06)
| Réf | Décision | Réponse Dom |
|---|---|---|
| **PROC-1** | « H3 en premier » = trancher les décisions produit d'abord, exécution séquencée ensuite | ✅ OK |
| **F6-1** | Niveau de mutualisation du fonds appris | ✅ **Cross-clinique ET intra-clinique** → la fédération anonymisée (cross) **est dans le périmètre**, + lever le silo `machine_id` entre postes d'une même clinique |
| **F11-1** | Accès distant | ✅ **Multi-VPN par site** (WireGuard=nôtre, Stormshield=clinique, autres à venir) ; SSH cert + RDP = transport commun |
| **F9-1** | Source de vérité workflows | ✅ **DB SQLAlchemy = vérité, JSON = échange** ; métrique produit = rejouables validés ; migration JSON→DB séquencée post-clinique (23/06) |
| **F1-1** | Critère de fusion workflows | ✅ **Signature de trajectoire** (hash de la séquence d'actions/écrans) → débloque U-A create-or-update (23/06) |
| **F2-1 / F14-1** | Principe rejeu intelligent | ✅ **Oui, prérequis** — le rejeu consulte le fonds appris (TargetMemory + FAISS anchors), pas de coords figées ; cohérent F6 cross-clinique (23/06) |
---
## ⏳ À trancher
> ✅ **Tranchés 23/06** (voir §Déjà tranché) : Q-F9-1 (DB), Q-F1-1 (signature trajectoire), Q-F2-1 + Q-F14-1 (rejeu intelligent = prérequis). **Restent** : Q-F2-2 (point d'entrée Léa — Claude trace en read-only), et les items **parkés** : Q-F8-* (natif/sécu mis de côté momentanément), Q-F11-2 (VPN).
### F2 — Rejeu intelligent (le plus urgent)
**Q-F2-1 — Principe « rejeu intelligent ».** Valides-tu que le rejeu **doit consulter le fonds appris** (TargetMemoryStore + FAISS anchors) au lieu de rejouer des coordonnées figées ? *(Vérif runtime 23/06 : aujourd'hui le rejeu est « brut », il ne consulte rien.)* C'est la décision qui change l'archi du replay direct (chantiers R2/R3).
> ⚠️ **Quasi-entraînée par ta décision F6 = cross-clinique** : un pack fédéré anonymisé n'a ni coords ni templates → la re-résolution visuelle (anchors/FAISS) devient le seul moyen de rejouer ailleurs. Voir Q-F14-1.
> **Réponse Dom :** _____
**Q-F2-2 — Provider Léa au runtime.** Quel modèle/route sert la **résolution** pendant le rejeu ? (impacte l'import auto R1 + la cascade de résolution). Options connues : Qwen3-VL-4B/vLLM (grounder prod), gemma4 (cerveau/lecteur), autre.
> **Tracé Claude 23/06** (read-only) : point d'entrée runtime = **agent_chat (5004)** → `SemanticMatcher.find_workflow()` (`agent_chat/app.py:906`) sur `/api/chat`. Sélection sur **fichiers JSON** (`data/workflows/`, `data/training/workflows/`, `…/live_sessions/workflows/`), **pas la DB** → ⚠️ **gap avec Q-F9-1 (DB=vérité)** : la sélection runtime devra migrer sur la DB. **Pas de filtre machine_id** à la sélection. `api_stream` (5005) = présent mais pas le chemin chat actif. Modèle de résolution = Qwen3-VL-4B grounder + gemma4 (bench 13/06).
### F1 / F9 — Apprentissage & workflows
**Q-F1-1 — Critère de fusion des workflows** *(tu as dit « on verra à tête reposée »)*. Quand deux sessions apprises sont-elles « le même » workflow (→ create-or-update plutôt que doublon) ? Par signature d'écran de départ ? nom ? séquence d'actions ? application cible ?
> **Réponse Dom :** _____
**Q-F9-1 — Source de vérité workflows + métrique produit.****Ma reco ci-dessous (§Reco F9-1)**, à valider ou amender.
> **Réponse Dom :** _____
### F14 — Unification Léa ↔ VWB (anchors)
**Q-F14-1 — Re-exécutabilité des packs fédérés (décision induite).** Un pack cross-clinique anonymisé n'emporte **ni coordonnées ni templates** (PII) → re-résolution **visuelle** obligatoire au rejeu (anchors/FAISS). ⇒ acte-t-on que **le « rejeu intelligent » (Q-F2-1) devient un prérequis** (pas une option) dès lors que F6 = cross-clinique ?
> **Réponse Dom :** _____
*(Le sous-chantier U-B « propagation des anchors aux substeps compound + ré-import » ne demande aucune décision produit — c'est un fix prêt, à exécuter post-stabilisation sous GO. Idem U-D « asymétrie grounding UI-DETR-1 vs cascade » = à trancher plus tard, sujet ouvert post-démo.)*
### F8 — Exécution native agentique (zéro-shot)
*Constat 23/06 : briques présentes et wirées (boucle ORA `/execute/instruction`, planner NL→plan gemma4), mais mode « free » peu mûr, **sans sandbox ni validation humaine**, exécution directe sur l'host.*
**Q-F8-1 — Périmètre du mode natif.** Desktop/navigateur **généraliste** (« ouvre YouTube ») ou **borné aux apps métier** (Easily…) ? (impacte le risque et le choix moteur)
> **Réponse Dom :** _____
**Q-F8-2 — Surface d'exécution / sandbox (sécurité).** Acte-t-on que le mode free ne tourne **QUE dans un sandbox** (VM/Xvfb + kill-switch + pause humaine), **jamais l'host** — prérequis avant tout élargissement ? (= la décision CUA P1)
> **Réponse Dom :** _____
**Q-F8-3 — Moteur agentique.** Garder ORA + gemma4/Qwen3-VL (100 % local) ou évaluer un agent computer-use dédié (UI-TARS mode agent, Qwen3-VL agent…) ? → un mini-état-de-l'art web peut éclairer (les modèles évoluent vite).
> **Réponse Dom :** _____
**Q-F8-4 — Niveau d'autonomie.** Validation humaine step-by-step au début (Copilot) puis desserrage progressif (cohérent F7 Shadow→Copilot→Autonomous + safety) ?
> **Réponse Dom :** _____
### F11 — Accès distant multi-VPN
**Q-F11-2 — Abstraction « fiche site → accès ».** Quels **types de site/VPN** anticiper au-delà de Stormshield + WireGuard ? Faut-il coder dès maintenant une abstraction générique « fiche site → méthode d'accès », ou gérer les 2 cas connus d'abord et généraliser plus tard ?
> **Réponse Dom :** _____
---
## 💡 Reco Claude — Q-F9-1 (source de vérité workflows)
**Recommandation : la DB (SQLAlchemy/SQLite `workflows.db`) = source de vérité unique ; le JSON = format d'échange/export uniquement (packs fédération, portabilité, import VWB), PAS un 2ᵉ magasin.**
**Pourquoi la DB :**
- Les workflows sont **relationnels** (workflow ↔ steps ↔ substeps ↔ anchors ↔ session/machine) : intégrité référentielle, requêtes, versionnement.
- Le **create-or-update à la fusion** (Q-F1-1) = un upsert atomique trivial en SQL, pénible et risqué sur des fichiers JSON.
- **Concurrence** : 5 Léa qui apprennent/s'enrôlent en parallèle (test de charge) → JSON + chemins relatifs au cwd = races + fragilité du symlink (DETTE-015 déjà constatée).
- **F6 fédération** a justement besoin de DB-comme-vérité + JSON-comme-échange : un `LearningPack` = sérialisation d'une requête DB → pack JSON ; l'import = désérialisation → DB. JSON-comme-vérité **entre en conflit** avec ça.
**Nuance de timing :** la **migration** JSON→DB est un refactor persistant → **séquencée post-clinique** (après stabilisation + merge), comme le prévoit déjà `PLAN_MIGRATION_WORKFLOWS_STORE`. D'ici là le symlink reste, dette connue, **on n'y touche pas pendant la livraison**.
**Métrique produit (24/79/37) :** la seule défendable face à un client/DSI = **les workflows rejouables validés** (≈ les **24** VWB aujourd'hui, ce que Léa exécute vraiment de bout en bout). Les **79** (catalogue agent-chat) et **37** (KB/FAISS) = compteurs **internes**, à ne pas exposer comme « N workflows ». Une fois la DB source de vérité : métrique = `count(workflows WHERE statut = rejouable_validé)`.

View File

@@ -0,0 +1,44 @@
# Design — Anonymisation par tokens typés (= apprentissage des variables)
- `Date`: 2026-06-28
- `Auteur`: Claude (idée Dom)
- `Statut`: design actif
- `Origine`: PII patient en clair confirmée en production clinique (titres de fenêtre + **contenu médical des `text_input`**). Idée Dom : remplacer la PII par des **tokens typés** plutôt que flouter.
## Principe directeur (Dom 28/06)
**Léa apprend l'INTERFACE, pas la DONNÉE.** Ce qui compte pour l'apprentissage, c'est **où sont les champs, leur type, l'enchaînement****pas leur contenu**. Après apprentissage, Léa devra :
- **saisir des données** dans les bons champs (contenu fourni au runtime par un agent / une extraction),
- **lire les écrans pour extraire des données** que l'« agent » traitera.
→ Le contenu capturé (texte saisi, valeurs OCR, nom patient dans un titre) est une **variable**, pas une constante à mémoriser. On le remplace par un **token typé** : on **anonymise** ET on **apprend la carte des variables** d'un seul geste. Flouter détruit l'info ; tokeniser la préserve **structurellement** sans la valeur.
Conforme au principe d'anonymisation déjà acté (`feedback_anonymisation_stricte` : remplacer uniquement les identités, ne jamais réécrire le clinique) — ici on va plus loin : **le contenu d'un champ saisi n'est même pas nécessaire**, on garde le champ, pas la valeur.
## Modèle d'anonymisation par type de capture
| Capture | Ce qu'on garde (interface) | Ce qu'on retire (donnée) | Comment |
|---|---|---|---|
| **`text_input`** (texte tapé) | le **fait** qu'un champ texte a été saisi (+ éventuellement nb de caractères) | **tout le contenu** (diagnostics, notes médicales = données de santé) | **`[SAISIE]`** (option **b**, décision Dom). Pas de NER nécessaire — on ne stocke simplement pas le contenu. **Résout la fuite la plus grave.** |
| **`active_window_title`** | l'**app/écran** (« GXD5 Pacs », « Expert Santé », « Firefox ») = contexte d'apprentissage | l'**identité patient** (nom, IPP, âge) | tokens typés `[NOM_1]`/`[IPP_1]`/`[AGE_1]` via **couche 1 (regex+structurel)** + **couche 2 (NER)** pour les noms libres |
| **OCR / lecture d'écran** | les **zones/champs** d'où extraire | les **valeurs** extraites (PII) | token typé de la zone (« extraire un `[NOM]` ici »), valeur réinjectée au runtime, non persistée |
## Architecture (2 couches + intégration)
- **Couche 1 — regex + structurel (FAIT, `agent_v0/server_v1/pii_sanitizer.py`)** : tokens typés cohérents, **sans modèle**, déployable partout. Capte IPP/NIR/TEL/EMAIL/AGE + noms format clinique (`NOM (NAISSANCE) Prénom`, `[Nom Prénom]` PACS) + blacklist logiciels. Couvre 5/7 patients du jour, **tous les IPP**. 5 tests verts.
- **Couche 2 — NER CamemBERT-bio (à vendorer)** : moteur `CamembertNerManager` du projet `~/ai/anonymisation` (ONNX **CPU**, ~9-12 ms/titre, labels typés PER/IPP/AGE/DATE/HOPITAL…). Pour les **noms libres** que la couche 1 rate (« Prénom NOM — Firefox »). Modèle **421 Mo → côté DGX** (postes cliniques trop légers — contrainte Dom). Lazy, optionnel.
- **Intégration** : une fonction `sanitize_event(event, mapping)` au **point de persistance serveur** : `text_input``[SAISIE]` ; titres → `anonymize_text` (couche 1 + 2) ; cohérence par **mapping de session** (même entité → même token).
## Placement & déploiement
- **Côté serveur (DGX)** : les events y remontent déjà ; le client reste léger. (Cible privacy-by-design = supprimer le contenu **au plus près du poste** avant stream — évolution, quand le client pourra être modifié.)
- **Déploiement GATED** : ne pas redémarrer le serveur DGX pendant des sessions live.
- **Ne casse pas l'apprentissage** : `workflow_trajectory_signature` tokenise déjà la PII pour le discriminant ; les tokens typés **renforcent** la carte des variables.
## Décisions Dom
-**Option (b)** pour `text_input` : placeholder `[SAISIE]`, on ne garde pas le contenu (28/06).
-**Donnée déjà capturée** (9 patients, 6 IPP, contenu médical) : assainir a posteriori vs **purger** — avec Amina (reco Claude : purger les sessions du jour une fois le fix en place).
## Connexe
- **Config-remontée** des specs postes (CPU/RAM/GPU/OS) pour cibler + fournir des prérequis (`screen_metadata` en remonte déjà une partie).
- Réutilise : `~/ai/anonymisation` (`camembert_ner_manager.py`, gazetteers INSEE, blacklists, regex, PLACEHOLDERS).

View File

@@ -0,0 +1,59 @@
# Design — Auto-réparation OVMF de la VM Léa (gap G2, reprise non-assistée)
- `Auteur`: Claude (infra)
- `Date`: 2026-06-20 ~03:15 CEST
- `Statut`: **PROPOSITION / design read-only** — à appliquer après revue Dom (garde-fou : aucun changement service prod sans Dom).
- `Référence`: post-mortem `docs/POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20.md` (gap G2).
## 1. Problème
Une coupure brutale corrompt `OVMF_VARS.fd` (NVRAM UEFI) → la VM boucle dans TianoCore/Windows Boot Manager. Le 2026-06-20, blocage 02:07→02:18 jusqu'à intervention manuelle de Codex (restore OVMF connu-bon + TPM frais). En clinique sans technicien, ce blocage serait **permanent**.
## 2. Pourquoi le service ne s'auto-répare pas aujourd'hui
`~/.config/systemd/user/win11-arm-lea.service` :
- `Restart=on-failure`**inopérant** : en boucle TianoCore, QEMU **ne sort pas** (process « running » à 99 % CPU). Aucun échec → aucun restart.
- `ExecStartPre` efface `tpm2-00.permall` (TPM frais à chaque boot) **mais pas `OVMF_VARS.fd`** → un OVMF corrompu **survit aux restarts** → boucle permanente.
## 3. Détecteur fiable
Le **guest agent QEMU** (`windows-11-arm-lea-agent.sock`) ne répond **que** si Windows a réellement booté. En boucle firmware, il ne répond jamais. Signature d'échec = *pas de réponse guest-agent après N min* **+** *CPU QEMU élevé*. (Plus robuste qu'une analyse framebuffer ; v1 suffisante.)
## 4. Design proposé (2 briques + garde-fous)
### Brique A — Snapshot « known-good » après boot sain
Watchdog compagnon (lancé en `ExecStartPost=... &` ou service apparié `vm-health-watchdog.service`) :
1. Fenêtre de boot (0→~6 min), poll guest-agent toutes les 30 s (`guest-ping` via socket agent).
2. **Guest-agent répond** → boot sain : copie atomique `OVMF_VARS.fd``OVMF_VARS.fd.known-good`, écrit sentinel `boot-ok`, log horodaté. C'est le point de restauration.
### Brique B — Détection boucle + restauration auto
Si à T+6 min le guest-agent **ne répond toujours pas** ET CPU QEMU > 80 % (signature boucle) :
1. Écrit sentinel `boot-failed`.
2. Archive l'OVMF suspect : `OVMF_VARS.fd``OVMF_VARS.fd.failed-<ts>` (convention déjà utilisée par Codex).
3. Restaure `OVMF_VARS.fd.known-good``OVMF_VARS.fd`.
4. Arrête le QEMU en boucle firmware. **Sûr** : aucun OS n'a booté (guest-agent jamais monté) → pas de risque de corruption Windows (≠ règle « jamais kill un Windows booté », qui ne s'applique pas ici).
5. systemd relance (`Restart=on-failure` se déclenche enfin) avec le bon OVMF.
### Garde-fous (anti-mauvais comportement)
- **Pas de known-good** (1er boot, jamais eu de boot sain) → ne PAS restaurer ; log + alerte, comportement actuel conservé.
- **Compteur d'essais** : max 2 auto-restaurations consécutives (sentinel compteur). Au-delà → stop + alerte (évite la boucle restore→échec→restore si le known-good est lui aussi mauvais).
- **Faux positifs** : Windows peut booter lentement → fenêtre 68 min + double critère (guest-agent ET CPU). Réglable.
- **TPM** : on garde l'effacement `tpm2-00.permall` existant (évite le hang TPM) ; l'auto-réparation OVMF est complémentaire.
- **Idempotence** : nettoyage des sentinels en début de cycle.
## 5. Points d'intégration (à valider Dom avant écriture)
- `win11-arm-lea.service` : ajouter `ExecStartPre` de garde (si `boot-failed` + known-good → restaurer avant lancement) et `ExecStartPost` qui lance le watchdog.
- Nouveau script `vm-health-watchdog.sh` (briques A+B) dans `~/quickemu-win11-arm-lea/`.
- Optionnel : `vm-health-watchdog.service` (PartOf=win11-arm-lea.service) plutôt qu'un `&`, pour un cycle de vie propre.
## 6. Plan de test (sans risque, sur la VM labo)
1. Boot sain → vérifier création `OVMF_VARS.fd.known-good` + sentinel `boot-ok`.
2. Simuler corruption (copier l'`OVMF_VARS.fd.failed-powercut-20260620-021854` archivé sur le live) → vérifier détection à T+6 min, archivage, restauration, restart, boot sain.
3. Vérifier le compteur d'essais (corrompre aussi le known-good) → stop + alerte, pas de boucle infinie.
4. Mesurer le temps total de reprise auto (cible < 10 min sans intervention).
## 7. Décision attendue (Dom)
GO/NO-GO sur l'écriture + le réglage de la fenêtre (6 vs 8 min) et du mécanisme d'alerte (log seul ? message coordination ? mail ?). Application supervisée, un changement / un test, après ton réveil.

View File

@@ -0,0 +1,540 @@
# Guide d'installation Lea - POC, MVP, production et multi-site
- Date: 2026-06-19
- Statut: version initiale exploitable, a durcir avant production
- Scope: installations Lea/RPA Vision V3 sur DGX + postes ou VM Windows
- Source: etat POC DGX, runbooks coordination, installeur Windows, systemd, dashboard/VWB/worker
## Objectif
Ce document sert de base d'installation reproductible pour plusieurs phases:
1. **POC**: installation controlee sur un site pilote, avec assistance technique forte.
2. **MVP**: installation repetable avec artefacts figes, checklist, rollback et preuves.
3. **Production**: installation industrialisee, secrets par site, supervision, sauvegardes, signature de l'installeur, support.
4. **Multi-etablissement**: meme produit, mais variables reseau, comptes, tokens, politiques de securite et donnees separees par etablissement.
Le principe directeur: une installation Lea ne doit pas dependre de la memoire d'un agent ou d'une session de debug. Elle doit etre executable par une checklist, testable, rollbackable et auditable.
## Architecture cible d'une installation
### Composants cote serveur
| Composant | Role | Port POC | Exposition attendue |
|---|---|---:|---|
| Dashboard Flask | supervision, verite produit, fleet, workflow status | 5001 | LAN/VPN autorise avec auth |
| VWB backend | workflows, anchors, base SQLite VWB | 5002 | local ou LAN controle selon packaging |
| Agent Chat | chat Lea, bulles, feedback temps reel | 5004 | accessible depuis poste/VM Windows |
| Streaming/Fleet | ingestion agent Windows, sessions, API agent | 5005 | accessible depuis poste/VM Windows |
| Upload/API core | API interne upload/traitement | 8000 | loopback par defaut |
| Worker | compilation/apprentissage/replay | n/a | service systemd |
| Ollama/VLM | inference locale | 11434 | local DGX, pas expose sauf decision explicite |
| VM/VNC Windows | console Windows DGX si utilisee | 5902 | tunnel SSH/VPN uniquement |
### Composants cote Windows
| Composant | Role | Artefact |
|---|---|---|
| Lea agent | capture, tray, chat, apprentissage, streaming | `deploy/lea_package` ou installeur Inno |
| `config.txt` | URL serveur, token API, machine id, label utilisateur | genere par dashboard ou installeur |
| Installeur Inno | installation MVP/prod, Python embedded, silent install | `deploy/installer/Lea.iss` |
| VM Windows | poste de test ou execution clinique | VMware, Hyper-V, QEMU standalone ou poste physique |
## Niveaux d'installation
| Niveau | Usage | Acceptable | Interdit |
|---|---|---|---|
| POC labo | tests rapides, validation technique | scripts manuels documentes, tunnel VNC, debug coordonne | secrets reutilises sans trace, actions non consignees |
| POC site | installation pilote chez client | artefact fige, runbook, rollback, preuves | activer reseau/site sans fenetre et rollback |
| MVP | repetition sur plusieurs postes | installeur signe ou controle, config par poste, smoke automatique | ZIP manuel non versionne comme seul chemin |
| Production | support multi-etablissement | CI release, signature, supervision, backups, rotation secrets | `DASHBOARD_AUTH_DISABLED`, tokens partages, ports remote ouverts |
## Fiche site obligatoire
Chaque etablissement doit avoir une fiche separee avant installation.
```text
SITE_ID=
Nom etablissement=
Contact technique=
Contact metier=
Fenetre installation=
Fenetre rollback=
DGX_HOSTNAME=
DGX_IP_LAB=
DGX_IP_SITE=
DGX_PREFIX=
DGX_GATEWAY=
DGX_DNS_1=
DGX_DNS_2=
IPv6=off/on
VLAN=non/oui + id
Interface Ethernet cible=
Dashboard URL=
Streaming URL agent=
Agent Chat URL=
VWB URL=
Windows cible=
Type Windows=poste physique / VMware / Hyper-V / QEMU DGX
Nom machine Windows=
Utilisateur Windows=
Mode acces distant=VNC / RDP / console hyperviseur / aucun
RPA_API_TOKEN=
DASHBOARD_USER=
DASHBOARD_PASSWORD=
ENCRYPTION_PASSWORD=
SECRET_KEY=
Politique retention=
Chemin backup=
Responsable validation GO=
```
Regle: aucun token ou mot de passe d'un site ne doit etre reutilise sur un autre site.
## Pre-requis avant installation
### Pre-requis DGX ou serveur Linux
- OS Linux valide pour le deploiement.
- Acces admin local ou fenetre avec administrateur.
- Python 3.10 a 3.12.
- GPU NVIDIA si inference VLM locale attendue.
- Ollama installe si le mode VLM local l'utilise.
- Repo disponible sur la branche cible, par exemple `poc-dgx` pour le POC.
- Ports internes/externes arbitres avant installation.
- Disque libre suffisant pour sessions, captures, backups et modeles.
- Politique backup validee avant toute migration ou reset.
### Pre-requis Windows
- Windows 10/11.
- Droits suffisants pour installer Lea.
- Acces reseau au DGX sur les ports agent requis.
- Pour MVP/prod, privilegier l'installeur Inno avec Python embedded.
- Pour POC, le ZIP `deploy/Lea_v<version>.zip` reste acceptable si documente.
### Pre-requis securite
- `DASHBOARD_AUTH_DISABLED` interdit hors dev local.
- `RPA_AUTH_DISABLED` interdit hors dev local.
- `RPA_API_TOKEN` obligatoire.
- `DASHBOARD_PASSWORD` obligatoire.
- Remote desktop/VNC/RDP ouverts uniquement en tunnel/VPN, jamais en LAN large par defaut.
- Les secrets sont stockes hors git.
- Le poste Windows doit afficher clairement l'etat d'enregistrement/apprentissage.
## Procedure POC actuelle
Cette section decrit l'etat connu du POC DGX. Elle n'est pas encore le chemin production final.
### 1. DGX - recuperer le code
```bash
git clone <repo> rpa_vision_v3
cd rpa_vision_v3
git checkout poc-dgx
```
Si le DGX contient deja des donnees runtime, ne jamais faire de reset destructif sans backup des chemins suivants:
```text
visual_workflow_builder/backend/instance/workflows.db
visual_workflow_builder/backend/data/
data/training/
data/runtime/
data/workflows
graphify-out/ si l'historique graphe doit etre preserve
```
### 2. DGX - environnement Python
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
### 3. DGX - configuration runtime
Creer `.env.local` depuis le modele:
```bash
cp deploy/systemd/rpa_vision_v3.env.example .env.local
```
Valeurs obligatoires a remplacer:
```text
ENCRYPTION_PASSWORD=CHANGE_ME
SECRET_KEY=CHANGE_ME
RPA_API_TOKEN=CHANGE_ME
DASHBOARD_USER=lea
DASHBOARD_PASSWORD=CHANGE_ME
ENVIRONMENT=production
RPA_PROCESSING_WORKER=external
RPA_API_HOST=127.0.0.1
RPA_DASHBOARD_HOST=0.0.0.0
RPA_VLM_MODEL=gemma4:26b
RPA_GROUNDING_ENGINE=qwen3vl_vllm
VLLM_MODEL=Qwen/Qwen3-VL-4B-Instruct
```
Pour un site, documenter explicitement quels services restent en loopback et quels services sont accessibles depuis la VM/poste Windows. En POC DGX, le dashboard est expose au LAN sur `0.0.0.0:5001` avec auth. En clinique durcie, preferer un reverse proxy HTTPS ou un bind loopback derriere proxy/VPN.
### 4. DGX - services
Services systemd attendus ou equivalents:
```text
rpa-agent-chat.service
rpa-firewall.service
rpa-streaming.service
rpa-vision-v3-api.service
rpa-vision-v3-dashboard.service ou fallback rpa-vision-v3-dashboard-user
rpa-vision-v3-worker.service
rpa-vision-v3-stream-worker.service
rpa-vision-v3-vwb-backend.service
rpa-vision-v3-vwb-frontend.service
rpa-vllm-grounder.service si active
rpa-vision.target
```
Commandes de controle:
```bash
systemctl status rpa-agent-chat.service
systemctl status rpa-firewall.service
systemctl status rpa-streaming.service
systemctl status rpa-vision-v3-dashboard.service
systemctl status rpa-vision-v3-worker.service
systemctl status rpa-vision-v3-stream-worker.service
systemctl status rpa-vision-v3-vwb-backend.service
systemctl status rpa-vision-v3-vwb-frontend.service
systemctl list-units 'rpa*'
```
En POC dev, `svc.sh` reste utilisable:
```bash
./svc.sh status
./svc.sh start
./svc.sh restart api
./svc.sh stop
```
### 5. DGX - modele local
Verifier Ollama et les modeles attendus. Le POC DGX distingue:
- cerveau/lecteur Ollama: par exemple `gemma4:26b` selon `.env.local`;
- grounder VLM via vLLM: `Qwen/Qwen3-VL-4B-Instruct` avec `think=false`.
```bash
curl http://localhost:11434/api/tags
ollama list
# Si le modele site manque:
ollama pull <modele_ollama_site>
```
Ne pas exposer `11434` au LAN sans decision explicite.
### 6. DGX - donnees VWB/apprentissage
Verifier:
```bash
test -f visual_workflow_builder/backend/instance/workflows.db
find visual_workflow_builder/backend/data -maxdepth 3 -type f | head
find data/training -maxdepth 3 -type f | head
```
Endpoints utiles:
```bash
curl -s http://127.0.0.1:5002/health
curl -s http://127.0.0.1:5005/health
curl -s http://127.0.0.1:5001/api/system/status
```
Selon l'auth dashboard, `5001` peut repondre `401`; c'est attendu si l'auth est active.
## Installation Windows
### Chemin POC ZIP
Construire le package:
```bash
./deploy/build_package.sh --clean
./deploy/build_package.sh
```
Copier `deploy/Lea_v<version>.zip` sur la machine Windows, puis:
1. Dezipper.
2. Modifier `config.txt`.
3. Lancer `install.bat`.
4. Lancer `Lea.bat`.
`config.txt` minimum:
```text
RPA_SERVER_URL=http://<DGX_IP>:5005/api/v1
RPA_API_TOKEN=<token_site_ou_poste>
RPA_MACHINE_ID=<site-machine-unique>
RPA_USER_LABEL=<nom_utilisateur_ou_poste>
RPA_BLUR_SENSITIVE=false
RPA_LOG_RETENTION_DAYS=180
```
### Chemin MVP/prod installeur
Le chemin cible est l'installeur Inno Setup:
```bash
./deploy/installer/build_installer.sh
```
Sortie attendue:
```text
deploy/releases/Lea-Setup-v1.0.1.exe
```
Installation silencieuse type:
```cmd
Lea-Setup-v1.0.1.exe /VERYSILENT /CONFIG=C:\temp\enroll.txt /DIR="C:\Lea" /LOG="C:\temp\lea-install.log"
```
`enroll.txt`:
```text
USER_NAME=Prenom Nom
USER_EMAIL=prenom.nom@example.local
USER_ID=EMP-00123
SERVER_URL=http://<DGX_IP>:5005/api/v1
API_TOKEN=<token>
```
Production: l'installeur doit etre signe pour eviter les alertes SmartScreen et faciliter le deploiement IT.
## VM Windows DGX - etat POC et regles
Etat confirme le 2026-06-19:
- Windows 11 ARM DGX fonctionne via QEMU standalone.
- Acces utilisateur via VNC tunnel `localhost:5902`.
- Le runtime actif utilise `disk.qcow2` cote utilisateur `aivanov`.
- La definition libvirt `win11-arm-lea` peut apparaitre arretee pendant que le standalone tourne.
Regles:
- Ne pas demarrer la VM libvirt `win11-arm-lea` pendant que QEMU standalone utilise deja `disk.qcow2`.
- Ne pas lancer deux VM sur le meme disque.
- En standalone, `disk.qcow2` doit etre accessible a l'utilisateur qui lance QEMU, actuellement `aivanov`.
- Si retour libvirt, prevoir de remettre l'ownership attendu par libvirt, par exemple `libvirt-qemu:libvirt-qemu`, apres arret complet du standalone.
- Le VNC doit rester en loopback/tunnel, par exemple `127.0.0.1:5902`, pas en exposition LAN large.
Loose ends a traiter avant MVP:
1. Persister proprement le VNC loopback dans le script original ou dans un wrapper.
2. Decider si le VNC a un password pose automatiquement via monitor, ou pas de password car tunnel obligatoire.
3. Choisir un seul runtime officiel pour la VM DGX: standalone documente ou libvirt corrige.
4. Documenter le rollback disk owner standalone <-> libvirt.
5. Decider et implementer l'auto-start au reboot DGX si la VM Windows doit etre disponible apres redemarrage:
- script persiste, pas `/tmp/vmvnc.sh`;
- service systemd `User=aivanov` ou user service avec `loginctl enable-linger`;
- `After=network-online.target`;
- garde-fou contre un demarrage libvirt parallele;
- choix VNC: sans password car loopback+tunnel, ou wrapper qui pose le password via monitor.
## Reseau site
Chaque site doit avoir un plan reseau valide avant installation.
Exemple clinique prepare:
```text
DGX_IP_SITE=192.168.1.178
PREFIX=/24
GATEWAY=192.168.1.243
DNS_1=192.168.1.9
DNS_2=192.168.1.8
IPv6=off
VLAN=non
Interface=Ethernet uniquement
```
Regles:
- Ne pas activer le profil Ethernet site pendant les tests labo sans GO.
- Prevoir un acces local ou une console avant toute bascule reseau.
- Noter le rollback exact avant modification IP.
- Valider depuis Windows: dashboard, chat, streaming.
## Smoke tests d'acceptation
### Serveur
| Test | Attendu |
|---|---|
| `systemctl status rpa-streaming` | actif |
| `systemctl status rpa-vision-v3-dashboard` | actif ou fallback documente |
| `curl :5005/health` | 200/healthy |
| dashboard `:5001` | login/401 ou UI, pas 500 |
| `/api/system/status` | coherent, pas de faux vert |
| `/api/workflows` | workflows VWB visibles |
| worker status | `healthy` ou `idle` non degrade si aucun job |
| ports remote VNC/RDP | fermes au LAN, tunnel only |
### Windows
| Test | Attendu |
|---|---|
| `config.txt` | valeurs site/poste remplacees, aucun `CONFIGURE_ME` |
| lancement Lea | tray visible |
| chat Lea | connexion au DGX |
| capture/apprentissage | demarre avec consentement utilisateur |
| stop apprentissage | session ecrite cote DGX |
| dashboard fleet | machine visible |
| streaming | session recue sur `5005` |
### VWB/apprentissage
| Test | Attendu |
|---|---|
| `workflows.db` | present, backup effectue |
| workflows dashboard | liste chargee |
| anchors | images visibles |
| replay supervise | action ou demande de confirmation, pas de clic non controle |
| worker | session traitee puis retour sain |
## Criteres GO / NO-GO
GO installation site si:
- artefacts versionnes et identifies;
- fiche site complete;
- secrets generes pour le site;
- DGX accessible et services verts;
- Windows connecte au DGX;
- dashboard/VWB/worker coherents;
- ports remote non exposes hors tunnel/VPN;
- rollback documente;
- preuves archivees.
NO-GO si:
- un secret `CHANGE_ME` ou `CONFIGURE_ME` reste en place;
- dashboard auth desactive hors dev;
- `RPA_AUTH_DISABLED=true` hors dev;
- VWB/workflows absents sans decision;
- Windows ne rejoint pas le streaming;
- VM ou poste controle le mauvais DGX;
- deux runtimes VM utilisent le meme disque;
- ports VNC/RDP exposes au LAN sans validation;
- pas de backup `workflows.db` avant reset/deploy.
## Sauvegarde et rollback
Avant chaque installation ou upgrade:
```bash
STAMP=$(date +%Y%m%dT%H%M%S)
mkdir -p .codex_backups/install-$STAMP
cp -a visual_workflow_builder/backend/instance/workflows.db .codex_backups/install-$STAMP/ 2>/dev/null || true
cp -a visual_workflow_builder/backend/data .codex_backups/install-$STAMP/vwb-data 2>/dev/null || true
cp -a data/training .codex_backups/install-$STAMP/training 2>/dev/null || true
cp -a .env.local .codex_backups/install-$STAMP/env.local 2>/dev/null || true
```
Rollback code:
```bash
git fetch origin
git checkout <commit_valide>
systemctl restart rpa-streaming rpa-vision-v3-dashboard rpa-vision-v3-worker rpa-agent-chat
```
Rollback donnees: restaurer uniquement les chemins sauvegardes, jamais faire `git clean -xfd` sur le DGX POC avec donnees runtime non trackees.
## Journal de preuve d'installation
Pour chaque installation, creer un dossier de preuve:
```text
installations/<SITE_ID>/<YYYY-MM-DD>/
site-sheet.md
versions.txt
services.txt
ports.txt
dashboard-smoke.txt
windows-agent-smoke.txt
vwb-smoke.txt
backups.txt
incidents.md
verdict.md
```
`versions.txt` doit contenir:
```bash
git rev-parse HEAD
git status -sb
python --version
pip freeze | sort
systemctl --version
ollama list
```
## Industrialisation requise avant production
| Sujet | Etat POC | Attendu MVP/prod |
|---|---|---|
| Installeur Windows | Inno disponible | release signee, tests install/desinstall |
| Enrollment | config/token manuel ou dashboard | token par poste, expiration/revocation |
| Secrets | `.env.local` manuel | coffre ou procedure secrete auditee |
| Services | systemd partiel selon DGX | target unique, healthcheck, recover |
| Supervision | dashboard + logs | alerting, retention, export incident |
| Backups | manuels | job planifie, test de restauration |
| VM Windows DGX | standalone VNC manuel fonctionnel | runtime officiel choisi, persiste et auto-start arbitre |
| Multi-site | variables en coordination | fiche site versionnee, aucun secret partage |
| Documentation utilisateur | `LISEZMOI.txt` | guide utilisateur + admin par site |
| Support | agents war-room | procedure support N1/N2/N3 |
## Check-list courte jour d'installation
1. Valider fiche site et fenetre rollback.
2. Verifier artefact serveur et installeur Windows.
3. Generer secrets site/poste.
4. Sauvegarder donnees DGX existantes.
5. Installer ou mettre a jour serveur.
6. Configurer reseau sans perdre l'acces de rollback.
7. Demarrer services.
8. Installer Lea sur Windows.
9. Lancer smoke serveur.
10. Lancer smoke Windows.
11. Tester dashboard, VWB, chat, streaming, worker.
12. Archiver preuves.
13. Donner verdict GO/NO-GO.
14. Si GO, remettre les acces temporaires en mode securise.
## Documents sources
- `README.md`
- `deploy/systemd/rpa_vision_v3.env.example`
- `deploy/lea_package/LISEZMOI.txt`
- `deploy/lea_package/config.txt`
- `deploy/build_package.sh`
- `deploy/installer/README.md`
- `deploy/installer/Lea.iss`
- `docs/coordination/RUNBOOK-DGX-POST-REBOOT-CHECK.md`
- `docs/coordination/RUNBOOK-LEA-LIVE-DRAFT.md`
- `docs/coordination/active/2026-06-19_1418_postaction-windows-dgx-fonctionne.md`
- `docs/coordination/inbox_codex/2026-06-19_1420_claude-to-codex_ACK-STOP-DIAG-VM-LOOSE-ENDS.md`

View File

@@ -0,0 +1,34 @@
# Mémo Jour J — Livraison définitive du DGX à la clinique (2026-06-23)
> Contexte : le DGX part **définitivement** à la clinique. Après, travail **100% à distance** depuis le labo. Tout doit marcher avant de débrancher.
## ✅ Validé la veille (2026-06-22) — rien à refaire
- **Installateur Léa 1-clic autoportant** (python-embed, **sans Python système**, raccourci Bureau + démarrage auto) — testé sur VM Win11, enrôlement confirmé (Qwen).
- **Apprentissage RPA** : OK.
- **Reboot complet du DGX** : 11 services + grounder (modèle ~60 s) + VM reviennent **seuls**.
- **Auto-réparation OVMF de la VM** (watchdog user `vm-ovmf-watchdog.service`) **LIVE** : après une coupure secteur, la VM se **répare et reboote seule** (~3-5 min). Testé (détection boucle CPU 99% → restauration known-good → relance).
- **IP DGX clinique `192.168.1.178`** : réservation DHCP côté clinique pour la MAC Ethernet `10:b6:76:f0:2f:f4`. Tous les services `enabled` (auto-start au boot).
- **Dashboard** bascule faite : `RPA_PUBLIC_URL=http://192.168.1.178:5005` → les Léa générées pointent direct `.178`.
## 🏥 Sur site (jour J)
1. Brancher le DGX sur l'**Ethernet clinique** + allumer. **Attendre ~3-5 min** (boot + grounder + VM).
2. Vérifier `.178` : depuis un poste clinique, ouvrir **http://192.168.1.178:5001** (login `lea`). Si le dashboard répond → DGX up + bonne IP.
3. Grounder vision : prêt ~3 min après le boot — **ne pas tester la vision de Léa avant**.
4. **Installer Léa sur les postes TIM** : sur chaque poste → `http://192.168.1.178:5001` → Fleet → enrôler → télécharger le ZIP → **clic droit Extraire** → double-clic **`Installer-Lea.bat`** → vérifier : pas d'erreur Python, Léa dans le systray, apparaît dans la fleet. **Tester l'apprentissage sur 1 poste avant de généraliser.**
5. 🔴 **AVANT DE PARTIR (point de non-retour)** : depuis ton laptop via le **VPN Stormshield**, ouvrir **http://192.168.1.178:5001**. Si le dashboard s'affiche → **ton accès distant est bon, tu peux partir**.
## 🏠 Après le départ — travail à distance (labo)
- Accès DGX : **VPN Stormshield → `http://192.168.1.178:5001`** (dashboard) + `ssh aivanov@192.168.1.178` (cert).
- ⚠️ Déploiement de code/correctifs : **scp/rsync par-dessus le VPN** — PAS `git pull` (la Gitea maison n'est pas exposée sur internet).
- Coupure secteur clinique → la VM s'auto-répare (watchdog OVMF). Accès VM : RDP (presse-papier) ou VNC.
## Identifiants
- Voir ta note `DGX SSH aivanov Dom31.txt` (Bureau VM / clé USB) et `~/ai/rpa_vision_v3/.env.local` sur le DGX. (Non recopiés ici : ce fichier est dans le dépôt.)
- VM Windows : compte `aivanov`.
## Reste / dette (post-installation — « on a encore du boulot »)
- **Compte VM `aivanov`** : définir un **mot de passe** (pas juste un PIN) pour que le RDP + presse-papier marchent.
- Code dev↔DGX : aligné (diff fait — seuls des tests + `config.py`/`index.html` mineurs diffèrent, non bloquant).
- Mot de passe VNC perdu à chaque reboot VM (cosmétique — RDP = voie normale). Persistance VNC à câbler si besoin.
- `hostname` Léa remonte `N/A` (python-embed) — cosmétique.
- Profil **Stormshield laptop** (2ᵉ accès) à confirmer avec PORQUET (atteint `.178` ?).

View File

@@ -0,0 +1,40 @@
# Note DSI — Déploiement / mise à jour de Léa par GPO (AD clinique)
- Date : 2026-06-28
- Pour : Nicolas PORQUET (DSI Hôpital privé Wallerstein)
- De : équipe Léa (Dom)
- Objet : utiliser l'AD/GPO de la clinique comme **canal de déploiement** des mises à jour de l'agent Léa sur les postes pilotes, en remplacement d'un updater applicatif maison.
## 1. Contexte technique de Léa (côté poste)
- Léa s'installe **par utilisateur** (`%LOCALAPPDATA%\Lea\`), **sans droits administrateur ni UAC**, sans Python système (Python embarqué).
- Le programme d'installation est un **EXE (Inno Setup)** supportant le mode **silencieux** (`/SILENT` / `/VERYSILENT`). **Pas de package MSI** à ce jour.
- L'app tourne en contexte utilisateur (`pythonw.exe`), démarrage par raccourci/observation.
## 2. Besoin
Pousser une **mise à jour** (la prochaine, et idéalement les suivantes) sur **un sous-ensemble de postes** (les postes pilotes Léa), **sans intervention manuelle poste par poste**.
## 3. Options GPO envisagées (à valider avec vous)
| Option | Mécanisme | Adapté ? |
|---|---|---|
| **Logon script (utilisateur)** | script au login qui lance l'installeur en silencieux ou copie les fichiers à jour dans `%LOCALAPPDATA%\Lea\` | ✅ **fit naturel** (per-user, sans admin) |
| **GPP — Files / Scheduled Task** | déploiement de fichiers ou tâche planifiée de mise à jour | ✅ alternative |
| GPO **Software Installation** | déploiement **MSI** assigné machine/utilisateur | ❌ nécessite un **MSI** (non disponible) |
- **Idempotence** : le script vérifiera un **marqueur de version** pour ne PAS réinstaller à chaque ouverture de session.
- **Payload** : les fichiers de mise à jour seront déposés sur un **partage atteignable** par les postes (SYSVOL ou share réseau dédié) — à définir avec vous.
- **Rollback** : en cas de version défaillante, repush de la version précédente par la même voie.
## 4. Questions / ce que nous vous demandons
1. **Topologie exacte des postes pilotes** : PC physiques joints à l'AD ? hôtes **RDP** ? applications **Citrix** publiées ? (cela décide GPO **machine** vs **utilisateur**).
2. Pouvez-vous créer une **OU dédiée** ou un **groupe de sécurité** ciblant les postes/utilisateurs Léa ?
3. Un **partage réseau** (ou SYSVOL) pour héberger le payload de mise à jour ?
4. Vos **contraintes de sécurité** : les **logon/startup scripts** sont-ils autorisés par votre politique ? Y a-t-il **AppLocker / SRP / WDAC** ou une exigence de **signature de code** sur les exécutables ? (le cas échéant on fournit l'empreinte SHA256 / on discute signature).
5. Validez-vous le principe **rollback = repush version précédente** ?
## 5. Ce que nous fournissons
- L'**installeur silencieux** (EXE) + son empreinte SHA256.
- Un **script de logon** type (lancement silencieux + contrôle de version/marqueur).
- La **liste des postes** pilotes concernés.
## 6. Bénéfice
Canal de déploiement **standard, piloté par la DSI, traçable**, sans updater applicatif maison (donc sans risque de « briquer » les postes par un mécanisme de mise à jour custom). Compatible mises à jour ultérieures.

View File

@@ -0,0 +1,138 @@
# PLAN — Accès distant DGX consolidé par SSH-certificat (labo + déplacement)
- `Auteur`: Claude (infra), co-construit avec Codex (réseau/box) + Qwen (client Windows)
- `Date`: 2026-06-20
- `Statut`: **PLAN / proposition** — décisions + application supervisées par Dom (rien de modifiant sans GO).
- `Réf`: [[project_remote_access_consolidation_20260620]], `reference_dgx_static_ip`.
## 1. Objectif
Accès distant **robuste et unifié** au DGX, authentifié par **certificat SSH (CA OpenSSH)**, dans 2 contextes — **labo** (LAN/WiFi) et **déplacement depuis le PC Windows de Dom** (internet) — vers 3 cibles :
1. **Terminal DGX** (SSH) ;
2. **VM Windows** (VNC `5902` over SSH) ;
3. **Bureau Ubuntu du DGX** (GNOME, via VNC — « éventuel »).
Contrainte forte : **on-premise / no-cloud** (pas de Tailscale-cloud ni Cloudflare).
## 2. État actuel (relevé read-only)
| Élément | Constat | Conséquence |
|---|---|---|
| SSH DGX | Aucune CA (`trustedusercakeys none`) ; auth par clé OK | terrain vierge propre pour une CA |
| sshd | ⚠️ `PasswordAuthentication yes` (la checklist DSI annonce "no") | à corriger dans le durcissement |
| VPN/overlay | Aucun installé | à mettre en place |
| Bureau Ubuntu | Pas de VNC `5900` actif (DGX headless) | cible 3 à créer |
| IPv4 box | **`82.64.97.95` routable** (Free, pas CGNAT) | **port-forward UDP possible → WireGuard direct viable** |
| Ingress | Box forwarde déjà 80/443 → NPM ; `lea.labs…``82.64.97.95` | DDNS de fait + infra réutilisable |
| IPv6 | DGX a une IPv6 globale `2a01:e0a:28:ad60:…` | option SSH IPv6 direct (secondaire) |
## 3. Architecture cible — 2 couches découplées
**Couche AUTH (identique partout) = CA OpenSSH.** On ne gère plus des `authorized_keys` poste par poste : une CA signe les clés. Un seul point de confiance, révocation centralisée, plus d'avertissement host-key.
**Couche TRANSPORT (selon contexte) :**
- Labo : SSH direct `192.168.1.45` (LAN).
- Déplacement : **WireGuard self-hosted** → une fois le tunnel monté, on est « sur le LAN » et **tout passe par SSH/tunnels** (terminal + VNC VM + VNC bureau) — *exactement le même geste qu'au labo*.
Bénéfice : **un seul mécanisme d'accès (SSH + tunnels) pour les 3 cibles**, que l'on soit au labo ou en déplacement. Le transport ne change que la « route ».
## 4. Approches transport comparées
| | A. WireGuard self-hosted ✅ reco | B. Mesh self-hosted (Netbird/Headscale) | C. SSH-over-443 (NPM/wstunnel) |
|---|---|---|---|
| NAT/CGNAT | OK (IPv4 routable + fwd UDP dispo) | OK même CGNAT (relais) | OK (443 quasi toujours ouvert) |
| No-cloud | ✅ total | ✅ si self-hosted (serveur ctrl à tenir) | ✅ (réutilise NPM) |
| Complexité | Faible/moyenne | Plus élevée (mgmt+signal+relay) | Moyenne (wrap TLS) |
| Réseaux hôtels restrictifs (UDP bloqué) | ⚠️ → fallback nécessaire | OK | ✅ idéal |
| Pertinence ici | **Forte** (on a IPv4 routable) | Surdimensionné maintenant | **Bon comme fallback** |
**Recommandation : A (WireGuard) en primaire + C (WireGuard-over-wstunnel sur 443) en fallback** pour les réseaux qui bloquent l'UDP (hôtels, certains 4G). B (mesh) seulement si une clinique se révèle CGNAT/durcie.
## 5. Couche AUTH — détail CA OpenSSH
Bonnes pratiques (Red Hat / Teleport / OpenSSH cookbook) : **2 CA séparées** (privilege separation), clés CA **hors DGX**.
1. **Générer les CA** (sur le poste Dom, stockage sûr, JAMAIS sur le DGX) :
- `ssh-keygen -t ed25519 -f user_ca -C "rpa-user-ca"`
- `ssh-keygen -t ed25519 -f host_ca -C "rpa-host-ca"`
2. **Certifier l'hôte DGX** (supprime l'avertissement host-key, survit aux rotations) :
- `ssh-keygen -s host_ca -I dgx-zgx2ff4 -h -n 192.168.1.45,dgx.lab,<ipv6> -V +52w /etc/ssh/ssh_host_ed25519_key.pub`
- sshd : `HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub`
- client `known_hosts` : `@cert-authority 192.168.1.45,*.lab <contenu host_ca.pub>`
3. **Certifier l'utilisateur Dom** (laptop + poste labo) :
- `ssh-keygen -s user_ca -I dom-laptop -n aivanov -V +52w id_ed25519.pub` (principal = compte cible)
- sshd : `TrustedUserCAKeys /etc/ssh/user_ca.pub` (+ option `AuthorizedPrincipalsFile`)
4. **Durcir sshd** : `PasswordAuthentication no`, `KbdInteractiveAuthentication no`, `PermitRootLogin prohibit-password`.
5. **Révocation** : KRL (`ssh-keygen -k`) + `RevokedKeys` dans sshd — révoquer un poste perdu sans toucher les autres.
Validité : **certs user 1 an** (simple, + KRL) ou **courts** (plus sûr, nécessite un re-signing régulier). Reco équipe réduite : **1 an + KRL**.
## 6. Couche TRANSPORT — détail
**WireGuard (primaire)** :
- Serveur `wg0` **sur le DGX** (miroir du futur déploiement clinique) : ex. `10.10.0.1/24`, `ListenPort 51820`.
- Box : **port-forward UDP 51820 → DGX**. Nom dédié `vpn.<…>.laurinebazin.design``82.64.97.95` (DDNS).
- Laptop = peer (`10.10.0.2/24`), `Endpoint vpn.…:51820`, `AllowedIPs` couvrant `10.10.0.0/24` + `192.168.1.45/32` (accès DGX + ses services).
- Connecté → `ssh dom@10.10.0.1` (cert) ; `-L 5902` VM ; `-L 5900` bureau.
**Fallback 443 (réseaux UDP-bloqués)** : `wstunnel` encapsule WireGuard dans du WebSocket/`wss` sur **443** via le NPM (`wstunnel … wss://vpn.…:443`). Tout sort en 443 → passe les proxys/hôtels. Réf : Hetzner/eduVPN.
**IPv6 direct (secondaire/dépannage)** : quand le réseau client a l'IPv6, `ssh` direct vers l'IPv6 globale du DGX (épingler `mngtmpaddr`, firewall allow, cert). Pratique mais non garanti (réseaux IPv4-only).
## 7. Accès aux 3 cibles (même geste partout)
| Cible | Mécanisme | Commande type |
|---|---|---|
| Terminal DGX | SSH cert | `ssh dom@<dgx>` |
| VM Windows | VNC over SSH | `ssh -N -L 5902:127.0.0.1:5902 dom@<dgx>` → VNC `localhost:5902` |
| Bureau Ubuntu DGX | VNC over SSH | **DGX = X11/Xorg (confirmé Qwen)**`x11vnc -localhost -rfbauth ~/.vnc/passwd` sur la session `:1` (ou TigerVNC), **bind loopback + password**`ssh -N -L 5900:127.0.0.1:5900 …` → VNC `localhost:5900`. ⚠️ l'ancien x11vnc était en `nohup` non-persisté (mort à la panne) et sans password — à recréer en **service systemd user sécurisé** (P0). |
`<dgx>` = `192.168.1.45` au labo, `10.10.0.1` (wg) en déplacement. **La seule variable est l'hôte.**
**Presse-papier / copier-coller (demande Dom)** :
- Bureau Ubuntu (x11vnc/TigerVNC) : **texte natif** ✅, rien à faire.
- VM Windows (VNC QEMU) : **à activer**. QEMU 8.2.2 supporte le clipboard vdagent et le bus `virtio-serial` est déjà présent → ajouter `-chardev qemu-vdagent,id=vdagent,clipboard=on` + `-device virtserialport,chardev=vdagent,name=com.redhat.spice.0` + installer **SPICE guest tools (vdagent)** dans Windows = **copier-coller texte bidirectionnel** (garde le viewer VNC). Le tunnel SSH ne gêne pas (in-band).
- Transfert **fichiers** (option) : SPICE (client virt-viewer) ou RDP natif (selon édition Windows — Home ARM = pas de serveur RDP).
- ⚠️ Sous-chantier séparé : modif `vm_launch.sh` + install guest + **redémarrage VM gracieux** (jamais kill) → GO Dom.
## 8. Décisions attendues (Dom)
1. **Endpoint WireGuard** : sur le **DGX** (reco, mirroir clinique) ou sur le **poste Dom** (gateway always-on) ?
2. **Fallback 443 (wstunnel)** : oui/non (utile si tu te connectes depuis hôtels/4G restrictifs) ?
3. **Validité cert user** : **1 an + KRL** (reco) ou courts ?
4. **Bureau Ubuntu DGX** : on le met en place maintenant ou « éventuel » plus tard ? (+ Wayland vs X11 — input Qwen).
5. **Garde des clés CA** : sur le poste Dom (reco) — confirmer le lieu de stockage.
**Convergence équipe (Qwen + Claude)** sur les 5 options recommandées : WG sur DGX ✅, fallback 443 ✅, certs 1 an + KRL ✅, bureau Ubuntu maintenant (X11, P0 sécurité) ✅, clés CA hors DGX ✅. Codex (box/transport) à compléter.
**À confirmer par Dom (poste Windows — Qwen n'y a pas accès)** :
- Version OpenSSH du laptop (`ssh -V`) — natif Win10 1803+/11, support cert OK normalement.
- Client VNC Windows : **RealVNC Viewer** ou **TigerVNC viewer** (reco ; Remmina = Linux only).
- A-t-il déjà un accès SSH depuis Windows vers le DGX (aujourd'hui : non testé) ?
**Note credentials** : seul `RPA_API_TOKEN` (config.txt Léa = `.env.local` DGX) est partagé entre Léa et le DGX ; le plan SSH-cert n'y touche pas → pas de conflit, mais si un renouvellement de token est fait par ailleurs, le répercuter aux 2 endroits.
## 9. Mise en œuvre — chirurgie itérative supervisée (un pas = un test)
| Phase | Contenu | Owner | Réversible |
|---|---|---|---|
| 0 | Backups (`sshd_config`, known state), inventaire exposition | Claude/Codex | — |
| 1 | CA générées (off-DGX) + host cert + `@cert-authority` client → **test : plus d'avertissement host-key** | Claude | oui |
| 2 | User cert + `TrustedUserCAKeys`**test : login par cert** *en gardant clé/MDP en secours* | Claude | oui |
| 3 | Durcissement (`PasswordAuthentication no`, KRL) → **test** ; retrait ancienne clé | Claude (GO Dom) | oui (backup) |
| 4 | WireGuard DGX + fwd UDP box + peer laptop → **test reach déplacement (terminal+VM+bureau)** | Codex+Claude | oui |
| 5 (opt) | Fallback wstunnel/443 | Codex | oui |
| 6 (opt) | Bureau Ubuntu (TigerVNC/grd headless) | Qwen | oui |
| 7 | Doc + **portage clinique** (Ethernet `.178`, même CA, même WG) | tous | — |
## 10. Sécurité & garde-fous
- Clé privée **CA hors DGX** (compromission DGX ≠ compromission CA).
- `PasswordAuthentication no` corrige l'écart actuel (DSI annonçait "no").
- WG = port unique UDP exposé (vs SSH brut exposé). SSH reste **non exposé en direct sur internet** (accès via WG), sauf option IPv6/fallback maîtrisée.
- Révocation par KRL (poste volé) sans réémettre les autres certs.
- Rollback : `sshd_config.bak` + redémarrage sshd ; WG = `wg-quick down`.
## 11. Portage clinique
Même CA + même WireGuard rejouables sur le DGX clinique (Ethernet statique `.178`). L'accès distant de Dom (support/maintenance à distance) devient un livrable DSI propre (un port UDP, auth cert, révocable) — argument sécurité pour Nicolas PORQUET.

View File

@@ -0,0 +1,205 @@
# Plan d'action — la suite (post-livraison DGX clinique)
- `Date`: 2026-06-23
- `Auteur`: Claude (mandat Dom)
- `Statut`: vivant — à mettre à jour au fil des validations
- `Portée`: **chapeaute** les plans existants (ne les remplace pas), dédoublonne et priorise les actions **encore ouvertes** après la livraison clinique du jour.
> Contexte : le DGX part **définitivement** en clinique aujourd'hui (`192.168.1.178`), puis travail **100 % à distance** (VPN Stormshield + scp). Le jour J lui-même est couvert par `MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23.md` — non répété ici.
---
## 0. Deux clarifications de cadrage (lire avant le reste)
1. **Les gaps « pré-clinique » sont clos.** L'audit (`AUDIT_GAPS_APPLI_100PCT_2026-06-10`) et le postmortem (`POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20`) listaient des gaps durs (OVMF G2, IP statique G1, reconnexion Léa G4, reboot 11 services). Le `MEMO_JOUR_J` + `TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21` confirment qu'ils sont **résolus** (watchdog OVMF LIVE testé, `.178` réservé DHCP, installateur autoportant). → **sortis de ce plan.**
2. **Accès distant = multi-VPN par site (correction Dom 23/06).** Pas « WireGuard caduc » : **WireGuard = notre VPN** (labo/éditeur, reste valide) ; **Stormshield = le VPN de la clinique** (côté client) ; d'autres clients auront **d'autres VPN**. ⇒ l'accès distant devient une **fonctionnalité paramétrée par fiche site** (`INSTALLATION_MULTI_SITE`), avec **SSH cert-only + RDP comme transport commun** au-dessus du VPN propre à chaque site. Le `PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20` (WireGuard) n'est pas suspendu — il devient *un* profil d'accès parmi d'autres.
---
## Tableau de bord — sous-projets (labo d'abord, WIP ≤ 3 actifs)
> Chaque feature F# = **un sous-projet** (objectif, branche, prérequis, statut, done). Backlog priorisé ; **on n'ouvre que 2-3 sous-projets actifs à la fois** (réactivité = focus). **Merge prod = supervisé Dom.** Codex orchestre par sous-projet à son retour (28/06).
**Prérequis socle (avant parallélisme)** : merger `fix/dashboard-complete-installer` (HEAD `d686c3ac2`) sur `poc-dgx` (`1d6efdb1b`) → base git propre ; ménage code mort (Qwen) cadré.
| SP | Feature | Statut | Prérequis bloquant |
|----|---------|--------|--------------------|
| **SP-0** | Socle git (merge branche + base propre) | ✅ **fait** (23/06 — FF `1d6efdb1b``d686c3ac2`, local) | — *(parallélisme débloqué)* |
| **SP-1** | **F14/U-B Anchors** (compound) | ✅ **code FAIT, commit `2cabc6cb7` (br. `sp1/anchors-compound`), validé données réelles + persistance** | ré-import **en place** entravé par U-A (import = doublon) → SP-4 |
| SP-2 | F2 Rejeu intelligent (R1→R7) | 🟢 **débloqué** (Q-F2-1 ✅ ; point d'entrée tracé = agent_chat 5004 / SemanticMatcher) | gros chantier R1→R7, à cadrer |
| SP-3 | F8 Exécution native (durcir + sandbox) | 🔴 bloqué | Q-F8-1..4 + vérif sécurité `/execute/instruction` |
| SP-4 | F1/U-A Consolidation fragmentation (T3) | 🟢 **débloqué (Q-F1-1 ✅ signature trajectoire, source=DB Q-F9-1 ✅)** | — (active quand on veut) |
| SP-5 | F6/U-C Mutualisation/fédération | 🟠 décidé, à coder | dépend SP-1 (anchors) |
| SP-Q | F13 Ménage code mort | 🔵 en cours (Qwen, read-only) | — |
| — | F3 F4 F5 F7 F9 F10 F11 F12 | ⚪ backlog priorisé | — |
**Cadrage 3 sous-projets (23/06, 3 agents read-only) — interfaces communes & séquencement** :
- **3 interfaces partagées à poser UNE fois** : (1) **signature de trajectoire** (SP-4 = propriétaire) ; (2) **index embeddings/FAISS partagé** (`core/embeddings/shared_index.py`, consommé par SP-2-R3 **et** la sélection skill↔tâche) ; (3) **guard `machine_id` centralisé** dans `stream_processor.py` (SP-4 ∩ SP-2-R6, ~L3197/3284).
- **Phase 0** (fondation, série, petit) : signature de trajectoire + accès index partagé.
- **Phase 1** (parallèle, WIP=2, faible collision) : **SP-4** (propriétaire `stream_processor.py`) ∥ **compétences** (propriétaire `core/competences/`).
- **Phase 2** : **SP-2 rejeu** (le plus intriqué : touche `stream_processor` *et* FAISS) — après stabilisation Phase 0 + SP-4.
- ⚠️ **Endpoints compétences `verdict`/`promote` EXISTENT déjà** (`api/lea_competences.py`, blueprint `app.py:277`, test `tests/unit/test_lea_competence_verdict_api.py`) → chantier compétences = **auto-déclenchement (hook) + sélection intelligente** seulement.
- 1 **branche par sous-projet**, merge supervisé Dom.
**QG Qwen (23/06, 18:30) — GO avec 4 ajustements** (intégrés) :
1. **Marqueurs de propriété dans `api_stream.py`** (commentaires par range d'endpoints SP-4 vs compétences) — seul point de contact Phase 1, éviter conflits silencieux. Pas de refactor.
2. **Fallback R2/R3 obligatoire** (SP-2) : chaque nouveau chemin de résolution **retombe sur les coords figées** si la cascade intelligente échoue. Le rejeu *enrichit*, ne remplace pas. **Non-négociable démo.**
3. **`machine_id` guard intouché en Phase 1** : le lifting du silo (`stream_processor` ~L3197/3284) est **entièrement Phase 2 / SP-2-R6**. → lève la collision SP-4 ↔ SP-2.
4. **Dead import `ExternalDecisionClient`** (`api_stream.py:L7285`, module absent, inoffensif via try/except) → à nettoyer dans le **ménage code mort** (catégorie C), pas dans SP-4.
---
## Axe central — Rejeu intelligent des actions apprises 🎯
**C'est le cœur produit** (« Léa apprend, comprend, **rejoue en exploitant ce qu'elle a appris** » — pas du record-and-replay). Vérification **runtime du 2026-06-23** (à reconfirmer `fichier:ligne` avant toute modif — méthode projet) :
### État réel de la chaîne apprentissage → rejeu
| Maillon | État | Preuve (à reconfirmer) | Constat |
|---|---|---|---|
| Import auto Shadow → workflow rejouable | ❌ **débranché** | `finalize` `api_stream` ~2430-2466 : enqueue worker VLM, **pas** de conversion/import auto ; `ShadowLearningHook` jamais appelé | Ce qui est appris n'est **pas** rendu rejouable sans geste manuel |
| Rejeu consulte le fonds appris (TargetMemoryStore) | ❌ **débranché** | `build_replay_from_raw_events` (~1841-2200) ne consulte rien ; `TargetResolver.lookup()` (~3263) jamais appelé par `replay-session` | Le rejeu rejoue des **coords/anchors figés**, pas la cible apprise |
| Lecture FAISS / GlobalFAISSIndex au rejeu | ❌ **write-only** | `workflow_replay.py` accepte `faiss_manager` en param mais ne l'utilise pas ; aucun `.search()` au rejeu | Index **écrit, jamais lu** au rejeu (cohérent fédération dormante) |
| verify post-condition (état UI après action) | ⚠️ **absent** | `safety_checks` + pause supervisée OK si mode supervisé (`api_stream` ~4299-4367) ; **pas** de `verify_screen` post-action | Pas de boucle de feedback succès/échec |
| Templating `{{var.field.sub}}` au rejeu | ✅ **marche** | `_resolve_runtime_vars()` `replay_engine` ~2027-2041, appelé ~4293 | Données récupérées (T2A, extract) **réinjectées** en temps réel — **acquis, ne pas refaire** |
| Filtre `machine_id` (cross-session) | ✅ actif = **silo** | `stream_processor` ~3197-3200 et ~3285 | Apprentissage **siloté par poste** ; rejeu direct non filtré |
**Verdict** : le rejeu est aujourd'hui **« brut » (events → coords), pas « intelligent »**. L'apprentissage tourne (ShadowLearning, TargetMemory, FAISS) mais **en silos jamais wirés au rejeu**. Le templating des données récupérées, lui, fonctionne déjà.
### Chaîne cible (ce vers quoi on va)
```
Capture/Shadow → finalize → [R1] import auto en workflow rejouable
→ au rejeu, chaque action résolue par : cascade UI (OCR/template/YOLO/VLM)
+ [R2] fonds appris (TargetMemoryStore) + [R3] FAISS anchors (similarité)
→ [✅] réinjection des données récupérées (templating)
→ [R4] verify post-condition (échec = pause supervisée, pas stop = apprentissage)
→ [R5] le résultat du rejeu réécrit le fonds (boucle)
→ [R6] mutualisation : lever silo machine_id + brancher fédération
```
### Chantiers rejeu (ordonnés, du plus structurant au plus fin)
| ID | Chantier | Dépend de |
|---|---|---|
| **R1** | Brancher l'**import auto** d'une session apprise en workflow rejouable post-finalize | décision « provider Léa runtime » |
| **R2** | Faire **consulter le fonds appris au rejeu** : câbler `TargetResolver.lookup()` / `TargetMemoryStore` dans le chemin `replay-session` (résoudre par cible apprise, pas coords figées) | R1 |
| **R3** | **Lire FAISS au rejeu** : utiliser le `faiss_manager` déjà passé à `workflow_replay.py` comme fallback de résolution par similarité d'anchor | R2 |
| **R4** | **verify post-condition** : vérif état UI après action ; échec → pause supervisée (cf. *failure-is-learning*) | — |
| **R5** | **Boucle d'apprentissage** : succès/échec/correction humaine du rejeu réécrivent le fonds (TargetMemory + FAISS) | R2, R4 |
| **R6** | **Mutualisation** : lever le filtre `machine_id` + brancher la fédération (`GlobalFAISSIndex.search()` jamais appelé) | décisions produit (silo vs fédéré, PII) |
| **R7** | Bugs rejeu résiduels : reprise sur crash (R4 audit), OCR span/centre-de-ligne (R5 audit) | — |
⚠️ **Tout cet axe touche le chemin runtime de la démo.** Méthode imposée : **chirurgie itérative supervisée** — un maillon = un test ≤ 2 min = GO Dom, jamais de batch, démo `Urgence_aiva_demo` intacte à chaque étape. Reconfirmer le wiring au runtime **avant** chaque modif (imports lazy).
---
## Feature — Unification Léa ↔ VWB (anchors) 🔗
**Manque corrigé (signalé Dom 23/06).** Chantier dédié : `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17.md`. Cœur = **les anchors**, le pont entre ce que VWB capture au recording et ce que Léa **récupère au rejeu**. Symptôme T3 « Léa ne trouve pas le bloc-notes » = **fragmentation de l'apprentissage**, pas silo.
Cycle de vie cible de l'anchor : capture (VWB recording / Shadow) → persistance (`visual_anchors`) → **propagation au workflow****récupération au rejeu** (résolution visuelle).
| ID | Sous-chantier | État | Risque |
|---|---|---|---|
| **U-B** | **Anchors — propager `anchor_image_base64` aux substeps *compound*** (`b8b963059` n'a fait que les actions simples ; les compound, majoritaires côté Léa, restent `anchor_id=NULL` → « Ancre requise » sans image) **+ ré-importer** les workflows. `learned_workflow_bridge.py` `_convert_compound_substep` ~L279 | **prêt — fix ciblé bloquant** | faible/additif |
| **U-A** | **Consolidation fragmentation** : `workflow_id` = signature stable de trajectoire + create-or-update (fusion + agrégation d'observations) — débloque T3 | design décidé | moyen (touche build/persist démo) |
| **U-C** | **Fonds commun** = **F6** (décidé cross + intra) : lever filtre `machine_id` (`stream_processor` ~L3197/L3284) + brancher fédération anonymisée (`GlobalFAISSIndex.search()` jamais appelé) | décision prise, à coder | moyen-élevé |
| **U-D** | **Asymétrie grounding** : VWB recording = UI-DETR-1 ; replay Léa = cascade OCR/template/VLM → unifier le chemin de résolution | sujet ouvert post-démo | à trancher |
**Lien fort avec F2 (rejeu intelligent)** : la récupération des anchors au rejeu (U-B) est le **substrat de R2/R3** — sans anchors propagés/retrouvés, le rejeu ne peut pas résoudre par la vision et retombe sur des coords figées.
**⚠️ Implication de la décision F6 = cross-clinique** : un pack fédéré anonymisé **n'emporte ni coordonnées ni templates** (PII) → seule la **re-résolution visuelle par anchors/FAISS** permet de le rejouer ailleurs. Donc **F6 cross-clinique entraîne quasi-mécaniquement le principe « rejeu intelligent » (Q-F2-1)** : il devient un prérequis, pas une option. *(Décision induite Q-F14-1 au registre.)*
**Ordre interne** (du chantier) : confirmer provider Léa (Q-F2-2) → **U-B anchors** (gain visuel immédiat, faible risque) → U-A consolidation → U-C fédération.
**Prép SP-1 / U-B (vérif runtime 23/06, read-only)** :
- **Gap confirmé** : `learned_workflow_bridge.py:_convert_compound_substep` (L279-321) ne pose **jamais** `_anchor_image_base64` ; la branche action simple (L226-233) le fait. → substeps compound = `anchor_id NULL`.
- **Source dispo** : dans le JSON core, l'ancre du compound est à `target.context_hints.anchor_image_base64` (pas `target.anchor_image_base64`). `target` (parent) est **déjà passé** à `_convert_compound_substep`.
- **Fix** (additif, ~1 endroit) : dans la boucle compound (L169-187), poser `_anchor_image_base64` (même fallback que simple) sur le **1er substep cliquable** uniquement.
- **Impact DB** : **487/582 steps `anchor_id NULL`** (84 %) ; démo `Urgence_aiva_demo` = **8/18** manquants.
- ⚠️ **Caveat ré-import** : le ré-import lit la source via `_load_core_workflow(workflow_id, machine_id)` dans `data/training/workflows/{machine_id}/`**disponible par workflow** ; la source de la démo n'est pas trouvée par nom → **vérifier par core_workflow_id avant de compter sur le ré-import de la démo**.
- **Test** : ré-importer un workflow compound → un substep cliquable doit afficher son image via `GET /api/v3/anchor/<id>/thumbnail` + `StepNode.tsx`. **Risque** : code = additif (faible) ; **ré-import = étape sensible** (backup `workflows.db` + par workflow + revérifier le replay).
- **FAIT 23/06** : fix commité (`2cabc6cb7`, br. `sp1/anchors-compound`), TDD RED→GREEN, **validé sur données réelles** (source réelle : 2/2 clics compound désormais ancrés). Persistance confirmée : `import_learned_workflow` (L332) `pop("_anchor_image_base64")``save_anchor_image``VisualAnchor` + `step.anchor_id` (même chemin que les actions simples). Backup DB : `instance/workflows.db.bak-sp1-2026-06-23`.
- ⚠️ **Découverte** : `import_learned_workflow` **crée un nouveau workflow** (`generate_id`, L301) — **pas de mise à jour en place**. Donc rafraîchir les anchors d'un workflow **existant** (ex. démo) par ré-import = **doublon** → dépend de **U-A** (create-or-update). Le fix U-B est correct **en avant** (tout nouvel import aura les anchors compound) ; le rafraîchissement des workflows déjà en base est porté par U-A (SP-4, décision Q-F1-1).
---
## Feature — Exécution native agentique (computer-use zéro-shot) 🤖
**Manque recadré (signalé Dom 23/06).** Donner un **objectif en langage naturel** (« ouvre un navigateur et va sur YouTube ») et l'exécuter **sans workflow appris**, par planification + grounding visuel. Complément du rejeu appris (F2).
**⚠️ Constat runtime (vérif 23/06) : ce n'est PAS absent — les briques existent et sont wirées.** Le manque réel = **durcissement + sandbox + validation humaine**.
| ID | Brique | État runtime | Manque |
|---|---|---|---|
| F8.1 | **Boucle ORA** observe→reason(VLM)→act→verify→retry (`core/execution/observe_reason_act.py:run_instruction`) | ✅ **wired** via endpoint `/execute/instruction` (`api_v3/execute.py:2033`) | durcissement |
| F8.2 | **Planner NL→plan** (`agent_v0/server_v1/task_planner.py:understand()` gemma4, mode `_execute_free()`) | ⚠️ présent, **mode « free » peu mûr/peu testé** | maturation + tests |
| F8.3 | **Grounding cascade** OCR→**UI-TARS**→VLM (= F3, partagé) | ✅ wired (`input_handler.py:_grounding_ui_tars`) | — |
| F8.4 | **Sandbox Worker** (VM/Xvfb/VNC + kill-switch + **validation humaine**) | ❌ **absent** — exécution **directe sur l'host**, sans isolation ni pause | **= le vrai manque** (décision CUA P1) |
| F8.5 | **Boucle vers l'apprentissage** : un run natif réussi → capturé comme workflow appris (alimente F1) | ❌ absent | à câbler |
| F8.6 | **Replanification dynamique** si l'écran change radicalement (app crash…) | ❌ absent (ORA linéaire) | à ajouter |
**🔴 Sécurité — vérifié 23/06 (audit read-only)** : `POST /api/v3/execute/instruction` (VWB backend **5002**, `app.py:321` / `execute.py:2033`) lance la boucle ORA qui pilote **directement l'écran X11 de l'host** (pyautogui/xdotool, **pas de sandbox**, pas de pause humaine, pas de kill-switch) — cible de fait la VM Léa affichée. Auth = middleware Basic global (`DASHBOARD_PASSWORD`) **mais loopback exempté** ; sur le DGX clinique 5002 est **atteignable sur le LAN** (401 sans creds / 200 loopback), pas d'expo WAN. → Un acteur du LAN clinique avec les creds partagés (faibles, `Medecin2026!`) **ou tout process local (loopback)** peut déclencher une instruction agentique arbitraire sur l'host. **Mitigation à décider (prod)** : restreindre `/execute/instruction` à loopback / désactiver le mode « free » tant que F8.4 (sandbox) n'existe pas. Tant que F8.4 n'est pas en place, le mode « free » ne doit **PAS** être ouvert au-delà d'un environnement jetable — « JAMAIS l'hôte » + safety agent.
**Articulation avec le mode appris** : routage — pas de workflow appris pour le but → **mode natif** ; sinon **rejeu** (F2). Et le natif réussi **devient** appris (F8.5, la boucle se referme). Décisions → registre F8.
---
## H1 — Stabilisation clinique (jour J → quelques jours, à distance)
| # | Action | Source |
|---|---|---|
| 1 | **Aligner DGX↔local avant débranchement** : git 5 commits behind (`ec1fb81``d686c3ac2`) ; **backup `workflows.db` AVANT** reset | Qwen + tableau B |
| 2 | Ajouter **`RPA_SIGNING_KEY`** dans `.env.local` DGX (HMAC métadonnées FAISS, absent) | KO-1 Qwen |
| 3 | **Vérifier accès Stormshield** depuis laptop = point de non-retour avant départ | MEMO §5 |
| 4 | **Mot de passe** (pas PIN) compte VM `aivanov` → RDP + presse-papier | handoff |
| 5 | Confirmer **profil Stormshield laptop** avec PORQUET (atteint `.178` ?) | handoff |
| 6 | **Apprentissage e2e sur 1 poste TIM réel** avant de généraliser aux 5 | MEMO + audit |
## H2 — Consolidation & dette (semaines suivantes, scp via VPN)
| # | Chantier | Pourquoi |
|---|---|---|
| 7 | **Merger les branches** : `fix/dashboard-complete-installer` non mergée sur `poc-dgx` ; clarifier topologie `poc-dgx``main` | base git propre avant tout ménage |
| 8 | **Ménage code mort** (mission Qwen en cours) → exécution **par lots + QG** | post-stabilisation |
| 9 | **Test de charge multi-agents** (2-3 puis 5 Léa simultanées) | 1 TIM démo ≠ 5 TIM réels |
| 10 | Bugs non-rejeu résiduels de l'audit (watchdog `_retry_pending` A2, écran verrouillé non détecté…) — **re-vérifier lesquels sont encore ouverts** | audit gaps |
## H3 — Produit & fond (après stabilisation, sur décisions Dom)
| # | Chantier | Bloqué par |
|---|---|---|
| 11 | **Chantier B anchors VWB** (propager `anchor_image_base64` aux substeps compound) — fix ciblé, risque faible | rien, prêt |
| 12 | **CUA Sandbox Worker P1** (décision Dom 18/06 : VM/Xvfb/VNC + kill-switch, jamais l'hôte) | priorisation |
| 13 | **Source vérité workflows** (migration JSON→SQLAlchemy, sortir DETTE-015) | post-POC |
| 14 | **Shadow → Copilot → Autonomous** au runtime (aujourd'hui design) | R1-R6 + décisions |
---
## Décisions qui reviennent à Dom (registre dédié)
→ Suivi vivant dans **`DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md`** (Dom remplit à tête reposée).
**Déjà tranché (23/06)** :
-**H3 en premier**, décisions avant exécution (séquencement préservé).
-**F6 = mutualisation cross-clinique ET intra-clinique** → fédération anonymisée **dans le périmètre** + lever le silo `machine_id` entre postes.
-**F11 = accès multi-VPN par site** (cf. §0.2).
**Encore ouvert** (bloque l'exécution des chantiers liés) :
1. **Principe « rejeu intelligent »** (R2/R3) — le rejeu *doit* consulter le fonds appris ? Change l'archi du replay.
2. **Provider Léa au runtime** (R1 + résolution).
3. **Critère de fusion des workflows** (create-or-update).
4. **Source de vérité workflows** (DB vs JSON) + **métrique produit** (24/79/37) — *reco Claude consignée : DB = vérité, JSON = échange ; métrique = rejouables validés*.
## Séquencement imposé
**Stabiliser (H1)****base git propre + merge (H2-7)****ménage code mort****axe rejeu R1→R7** (chirurgie supervisée) → **fond produit (H3)**.
On ne dégraisse pas, et on ne recâble pas le rejeu, sur une base mouvante.
---
## Plans sources (référence, ne pas dupliquer)
`MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23` · `TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21` · `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17` · `PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20` (volet WG suspendu) · `PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09` · `AUDIT_GAPS_APPLI_100PCT_2026-06-10` · `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16` · `CHECKLIST_DGX_PRE_CLINIQUE` · `INSTALLATION_MULTI_SITE`.

View File

@@ -0,0 +1,92 @@
# Plan de chantier — Unification Léa + VWB (préparé le 2026-06-17 au soir)
> Préparé par analyse multi-agents (3 agents read-only) + graphify, après le check UI post-reboot DGX.
> **Rien n'a été modifié.** Document de cadrage pour la reprise.
> Méthode imposée : chirurgie itérative supervisée (1 modif = 1 test = validation Dom). Pas de batch.
## 0. Diagnostic corrigé (important — deux hypothèses du soir invalidées)
Le check UI a fait remonter 3 symptômes : **T3** (Léa « ne trouve pas » le bloc-notes), **T5/anchors** (images d'ancres absentes au VWB), et le sujet de fond **fonds commun**. L'analyse de code rectifie le diagnostic « à chaud » :
| Hypothèse du soir | Verdict après analyse code |
|---|---|
| T3 = silo machine_id (Léa-VM ne voit pas le savoir du .11) | **FAUX au niveau sélection.** Le `SemanticMatcher` ne filtre par aucune machine ; Léa tourne sur le DGX qui héberge les dossiers des deux postes → elle *voit* déjà tous les workflows. |
| T3 = filtre `is_production_ready` | **FAUX.** Aucun composant runtime (matcher, exécuteur, chat) ne lit `is_production_ready`/`learning_state`. |
| **Vraie cause T3** | **Fragmentation de l'apprentissage.** 100 répétitions → ~20 workflows distincts nommés d'après les apps vues (« Bloc-notes, Explorateur et Terminal (2)(3)… »), aucun nommé franchement « ouvrir le bloc-notes » → le matching sémantique se dilue sur 20 quasi-doublons. (Le silo existe, mais au niveau *renforcement cross-machine*, pas sélection.) |
---
## Chantier A — Consolidation de l'apprentissage (débloque T3) — PRIORITÉ HAUTE
### Cause racine
Chaque session de streaming **crée** un workflow neuf, **sans jamais chercher ni fusionner** un existant.
- Nommage + suffixe `(2)(3)` : `agent_v0/server_v1/stream_processor.py:4335-4344` (`_generate_workflow_name`) — collision de nom → variante numérotée au lieu de renforcement.
- Persistance directe sans dédup : `stream_processor.py:4417-4445` (`_persist_workflow`), build `:2966-3112` (`_build_workflow_from_session`).
- 1 observation / node : build **toujours séquentiel** (`graph_builder.py:345` `clusters={i:[i]}`), donc `observation_count = 1` (`graph_builder.py:909`). DBSCAN d'agrégation volontairement désactivé.
- **Aucune fonction de merge/dédup de workflows** dans tout `core/`. Le `VariantManager` (`core/variants/variant_manager.py:266`, seul code qui fait `observation_count += 1`) **n'est jamais appelé**.
- `is_production_ready` calculé dans `core/training/quality_validator.py:114,238-246` (seuil `min_observations_per_node=3`) — toujours False car 1 obs/node, **mais sans effet runtime** (label informatif uniquement).
### Leviers (effort / risque / impact)
- **A. Découpler « exécutable » de « production_ready »** — faible / faible / moyen — quick-win sémantique.
- **B. Fusion/dédup create-or-update à la persistance** — élevé / moyen / **fort** — cause racine.
- **C. Rebrancher l'agrégation des observations** (`VariantManager` ou `_run_cross_session_learning` qui réécrit `observation_count`) — moyen / moyen / fort.
- **D. Seuil `min_observations` configurable/contextuel** — faible / moyen / faible — cosmétique seul.
- **E. `workflow_id` = signature stable de la trajectoire** (hash de la séquence d'actions) au lieu des apps vues — moyen / moyen / fort — supprime le `(2)(3)` à la racine, rend B trivial.
**Reco** : **E + B** (signature stable + create-or-update avec agrégation d'observations), D en complément, A en quick-win si on veut juste « rendre exécutable » vite.
### Composants
`stream_processor.py` (L2966-3112, L4335-4344, L4417-4445, cross-session L3149-3268, list L4518) · `graph_builder.py` (L345, L909, L384-456) · `quality_validator.py` (L114, L238) · `semantic_matcher.py` (sélection) · `variant_manager.py` (L266, à rebrancher) · `replay_learner.py:358` (consolidate = hints seulement, leurre).
---
## Chantier B — Affichage anchors VWB (débloque T5) — FIX PRÉCIS, FAIBLE RISQUE
### Cause racine
Le commit `b8b963059` n'a corrigé **que la moitié** : l'import lit `target.context_hints.anchor_image_base64` **uniquement pour les actions simples**.
- `services/learned_workflow_bridge.py` : branche action simple `else` L226-233 (lit le base64, ajouté par le commit) ; **les actions *compound*** (majoritaires dans les workflows Léa) passent par `_convert_compound_substep` L279 qui **ne lit jamais le base64** → substeps `anchor_id=NULL` → frontend affiche « Ancre requise » sans image (`frontend_v4/.../StepNode.tsx:113-119`).
- Aggravant état DGX : les workflows en base datent d'avril, **100% des steps `anchor_id=NULL`** → ré-import nécessaire après fix.
### Chaîne (pour mémoire)
import `api_v3/learned_workflows.py:249` → convert `learned_workflow_bridge.py:72``save_anchor_image` → table `visual_anchors` (`db/models.py:163`) → API `GET /api/v3/anchor/<id>/thumbnail` (`api_v3/capture.py:356`) → React `StepNode.tsx` (`api.ts:136`). Pas de mismatch URL/chemin.
### Fix
Propager `anchor_image_base64` aux substeps compound (passer `target` dans la boucle compound L169-187 / `_convert_compound_substep`, poser l'ancre sur le 1er substep cliquable — éviter de dupliquer sur N substeps). Risque faible/additif. **Puis ré-importer** les workflows cibles.
---
## Chantier C — Fonds commun / mutualisation cross-poste — STRATÉGIQUE, DÉCISION PRODUIT D'ABORD
### État
- Fédération `core/federation/` **entièrement débranchée au runtime** : `GlobalFAISSIndex.search()` (`faiss_global.py:199`) **jamais appelé** ; endpoints export/import (`api_stream.py:6277-6372`) sans aucun déclencheur (ni cron, ni frontend).
- **Anonymisation = le maillon le plus mûr et prêt** : `learning_pack.py` exclut machine_id/hostname/patient/nip/ipp (`_clean_metadata` L410, `_SENSITIVE_METADATA_KEYS` L61-66), hash SHA-256 (L388), export = embeddings 512d + signatures (pas de pixels/OCR brut).
- Silo réel = `stream_processor._run_cross_session_learning` L3197/L3284 (`workflow_machine != machine_id → continue`) : bloque le renforcement cross-machine.
- Identité : `machine_id` workflows = `hostname_os` (ex `DESKTOP-58D5CAC_windows`, `agent_v1/config.py:34-37`) ≠ token enrôlement `cbd8f9f0…` (`agent_registry.py:107-111`, sécurité parc). À ne pas confondre.
### Options
- **Intra-clinique brut** (lever filtre cross-session L3197/L3284, charger toutes machines) — simple, **non anonymisé** → acceptable seulement intra-site.
- **Cross-clinique anonymisé** (brancher `search()` global + déclencheur export/import + index global peuplé) — la vraie « fédération », effort moyen-élevé, **seul canal sûr PII**.
---
## Décisions produit à trancher AVANT de coder (pour Dom)
1. **Quel point d'entrée Léa au runtime ?** Deux providers concurrents : `agent_chat/app.py:678,906` (port 5004, SemanticMatcher sur `data/training/workflows/`) vs chat serveur `api_stream.py:6623` (`_list_available_workflows` qui liste des **sessions live**, pas les workflows entraînés). Les logs DGX du soir montrent le 5004 + SemanticMatcher → **probablement 5004**, à confirmer formellement avant de coder A.
2. **Critère de « même parcours »** pour la fusion (levier E/B) : signature de trajectoire ? nom de base ? (workflows de 7 à 89 nodes pour « le même » parcours → alignement non trivial).
3. **Source de vérité workflows** (DETTE-015) : DB VWB SQLite (5002) vs JSON `data/training/workflows/` — la route `/api/workflows` de Léa fusionne les deux avec une dédup fragile. Trancher la source canonique.
4. **Niveau de mutualisation** : intra-clinique brut (rapide, non anonymisé) vs fonds commun cross-clinique anonymisé (fédération). Implications réglementaires opposées.
5. **Re-exécutabilité des packs fédérés** : l'export n'emporte que des squelettes anonymisés (pas les templates/coordonnées) → un chemin de re-résolution visuelle est nécessaire (cohérent avec le contrat 100% vision).
---
## Ordre recommandé pour la reprise
1. **Confirmer le provider runtime de Léa** (Q1) — 10 min, read-only, débloque tout le reste.
2. **Chantier B (anchors)** — fix ciblé compound + ré-import. Faible risque, gain visuel immédiat (T5).
3. **Chantier A (consolidation)** — E + B, chirurgie itérative. Débloque T3 (le bloc-notes « connu »).
4. **Chantier C (fonds commun)** — décision produit (Q4) puis implémentation anonymisée. Le plus stratégique pour la proposition de valeur, mais le moins urgent pour une démo.
## Garde-fous
- Tout changement au build/persist (Chantier A) touche le chemin qui alimente la démo Urgence → chirurgie itérative, 1 test par modif.
- Champ de mines `core/` : vérifier le wiring runtime réel avant de rebrancher (`VariantManager`), pas seulement la présence du code.
- Mutualisation cross-site = uniquement via `LearningPack` anonymisé, jamais recopie de JSON bruts (PII : OCR, titres fenêtres patients).

View File

@@ -0,0 +1,474 @@
# D1 — NavigateCoords Implementation Plan
**Auteur**: Qwen
**Date**: 2026-07-02
**Statut**: EN ATTENTE GO Dom/Claude — Option 1 vs Option 2
**Référence**: `docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md` (3 gaps documentés)
---
## Résumé des gaps à résoudre
| Gap | Description | Fichier:Ligne | Preuve |
|-----|-------------|---------------|--------|
| A | Compiler bake floats littéraux — aucun template pour coords | `replay_engine.py:1821-1833` | `x_pct = px` (literal float) |
| B | Zéro consommateur de `navigate_*_coords` variables | `replay_engine.py` + `api_stream.py` | grep: 0 occurrences |
| C | `_edge_to_normalized_actions` pas de branche `navigate``[]` | `replay_engine.py:1951-1953` | `else: return []` |
---
## Infrastructure existante (non-modifiée)
### `_ALLOWED_ACTION_TYPES` (replay_engine.py:35-50)
`"navigate"` est **déjà présent** (ligne 44). La validation de sécurité l'accepte déjà.
### `_SERVER_SIDE_ACTION_TYPES` (replay_engine.py:55-64)
`"navigate"` est **déjà présent** (ligne 59). Le dispatch loop le traite comme serveur-side.
### `_handle_navigate_action` (core/navigation/__init__.py:24-113)
Handler **déjà câblé** dans api_stream.py (ligne 4459-4467). Résout screenshot, OCR/VLM, stocke coords dans `replay_state["variables"]`.
### `_resolve_runtime_vars` (replay_engine.py:2031-2045)
Resolver **existant** pour `{{var.field}}` — récursif sur dict/list/str. Retourne `str(value)` au niveau leaf → float→string conversion nécessaire pour coords.
---
## OPTION 1 — Compiler Injection (~2h)
### Principe
Ajouter une branche `navigate` dans `_edge_to_normalized_actions` + ajouter `coords_var` mechanism dans les branches `mouse_click`/`text_input` + runtime resolution + float conversion.
### Patch P1-A : Branche navigate dans `_edge_to_normalized_actions`
**Fichier**: `agent_v0/server_v1/replay_engine.py`
**Position**: Après `elif action_type == "llm_generate":` (ligne 1949), avant `else:` (ligne 1951)
```python
elif action_type == "navigate":
normalized["type"] = "navigate"
normalized["parameters"] = {
"login_field": action_params.get("login_field", "login"),
"password_field": action_params.get("password_field", "password"),
"submit_button": action_params.get("submit_button", "submit"),
"login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"),
"password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"),
"submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"),
}
return [normalized]
```
**Justification**: Action serveur-side — pas besoin de `x_pct/y_pct` ni `target_spec`. Le handler `_handle_navigate_action` lit `parameters` pour config, résout coords au runtime.
**Impact**: Gap C résolu. Navigate edge → 1 normalized action au lieu de `[]`.
### Patch P1-B : coords_var dans branches mouse_click / text_input
**Fichier**: `agent_v0/server_v1/replay_engine.py`
**Position**: Lignes 1844-1856 (branches click et type)
**mouse_click** (ligne 1844-1848) — AVANT :
```python
if action_type == "mouse_click":
normalized["type"] = "click"
normalized["x_pct"] = x_pct
normalized["y_pct"] = y_pct
normalized["button"] = action_params.get("button", "left")
```
**mouse_click** — APRES :
```python
if action_type == "mouse_click":
normalized["type"] = "click"
coords_var = action_params.get("coords_var")
if coords_var:
normalized["x_pct"] = f"{{{{{coords_var}.x_pct}}}}"
normalized["y_pct"] = f"{{{{{coords_var}.y_pct}}}}"
normalized["coords_var"] = coords_var
else:
normalized["x_pct"] = x_pct
normalized["y_pct"] = y_pct
normalized["button"] = action_params.get("button", "left")
```
**text_input** (ligne 1850-1856) — AVANT :
```python
elif action_type == "text_input":
normalized["type"] = "type"
text = action_params.get("text", "")
text = _substitute_variables(text, params, action_params.get("defaults", {}))
normalized["text"] = text
normalized["x_pct"] = x_pct
normalized["y_pct"] = y_pct
```
**text_input** — APRES :
```python
elif action_type == "text_input":
normalized["type"] = "type"
text = action_params.get("text", "")
text = _substitute_variables(text, params, action_params.get("defaults", {}))
normalized["text"] = text
coords_var = action_params.get("coords_var")
if coords_var:
normalized["x_pct"] = f"{{{{{coords_var}.y_pct}}}}"
normalized["y_pct"] = f"{{{{{coords_var}.y_pct}}}}"
normalized["coords_var"] = coords_var
else:
normalized["x_pct"] = x_pct
normalized["y_pct"] = y_pct
```
**⚠️ BUG dans le draft ci-dessus**: `x_pct` template pour text_input doit être `{{coords_var.x_pct}}` (pas `.y_pct` deux fois). Version corrigée :
```python
elif action_type == "text_input":
normalized["type"] = "type"
text = action_params.get("text", "")
text = _substitute_variables(text, params, action_params.get("defaults", {}))
normalized["text"] = text
coords_var = action_params.get("coords_var")
if coords_var:
normalized["x_pct"] = f"{{{{{coords_var}.x_pct}}}}"
normalized["y_pct"] = f"{{{{{coords_var}.y_pct}}}}"
normalized["coords_var"] = coords_var
else:
normalized["x_pct"] = x_pct
normalized["y_pct"] = y_pct
```
**Justification**: `coords_var` = mécanisme minimal pour déclarer "ces coords viennent de la variable navigate_login_coords". Template strings résolus au runtime par `_resolve_runtime_vars`.
**Impact**: Gap A résolu. Gap B partiellement — les actions click/type deviennent consommatrices via `coords_var`.
### Patch P1-C : Coercion helper après resolver existant
**⚠️ CORRECTION IMPORTANT (2026-07-02 14:45)** : Le plan original sur-dimensionnait P1-C en proposant un second resolver runtime. **Codex a correctement identifié** que `_resolve_runtime_vars` est **déjà appelé** dans la boucle dispatch à `api_stream.py:4331-4335` :
```python
# L4331-4335 (EXISTANT, ne pas modifier)
if owning_replay is not None:
runtime_vars = owning_replay.get("variables") or {}
if runtime_vars:
action = _resolve_runtime_vars(action, runtime_vars)
```
**Besoin réel = coercion helper uniquement** : `_resolve_runtime_vars` résout les templates `{{var.field}}` mais retourne `str(value)` au leaf → `{{navigate_login_coords.x_pct}}` devient `"0.15"` (string), pas `0.15` (float). Le client attend des floats pour x_pct/y_pct.
**Fichier**: `agent_v0/server_v1/api_stream.py`
**Position**: Juste après la ligne 4335 (`action = _resolve_runtime_vars(action, runtime_vars)`)
**Politique coords_var non résolu** : Skip + pause supervisée (AGREED Qwen/Codex). Jamais fallback 0.0/0.0 — un clic sur coords (0,0) = top-left = potentiellement dangereux.
```python
def _coerce_action_coords(action: dict) -> dict:
"""Cast x_pct/y_pct en float après template resolution par _resolve_runtime_vars.
Politique : si string non convertible ou template encore present → skip + pause_for_human.
Idempotent sur les actions qui ont déjà des floats (mouse_click existant).
Appelé APRÈS _resolve_runtime_vars dans la boucle dispatch (api_stream.py ~L4335).
"""
for key in ("x_pct", "y_pct"):
val = action.get(key)
if val is None:
continue
if isinstance(val, float):
continue # déjà float, idempotent
if isinstance(val, str):
# Template encore présent = non résolu par _resolve_runtime_vars
if val.startswith("{{") and val.endswith("}}"):
action["_skip_reason"] = f"coords_var non résolu: {key}={val}"
action["type"] = "pause_for_human"
action["safety_level"] = "high"
return action
try:
action[key] = float(val)
except (ValueError, TypeError):
action["_skip_reason"] = f"coords invalide: {key}={val}"
action["type"] = "pause_for_human"
action["safety_level"] = "high"
return action
return action
```
**Appel dans la boucle dispatch** (à insérer après L4335) :
```python
# L4335 existant: action = _resolve_runtime_vars(action, runtime_vars)
# NOUVEAU — coercion coords après resolver existant
action = _coerce_action_coords(action)
```
**Justification**: `_resolve_runtime_vars` (existant à L4335) résout les templates → strings. `_coerce_action_coords` cast les strings en floats. Si template non résolu ou conversion impossible → pause_for_human (fail-safe), jamais fallback coords (0,0). Idempotent sur actions existantes (floats déjà présents).
**Risques additionnels identifiés** :
1. **Résolution partielle** : si seul y_pct est résolu mais x_pct reste template → `_coerce_action_coords` convertit pause_for_human (safe stop, pas top-left click).
2. **Idempotence** : si action existante a déjà x_pct=0.35 (float) → helper passe sans modification (isinstance(float) → continue).
3. **Race condition** : variables dict partagé entre navigate handler et dispatch loop — mais BFS séquentiel garantit que navigate stocke AVANT click consomme.
**Impact**: Gap B résolu — les coords navigate sont consommées au runtime par click/type, avec coercion + fail-safe.
### Patch P1-D : VWB YAML schema — coords_var field
**Fichier**: Schema VWB (workflow YAML format) — documentation
**Nature**: Ajout d'un champ `coords_var` dans `action.parameters` pour les steps `mouse_click` et `text_input`
Exemple de workflow YAML avec navigate + click consommateur :
```yaml
steps:
- id: s1
action:
type: navigate
parameters:
login_coords_var: navigate_login_coords
password_coords_var: navigate_password_coords
to_node: n2
- id: s2
action:
type: mouse_click
parameters:
coords_var: navigate_login_coords
button: left
to_node: n3
- id: s3
action:
type: text_input
parameters:
coords_var: navigate_password_coords
text: "${password}"
to_node: n4
```
**Justification**: Le VWB builder doit savoir qu'un click peut référencer une variable coords au lieu de fournir des pixels littéraux. C'est un changement de schema minimal (1 champ optionnel).
---
## OPTION 2 — Declarative YAML Templates (~4h)
### Principe
Introduire un `coords_template` field dans les step definitions + un resolver typed qui extrait directement les floats du dict variables sans passage string→float.
### Patch P2-A : Même branche navigate (identique à P1-A)
Inchangé — Gap C résolu par la même branche.
### Patch P2-B : coords_template field + typed resolver
**Fichier**: `agent_v0/server_v1/replay_engine.py`
Nouvelle fonction `_resolve_coords_template` :
```python
def _resolve_coords_template(
coords_template: str,
variables: Dict[str, Any],
) -> Optional[Dict[str, float]]:
"""Résoudre un coords_template en dict {x_pct, y_pct, bbox_pct} depuis variables.
Retourne None si la variable n'existe pas ou si les champs ne sont pas floats.
Pas de conversion string→float : les valeurs doivent déjà être des floats.
"""
coords_dict = variables.get(coords_template)
if not coords_dict or not isinstance(coords_dict, dict):
return None
x_pct = coords_dict.get("x_pct")
y_pct = coords_dict.get("y_pct")
if not isinstance(x_pct, (int, float)) or not isinstance(y_pct, (int, float)):
logger.warning(
f"coords_template {coords_template}: x_pct/y_pct not numeric "
f"(x_pct={x_pct}, y_pct={y_pct})"
)
return None
result = {"x_pct": float(x_pct), "y_pct": float(y_pct)}
bbox_pct = coords_dict.get("bbox_pct")
if bbox_pct:
result["bbox_pct"] = bbox_pct # tuple, pas de conversion
return result
```
### Patch P2-C : Branches mouse_click / text_input avec coords_template
```python
if action_type == "mouse_click":
normalized["type"] = "click"
coords_template = action_params.get("coords_template")
if coords_template:
normalized["coords_template"] = coords_template
# x_pct/y_pct résolus au runtime par _resolve_coords_template
normalized["x_pct"] = None # placeholder → resolved at runtime
normalized["y_pct"] = None
else:
normalized["x_pct"] = x_pct
normalized["y_pct"] = y_pct
normalized["button"] = action_params.get("button", "left")
```
### Patch P2-D : Runtime resolution typed dans dispatch loop
```python
# --- Résolution coords_template (typed, no string→float) ---
if action.get("coords_template"):
variables = owning_replay.replay_state.get("variables", {})
from agent_v0.server_v1.replay_engine import _resolve_coords_template
coords = _resolve_coords_template(action["coords_template"], variables)
if coords:
action["x_pct"] = coords["x_pct"]
action["y_pct"] = coords["y_pct"]
if coords.get("bbox_pct"):
action["bbox_pct"] = coords["bbox_pct"]
del action["coords_template"] # résolu, pas besoin de garder le ref
else:
logger.warning(
f"coords_template {action['coords_template']} unresolved — skipping action"
)
# skip → next action
```
**Avantage Option 2**: Pas de string→float conversion. Les coords restent des floats du navigate handler au click handler. Plus clean, plus safe.
**Inconvénient Option 2**: `_resolve_coords_template` est une nouvelle fonction + le `x_pct = None` placeholder nécessite que le client tolère les None temporairement (ou que la resolution se fasse AVANT transmission). Le schema VWB doit documenter `coords_template` comme champ alternatif à `by_position`.
---
## Comparative Table — Patches
| Aspect | Option 1 (Compiler Injection) | Option 2 (YAML Templates) |
|--------|-------------------------------|---------------------------|
| **Gap C fix** | Identique (branche navigate) | Identique (branche navigate) |
| **Gap A fix** | Template strings `{{var.field}}` dans x_pct/y_pct | `x_pct = None` placeholder + typed resolver |
| **Gap B fix** | `_resolve_runtime_vars` + float conversion | `_resolve_coords_template` typed (no conversion) |
| **String→float** | Nécessaire (design smell) | Aucun (floats passent directement) |
| **Nouvelles fonctions** | 0 (reuse `_resolve_runtime_vars`) | 1 (`_resolve_coords_template`) |
| **Schema VWB** | 1 champ `coords_var` | 1 champ `coords_template` |
| **Temps implémentation** | ~2h | ~4h |
| **Extensibilité** | Limitée (coupling navigate→click) | Extensible (any coords source) |
| **Risque POC** | Minimal | Moyen (placeholder None + typed resolver) |
| **Migration post-POC** | Option 2 refactor needed | Already Option 2 |
---
## Test Rouge Proposal
### Test TR-1 : Prouve Gap C (navigate → [])
```python
def test_edge_to_normalized_actionsnavigate_returns_empty():
"""Gap C: _edge_to_normalized_actions retourne [] pour navigate type."""
from agent_v0.server_v1.replay_engine import _edge_to_normalized_actions
edge = WorkflowEdge(
edge_id="e1",
from_node="n1",
to_node="n2",
action=ActionSpec(
type="navigate",
parameters={"login_coords_var": "navigate_login_coords"},
),
)
result = _edge_to_normalized_actions(edge, {})
# BEFORE fix: result == [] (Gap C)
# AFTER fix: result == [{"type": "navigate", "parameters": {...}}]
assert len(result) >= 1, "navigate must produce at least 1 normalized action"
assert result[0]["type"] == "navigate"
```
### Test TR-2 : Prouve coords_var resolution (Option 1)
```python
def test_coords_var_runtime_resolution():
"""Option 1: coords_var template resolved + float conversion."""
from agent_v0.server_v1.replay_engine import _resolve_runtime_vars
variables = {
"navigate_login_coords": {
"x_pct": 0.15,
"y_pct": 0.35,
"method": "ocr+vlm",
}
}
action = {
"type": "click",
"x_pct": "{{navigate_login_coords.x_pct}}",
"y_pct": "{{navigate_login_coords.y_pct}}",
"coords_var": "navigate_login_coords",
}
resolved = _resolve_runtime_vars(action, variables)
# resolved["x_pct"] == "0.15" (string) → needs float conversion
assert resolved["x_pct"] == "0.15" # string from resolver
assert float(resolved["x_pct"]) == 0.15 # conversion works
```
### Test TR-3 : Prouve coords_template typed resolution (Option 2)
```python
def test_coords_template_typed_resolution():
"""Option 2: coords_template returns floats directly, no conversion."""
from agent_v0.server_v1.replay_engine import _resolve_coords_template
variables = {
"navigate_login_coords": {
"x_pct": 0.15,
"y_pct": 0.35,
"method": "ocr+vlm",
}
}
coords = _resolve_coords_template("navigate_login_coords", variables)
assert coords is not None
assert isinstance(coords["x_pct"], float) # float, not string
assert coords["x_pct"] == 0.15
assert coords["y_pct"] == 0.35
```
---
## BFS Ordonnancement — Risque scheduling
Le dispatch loop (`api_stream.py:get_next_action`) traite les actions séquentiellement par path BFS. Navigate est serveur-side → traité en boucle interne avant transmission. Click/type consommant coords_var/template sont visuels → transmis au client.
**Flows correct**:
1. BFS traverse edge navigate → normalized action `type=navigate`
2. Loop interne: `_handle_navigate_action` → stocke coords dans variables
3. BFS traverse edge click → normalized action avec `coords_var`
4. Loop: resolution runtime → float conversion → transmission client
**Risque**: Si le BFS ordonnance le click AVANT le navigate (par ex. edges parallèles), coords_var sera unresolved → fallback 0.0/0.0.
**Mitigation**: VWB builder doit garantir que navigate edge précède click consommateur dans le path topologique. C'est une contrainte de schema, pas un bug runtime.
---
## Decision Matrix
| Critère | Option 1 | Option 2 | Recommandation POC |
|---------|----------|----------|--------------------|
| Temps | 2h | 4h | **Option 1** |
| Risque runtime | string→float edge | None placeholder | Option 1 (conversion simple) |
| Extensibilité | Limitée | Extensible | Option 1 pour POC, migration Option 2 post-POC |
| Code mort risk | 0 nouvelles fonctions | 1 nouvelle fonction | Option 1 |
| Test coverage | TR-1 + TR-2 | TR-1 + TR-3 | Option 1 |
**Recommandation Qwen**: Option 1 pour POC (2h, minimal risk, reuse infrastructure existante). Migration Option 2 post-POC si scaling multi-coords est confirmé (search, dossier).
**GO requis**: Dom + Claude (décision D1).
---
*Qwen — plan implémentation D1 déposé, awaiting GO.*

View File

@@ -0,0 +1,98 @@
---
name: plan-deploiement-navigation-2026-07-01
description: Plan déploiement + vérification pour wiring navigate — diff, smoke, rollback. Commit = GO Dom supervisé. v2 corrigée après revue croisée Claude.
type: project
---
# Plan de déploiement — Wiring navigate (brique navigation serveur)
- Date : 2026-07-01 23:50 → **v2 2026-07-02 11:00** (corrections après revue croisée Claude)
- Branche : `feat/push-log-dgx`
- Commit = **GO Dom supervisé** (serveur DGX clinique live)
## Fichiers modifiés (3 hotspots + 4 modules navigation + 6 fichiers tests)
| Fichier | Changement | Lignes |
|---------|-----------|--------|
| `agent_v0/server_v1/api_stream.py` | +1 import `from core.navigation import _handle_navigate_action` + +1 dispatch `elif type_ == "navigate"` (asyncio.wait_for, timeout=180s) | +10 |
| `agent_v0/server_v1/replay_engine.py` | +1 `"navigate"` dans `_ALLOWED_ACTION_TYPES` + +1 `"navigate"` dans `_SERVER_SIDE_ACTION_TYPES` | +2 |
| `core/navigation/__init__.py` | Nouveau : handler `_handle_navigate_action` + exports `__all__` | +115 |
| `core/navigation/visual_verifier.py` | Nouveau : OCR-ancré verify_before/after, fuzzy match | +408 |
| `core/navigation/grounding.py` | Nouveau : OCR-anchor-first + VLM fallback + coords cache | +375 |
| `core/navigation/visual_login.py` | Nouveau : DPI urgences login, verify + resolve | +227 |
| `core/navigation/action_resolver.py` | Nouveau : coords normalisés, OCR adapters | +205 |
**Tests ajoutés (6 fichiers, 131 tests) :**
| Fichier | Tests | Rôle |
|---------|-------|------|
| `tests/unit/test_visual_verifier.py` | 48 | OCR-ancré, fuzzy match, verify_before/after |
| `tests/unit/test_grounding.py` | 39 | OCR-anchor, VLM fallback, coords cache |
| `tests/unit/test_visual_login.py` | 17 | DPI urgences login, verify + resolve |
| `tests/unit/test_action_resolver.py` | 14 | coords normalisés, OCR adapters |
| `tests/unit/test_navigate_wiring.py` | 7 | Boot non-régression (import, allowed types, handler callable) |
| `tests/unit/test_navigate_handler_e2e.py` | 6 | E2e mocké (nominal, OCR miss, no screenshot, never-fail) |
## Smoke commands post-commit (à exécuter sur DGX après deploy)
```bash
# 1. Boot serveur streaming — pas d'ImportError
RPA_AUTH_DISABLED=true python -c "from agent_v0.server_v1 import api_stream; print('api_stream OK')"
# 2. Types autorisés — navigate présent
RPA_AUTH_DISABLED=true python -c "from agent_v0.server_v1.replay_engine import _ALLOWED_ACTION_TYPES, _SERVER_SIDE_ACTION_TYPES; print('navigate in ALLOWED:', 'navigate' in _ALLOWED_ACTION_TYPES); print('navigate in SERVER_SIDE:', 'navigate' in _SERVER_SIDE_ACTION_TYPES)"
# 3. Handler callable
RPA_AUTH_DISABLED=true python -c "from core.navigation import _handle_navigate_action; print('handler callable:', callable(_handle_navigate_action))"
# 4. Tests non-regression (navigation + wiring)
RPA_AUTH_DISABLED=true python -m pytest tests/unit/ -k "navigat or visual_verifier or grounding or visual_login or action_resolver or wiring or e2e" -v --tb=short
# 5. Service rpa-streaming actif + health endpoint
systemctl is-active rpa-streaming.service
curl -s http://localhost:5005/health | python3 -m json.tool
```
## Critères de rollback (si smoke échoue)
| Critère | Action |
|---------|--------|
| `ImportError` sur `api_stream` | Rollback git — `git revert --no-edit <SHA_PRE_DEPLOY>..HEAD` |
| `"navigate"` absent des `_ALLOWED`/`_SERVER_SIDE` | Rollback — `git revert --no-edit <SHA_PRE_DEPLOY>..HEAD` |
| Handler non callable | Rollback — `git revert --no-edit <SHA_PRE_DEPLOY>..HEAD` |
| Tests wiring/e2e FAIL | Ne pas deploy — investiguer avant |
| Serveur 5005 ne boote pas | `systemctl restart rpa-streaming` + vérifier logs |
## Données à préserver sur DGX (ne pas écraser)
- `visual_workflow_builder/backend/instance/workflows.db`**TRACKÉ par git** (modifié dans working tree). `git reset --hard` l'écraserait → **INTERDIT**. Backup obligatoire avant deploy.
- `.env.local` — creds clinique (DASHBOARD_PASSWORD, RPA_VLM_MODEL)
## Procédure de rollback rapide
```bash
# Étape 0 : NOTER SHA_PRE_DEPLOY avant le merge
cd /home/aivanov/ai/rpa_vision_v3
SHA_PRE_DEPLOY=$(git rev-parse HEAD) # ← noter ce SHA avant git pull/merge
# Backup workflows.db (tracké par git — ne jamais reset --hard)
cp visual_workflow_builder/backend/instance/workflows.db /tmp/workflows.db.backup
# Rollback : revert vers SHA pré-deploy
git revert --no-edit <SHA_PRE_DEPLOY>..HEAD # annule tout ce qui est arrivé APRÈS le SHA noté (pas HEAD~1 — core/navigation/__init__.py est nouveau, checkout échouerait)
systemctl restart rpa-streaming rpa-vision-v3-api
# Restaurer workflows.db runtime si besoin
cp /tmp/workflows.db.backup visual_workflow_builder/backend/instance/workflows.db
```
> **⚠️ INTERDICTION** : `git reset --hard` est **INTERDIT** sur DGX — `workflows.db` est tracké par git et modifié dans le working tree. Un reset hard écraserait les données runtime.
## Statut
- Build+TDD : **FAIT** (131 tests verts, 0 régression)
- Plan déploiement : **FAIT v2** (4 corrections revue croisée Claude appliquées : service name, health URL, DGX path, rollback command + workflows.db tracking)
- Commit : **EN ATTENTE GO Dom** (demain matin)
- Deploy DGX : **EN ATTENTE GO Dom** (supervisé, serveur clinique live)
— Qwen

View File

@@ -0,0 +1,263 @@
# Plan de ménage code mort — 2026-06-23
- `Auteur`: Qwen (audit read-only)
- `Date`: 2026-06-23
- `Statut`: plan — **aucune exécution sans GO Dom**
- `Méthode`: existing-first (graphify-out/ + grep imports), vérification wiring runtime
---
## Synthèse
| Zone | Fichiers Python | A (WIRED) | B (ORPHELIN) | C (MORT) | Lignes C estimées |
|------|----------------|-----------|-------------|----------|-------------------|
| core/ | 226 | ~70 (31%) | ~75 (33%) | ~22 (10%) | ~800 |
| agent_v0/server_v1/ | 26 | 21 | 1 | 4 | ~510 |
| agent_v0/agent_v1/ | 44 | 36 | 2 | 6 | ~290 |
| server/ | 8 | 4 | 2 | 2 | ~300 |
| scripts/ | ~40 | 3 | 0 | ~37 | ~2500 |
| deploy/ | 10+ | 3 | 5 | 2 | ~100 |
| root | 4 | 1 | 2 | 1 | ~80 |
| **Total** | ~360 | ~138 | ~84 | ~74 | **~4580** |
**~20% du codebase est MORT confirmé (catégorie C), ~23% est ORPHELIN/projection (B).**
Les zones les plus chargées : `core/analytics/` (13/17 orphelins), `core/cognition/` (4/5 morts), `core/extraction/` (4/5 morts), `scripts/` (37/40 morts).
---
## NE PAS TOUCHER (runtime démo + systemd DGX + installateur)
- `agent_v0/server_v1/api_stream.py` (7747 lignes) — serveur principal runtime
- `agent_v0/server_v1/stream_processor.py` (6085 lignes) — orchestrateur central
- `agent_v0/server_v1/resolve_engine.py` — résolution anchors OCR/VLM
- `agent_v0/server_v1/replay_engine.py` — replay actions
- `agent_v0/agent_v1/core/executor.py` — exécuteur agent Windows
- `visual_workflow_builder/backend/app.py` — VWB backend
- `web_dashboard/app.py` — dashboard
- `server/api_upload.py` — upload API (loopback DGX)
- Tous les services systemd DGX (rpa-vision-v3-*.service, ollama, rpa-vllm-grounder)
- `deploy/installer/` (Lea.iss, config_template.txt, build_installer.sh)
- `deploy/lea_package/` (config.txt, requirements_agent.txt)
- Chemin démo `Urgence_aiva_demo` end-to-end
---
## Inventaire par zone — catégorie C (MORT confirmé)
### core/ — MORT (22 fichiers, ~800 lignes)
| Fichier | Lignes | Preuve mort | Doublon de ? |
|---------|--------|-------------|-------------|
| `cognition/precondition.py` | ~50 | Zero imports | — |
| `cognition/scene_expected.py` | ~50 | Zero imports | — |
| `cognition/trace.py` | ~80 | Zero imports | — |
| `cognition/vram_orchestrator.py` | ~100 | Zero imports (sauf __init__ test) | gpu/device_policy.py |
| `extraction/data_store.py` | ~80 | Zero imports | — |
| `extraction/extraction_engine.py` | ~120 | Zero imports (sauf __init__ try/except) | field_extractor.py |
| `extraction/iteration_controller.py` | ~60 | Zero imports | — |
| `extraction/schema.py` | ~40 | Zero imports | — |
| `execution/spatial_index.py` | ~60 | Zero imports | — |
| `execution/target_memory.py` | ~80 | Zero imports | learning/target_memory_store.py |
| `execution/workflow_runner.py` | ~200 | Zero imports (sauf __init__.py) | dag_executor.py |
| `interfaces/action_executor_interface.py` | ~40 | Tests only | — |
| `interfaces/error_handler_interface.py` | ~30 | Tests only | — |
| `interfaces/target_resolver_interface.py` | ~30 | Tests only | — |
| `graph/simple_state.py` | ~40 | Zero imports | — |
| `grounding/server.py` | ~500 | **SUPPRIMÉ** (n'existe plus) | — |
| `detection/seeclick_adapter.py` | ~300 | Zero runtime imports, __init__ fallback mort | — |
| `supervision/circuit_breaker.py` | ~40 | Zero imports, doublon system/circuit_breaker | system/circuit_breaker |
| `supervision/supervisor.py` | ~200 | Zero imports, docstrings mentionnent modules jamais wired | — |
| `gpu/clip_manager.py` | ~60 | Zero imports | embedding/clip_embedder |
| `gpu/ollama_manager.py` | ~80 | Zero imports | detection/ollama_client |
| `auth/manage_vault.py` | ~40 | Zero imports | auth/credential_vault |
### agent_v0/ — MORT (10 fichiers, ~800 lignes)
| Fichier | Lignes | Preuve mort | Doublon de ? |
|---------|--------|-------------|-------------|
| `server_v1/vm_controller.py` | 143 | Importé uniquement par visual_wait (mort) | — |
| `server_v1/visual_wait.py` | 54 | Importé uniquement par vm_controller (mort) | — |
| `server_v1/workflow_replay.py` | 256 | Zero imports, projection jamais intégrée | replay_engine.py |
| `agent_v1/window_info.py` | 55 | Zero imports | window_info_crossplatform.py |
| `agent_v1/tools/test_lea_pause_flow.py` | ~60 | Script debug standalone | — |
| `agent_v1/tools/test_lea_toast.py` | ~80 | Script debug standalone | — |
| `agent_v1/ui/_test_paused_toast.py` | ~40 | Script debug standalone | — |
| `agent_v1/monitoring/__init__.py` | 0 | Stub vide jamais développé | — |
| `agent_v0/config.py` | 58 | Zero imports | agent_v1/config.py |
| `agent_v0/setup_v1.sh` | 30 | Vestige, requirements.txt absent | — |
### server/ — MORT (2 fichiers, ~300 lignes)
| Fichier | Lignes | Preuve mort | Doublon de ? |
|---------|--------|-------------|-------------|
| `server/api_upload_dev_8001.py` | ~150 | Pas dans services.conf, dev-only | api_upload.py |
| `server/api_upload_dev_8002.py` | ~150 | Pas dans services.conf, dev-only | api_upload.py |
### scripts/ — MORT (~37 fichiers, ~2500 lignes)
| Fichier pattern | Count | Preuve mort | Notes |
|----------------|-------|-------------|-------|
| `demo_*_*_vwb_*_*.py` (dated) | ~12 | Scripts debug Jan 2026, jamais appelés par runtime | Vestiges développement VWB propriétés |
| `diagnostic_*_*.py` (dated) | ~6 | Scripts debug Jan 2026 | Diagnostic palette/catalogue |
| `test_*_vwb_*_*.py` (dated) | ~10 | Scripts test standalone Jan 2026 | Tests ad-hoc palette/propriétés |
| `implementer_*.py`, `implementation_*.py` | ~2 | Scripts debug Jan 2026 | Implémentation propriétés |
| `creer_sauvegarde_vwb_*.py` | 1 | Script debug | Sauvegarde ad-hoc |
| `analyse_cas_undefined_*.py` | 1 | Script debug | Analyse ad-hoc |
| `start_vwb_backend_*.py` (4 variants) | 4 | Doublons de `start_vwb_backend.py` | Versions ultra_stable, thread_safe, final, catalogue_complet |
| `start_system_complet_*.sh` | 1 | Script dated | Remplacé par systemd |
| `start_vwb_complete_*.sh` (2) | 2 | Scripts dated | Remplacé par systemd |
**scripts/ WIRED (3)** : `record_and_build.py` (A), `bench_t2a_dryrun.py` (A), `backup_vwb_and_audit.sh` (A)
### deploy/ — MORT (2 fichiers)
| Fichier | Preuve mort | Doublon de ? |
|---------|-------------|-------------|
| `deploy/configs/config_dev_windows.txt` | Config dev, pas utilisé par installateur | config_template.txt |
| `agent_v0/setup_v1.sh` | Vestige, requirements.txt absent | — |
### root — MORT (1 fichier)
| Fichier | Preuve mort |
|---------|-------------|
| `mcp_rpa_vision.py` | MCP server jamais importé/callé par runtime ou systemd |
---
## Inventaire par zone — catégorie B (ORPHELIN/projection)
### core/ — ORPHELIN (~45+30 borderline, principalement analytics/ et healing/strategies/)
| Zone | Fichiers | Preuve orphelin | Projection plausible ? |
|------|---------|-----------------|----------------------|
| `analytics/` (13 orphelins) | metrics_collector, anomaly_detector, insight_generator, performance_analyzer, success_rate_calculator, archive_storage, timeseries_store, query_engine, report_generator, analytics_api, dashboard_manager, realtime_analytics, resource_collector | Test-only ou zero imports | ✅ Analytics = fonctionnalité produit future (dashboard insights) |
| `healing/strategies/` (5) | base_strategy, format_transformation, semantic_variants, spatial_fallback, timing_adaptation | Test-only via `test_self_healing.py` | ✅ Self-healing = produit prévu |
| `cognition/working_memory.py` (1) | Intra-core lazy import seulement | ✅ Cognition = produit futur |
| `detection/owl_detector.py` (1) | Importé par agent_chat/autonomous_planner seulement | ✅ OWL = grounder alternatif |
| `detection/roi_optimizer.py`, `spatial_analyzer.py` | Test-only | ✅ Optimisation ROI future |
| `grounding/smart_resize.py` | Test-only, jamais importé runtime | ✅ Resize intelligent utile |
| `gpu/vram_monitor.py` | Zero imports | ✅ GPU monitoring utile |
| `monitoring/automation_scheduler.py` | __init__ try/except | ✅ Automation scheduling |
| `security/flask_security.py`, `input_validator.py`, `ip_allowlist.py`, `rate_limiter.py`, `audit_log.py` | __init__ exports mais jamais importés directement | ✅ Sécurité = P0 produit |
| `precision/` (5 fichiers) | Test-only | ✅ Metrics/precision utile |
| `system/artifact_retention.py` | __init__ export, jamais utilisé runtime | ✅ Rétention utile |
### agent_v0/ — ORPHELIN
| Fichier | Preuve | Projection ? |
|---------|--------|-------------|
| `server_v1/session_worker.py` | Commentaires seulement, pas importé | ✅ Background session processing |
| `agent_v1/core/anchor_catalog.py` + `anchor_relative.py` | Importé par tests seulement | ✅ Anchor resolution future |
| `deploy_windows.py` | Script packaging manuel | ✅ Packaging Windows |
| `deploy/windows_client/` (23 fichiers) | Copie déployée sur Windows, pas importée depuis repo Linux | ✅ = package agent Windows |
### server/ — ORPHELIN
| Fichier | Preuve | Projection ? |
|---------|--------|-------------|
| `server/processing_pipeline.py` | Importé par api_upload seulement | ✅ Pipeline processing |
| `server/processing_queue.py` | Importé par api_upload seulement | ✅ Queue processing |
| `server/storage_encrypted.py` | Importé par api_upload seulement | ✅ Encrypted storage |
| `server/worker_daemon.py` | Importé par api_upload seulement | ✅ Worker daemon |
### root — ORPHELIN
| Fichier | Preuve | Projection ? |
|---------|--------|-------------|
| `monitoring_server.py` | Optional dans services.conf (5003), pas déployé DGX | ✅ Monitoring utile |
| `run_gui.py` | Script standalone GUI launcher | ✅ GUI dev tool |
| `cli.py` | CLI entry point, importé par core modules | ✅ CLI utile |
### deploy/ — ORPHELIN
| Fichier | Preuve | Projection ? |
|---------|--------|-------------|
| `deploy/dgx/vm_launch.sh` + `vm_stop.sh` | Scripts QEMU VM DGX | ✅ VM management |
| `deploy/configs/config_pc_fixe_lan.txt` + `config_tim_pauline.txt` + `config_vm_lan.txt` | Configs déployées manuellement | ✅ Déploiement multi-site |
| `deploy/windows-rdp-launcher/` | Scripts RDP | ✅ Remote access |
| `deploy/hyperv_glpi_ubuntu24/` | Script HyperV | ✅ VM creation |
---
## Quick wins sûrs (C évident, zéro risque)
| # | Fichier | Lignes | Risque | Action |
|---|---------|--------|--------|--------|
| 1 | `core/interfaces/` (3 fichiers) | ~100 | Zéro | Supprimer — jamais importé runtime |
| 2 | `core/cognition/` (4 morts: precondition, scene_expected, trace, vram_orchestrator) | ~280 | Zéro | Supprimer — zero imports |
| 3 | `core/extraction/` (4 morts: data_store, engine, controller, schema) | ~300 | Zéro | Supprimer — field_extractor seul wired |
| 4 | `core/supervision/` (2: circuit_breaker doublon, supervisor jamais wired) | ~240 | Zéro | Supprimer — doublons |
| 5 | `core/detection/seeclick_adapter.py` | ~300 | Zéro | Supprimer — fallback mort |
| 6 | `server_v1/vm_controller.py` + `visual_wait.py` | ~200 | Zéro | Supprimer pair — jamais importé |
| 7 | `agent_v1/window_info.py` | 55 | Zéro | Supprimer doublon |
| 8 | `agent_v0/config.py` | 58 | Zéro | Supprimer doublon |
| 9 | `scripts/ dated Jan 2026` (~37) | ~2500 | Zéro | Supprimer — vestiges dev |
| 10 | `server/api_upload_dev_8001.py` + `api_upload_dev_8002.py` | ~300 | Faible | Supprimer dev-only |
**Total quick wins** : ~10 actions, ~4133 lignes supprimées, zéro risque runtime
---
## Zones à risque (à instruire avant GO)
| Zone | Risque | Pourquoi |
|------|--------|----------|
| `core/analytics/` (13 orphelins) | Moyen | Projection produit plausible — supprimer = perdre design analytics |
| `core/healing/strategies/` (5) | Moyen | Self-healing = produit prévu — test-only mais intention future |
| `core/grounding/infigui_server.py` + `infigui_worker.py` | Moyen | Server jamais lancé mais worker existe — dépendance circulaire mort |
| `core/security/` (5 orphelins) | Moyen | Sécurité P0 produit — supprimer = perdre auth/rate-limiting design |
| `core/gpu/` (3 morts: vram_monitor, clip_manager, ollama_manager) | Faible | GPU monitoring plausible mais doublon vérifié |
---
## Proposition de réorganisation
**Pas de réorganisation proposée à ce stade.** Le ménage C suffira à dégraisser ~4500 lignes. Les B (projections) resteront en place avec documentation `# PROJECTION: <raison>` pour les distinguer du code wired.
**Après ménage C**, réorganisation possible :
- `scripts/` dated → archiver dans `scripts/archive/` ou supprimer
- `core/interfaces/` mort → supprimer (remplacé par protocols Python si besoin)
- `deploy/configs/` → consolider dans `deploy/installer/config_template.txt`
---
## Plan d'exécution par lots (chirurgie itérative supervisée)
**Chaque lot = petit, testable (≤ 2 min), QG entre chaque, GO Dom requis.**
| Lot | Action | Lignes | Test QG | Risque |
|-----|--------|--------|---------|--------|
| **L1** | Supprimer `core/interfaces/` (3 fichiers) | ~100 | `pytest tests/ -k "not e2e"` | Zéro |
| **L2** | Supprimer `core/cognition/` morts (4 fichiers) | ~280 | pytest | Zéro |
| **L3** | Supprimer `core/extraction/` morts (4 fichiers) | ~300 | pytest | Zéro |
| **L4** | Supprimer `core/supervision/` (2 fichiers) | ~240 | pytest | Zéro |
| **L5** | Supprimer `core/detection/seeclick_adapter.py` | ~300 | pytest + vérifier __init__.py fallback | Zéro |
| **L6** | Supprimer `server_v1/vm_controller.py` + `visual_wait.py` | ~200 | pytest | Zéro |
| **L7** | Supprimer `agent_v1/window_info.py` + `agent_v0/config.py` + `setup_v1.sh` | ~140 | pytest | Zéro |
| **L8** | Nettoyer __init__.py exports morts dans core/ | ~50 lignes | pytest + import check | Faible |
| **L9** | Supprimer `scripts/` dated Jan 2026 (~37 fichiers) | ~2500 | pytest (aucun script dans test path) | Zéro |
| **L10** | Supprimer `server/api_upload_dev_8001/8002.py` | ~300 | pytest + vérifier services.conf | Faible |
| **L11** | Supprimer `core/gpu/` morts (3 fichiers) | ~200 | pytest | Faible |
| **L12** | Supprimer `core/auth/manage_vault.py` + `execution/workflow_runner.py` | ~240 | pytest | Faible |
**Total** : 12 lots, ~4580 lignes, risque zéro à faible. Chaque lot précédé de GO Dom + QG post-lot.
---
## Liste NE PAS TOUCHER
- Runtime démo `Urgence_aiva_demo` (VWB → backend → agent_v1 → Léa → Easily)
- Services systemd DGX (10 services actifs)
- Installateur clinique (`deploy/installer/`, `deploy/lea_package/`)
- `api_stream.py` (7747 lignes, serveur principal)
- `stream_processor.py` (6085 lignes, orchestrateur)
- `resolve_engine.py` (résolution anchors)
- `replay_engine.py` (replay actions)
- `learned_workflow_bridge.py` (pont VWB ↔ core)
- `core/competences/` (verdicts, promotions, persist, catalog — wired)
- `core/federation/` (GlobalFAISSIndex, LearningPack — wired)
- `core/embedding/` (clip, faiss, state, fusion — wired)
- `core/execution/` (observe_reason_act, target_resolver, input_handler — wired)
- `core/healing/healing_engine.py` + `execution_integration.py` — wired
- `core/analytics/analytics_system.py` + `screen_change_detector.py` — wired
- `core/workflow/` (semantic_matcher, variable_manager, execution_plan, shadow_observer — wired)

View File

@@ -0,0 +1,46 @@
# Plan de migration — persistance workflows (JSON → SQLAlchemy)
- `Date`: 2026-06-09
- `Auteur`: Claude (proposition, validation Dom requise)
- `Statut`: PLANIFIÉ — **post-POC, aucun refactor engagé**
- `Réf dette`: DETTE-015 (docs/DETTE_TECHNIQUE.md)
- `Priorité`: P2 (fragilité réelle, contournée proprement pour le POC par symlink)
## Constat
Trois stockages de workflows coexistent, sans source de vérité unique :
| Store | Emplacement | Utilisé par | État |
|-------|-------------|-------------|------|
| Fichiers JSON | `visual_workflow_builder/backend/data/workflows/*.json` | route API VWB `/api/workflows/` (`api/workflows.py:53`, **relatif au cwd**) | **source réelle** (42 fichiers) |
| DB SQLAlchemy | `…/backend/instance/workflows.db`, table `workflows` + Alembic | composants SQLAlchemy (`db.models`) | propre mais **pas lue par la route** (23 lignes) |
| JSON legacy | `data/training/workflows/` | dashboard `web_dashboard` (`app.py:187-189`) | vide partout |
### Problèmes
1. **Pas de source unique** → divergences (les 42 JSON ≠ 23 lignes DB).
2. **Résolution par cwd** → le bug P0-1 du 2026-06-09 (dev cwd=backend OK ; DGX cwd=racine = 0 workflows).
3. Pas d'écriture atomique ni de validation de schéma côté JSON.
4. Confusion : 3 emplacements, dont un legacy mort.
## Cible
**Unifier sur la DB SQLAlchemy déjà présente** (infra à moitié en place : table + Alembic). La route API lit/écrit la DB ; le store fichier JSON et le legacy sont retirés. Bénéfices : source unique, transactions, intégrité, requêtes, fin de la dépendance au cwd.
> Note : un store fichier n'est pas disqualifiant en soi ; c'est la **coexistence non synchronisée** + la dépendance au cwd qui posent problème. On choisit SQLAlchemy car l'infra existe déjà (vs fiabiliser le JSON, qui laisserait le double store).
## Plan (TDD, post-POC, validation Dom entre étapes)
1. **Audit d'usage** : recenser tous les appels à `WorkflowDatabase` (lecture **et** écriture) — API, moteur d'exécution, agent, frontend VWB. Cartographier le contrat (méthodes `list/load/save/delete`).
2. **Repository SQLAlchemy** : implémenter un `WorkflowRepository` exposant le **même contrat** que `WorkflowDatabase`, adossé à la table `workflows` (réutiliser `db.models`). Tests unitaires CRUD.
3. **Script de migration** : importer les 42 JSON → table `workflows` (idempotent, backup DB avant). Vérifier parité (42 JSON ↔ N lignes, diff de contenu).
4. **Bascule de la route** derrière un flag (`RPA_WORKFLOWS_BACKEND=sqlalchemy|json`, défaut `json`) → tests d'équivalence API (mêmes réponses qu'avant).
5. **Bascule par défaut** sur SQLAlchemy une fois la parité prouvée ; retrait du symlink (DETTE-015).
6. **Nettoyage** : retirer le store legacy `data/training/workflows` (dashboard) ou le rebrancher sur le repository ; supprimer `WorkflowDatabase` quand plus aucun appelant.
## Préconditions / risques
- **Ne pas engager avant la fin du POC** (refactor de persistance = risque pour la démo).
- Touche frontend VWB + agent + moteur d'exécution → bascule progressive sous flag obligatoire.
- Le symlink (DETTE-015) reste le contournement stable jusqu'à la migration.
## Effort estimé
~1,52,5 j en TDD (audit + repository + migration + tests d'équivalence + bascule).

View File

@@ -0,0 +1,103 @@
# Plan de remise au carré — chaîne d'apprentissage & rejeu de Léa
- `Date`: 2026-06-27
- `Auteur`: Claude (mandat Dom)
- `Statut`: actif — formalise l'analyse du 27/06 ; **n'invente rien**, chapeaute `PLAN_ACTION_SUITE_2026-06-23` (axe « Rejeu intelligent ») et `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16`.
- `Contrainte cardinale (Dom 27/06)`: **câbler la chaîne ET rendre Léa correcte AVANT la dernière manip manuelle de Dom** (= avant la perte de l'accès hands-on / on-site). Cette contrainte est la **barre d'acceptation** de ce plan. *(À préciser : quoi exactement, et quelle date.)*
---
## 1. Pourquoi ça ne marche pas (vérifié le 27/06)
**En une phrase : la chaîne a été diagnostiquée (16/06), décidée (23/06) et planifiée (R1→R7), puis la livraison clinique a absorbé les 2 semaines — le câblage end-to-end n'a jamais démarré. Le blocage est d'EXÉCUTION, pas de décision.**
Faits vérifiés (grep/fichier, ce jour) — la chaîne n'est pas câblée au runtime :
| Maillon | Preuve vérifiée 27/06 | Effet |
|---|---|---|
| Worker d'enrichissement | `data/training/_worker_queue.txt` = **0 octet, mtime 11/06** | enrichissement **à l'arrêt depuis 16 j** |
| Import auto session→workflow | `ShadowLearningHook()` instancié **uniquement dans son propre fichier**, jamais dans `finalize`/runtime | l'appris **n'est pas** rendu rejouable sans geste manuel |
| Lecture du fonds au rejeu | `GlobalFAISSIndex.search()`**grep = 0 appel** ; `TargetResolver.lookup()` jamais appelé par `replay-session` | rejeu = **coords figées**, pas cible apprise |
| Dé-silo | filtre `machine_id` à **5 points** de `stream_processor.py` (3199, 3285, 4499, 4530, 5062) | apprentissage **siloté par poste** |
Causes racines (au-delà du « c'est débranché ») :
1. **Les 2 semaines = infra de survie** (portage DGX ARM, installateur EXE, réseau/firewall/VPN, reprise panne secteur + reboot, watchdog OVMF, enrôlement, streaming, push-log). Indispensable pour livrer — mais **0 h sur le câblage de la boucle**.
2. **Découpage par composant, jamais par boucle.** Hook, FAISS, TargetMemory bâtis et validés isolément (sessions/agents différents). **Personne n'a possédé ni testé la boucle entière sur une vraie session** → trous accumulés à chaque soudure, invisibles.
3. **Le raccourci démo est devenu permanent.** Le rejeu `Urgence_aiva` marchait en coords figées + templating `{{var}}` (seule pièce câblée). « Ça marchait » → le chemin intelligent n'a **jamais été allumé**.
⚠️ **À confirmer en amont (audit runtime Qwen, NON vérifié indépendamment)** : « **11/15 postes heartbeat-only, 0 % résolution vision/OCR/anchors** ». Si avéré, le premier problème n'est pas l'apprentissage mais que **les postes ne font pas encore le geste du POC** (pas de vraie capture, cascade vision non exercée). → **à prouver, chiffré, AVANT tout recâblage** (Phase 0).
---
## 1bis. CARTE DE CÂBLAGE VÉRIFIÉE (28/06 — 3 agents read-only, sourcé code)
> Cette section **corrige** le §1 sur deux points (diagnostic affiné, moins grave qu'annoncé).
**Deux corrections vérifiées (mes affirmations antérieures étaient fausses) :**
1. **« sessions → squelettes sans action » = FAUX.** Les actions (clics/saisies) sont attachées au workflow sur les **edges** (`WorkflowEdge.action`, `graph_builder.py:1457`), pas sur `node.variants` (qui = variantes *visuelles* d'écran, champ non peuplé au runtime). 48/71 workflows auto-appris portent leurs actions ; 23 sont vides (sessions trop courtes).
2. **« rejeu = coords figées » = FAUX.** Léa **résout chaque cible par la vue** à chaque rejeu (cascade OCR→template→YOLO→VLM sur anchors, `resolve_engine.py:1804`). Coords = fallback ultime seulement. Conforme 100 % vision.
**Ce qui MARCHE** : capture→workflow avec actions ; worker traite ; 36/71 atteignent `AUTO_CANDIDATE` ; rejeu visuel (VWB-DB 226 `click_anchor` + JSON auto-appris non vides) ; **R2 à moitié branché** (`TargetMemoryStore` consulté en tête de `_resolve_target_sync`, `resolve_engine.py:1862`).
**Les 4 vrais trous (sourcés) :**
| # | Trou | Preuve | Type |
|---|---|---|---|
| **1 (P0)** | **11/15 postes n'enregistrent rien** | démarrage capture 100 % manuel (clic TIM « Apprenez-moi », `smart_tray.py:349` / `chat_window.py:1716`) ; heartbeats auto (`main.py:378`). Risque : dialogue consentement `Tk()` (`smart_tray.py:54`) invisible en RDP/Citrix `pythonw` | **amont / UX (à confirmer bug vs usage par logs client)** |
| 2 | **Apprentissage incrémental débranché** | `LearningManager` non instancié serveur ; mute `WorkflowStats` mémoire non re-persisté ; `record_observation` (`learning_manager.py:54`) **0 appelant**. Seul `GraphBuilder` écrit `learning_state`, fige sur OBSERVATION si qualité faible (`graph_builder.py:400`) | promotion jamais déclenchée |
| 3 | **2 mondes disjoints** : JSON auto-appris ≠ DB VWB rejouable | stores/loaders/matchers séparés ; une session apprise ne devient pas un workflow DB rejouable | = **R1** (pont JSON→DB) |
| 4 | **Fonds commun jamais lu au rejeu** | `GlobalFAISSIndex.search()` = 0 appel (seul `add_pack` écrit) | = **R3** (FAISS au rejeu) |
**Points d'insertion confirmés** : R1 = worker `_process_session` après `_persist_workflow` (réutiliser `import_learned_workflow`/`learned_workflow_bridge`, idempotence par `workflow_trajectory_signature` existante). R2/R3 = `resolve_engine.py:1862-1878` (élargir `memory_lookup` + insérer `GlobalFAISSIndex.search()`).
**Priorité (contrainte « Léa correcte avant dernière manip ») : #1 (amont) d'abord** — si 11/15 postes ne capturent pas, l'aval est sans objet. Test décisif = grep logs client `"Session … en cours"` vs `"Session finalisée"` (Qwen).
---
## 2. État des décisions (rappel — la plupart sont déjà prises)
Tranchées le 23/06 (`DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md`) → **on exécute, on ne re-décide pas** :
- **F2-1/F14-1** : rejeu intelligent = **OUI, prérequis** (consulter le fonds appris, pas de coords figées).
- **F1-1** : critère de fusion = **signature de trajectoire**. ~~create-or-update~~**create-or-skip** (révisé Dom 2026-07-02 : un ré-apprentissage ne doit PAS écraser un workflow validé par revue humaine ; la 1ʳᵉ version importée fait foi. Refresh explicite = chantier séparé si besoin. Implémenté ainsi dans `learned_workflow_bridge.py`, cf. `CARTO_CODE_NON_BRANCHE_2026-07-02.md`).
- **F9-1** : **DB = vérité**, JSON = échange ; métrique = workflows rejouables validés.
- **F6-1** : mutualisation **cross + intra-clinique** (fédération anonymisée dans le périmètre + lever silo `machine_id`).
**Reste ouvert (1 seule, vraie décision) :**
- **Q-F2-2 — Provider Léa au runtime** : quel modèle/route sert la **résolution** au rejeu. ⚠️ Gap tracé 23/06 : le point d'entrée actif = **agent_chat 5004 → `SemanticMatcher.find_workflow()` sur fichiers JSON**, pas la DB → **contredit F9-1**. Se résout **en chemin** à la Phase 2 (où la résolution est recâblée). Reco modèle : Qwen3-VL-4B grounder + gemma4 (bench 13/06).
**Décisions potentiellement induites par la Phase 0** : si « 0 % vision » confirmé, une décision « comment forcer/garantir la capture vision réelle sur poste » surgira (priorité absolue, avant R1).
---
## 3. Plan d'exécution (séquencé, chirurgie supervisée)
> Les chantiers R1→R7 détaillés sont dans `PLAN_ACTION_SUITE_2026-06-23` (§ Axe central) — **non dupliqués ici**. Ce plan ajoute la **Phase 0 de mesure** (nouvelle) et l'**ordre/critères**.
**Phase 0 — MESURER (avant tout recâblage).** Établir la vérité terrain : par poste, nb de **vraies sessions**, la **cascade vision est-elle déclenchée** (compteur de résolutions par méthode), captures reçues, état queue. **C'est ce que push-log + une télémétrie vision apportent** (lien direct avec briques 1-4 livrées). → *Mission Qwen (accès runtime DGX).* **Critère de sortie : on sait, chiffré, ce que font les 15 postes.**
**Phase 1 — RECONNECTER L'AMONT (R1).** Import auto session→workflow post-`finalize` + **relancer le worker** (queue morte 11/06). *Critère : une session TIM réelle devient un workflow rejouable sans geste manuel.*
**Phase 2 — RECONNECTER L'AVAL (R2+R3) + résoudre Q-F2-2.** Câbler `TargetResolver.lookup()` + lecture FAISS au rejeu, **fallback obligatoire sur coords figées** (non négociable Qwen — enrichir, pas casser) ; aligner le point d'entrée résolution sur la DB (F9-1). *Critère : Léa résout par cible apprise, retombe sur coords si échec.*
**Phase 3 — BOUCLE + DÉ-SILO (R4/R5/R6).** verify post-condition (échec → pause supervisée), réécriture du fonds, lever silo `machine_id` + brancher fédération (`GlobalFAISSIndex.search()`).
---
## 4. Gouvernance (corrige la cause racine #2)
- **Un seul propriétaire de la boucle entière** (pas un découpage par composant).
- **Critère d'acceptation = un test end-to-end sur une session réelle**, pas une validation par brique.
- Chirurgie itérative supervisée : un maillon = un test ≤ 2 min = GO Dom ; démo `Urgence_aiva` intacte à chaque étape ; reconfirmer le wiring runtime avant chaque modif (imports lazy = verdicts « orphelin » non fiables).
- **Merge prod supervisé Dom.**
## 5. Lien avec la « dernière manip manuelle » (deadline)
La contrainte de Dom fait de ce plan un **chemin critique** : tant que Léa n'est pas correcte (au moins Phase 0 + Phase 1-2 sur 1 poste pilote), **la dernière manip manuelle ne doit pas avoir lieu** — sinon plus d'accès hands-on pour réparer. → **Définir avec Dom : quelle est cette manip, et sa date butoir**, pour caler le séquencement.
## 6. Première action concrète
**Phase 0 confiée à Qwen** (chiffres runtime). Doc + page décisions = ce fichier. Reste : GO Dom sur le séquencement + définition de la deadline « dernière manip ».
---
*Plans sources (ne pas dupliquer) : `PLAN_ACTION_SUITE_2026-06-23`, `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16`, `DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23`, `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17`.*

View File

@@ -0,0 +1,74 @@
# Post-mortem — Panne secteur DGX 2026-06-20 (test reboot non planifié)
- `Auteur`: Claude (infra)
- `Date`: 2026-06-20 ~03:00 CEST
- `Scope`: La coupure électrique réelle de 02:07 traitée comme **exécution non planifiée du test de stabilité reboot** (`CHECKLIST_DGX_PRE_CLINIQUE.md` §6). Objectif : mesurer ce qui se rétablit **seul** vs ce qui exige une **intervention** — c'est le critère clé pour une clinique sans personnel technique sur place.
- `Méthode`: diagnostic read-only multi-agents (Claude infra, Qwen appli/guest, Codex consolidation). Aucun reset Git, aucun changement réseau hors IP statique arbitrée par Dom.
---
## 1. Timeline
| Heure | Événement |
|---|---|
| 02:07 | Coupure secteur. DGX reboot. Poste Dom (Linux) reboot. Laptop Windows `.11` (sur batterie) jamais coupé. |
| 02:07:42 | `win11-arm-lea.service` (user) auto-démarre la VM. |
| ~02:09 | Watcher coordination (systemd user) revenu seul. Services rpa système remontés. |
| 02:07→02:18 | VM bloquée en boucle TianoCore (QEMU 99% CPU, guest agent/SSH absents). |
| 02:18 | Codex diagnostique : `OVMF_VARS.fd` corrompu par coupure brutale. |
| 02:21 | Codex restaure OVMF connu-bon (18/06) + TPM frais → VM boot prouvé (écran verrouillage Windows). |
| ~02:28 | Claude : DGX revenu en DHCP sur `.46` (au lieu de `.45`) → IP statique `.45` appliquée (décision Dom). |
| ~02:35 | Accès VM Dom rétabli (tunnel + VNC, mot de passe OK). |
| ~02:55 | Crash-loop `dashboard-user` éteint (Qwen). Revue infra/appli consolidée. |
---
## 2. Ce qui s'est rétabli SEUL (✅ socle solide)
| Domaine | Constat | Réf checklist |
|---|---|---|
| Boot DGX + services rpa | Tous `active+enabled` (dashboard, streaming, agent-chat, vwb back/front, api, worker, stream-worker, **vllm-grounder**, firewall) | §1 PASS |
| Firewall | Réappliqué : 5900/5902/3389/22220/8000/11434 **filtrés LAN**, seuls 5001/5002/5004/5005 ouverts | §2 PASS (fort) |
| Auth | Dashboard 401, VWB 401 (basic auth), streaming Bearer | §3 majoritaire PASS |
| Auto-start VM | `win11-arm-lea.service` a bien démarré la VM (linger=yes) | §4.1 — prouvé (était « à implémenter ») |
| Coordination | Watcher couche-1 (systemd user) revenu seul | — |
**Le socle infra/services/sécurité survit à une coupure brutale sans intervention.**
## 3. Ce qui a EXIGÉ une intervention (⚠️ gaps reprise non-assistée)
| # | Problème | Cause | Correctif (qui) | Risque clinique |
|---|---|---|---|---|
| G1 | **DGX IP a dérivé `.45`→`.46`** | bail DHCP après reboot | IP statique `.45` (Claude/Dom) | **Élevé** — casse tous clients/tunnels pointant `.45`. DHCP non fiable. |
| G2 | **VM bloquée TianoCore** | `OVMF_VARS.fd` corrompu (coupure brutale) | restore OVMF connu-bon + TPM frais (Codex) | **Élevé** — sans agent, VM morte jusqu'à intervention manuelle. |
| G3 | **`dashboard-user` crash-loop** (244 restarts) | fallback user clash port 5001 (service système le sert déjà) | stop + mask session (Qwen) | Moyen — bruit/ressources ; `disabled` mais relancé. |
| G4 | **Léa guest non reconnectée** | `config.txt` = `CONFIGURE_ME` + login Windows requis | à renseigner `.45`+token (Qwen) | **Élevé** — VM redémarre mais Léa ne reprend pas le travail seule. |
| G5 | **Mot de passe VNC** | `-vnc password=on` sans `set_password` dans les scripts | rétabli de fait (tunnel) | Faible/Moyen — fragile si relaunch sans repose mdp. |
---
## 4. Recommandations de durcissement — reprise CLINIQUE non-assistée
> Toutes **modifiantes → à valider par Dom** (mises en file, non appliquées cette nuit).
1. **BIOS DGX = « Power On » au retour AC** (physique, Dom) — sinon une coupure laisse le DGX éteint.
2. **IP statique** : fait au labo (`.45`) ; cible clinique = Ethernet statique `.178` (DHCP = point faible démontré par G1).
3. **Auto-réparation OVMF** dans `win11-arm-lea.service` : au boot Windows réussi, snapshot `OVMF_VARS.fd` sain ; `ExecStartPre` : si boucle TianoCore détectée (CPU 99% + guest agent absent N s), restaurer le snapshot sain automatiquement. → neutralise G2 sans agent.
4. **`rpa-vision-v3-dashboard-user`** : `mask` **persistant** (pas seulement session) — G3.
5. **Léa reprise auto** (G4) : `config.txt` persistant vers IP DGX + token ; auto-login Windows + Léa auto-start (`pythonw`) + reconnexion fleet sans geste humain.
6. **Mot de passe VNC** (G5) : poser le mot de passe au lancement via le monitor (script), ou documenter la procédure de repose.
---
## 5. Propositions de MAJ pour `CHECKLIST_DGX_PRE_CLINIQUE.md` (Qwen, propriétaire)
- §4.1 « auto-start VM » : passer **À IMPLÉMENTER → VALIDÉ** (prouvé par la panne, 02:07:42).
- §1.10 / Items à fixer #1 : dashboard service **système actif** confirmé ; le fallback user est l'orphelin → masquer.
- §6 « Test reboot » : **exécuté en réel le 2026-06-20** → renseigner les résultats (col. Résultat) à partir des sections 2 et 3 ci-dessus.
- Ajouter une ligne **G1 dérive IP DHCP** et **G2 corruption OVMF** comme items de durcissement explicites.
---
## 6. Verdict test
Le **socle technique tient** (services, firewall, auth, auto-start VM). Les **deux points durs** pour une clinique sans technicien sur place sont **G1 (dérive IP DHCP)** et **G2 (corruption OVMF VM non auto-réparée)** : tous deux ont nécessité un agent cette nuit. La cible clinique doit les **automatiser** (IP statique Ethernet + auto-réparation OVMF). G4 (Léa ne reprend pas seule) est le troisième chantier reprise.

View File

@@ -0,0 +1,135 @@
# QG Review Framework — D1 NavigateCoords Patch
**Auteur**: Qwen
**Date**: 2026-07-02
**Statut**: EN ATTENTE patch Codex
**Scope**: Review du patch D1 (Option 1 — Compiler Injection) produit par Codex
---
## Baseline test coverage (pré-patch)
| Fichier | Classes | Tests | Rôle |
|---------|---------|-------|------|
| `test_navigate_handler_e2e.py` | 4 | 8 | Handler mock — nominal, OCR miss, no screenshot, never-fail |
| `test_navigate_wiring.py` | 4 | 5 | Import/wiring non-regression |
| `test_action_resolver.py` | 6 | 10 | NavigateCoords, NavigateResult, grounded_to_coords, navigate_login |
| `test_coords_consumption_gap.py` | 3 | 10 | **GAP DOCUMENTATION** — résolution viable, compiler gap, navigate→[] |
| **Total** | **17** | **33** | |
**Tests critiques à mettre à jour après D1 patch**:
- `test_coords_consumption_gap.py::test_navigate_action_type_unknown` — affirme actuellement `actions == []`; doit affirmer `len(actions) >= 1` et `actions[0]["type"] == "navigate"` après D1
- `test_coords_consumption_gap.py::TestCompilerGapLiteralFloats` — 4 tests documentant le gap literal-floats; après D1, les tests coords_var doivent affirmer templates strings ARE produites quand coords_var présent
**Point d'insertion exact D1**:
- Fichier: `replay_engine.py`
- Entre `elif action_type == "llm_generate"` (retourne `[normalized]` ~L1949) et `else:` clause (~L1953)
- Navigate branch: `elif action_type == "navigate"``normalized["type"] = "navigate"` + parameters dict → `return [normalized]`
**P1-C root cause**:
- `_resolve_runtime_vars_in_str` (L2025): `return str(value)` — tout {{var.field}} résolu devient string "0.35" pas float 0.35
- Coercion helper `_coerce_action_coords` doit agir APRÈS `_resolve_runtime_vars` (L4335), AVANT `type_ = action.get("type")` (L4337)
---
## Critères de review — Checklist
### C1 : Branche navigate dans `_edge_to_normalized_actions` (Gap C)
| # | Critère | GO | NOGO |
|---|---------|----|------|
| C1-1 | Branche `elif action_type == "navigate"` ajoutée entre `llm_generate` (L1949) et `else` (L1951) | Present, position correcte | Absente ou mal positionnée |
| C1-2 | `normalized["type"] = "navigate"` | Oui | Type incorrect |
| C1-3 | Parameters dict avec `login_coords_var`, `password_coords_var`, `submit_coords_var` | Noms exacts, valeurs default | Noms divergent ou absents |
| C1-4 | Retourne `[normalized]` (1 action serveur-side) | `[normalized]` | `[]` ou autre |
| C1-5 | Test TR-1 : `test_navigate_action_type_unknown` mis à jour — affirme `len(result) >= 1` et `result[0]["type"] == "navigate"` | Test updated + passes | Test non mis à jour ou fails |
### C2 : coords_var dans branches mouse_click / text_input (Gap A)
| # | Critère | GO | NOGO |
|---|---------|----|------|
| C2-1 | `coords_var = action_params.get("coords_var")` check dans mouse_click | Present | Absent |
| C2-2 | Si coords_var → `x_pct = f"{{{{{coords_var}.x_pct}}}"` et `y_pct = f"{{{{{coords_var}.y_pct}}}"` | Template strings correctes | Syntaxe template incorrecte ou .y_pct pour x_pct |
| C2-3 | Si coords_var absent → literal floats comme avant (fallback existant) | Branch else intacte | Branch else modifiée ou supprimée |
| C2-4 | `normalized["coords_var"] = coords_var` ajouté pour traçabilité | Oui | Absent |
| C2-5 | Même mécanisme dans text_input branch | Identique à mouse_click | Absent ou divergent |
| C2-6 | BUG vérifié : text_input x_pct template = `{{coords_var.x_pct}}` (pas `.y_pct` deux fois) | Correct | y_pct en double |
### C3 : `_coerce_action_coords()` helper (Gap B / P1-C)
| # | Critère | GO | NOGO |
|---|---------|----|------|
| C3-1 | Helper défini dans api_stream.py (pas replay_engine.py) | api_stream.py | Autre fichier |
| C3-2 | Appel APRÈS `_resolve_runtime_vars` (L4335), AVANT `type_ = action.get("type")` (L4337) | Position correcte | Avant resolver ou après type_ check |
| C3-3 | float pass-through : `isinstance(val, float) → continue` | Idempotent sur actions existantes | Pas de float check → conversion inutile |
| C3-4 | string→float : `try: action[key] = float(val)` | Conversion correcte | Pas de try/except → crash possible |
| C3-5 | Template non résolu → pause_for_human (pas fallback 0.0/0.0) | `val.startswith("{{") and val.endswith("}}")` → pause_for_human | Fallback 0.0/0.0 ou autre coords dangereux |
| C3-6 | Conversion impossible → pause_for_human | ValueError/TypeError → pause_for_human | Exception non catchée |
| C3-7 | `_skip_reason` documenté pour debug | Oui | Absent |
| C3-8 | `safety_level = "high"` pour pause_for_human | Oui | Absent ou autre valeur |
| C3-9 | Retourne action mutée (pas de new dict) | Mutation in-place | Copie → risque race |
| C3-10 | Keys itérées = ("x_pct", "y_pct") uniquement | Pas de sur-itération | Autres keys modifiées |
### C4 : Never-fail contract
| # | Critère | GO | NOGO |
|---|---------|----|------|
| C4-1 | `_handle_navigate_action` ne lance jamais d'exception non catchée | Contract preserved | Nouvelle exception possible |
| C4-2 | `_coerce_action_coords` ne lance jamais — tout cas couvert par try/except ou pause_for_human | Contract preserved | Exception possible |
### C5 : Limites de scope POC
| # | Critère | GO | NOGO |
|---|---------|----|------|
| C5-1 | Maximum 4 fichiers modifiés | ≤ 4 | > 4 |
| C5-2 | Pas de changement schema VWB dans POC patch | Pas de modification VWB code | VWB code modifié |
| C5-3 | Pas de nouvelle dépendance pip | 0 nouvelles deps | Nouvelle dep |
| C5-4 | Pas de modification OmniParser wiring | `_omniparser_available = False` intact | Modifié |
### C6 : Test coverage
| # | Critère | GO | NOGO |
|---|---------|----|------|
| C6-1 | TR-1 : navigate compile à 1 action (pas []) | Passes | Fails |
| C6-2 | TR-2 : coords_var template resolution + float conversion | Passes | Fails |
| C6-3 | Test `_coerce_action_coords` : float pass-through | Passes | Absent |
| C6-4 | Test `_coerce_action_coords` : string→float conversion | Passes | Absent |
| C6-5 | Test `_coerce_action_coords` : template non résolu → pause_for_human | Passes | Absent |
| C6-6 | Test `_coerce_action_coords` : conversion impossible → pause_for_human | Passes | Absent |
| C6-7 | Test idempotence : action existante float non modifiée | Passes | Absent |
| C6-8 | `pytest tests/unit/` passe en intégralité | 0 failures | ≥1 failure |
### C7 : Risques additionnels (3 identifiés dans PLAN_D1)
| # | Risque | Mitigation attendue | GO | NOGO |
|---|--------|--------------------|----|------|
| C7-1 | Résolution partielle (x_pct résolu, y_pct template) | `_coerce_action_coords` → pause_for_human si ANY key unresolved | Mitigation presente | Pas de mitigation |
| C7-2 | Idempotence sur mouse_click existant | `isinstance(val, float) → continue` | Idempotent | Risque de double conversion |
| C7-3 | Race condition sur variables dict partagé | BFS séquentiel garantit navigate→click ordre | Note dans code/doc | Pas de mention |
---
## Procédure de review
1. **Lire le patch** : `git diff` sur les fichiers modifiés par Codex
2. **Vérifier chaque critère C1-C7** : GO/NOGO par ligne
3. **Exécuter les tests** : `cd /home/dom/ai/rpa_vision_v3 && .venv/bin/python -m pytest tests/unit/ -x -v`
4. **Produire le verdict** : Table GO/NOGO avec justification + verdict global
## Format verdict
```
## QG Verdict — D1 NavigateCoords Patch
| Critère | GO/NOGO | Note |
|---------|---------|------|
| C1 | GO | Branche navigate correcte |
| C2 | NOGO | BUG: y_pct en double dans text_input |
| ... | ... | ... |
**Verdict global**: GO / NOGO (avec réserves listées)
```
---
*Qwen — framework QG prêt, awaiting Codex patch pour exécution.*

View File

@@ -0,0 +1,272 @@
# RPA Vision V3 — Automatisation basée sur la compréhension visuelle des interfaces
> ⚠️ **Projet en phase POC clinique** — déployé mais **non production-ready**.
> Voir [`docs/STATUS.md`](STATUS.md) pour l'état réel par module. Certaines
> briques tournent en clinique de bout en bout, d'autres sont **codées mais
> gated/dormantes** (activation supervisée par Dom uniquement).
*Dernière mise à jour : 2 juillet 2026 (brouillon proposé — remplace le README daté du 14 avril 2026)*
> **Note de relecture** : ce brouillon reflète la trajectoire mai→juillet 2026
> (livraison DGX Spark, flotte clinique Wallerstein, PII, extraction, grounder
> Qwen3-VL, installeur EXE, MAJ silencieuse). L'ancien README/STATUS s'arrêtait
> au « premier replay Notepad du 13 avril 2026 » et ne reflétait plus la réalité.
## Intention
Automatiser des workflows métier hospitaliers par **compréhension sémantique
de l'écran** plutôt que par coordonnées de clic fixes. Le système observe le
TIM, reconstruit un graphe d'états de l'interface, et cherche à **rejouer
intelligemment** la procédure en reconnaissant visuellement les éléments cibles
— y compris quand l'UI change légèrement. Objectif produit : Léa **apprend** un
parcours et le **généralise**, ce n'est **pas** du record-and-replay.
Terrain cible : postes hospitaliers hétérogènes (**vrais logiciels métier en
mode web, RDP et Citrix**), TIM sur **2 écrans** → capture de la **fenêtre
active**. C'est cette hétérogénéité qui justifie l'approche « 100 % vision ».
Contraintes fortes :
- **100 % vision** : résolution de l'UI par la vue, pas par sélecteurs DOM/API.
- **100 % local** : inférence sur GPU local (Ollama / vLLM). Aucun appel cloud
dans le pipeline par défaut (passeport souverain santé).
> Historique : la maquette « Easily Assure » a servi de banc de test jusqu'à
> mi-2026 ; elle est **abandonnée comme cible** (recadrage juin 2026). Ne plus
> raisonner « Easily ».
## Architecture en couches
```
RawSession (couche 0) — capture événements + screenshots (Agent V1 Windows)
ScreenState (couche 1) — états d'écran à plusieurs niveaux d'abstraction
UIElement (couche 2) — détection sémantique (OCR / template / VLM)
State Embedding (couche 3) — fusion multi-modale + index FAISS
Workflow Graph (couche 4) — nœuds, transitions, résolution de cibles par la vue
```
### Topologie runtime réelle (POC clinique)
```
[Léa — client Windows sur poste TIM] ~15-19 postes enrôlés
│ (HTTPS/Bearer, sortant uniquement)
[DGX Spark — 192.168.1.178 — serveur clinique unique]
├─ Streaming server (5005) — sessions live, pipeline core, replay
├─ Dashboard Fleet (5001) — enrôlement + build installeur + supervision
├─ Agent Chat (5004)
├─ VWB backend/frontend (5002/3002) — admin
└─ Grounder VLM (Qwen-VL, vLLM/Ollama)
```
Le serveur **n'initie jamais** de connexion vers les postes. Accès distant Dom
via VPN clinique (Stormshield) + SSH cert ; déploiement code par scp/rsync
(le DGX ne fait pas `git pull`).
## État des fonctionnalités (synthèse)
Le détail par module est dans [`docs/STATUS.md`](STATUS.md).
Légende : **opérationnel** (utilisé en clinique, sans régression connue) /
**alpha** (fonctionnel sur cas de référence, peu généralisé) /
**gated** (codé + testé mais désactivé par défaut, activation supervisée) /
**débranché** (code présent mais jamais appelé au runtime) /
**en cours**.
**Opérationnel (tourne en clinique)**
- Capture Windows (Agent V1) + streaming vers le DGX (JPEG + downscale, PII-safe).
- Client Léa autonome : **installeur EXE Windows** (python-embed 3.12.8, sans
Python système, install per-user sans UAC), enrôlement via dashboard Fleet,
démarrage auto (raccourci Bureau optionnel à l'install). Version client
**1.0.2** (upgrade-safe).
- Résilience client RDP/Citrix : watchdog re-affiche le tray si Léa disparaît.
- Streaming server FastAPI (5005). **Sessions live en mémoire**
(`live_session_manager.py`) ; **persistances SQLite/JSONL** en place à côté :
registre d'agents enrôlés, store de logs clients par `machine_id`, mémoire de
résolutions (`replay_memory`), DB workflows VWB.
- **Assainissement PII** couche 1 (regex + structurel, `pii_sanitizer.py`) :
câblé au chokepoint `stream_event`, floute aussi les `focus_*`,
`text_input` → token `[SAISIE]`. Déployé sur le DGX.
- Enrôlement flotte : dashboard construit à la volée le ZIP Léa complet
autoportant avec config (URL serveur / token / machine_id).
- Grounding visuel : résolution **par la vue** au replay — stratégie **VLM-first**
sur les éléments texte, ordre pré-compilé **OCR → template → VLM** (YOLO présent
dans le code mais sans appelant au runtime). Les coords figées ne sont
qu'ultime fallback.
Grounder **Qwen3-VL** câblé (`RPA_GROUNDING_ENGINE=qwen3vl_vllm`) — activé en
override runtime sur le DGX (défaut du repo = legacy Qwen2.5-VL).
**Alpha**
- Construction de workflow graph depuis une session ; matching heuristique.
- Replay E2E supervisé multi-étapes (bien au-delà du 1er succès Notepad d'avril).
- Mode apprentissage : pause + demande d'aide humaine quand la résolution échoue
(l'échec est un signal d'apprentissage, pas un stop en erreur).
- Embeddings CLIP + index FAISS (construit ; **lecture au replay débranchée**).
- **Extraction dossier patient** (`core/extraction/`) : orchestrateur
OCR(valeurs) → VLM(rôles, ancré sur ids OCR = 0 hallucination) → qualité,
persistance en DB VWB. Handler `extract_dossier` dispatché côté serveur.
*Lecture écran→JSON prouvée sur vrai DPI urgences ; à confirmer en usage courant.*
- **Navigation visuelle** (`core/navigation/`, nouveau) : login visuel, grounding
OCR-first, `verify_before`/`verify_after` (vision = validateur). Handler
`navigate` dispatché côté serveur. *Naissant — à éprouver.*
- Web Dashboard (5001), Agent Chat (5004), module auth (Fernet + TOTP),
federation (LearningPack, export/import).
- Visual Workflow Builder (VWB) : catalogue d'actions, import d'anchors Léa,
tests de compétence supervisés, Basic auth LAN. *Les bugs DB runtime
historiques ont été largement corrigés ; durcissement encore en cours.*
**Gated (codé + testé, OFF par défaut — activation supervisée)**
- **MAJ silencieuse client** (canary par poste, DETTE-022) : résolveur canary
serveur + orchestrateur client implémentés et testés. `RPA_AUTO_UPDATE_ENABLED`
OFF ; swap atomique + rollback **implémentés** (`Lea.bat`, marqueur
`UPDATE_READY`) mais **jamais exercés en prod** — revue humaine obligatoire
avant activation.
- **Import auto de l'appris → DB VWB rejouable** (R1) : `RPA_R1_AUTO_IMPORT` OFF.
- **Log shipper client** (remontée auto des logs vers le serveur) : gated OFF.
- PII couche 2 (NER CamemBERT-bio, ONNX CPU côté DGX) : à embarquer, dormant.
**Débranché / en cours**
- Chaîne apprentissage **non bouclée end-to-end** : R1 (pont JSON appris ↔ DB VWB)
et R3 (lecture FAISS au replay) manquants ; apprentissage incrémental débranché ;
savoir siloté par `machine_id`.
- Self-healing / recovery global ; analytics / reporting.
## Limitations connues
- Le replay est validé sur un nombre restreint d'applications ; robustesse
grounding encore un sujet ouvert (dette DETTE-006/010).
- Chaîne apprentissage : capture→workflow marche, mais le **bouclage
observation→rejeu généralisé n'est pas câblé** (R1/R3). FAISS construit mais
jamais lu au replay.
- **Combien de postes exercent réellement le geste complet est non vérifié**
(Phase 0 de mesure en attente). Postes possiblement muets sous RDP/Citrix.
- PII : couche 1 déployée ; couche 2 (NER) dormante. Historique de données
capturées en clair — décision purge/assainissement en attente.
- Asymétrie connue : VWB direct utilise un détecteur d'UI au recording que le
replay sur Léa n'utilise pas (sujet ouvert post-POC, à ne pas « fixer » là).
- 🔴 `POST /api/v3/execute/instruction` pilote l'écran X11 de l'hôte **sans
sandbox/kill-switch** ; atteignable sur LAN clinique. Chantier sandbox (F8.4)
identifié comme prioritaire.
## Démarrage
### Prérequis
- Python 3.10 à 3.12
- Serveur : GPU NVIDIA local. Cible clinique = **DGX Spark** (GB10, mémoire
unifiée, ARM64, headless). Alternative dev = x86 RTX 5070.
- LLM local : **Ollama** (`:11434`) et/ou **vLLM** (grounder Qwen3-VL).
- Windows 10/11 pour le client Agent V1.
### Installation (serveur / dev Linux)
```bash
python3 -m venv .venv # venv unique du repo (svc.sh + unités systemd utilisent .venv)
source .venv/bin/activate # (le DGX historique utilise venv_v3 ; réfs venv_v3 du Makefile = périmées en local)
pip install -r requirements.txt
# Ollama local + modèle VLM
ollama serve &
ollama pull gemma4:latest # modèle VLM par défaut du repo (core/config.py, .env.example)
# Le grounder Qwen3-VL est servi par vLLM, pas par un pull Ollama — voir docs/STATUS.md
cp .env.example .env # ajuster RPA_VLM_MODEL, VLM_ENDPOINT, ports
```
### Lancer les services
Services pilotés par `svc.sh` (source de vérité : `services.conf`).
```bash
./svc.sh status
./svc.sh start boot # groupe nominal (variantes : full, vwb, ou un service seul)
./svc.sh start streaming # streaming server seul (5005)
./svc.sh stop boot
```
| Port | Service |
|---|---|
| 8000 | API Server (upload / traitement core) |
| 5001 | Web Dashboard / Fleet (enrôlement, supervision) |
| 5002 | VWB Backend (Flask) |
| 5003 | Monitoring |
| 5004 | Agent Chat |
| 5005 | Streaming Server (Agent V1 → pipeline core) — canal principal Léa |
| 5006 | Session Cleaner |
| 5099 | Worker VLM (analyse sessions → workflows JSON) — lancé avec le groupe boot |
| 3002 | VWB Frontend (Vite/React) |
### Client Windows (Agent V1) — déploiement clinique
Le client capture souris/clavier/écran et envoie au serveur (sortant uniquement).
Déploiement recommandé via le **dashboard Fleet** (enrôlement + ZIP autoportant),
puis `Installer-Lea.bat` sur le poste (installeur EXE Python-embedded).
```bash
# Build du ZIP complet autoportant (python-embed + source à jour)
./deploy/build_package_full.sh
# Build de l'installeur EXE (Inno Setup, per-user)
./deploy/installer/build_installer.sh
```
## Arborescence du dépôt
```
rpa_vision_v3/
├── agent_v0/
│ ├── agent_v1/ # Client Windows (capture, tray, MAJ, PII-safe logs)
│ └── server_v1/ # FastAPI streaming + pipeline (replay, resolve, PII, extraction)
├── core/
│ ├── detection/ # Cascade OCR / template / YOLO / VLM
│ ├── embedding/ # CLIP + FAISS
│ ├── graph/ # Workflow graphs
│ ├── extraction/ # Lecture écran→JSON (OCR→VLM→qualité) [nouveau]
│ ├── navigation/ # Login visuel, grounding, verify [nouveau]
│ ├── execution/, learning/, auth/, federation/, gpu/
├── server/ # API upload / traitement core (8000)
├── visual_workflow_builder/ # VWB (Flask + React Vite)
├── web_dashboard/ # Dashboard + Fleet
├── tools/ # Outils CLI (session cleaner 5006, POC lecture écran)
├── agent_chat/ # Interface conversationnelle + planner
├── deploy/ # Build ZIP, installeur EXE, systemd, dgx/
├── data/ # Sessions, embeddings, FAISS, apprentissage
├── docs/ # Documentation technique (voir STATUS.md)
├── tests/ # pytest (unit, integration, e2e)
├── services.conf, svc.sh, run.sh
```
## Tests
```bash
source .venv/bin/activate
pytest -m "not slow" -q
pytest tests/integration/ -q
```
Quelques tests legacy sont connus comme cassés — voir `docs/` et la mémoire projet.
## Documentation
- [`docs/STATUS.md`](STATUS.md) — état réel par module (⚠ à réaligner sur juillet 2026)
- [`docs/PLAN_ACTION_SUITE_2026-06-23.md`](PLAN_ACTION_SUITE_2026-06-23.md) — plan consolidé post-livraison clinique
- [`docs/PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md`](PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md) — pourquoi la chaîne apprentissage n'est pas câblée + plan
- [`docs/DESIGN_ANONYMISATION_TOKENS_TYPES_2026-06-28.md`](DESIGN_ANONYMISATION_TOKENS_TYPES_2026-06-28.md) — PII par tokens typés
- [`docs/DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md`](DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md) — MAJ silencieuse canary (gated)
- [`docs/POC/PORTAGE_DGX_SPARK_2026-05-28.md`](POC/PORTAGE_DGX_SPARK_2026-05-28.md) — portage DGX Spark
- [`docs/EXECUTION_LOOP_FLAGS.md`](EXECUTION_LOOP_FLAGS.md), [`docs/CONFORMITE_AI_ACT.md`](CONFORMITE_AI_ACT.md)
## Concepts clés
- **RPA 100 % vision** : l'agent localise un élément par ce qu'il voit
(label + contexte visuel), pas par `x,y`. La vision est **source de vérité** :
les coords servent en exécution, mais l'écran est re-résolu si divergence.
- **Léa apprend, comprend, généralise** — pas record-and-replay.
- **Apprentissage progressif** : shadow → assisté → autonome, supervisé.
- **LLM 100 % local** : Ollama / vLLM sur GPU local. Aucun appel cloud dans le
pipeline par défaut.
## Licence
Propriétaire — tous droits réservés.

View File

@@ -0,0 +1,43 @@
# Flux réseau Léa ↔ DGX — demande DSI (mise en service clinique)
- `Date` : 2026-06-24
- `Objet` : autoriser les postes TIM (client **Léa**) à dialoguer avec le serveur **DGX**, en environnement **VLAN segmenté**.
- `Interlocuteur` : DSI clinique.
---
## 1. Contexte
- Le **serveur DGX** est raccordé au VLAN `192.168.1.0/24`, adresse **`192.168.1.178`** (réservation DHCP / IP statique, MAC `10:B6:76:F0:2F:F4`, gateway `192.168.1.243`).
- Les **postes TIM** exécutent le client **Léa** (agent léger). Léa se connecte **en sortie** vers le DGX : **le poste appelle le serveur**, le serveur n'initie jamais de connexion vers le poste.
- Le réseau étant **segmenté en VLAN** : si les postes sont sur un VLAN différent de celui du DGX, le **routage inter-VLAN** doit autoriser les flux ci-dessous.
- Déploiement **100 % local** : **aucun flux internet** n'est requis pour Léa (pas de cloud, pas de NAT entrant).
## 2. Flux à autoriser — poste TIM → DGX `192.168.1.178` (TCP)
| Port | Service | Usage | Auth applicative |
|------|---------|-------|------------------|
| **5005** | Streaming Léa | **Canal principal** : enrôlement, remontée des captures, réception des étapes | Jeton Bearer |
| **5001** | Dashboard | Enrôler un poste + télécharger l'installeur (navigateur) | Basic (compte `lea`) |
| **5004** | Agent-chat | Orchestration Léa | Jeton |
| *(3002 / 5002)* | Visual Workflow Builder | *Optionnel — poste admin uniquement, non requis sur chaque poste TIM* | Basic (compte `lea`) |
- **Sens** : connexions **sortantes du poste** vers le DGX. Le trafic retour passe par les connexions établies → si le pare-feu DSI est **stateful**, **aucune règle entrante spécifique** n'est à créer.
- Uniquement **TCP unicast** vers `192.168.1.178`. Pas de multicast/broadcast.
## 3. Sécurité
- Tous les services exposés sont **authentifiés** (jeton Bearer ou Basic) → l'ouverture n'expose **aucun service anonyme**.
- **Aucune exposition internet** : pas de NAT entrant, pas de cloud, traitement 100 % local sur le DGX.
- Côté DGX, un **pare-feu local** (`rpa-firewall`) restreint déjà 5004/5005 à une **liste blanche** de sous-réseaux ; il sera **complété avec le sous-réseau du VLAN des postes**.
## 4. Demandes à la DSI
1. **Communiquer le sous-réseau du VLAN des postes TIM** (ex. `192.168.X.0/24`) → pour compléter le pare-feu du DGX.
2. **Autoriser le routage / les ACL inter-VLAN** pour les flux du §2, du VLAN postes vers `192.168.1.178`.
3. **Alternative la plus simple** (si acceptable côté sécurité) : raccorder les postes TIM **sur le même VLAN que le DGX** (`192.168.1.0/24`) → plus aucun réglage inter-VLAN ; le pare-feu DGX les autorise déjà.
## 5. Côté éditeur (déjà en place)
- DGX : 12 services actifs, pare-feu local opérationnel et persistant (survit au reboot).
- Ajout du sous-réseau postes au pare-feu DGX = **une seule règle**, **persistée** et **réversible**, applicable **immédiatement en live** dès que le sous-réseau est connu.

View File

@@ -0,0 +1,82 @@
# Tableau d'actions Dom — finalisation phase pré-clinique
- `Date`: 2026-06-21 (vivant, mis à jour par Claude + Qwen au fil des validations)
- `Objectif`: **à la clinique, Dom ne fait que brancher le DGX + installer Léa.** Tout le reste validé avant.
---
## A. Validé automatiquement — AUCUNE action Dom ✅
| Item | Preuve |
|---|---|
| 9 services RPA actifs **+ enabled** (reboot-persistant) | `systemctl is-enabled` = enabled ×9 |
| Dashboard online, **24 workflows**, KB OK | `/api/system/status`, `/api/knowledge-base/stats` |
| Worker apprentissage armé (idle) | `processing status: running/armed` |
| VWB backend 5002 + frontend 3002 | HTTP 200 |
| Agent-chat 5004 | `/api/status` OK |
| Grounder vLLM | service enabled+active (8000/8001) |
| VM Win11 auto-start | `win11-arm-lea` enabled + Linger=yes |
| WireGuard serveur DGX | `wg-quick@wg0` enabled+active |
| **SSH cert-only** (no password, CA) | prouvé depuis `.40` |
| RDP VM (chaîne tunnel→guest) | prouvé depuis `.40` (LOGON nego OK) |
| Firewall persistant (ports sensibles loopback) | `rpa-firewall` enabled, scan LAN |
| Fleet / enrôlement | ✅ `/api/v1/agents/fleet`**7 machines enrôlées** (corrige Qwen « 0 ») |
| Grounder vLLM **Qwen3-VL-4B** | ✅ enabled+active (8000/8001) = grounder de prod (corrige Qwen « dégradé ») |
| Données : FAISS 13666, anchors 468, 24 wf | ✅ (Qwen, re-vérifié) |
## B. Actions DOM — à faire (avec mon guidage) 👤
| # | Action | État |
|---|---|---|
| 1 | **Box** : port-forward UDP 51820 → 192.168.1.45 (Freebox) | ✅ fait — reste **activer WireGuard** sur le laptop |
| 2 | **Laptop `.11`** : installer bundle SSH (cert) + `DGX-Lea.conf` + lanceur RDP | à faire |
| 3 | **GO** sur les correctifs prod confirmés (tableau B ci-dessous) | à faire |
| 4 | Valider `config.txt` Léa (`.45`+token) avant déploiement VM | à faire |
| 5 | GO design **auto-réparation OVMF** | à faire |
| 6 | (Jour clinique) IP statique Ethernet `.178` + exclusion DHCP `.45` labo | clinique |
| 7 | (Hardening) BIOS DGX **Power-On au retour secteur** | clinique |
### B. Correctifs prod confirmés (cross-validés) — à exécuter sous GO Dom 🔧
| Item | Détail | Owner |
|---|---|---|
| **RPA_SIGNING_KEY** absent `.env.local` | HMAC métadonnées FAISS (« Option A ») | Claude/Qwen + GO |
| **Git DGX pas aligné** (`ec1fb81``cf81ce4c7`) | active auth VWB LAN ; **backup `workflows.db` AVANT reset** | Claude + GO |
| **config.txt Léa non déployée** (VM) | transférer après validation `.45`+token | Qwen + GO |
| **Guest SSH VM (22220)** ne répond pas | sshd guest down (forward OK) ; Léa gérable via RDP sinon | Qwen/Dom |
| **workflows_count** 24/79/37 | expliqué : 24=VWB visuels, 79=catalogue agent-chat, 37=KB/FAISS → **Dom choisit la métrique produit** | clarifié |
## C. En cours de validation — Claude + Qwen (preuve + re-vérif croisée) 🔄
| Item | Owner | État |
|---|---|---|
| WireGuard bout-en-bout (handshake depuis l'extérieur) | Claude | bloqué sur B-1 (box) |
| RDP depuis `.11` | Dom+Claude | bloqué sur B-2 |
| **Chaîne d'apprentissage e2e** (capture→upload→grounding→workflow→replay) | Qwen | à prouver (worker armé, 0 session) |
| **Léa enrôlement fleet** | Claude | ✅ système OK (`/api/v1/agents/fleet`, 2 machines) MAIS `last_seen` pré-panne → **relancer Léa dans la VM** pour re-check |
| **Léa sur VM Win11** | Dom (RDP)+Qwen | à lancer (enrôlée, pas connectée depuis reboot) |
| **Léa sur laptop `.11`** | Dom | bloqué sur B-2 (install) |
| **Léa sur serveur `.40` (Linux)** | Qwen | ✅ faisable (`agent_v1` cross-platform : config Linux, window_info X11/xdotool, orchestrateur Léa-first agent-chat Linux) → à tester |
| Fleet endpoint | Claude | ✅ résolu : `/api/v1/agents/fleet` (Bearer) ou proxy `/api/fleet/fleet` |
| Perf : agent-chat CLIP sur CPU (pas GPU) | Claude/Qwen | à noter (vérifier si voulu) |
## D. Jour clinique — le minimum (objectif atteint si A+B+C verts)
1. **Brancher le DGX** (réseau clinique Ethernet `.178`).
2. **Installer Léa** sur les postes.
→ tout le reste déjà validé et figé.
---
## E. Portage clinique — bascule WiFi `.45` → Ethernet `.178` (point Dom)
**Principe** : sur WireGuard, le DGX est toujours `10.10.0.1`, **indépendant du réseau physique**. `ssh dgx-vpn` + RDP (`DGX_HOST=10.10.0.1`) marchent à l'identique labo et clinique. Le changement d'IP est **invisible** pour l'accès distant.
| Ce qui change à la clinique | Action |
|---|---|
| Routeur clinique forward **UDP 51820 → 192.168.1.178** | Dom + DSI (équivalent du forward Freebox vers `.45`) |
| **Endpoint** config WireGuard = IP publique clinique | Claude régénère `DGX-Lea.conf` quand IP connue |
| DGX bascule sur profil Ethernet `.178` | déjà pré-configuré (`Connexion filaire 3`) |
| Host cert SSH | ✅ déjà : principals incluent `192.168.1.178` |
| CA, SSH cert-only, serveur WG, forward RDP, lanceurs | ✅ config locale DGX → rejouée telle quelle |
**Ne PAS toucher la carte Ethernet `.178` au labo** (consigne Dom). Au labo = WiFi only.

View File

@@ -0,0 +1,83 @@
# Veille — OCR.space Engine 3 vs notre brique OCR locale (2026-07-02)
> Origine : lien trouvé par Dom (https://ocr.space/ocrapi#ocrengine3), analysé par agent de
> recherche web (Claude) le 2026-07-02. Verdict court : **sans suite pour la prod** (cloud /
> on-prem Windows propriétaire, bbox Engine 3 moins précises), mais **3 idées à retenir**,
> dont un bench PP-OCRv5 à faire.
## 1. Ce qu'est OCR.space
**API OCR cloud** (`https://api.ocr.space/parse/image`), JSON, éditée par a9t9 Software —
même éditeur que **UI.Vision RPA** (d'où la proximité ressentie avec notre besoin).
Plans : Free (25 000 req/mois, 1 MB/image), PRO (300 000/mois, 5 MB), PRO PDF (100+ MB).
### Les 3 moteurs
| | Engine 1 | Engine 2 | Engine 3 |
|---|---|---|---|
| Positionnement | le plus rapide, langues asiatiques | « meilleur all-round », auto-détect langue | le plus récent, « précision la plus élevée » |
| Langues | ~25 dont français | latines + chinois | **200+, auto-détection** |
| Spécifique | — | orientations mixtes | **tables → Markdown, manuscrit, cases à cocher** |
| Overlay bbox | précis, rapide | précis, rapide | dispo mais **« not as precise as Engine 1/2 »**, appel **2-3× plus lent** |
| Quotas free | 25 000/mois | idem | **2 500/mois** (compute-intensive) |
| PDF searchable | oui | oui | **non** |
Doc officielle : *« Engine 3 prioritizes OCR accuracy and Markdown output over spatial
precision »* → moteur type « document VLM » orienté texte/structure, **pas** grounding
spatial. **L'inverse de notre besoin RPA** (bbox mot fiables pour cliquer).
### Format de sortie
`isOverlayRequired=true``TextOverlay.Lines[].Words[]` avec `WordText/Left/Top/Width/Height`.
= équivalent de ce que **docTR nous donne déjà** (hiérarchie mots+bbox), gratuit, Apache 2.0.
Paramètres notables : `detectOrientation` (auto-rotation), `scale` (upscale interne images
basse résolution — cas d'école screenshots 96 DPI), `isTable`, `OCREngine=1|2|3` (même JSON
quel que soit le moteur).
## 2. On-premise ?
**« OCR.space Local » existe** (section `#local`) : 100 % offline, mêmes API. MAIS :
- **Windows Server 2022+** (DGX = ARM Linux → VM Windows dédiée rien que pour l'OCR)
- Prix opaque (contact sales ; avis tiers : ~999 $/mois entreprise, non confirmé)
- Boîte noire propriétaire installée par leur support via RDP
- **Engine 3 en local non garanti** (blog on-prem ne mentionne que l'Engine 2)
Sources : https://ocr.space/ocrapi (#local), https://forum.ocr.space/t/how-about-pricing-and-order-ocr-space/28246,
https://www.koncile.ai/en/ressources/ocr-space-test-review
## 3. Verdict (contrainte 100 % local)
**Rien pour la prod.** Cloud exclu (données patient) ; version locale = Windows Server
propriétaire à prix opaque ; le moteur intéressant (Engine 3) a des bbox explicitement
moins précises et plus lentes — rédhibitoire pour du grounding de clic.
**À retenir (transposable chez nous) :**
1. **Upscale ×2 des crops basse résolution avant OCR** (leur param `scale`) — gain connu,
quasi gratuit, mesurable immédiatement.
2. **Schéma overlay unifié multi-moteurs** (Lines→Words {text, bbox, confidence}) en sortie
de toute la cascade → moteurs interchangeables sans toucher l'aval. La seule vraie bonne
idée d'architecture à copier.
3. **Leçon Engine 3** : les moteurs « haute précision texte » sacrifient la précision
spatiale → valide notre split OCR (valeurs+bbox) / VLM (rôles). Ne pas chercher un
moteur unique qui fait les deux.
## 4. Alternatives locales — état de l'art screenshots/UI (sources < 12 mois)
- **PaddleOCR 3.x / PP-OCRv5** — piste n°1. Apache 2.0, 106 langues dont **français**,
**`return_word_box=True` = bbox au niveau mot** (post-merge ponctuation/accents à prévoir).
**Moteur OCR retenu par OmniParser (Microsoft) pour les GUI agents** = validation directe
sur notre cas. Sources : paddleocr.ai (PP-OCRv5 multi-languages), arxiv 2408.00203
(OmniParser), huggingface.co/blog/baidu/ppocrv5
- **Qwen3-VL (déjà déployé vLLM)** — text spotting coords normalisées [0,1000] ; utilisable
en validateur/fallback OCR sans nouveau composant ; moins déterministe qu'un OCR classique.
- **Surya / Surya 2** — très bon mais **licence OpenRAIL-M à seuil de revenus** → risque
licence produit commercial. À écarter ou budgéter.
- Repères 2026 : PP-OCRv5 = meilleure précision/débit généraliste ; docTR/EasyOCR corrects
sur texte numérique mais dépassés en multilingue ; GOT-OCR2/dots.ocr = document, pas UI.
## 5. Actions proposées (non lancées — décision Dom)
1. **Bench PP-OCRv5 (`lang=fr`, `return_word_box=True`) vs docTR/EasyOCR** sur nos vraies
captures DPI (jeu du POC `tools/poc_lecture_ecran.py`, 13 champs GEMSA/CCMU) : précision
petites polices, bbox mot, latence GPU DGX.
2. **Upscale ×2 systématique** des crops avant OCR — à mesurer sur le même bench.
3. **Schéma overlay unifié** en sortie de cascade (docTR/EasyOCR/PP-OCRv5/Qwen3-VL).

View File

@@ -1,4 +0,0 @@
inbox_qwen:200
inbox_codex:392
inbox_claude:277
timestamp:2026-06-08_1625

View File

@@ -1,462 +0,0 @@
=== Coordination loop started 2026-06-08 09:51 ===
[2026-06-08 09:51] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 09:51] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0950_qwen-to-codex-ACK-reprise-3j-et-plan-p1g.md
[2026-06-08 09:51] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-claude_REPRISE-LOOP-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 09:51] 📋 active/: 41 fichiers
=== Coordination loop started 2026-06-08 09:54 ===
[2026-06-08 09:54] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 09:54] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
[2026-06-08 09:54] 📋 active/: 41 fichiers
=== Coordination loop started 2026-06-08 09:57 ===
[2026-06-08 09:57] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 09:57] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
[2026-06-08 09:57] 📋 active/: 41 fichiers
=== Coordination loop started 2026-06-08 10:00 ===
[2026-06-08 10:00] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 10:00] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
[2026-06-08 10:00] 📋 active/: 41 fichiers
=== Coordination loop started 2026-06-08 10:03 ===
[2026-06-08 10:03] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 10:03] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
[2026-06-08 10:03] 📋 active/: 41 fichiers
=== Coordination loop started 2026-06-08 10:06 ===
[2026-06-08 10:06] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
[2026-06-08 10:06] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
[2026-06-08 10:06] 📋 active/: 41 fichiers
[2026-06-08 10:06] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
- `Statut`: open
[2026-06-08 10:06] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
- `Statut`: **GO PROVISIONNEL** (merge + bench)
[2026-06-08 10:09] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
- `Statut`: **GO PROVISIONNEL** (merge + bench)
[2026-06-08 10:32] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1031_claude-to-qwen-codex_INFO-MAJ-ollama-dgx-et-bench-gemma4.md
- `Statut`: INFO avancement (sujet GPU/technos, demande directe Dom)
[2026-06-08 10:32] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1031_claude-to-codex-qwen_INFO-MAJ-ollama-dgx-et-bench-gemma4.md
- `Statut`: INFO avancement (sujet GPU/technos, demande directe Dom)
[2026-06-08 10:35] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1031_claude-to-codex-qwen_INFO-MAJ-ollama-dgx-et-bench-gemma4.md
- `Statut`: INFO avancement (sujet GPU/technos, demande directe Dom)
[2026-06-08 10:38] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1039_claude-to-qwen-codex_ACK-verdict-gemma4-bench12b-lance.md
- `Statut`: ACK
[2026-06-08 10:44] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1048_claude-to-qwen-codex_INFO-bench-gemma4-trio-complet.md
- `Statut`: INFO (clôture du job bench gemma4 demandé par Dom)
[2026-06-08 10:44] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1048_claude-to-codex-qwen_INFO-bench-gemma4-trio-complet.md
- `Statut`: INFO (clôture du job bench gemma4 demandé par Dom)
[2026-06-08 11:02] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1108_claude-to-qwen-codex_ALERTE-uitars-aveugle-grounding-niveau2-casse.md
- `Statut`: ALERTE runtime (à vérifier si chemin exercé) + suite bench
[2026-06-08 11:02] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1108_claude-to-codex-qwen_ALERTE-uitars-aveugle-grounding-niveau2-casse.md
- `Statut`: ALERTE runtime (à vérifier si chemin exercé) + suite bench
[2026-06-08 11:05] 📥 inbox_qwen: +2 nouveau(x) message(s)
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
- `Statut`: open
→ 2026-06-08_1112_claude-to-qwen-codex_ACK-qg-correctif-uitars-attente-go-dom.md
- `Statut`: ACK
[2026-06-08 11:05] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1102_codex-to-claude_MISSION-JOURNEE-lea-live-dgx-dashboard-agents.md
- `Statut`: open
[2026-06-08 11:08] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
- `Statut`: open
[2026-06-08 11:08] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1114_claude-to-codex_ACK-JOURNEE-CAPACITES-AGENTS.md
- `Statut`: ACK + Mission A livrée ; B/C/D en cours
[2026-06-08 11:11] 📥 inbox_qwen: +2 nouveau(x) message(s)
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
- `Statut`: open
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
- `Statut`: open
[2026-06-08 11:11] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1117_claude-to-codex_PLAN-REPARATION-UITARS-MMPROJ-en-cours.md
- `Statut`: ACK + plan en cours
[2026-06-08 11:11] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1116_codex-to-claude_GO-P0-REPARATION-UITARS-MMProj.md
- `Statut`: GO P0
[2026-06-08 11:14] 📥 inbox_qwen: +3 nouveau(x) message(s)
→ 2026-06-08_1142_claude-to-qwen-codex_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
- `Statut`: open
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
- `Statut`: open
[2026-06-08 11:14] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
- `Statut`: GO contrat QG
[2026-06-08 11:14] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1116_codex-to-claude_GO-P0-REPARATION-UITARS-MMProj.md
- `Statut`: GO P0
[2026-06-08 11:17] 📥 inbox_qwen: +3 nouveau(x) message(s)
→ 2026-06-08_1146_claude-to-qwen-codex_ACK-ordre-grounders-vllm-en-cours.md
- `Statut`: ACK
→ 2026-06-08_1142_claude-to-qwen-codex_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
- `Statut`: open
[2026-06-08 11:17] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
- `Statut`: GO contrat QG
[2026-06-08 11:17] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1145_qwen-to-claude-codex_ACK-SOTA-grounders-vllm.md
- `Statut`: ACK INFO + verdict QG flash
[2026-06-08 11:20] 📥 inbox_qwen: +3 nouveau(x) message(s)
→ 2026-06-08_1146_claude-to-qwen-codex_ACK-ordre-grounders-vllm-en-cours.md
- `Statut`: ACK
→ 2026-06-08_1142_claude-to-qwen-codex_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
- `Statut`: open
[2026-06-08 11:20] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
- `Statut`: GO contrat QG
[2026-06-08 11:20] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1145_qwen-to-claude-codex_ACK-SOTA-grounders-vllm.md
- `Statut`: ACK INFO + verdict QG flash
[2026-06-08 11:23] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
- `Statut`: INFO réorientation (impacte mission B/C)
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
- `Statut`: GO contrat QG
[2026-06-08 11:23] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1145_qwen-to-claude-codex_ACK-SOTA-grounders-vllm.md
- `Statut`: ACK INFO + verdict QG flash
[2026-06-08 11:29] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1200_claude-to-qwen-codex_RESULTAT-REPARATION-UITARS-BENCH.md
- `Statut`: RESULTAT (suite GO P0 Codex)
[2026-06-08 11:29] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1200_claude-to-codex-qwen_RESULTAT-REPARATION-UITARS-BENCH.md
- `Statut`: RESULTAT (suite GO P0 Codex)
[2026-06-08 11:32] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1202_qwen-to-codex-claude_QG-RESULTAT-UITARS.md
- `Statut`: RESULTAT validé + GO correctif code gate vision
[2026-06-08 11:35] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1202_qwen-to-codex-claude_QG-RESULTAT-UITARS.md
- `Statut`: RESULTAT validé + GO correctif code gate vision
[2026-06-08 11:38] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1202_qwen-to-codex-claude_QG-RESULTAT-UITARS.md
- `Statut`: RESULTAT validé + GO correctif code gate vision
[2026-06-08 11:41] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1210_claude-to-codex-qwen_DIAGNOSTIC-cause-racine-uitars-non-detecte-GO-cablage.md
- `Statut`: diagnostic + GO Dom pour câblage (QG Qwen demandé sur le diff)
[2026-06-08 11:44] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1141_codex-to-qwen_MANDAT-AUDIT-ANTI-BORDELISATION.md
- `Statut`: open
[2026-06-08 11:47] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1141_codex-to-qwen_MANDAT-AUDIT-ANTI-BORDELISATION.md
- `Statut`: open
[2026-06-08 11:50] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1141_codex-to-qwen_MANDAT-AUDIT-ANTI-BORDELISATION.md
- `Statut`: open
[2026-06-08 11:50] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_1217_qwen-to-codex_QWEN-PLAN-TESTS-CHEMINS-REELS.md
- `Statut`: Plan de tests
→ 2026-06-08_1216_qwen-to-codex_QWEN-QG-P0-BLOQUANTS-FIN-SEMAINE.md
- `Statut`: QG P0 bloquants
[2026-06-08 11:53] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1225_claude-to-qwen-codex_RESULTAT-cablage-health-vision-diff-QG.md
- `Statut`: livré, commit `d00fe7b00` — **QG Qwen demandé sur le diff ci-dessous**
[2026-06-08 11:53] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1225_claude-to-codex-qwen_RESULTAT-cablage-health-vision-diff-QG.md
- `Statut`: livré, commit `d00fe7b00` — **QG Qwen demandé sur le diff ci-dessous**
[2026-06-08 11:59] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1156_codex-to-qwen_QG-INSTALL-DGX-PROPRE-COMPLETE.md
- `Statut`: open
[2026-06-08 11:59] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1230_claude-to-codex_ACK-mission-install-dgx-lead-pris-plans-en-production.md
- `Statut`: ACK + production lancée (fan-out)
[2026-06-08 11:59] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1156_codex-to-claude_MISSION-INSTALL-DGX-PROPRE-COMPLETE.md
- `Statut`: open
[2026-06-08 12:02] 📥 inbox_qwen: +3 nouveau(x) message(s)
→ 2026-06-08_claude-to-qwen_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
→ 2026-06-08_claude-to-qwen_PLAN-LEA-LIVE-GRANDEUR-NATURE.md
- `Statut`: actif — protocole écrit, **aucune exécution incluse dans ce document**
→ 2026-06-08_1159_codex-to-qwen_PARALLELISATION-QG-LANES.md
- `Statut`: open
[2026-06-08 12:02] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
→ 2026-06-08_PLAN-LEA-LIVE-GRANDEUR-NATURE.md
- `Statut`: actif — protocole écrit, **aucune exécution incluse dans ce document**
[2026-06-08 12:05] 📥 inbox_qwen: +4 nouveau(x) message(s)
→ 2026-06-08_1240_claude-to-qwen-codex_RESULTAT-bench-vllm-grounders-verdict-final.md
- `Statut`: RESULTAT (clôture chantier grounder du jour)
→ 2026-06-08_claude-to-qwen_AUDIT-DASHBOARD-AGENTS-SECU.md
→ 2026-06-08_claude-to-qwen_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
→ 2026-06-08_claude-to-qwen_PLAN-LEA-LIVE-GRANDEUR-NATURE.md
- `Statut`: actif — protocole écrit, **aucune exécution incluse dans ce document**
[2026-06-08 12:05] 📥 inbox_codex: +4 nouveau(x) message(s)
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
- `Statut`: RESULTAT (clôture chantier grounder du jour)
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
→ 2026-06-08_AUDIT-DASHBOARD-AGENTS-SECU.md
→ 2026-06-08_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
[2026-06-08 12:08] 📥 inbox_codex: +4 nouveau(x) message(s)
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
- `Statut`: QG validé + GO workpacks
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
- `Statut`: RESULTAT (clôture chantier grounder du jour)
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
[2026-06-08 12:11] 📥 inbox_codex: +4 nouveau(x) message(s)
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
- `Statut`: QG validé + GO workpacks
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
- `Statut`: RESULTAT (clôture chantier grounder du jour)
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
[2026-06-08 12:14] 📥 inbox_codex: +4 nouveau(x) message(s)
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
- `Statut`: QG validé + GO workpacks
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
- `Statut`: RESULTAT (clôture chantier grounder du jour)
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
[2026-06-08 12:17] 📥 inbox_codex: +4 nouveau(x) message(s)
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
- `Statut`: QG validé + GO workpacks
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
- `Statut`: RESULTAT (clôture chantier grounder du jour)
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
[2026-06-08 15:18] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1515_codex-to-qwen_QG-GO-DOM-OPTION-A-WPAB-P1G-LEA.md
- `Statut`: mandat QG actif
[2026-06-08 15:18] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1515_codex-to-claude_GO-DOM-OPTION-A-WPAB-P1G-LEA.md
- `Statut`: GO execution borne
[2026-06-08 15:20] 📥 inbox_codex: +15 nouveau(x) message(s)
→ 2026-05-29_qwen-to-codex_ACK-ADDENDUM-VWB-PASSERELLE.md
- `Statut`: ACK avec réserve
→ 2026-05-29_qwen-to-codex_ACK-handoff-patch3-reprise.md
→ 2026-05-29_qwen-to-codex_ACK-patch3bis-post-impl.md
- `Statut`: ACK
→ 2026-05-29_qwen-to-codex_ACK-patch4-apply-allow-list.md
- `Statut`: ACK
→ 2026-05-29_qwen-to-codex_ACK-PATCH-A-REPONSES-MAPPING.md
- `Statut`: ACK
→ 2026-05-29_qwen-to-codex_ACK-PATCH-B-PLAN-PATCH-C.md
- `Statut`: ACK + plan
→ 2026-05-29_qwen-to-codex_ACK-PATCH-correction-semantique-altf4.md
- `Statut`: ACK PATCH
→ 2026-05-29_qwen-to-codex_ACK-RECADRAGE-LEA-DIRECT.md
- `Statut`: ACK
→ 2026-05-29_qwen-to-codex_ACK-REGLE-GARDE-FOUS-VISION.md
- `Statut`: ACK
→ 2026-05-29_qwen-to-codex_REVUE-batch1-apply-yaml-observed.md
- `Statut`: ACK avec réserves mineures
→ 2026-06-01_qwen-to-codex-claude_GO-P1-LEA-SHADOW-NOGO-LEVE.md
- `Statut`: **GO — NO-GO LEVÉ**
→ 2026-06-01_qwen-to-codex-claude_LEVEE-GO-P1-SEMANTIQUE.md
- `Statut`: **GO CONFIRMÉ — conditionnel levé**
→ 2026-06-01_qwen-to-codex_DIAGNOSTIC-P0-SINGLE-INFLIGHT.md
- `Statut`: DIAGNOSTIC + PATCH PROPOSE
→ 2026-06-01_qwen-to-codex_LIVRAISON-GARDE-REPLAY-SESSION.md
- `Statut`: LIVRAISON
→ 2026-06-08_1518_claude-to-codex_ACK-GO-execution-ordre-eta.md
- `Statut`: ACK, exécution démarrée
[2026-06-08 15:24] 📥 inbox_qwen: +2 nouveau(x) message(s)
→ 2026-06-08_1522_claude-to-qwen-codex_RESULTAT-P1g-merge-commit.md
- `Statut`: livré, commit `0e215da84`
→ 2026-06-08_claude-to-qwen_RAPPORT-PREFLIGHT-DGX-OPTION-A.md
> Statut global : **préflight VERT**, mais **bloqueur de transfert** identifié (§2) à trancher par Dom avant tout clone. Le dossier cible n'a PAS été créé/cloné (décision transfert en attente). Parent `/home/aivanov/ai/` créé. Artefacts systemd + `.env.local` modèle rendus pour revue dans `/tmp/rpa_systemd_optionA/` sur le DGX.
[2026-06-08 15:24] 📥 inbox_codex: +3 nouveau(x) message(s)
→ 2026-06-08_1522_claude-to-codex-qwen_RESULTAT-P1g-merge-commit.md
- `Statut`: livré, commit `0e215da84`
→ 2026-06-08_1525_qwen-to-codex_QG-4-LANES-P1G-GO.md
- `Statut`: QG 4 lanes
→ 2026-06-08_RAPPORT-PREFLIGHT-DGX-OPTION-A.md
> Statut global : **préflight VERT**, mais **bloqueur de transfert** identifié (§2) à trancher par Dom avant tout clone. Le dossier cible n'a PAS été créé/cloné (décision transfert en attente). Parent `/home/aivanov/ai/` créé. Artefacts systemd + `.env.local` modèle rendus pour revue dans `/tmp/rpa_systemd_optionA/` sur le DGX.
[2026-06-08 15:24] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1525_qwen-to-claude-codex_QG-P1G-VALIDE.md
- `Statut`: GO — commit validé
[2026-06-08 15:27] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1528_qwen-to-codex-claude_QG-PREFLIGHT-DGX.md
- `Statut`: QG préflight GO + verdict transfert
[2026-06-08 15:27] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1528_qwen-to-claude-codex_QG-PREFLIGHT-DGX.md
- `Statut`: GO préflight + GO option push
[2026-06-08 15:44] 📥 inbox_qwen: +2 nouveau(x) message(s)
→ 2026-06-08_1543_codex-to-qwen_QG-DGX-donnees-entrainees.md
- `Statut`: mandat QG actif
→ 2026-06-08_1545_claude-to-qwen-codex_RESULTAT-P0-securite-WPA-WPB.md
- `Statut`: livré (2 commits) — QG Qwen demandé
[2026-06-08 15:44] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1545_claude-to-codex-qwen_RESULTAT-P0-securite-WPA-WPB.md
- `Statut`: livré (2 commits) — QG Qwen demandé
[2026-06-08 15:44] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1543_codex-to-claude_ADDITIF-DGX-transfert-donnees-entrainees.md
- `Statut`: open
[2026-06-08 15:45] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1546_claude-to-codex_ACK-additif-trained-artifacts-en-production.md
- `Statut`: ACK, agent lancé
[2026-06-08 15:48] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_claude-to-qwen_MANIFESTE-TRANSFERT-TRAINED-ARTIFACTS-DGX.md
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert exécuté.
[2026-06-08 15:48] 📥 inbox_codex: +2 nouveau(x) message(s)
→ 2026-06-08_1548_qwen-to-codex-claude_QG-WPA-WPB-GO.md
- `Statut**: **GO — WP-A et WP-B validés**
→ 2026-06-08_MANIFESTE-TRANSFERT-TRAINED-ARTIFACTS-DGX.md
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert exécuté.
[2026-06-08 15:51] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1550_qwen-to-codex-claude_QG-MANIFESTE-TRAINED-ARTIFACTS.md
- `Statut**: **GO avec réserves** (75 Mo, 7283 fichiers)
[2026-06-08 15:51] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1550_qwen-to-claude-codex_QG-MANIFESTE-TRAINED-ARTIFACTS.md
- `Statut**: GO avec réserves
[2026-06-08 16:07] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_1606_codex-to-qwen_ACK-QG-trained-artifacts-et-WPAB.md
- `Statut`: ACK + attente V2 Claude
[2026-06-08 16:07] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1606_codex-to-claude_ACK-QG-trained-artifacts-V2-et-consolidation.md
- `Statut`: action demandee
[2026-06-08 16:09] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1608_claude-to-codex_ACK-manifeste-V2-en-production.md
- `Statut`: ACK, agent lancé
[2026-06-08 16:12] 📥 inbox_qwen: +1 nouveau(x) message(s)
→ 2026-06-08_claude-to-qwen_MANIFESTE-V2-TRAINED-ARTIFACTS-DGX.md
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert ni rewrite exécuté. Les commandes ci-dessous sont PROPOSÉES.
[2026-06-08 16:12] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_MANIFESTE-V2-TRAINED-ARTIFACTS-DGX.md
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert ni rewrite exécuté. Les commandes ci-dessous sont PROPOSÉES.
[2026-06-08 16:15] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1610_qwen-to-codex-claude_QG-MANIFESTE-V2-TRAINED-ARTIFACTS.md
- `Statut**: **GO avec réserves** (~306 Mo)
[2026-06-08 16:15] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1610_qwen-to-claude-codex_QG-MANIFESTE-V2.md
- `Statut**: GO avec réserves
[2026-06-08 16:27] 📥 inbox_codex: +1 nouveau(x) message(s)
→ 2026-06-08_1625_qwen-to-codex-claude-dom_PROPOSITION-8-PISTES.md
- `Statut`: PROPOSITION — GO collectif requis
[2026-06-08 16:30] 📥 inbox_claude: +1 nouveau(x) message(s)
→ 2026-06-08_1625_qwen-to-claude-codex-dom_PROPOSITION-8-PISTES.md
- `Statut`: PROPOSITION — GO collectif requis

View File

@@ -0,0 +1,81 @@
# Rôles — Coordination multi-agents rpa_vision_v3
- `Date`: 2026-06-08
- `Auteur`: Qwen
- `Statut`: actif
---
## Équipe
| Agent | Rôle | Inbox | Sortie attendue |
|---|---|---|---|
| **Dom** | Propriétaire du projet, décideur final, filtre produit | Direct | GO/NOGO, arbitrages, validation démo |
| **Codex** | Coordinateur, orchestration, arbitrages techniques, synthèse | `inbox_codex/` | Missions, QG consolidés, plans, décisions |
| **Claude** | Implémentation, patches, benchmarks, plans techniques | `inbox_claude/` | Commits, RESULTAT, plans, analyses |
| **Qwen** | Quality Gate (QG), audit, historien, garde-fou, propositions | `inbox_qwen/` | Verdicts GO/NOGO, audits, synthèses, docs |
| **Gemini** | Consultation ponctuelle, recherche externe | `inbox_gemini/` | Recherche, analyse comparative |
---
## Règles de coordination
### 1. Inbox routing
- 1 question = 1 fichier dans l'inbox du destinataire
- Format : `YYYY-MM-DD_HHMM_auteur-to-destinataire_SUJET.md`
- Réponse courte et actionnable, pas de rapport long sauf demande explicite
### 1bis. Orchestration Codex
- Codex est l'orchestrateur actif du projet.
- Tant que Claude et Qwen ont des loops de coordination actifs, Codex doit leur donner en continu une prochaine tâche actionnable.
- Exception unique : Codex attend explicitement un feu vert de Dom ou un QG bloquant avant d'autoriser l'étape suivante.
- Si une exécution est bloquée, Codex doit quand même distribuer du travail non destructif utile : préparation, QG, audit read-only, rollback, plan de tests, registre décisions.
- Claude ne doit pas rester sans mission d'exécution/preuves ou préparation technique.
- Qwen ne doit pas rester sans mission de QG, audit, contradiction check ou registre.
### 2. Statuts
| Statut | Signification |
|---|---|
| `open` | En attente de réponse |
| `ACK` | Lu et pris en compte |
| `RESULTAT` | Livraison d'un résultat |
| `GO` / `NO-GO` | Verdict qualité |
| `DRAFT` | Brouillon, pas encore prêt pour revue |
| `archived` | Archivé (ne pas supprimer) |
### 3. Règle QG
- Pas d'action structurelle sans QG (Quality Gate) explicite
- Pas de promotion de competence sans QG + GO Dom
- Qwen signale à Dom toute contradiction entre agents
### 4. Règles de sécurité
- Pas de secret en clair dans les messages de coordination
- Pas de replay autonome sans Dom devant Windows
- Pas de code mort supprimé sans GO Dom explicite
- Pas d'activation modèle/grounder sans bench + QG + GO Dom
### 5. Capitalisation
- Chaque avancée importante doit être capitalisée dans `active/` ou `registre/`
- Les inboxes sont un flux, pas une mémoire
- `syntheses/` = rapports d'inventaires et benchmarks
- `registre/` = décisions formelles
### 6. Sessions
- Qwen reste dans la session longue (historien, garde le fil complet)
- Codex et Claude ont chacun leur session propre
- Si Qwen voit des contradictions entre les instructions de Codex/Claude, il signale à Dom avant d'agir
---
## Flux de travail typique
```
Codex → mission → Claude/ Qwen
Claude → implémentation → commit → RESULTAT → Codex/Qwen
Qwen → QG → GO/NO-GO → Codex
Codex → synthèse → Dom → GO/NOGO
```
---
*Document vivant — mettre à jour quand les rôles évoluent.*

View File

@@ -0,0 +1,233 @@
# RUNBOOK DGX - Check post-deploiement et post-reboot POC
- Date: 2026-06-17
- Responsable coordination: Codex
- Scope: POC DGX uniquement
- Cible labo actuelle: DGX `192.168.1.45`
- Cible clinique preparee: Ethernet `192.168.1.178/24`, gateway `192.168.1.243`, DNS `192.168.1.9` puis `192.168.1.8`, IPv6 inactive, profil inactif tant que Dom/Codex ne donnent pas le GO.
## Objectif
Valider qu'apres deploiement puis redemarrage du DGX, toute la chaine POC reste fonctionnelle sans dependance `localhost` cote utilisateur:
- dashboard;
- VWB;
- import workflow;
- anchors visibles;
- agent-chat / "Discuter avec Lea";
- streaming/fleet;
- worker/replay;
- installateur Lea autonome;
- configuration reseau DGX sans bascule clinique accidentelle.
## Regle de conduite
- Dom execute uniquement les gestes UI de la section "Check Dom".
- Les agents techniques collectent les preuves systeme et reseau.
- Toute divergence est notee avec: etape, symptome, impact, rollback propose.
- Pas de changement reseau clinique actif pendant le check labo: le Wi-Fi reste le lien de test.
## 0. Prerequis avant reboot
| Point | Attendu | Statut |
|---|---|---|
| Branche DGX | `poc-dgx` | A confirmer par Claude |
| Commit DGX | `9605cc9d9` ou commit ulterieur explicitement valide | A confirmer par Claude |
| Commits POC presents | `667575c3a`, `9605cc9d9` | A confirmer par Claude/Qwen |
| Deploy propre | pas de fichier parasite embarque dans le runtime POC | A confirmer |
| Donnees runtime | backup `backend/instance/workflows.db` avant reset/deploy, restore apres deploy, hash/size verifies | A confirmer par Claude |
| Donnees non trackees | `data/workflows` (symlink) + donnees VWB sous `visual_workflow_builder/backend/data/` non supprimes | A confirmer |
| VWB build | bundle reconstruit depuis `poc-dgx` | A confirmer |
| Installateur Lea | version `1.0.1`, Python embedded obligatoire | A confirmer |
| Profil Ethernet clinique | prepare si besoin, `autoconnect off`, non active | A confirmer par Qwen |
| Enrolement VM Lea | VM/poste pointe vers DGX labo `192.168.1.45` pour le test, pas vers l'URL cloud | A confirmer |
NO-GO reboot si le deploiement n'est pas fige, si le DGX ne pointe pas sur le bon commit, ou si `workflows.db`/anchors/workflows ne sont pas verifies apres deploy.
## 1. Baseline avant reboot
Un agent technique releve l'etat avant redemarrage:
| Famille | Controle | Attendu |
|---|---|---|
| Services principaux | dashboard, VWB backend, streaming, agent-chat, worker/fleet actifs | actifs |
| Ports utilisateur | `5001`, `5004`, `5005` disponibles depuis le LAN autorise | OK |
| Ports internes | `5002`, `8000`, `11434` selon architecture DGX | OK ou documente |
| Dashboard | acces par `http://192.168.1.45:5001` | login ou 401 attendu, pas page blanche |
| Streaming | health `:5005` | 200/healthy |
| Auth streaming | route protegee sans token | 401 attendu |
| Agent chat | status/health `:5004` | 200/online |
| VWB | frontend charge depuis IP DGX | OK |
| Bundle VWB | aucune URL active `localhost`/`127.0.0.1` | OK |
| VM Windows Lea | Lea pointe vers DGX `192.168.1.45` pour `5004/5005` | OK |
Si un point est deja rouge avant reboot, on corrige avant de redemarrer. Le reboot ne doit pas servir a masquer une panne.
## 2. Reboot DGX
1. Claude annonce le debut du reboot dans la coordination.
2. Codex garde la coordination ouverte.
3. Attendre le retour reseau du DGX sur le Wi-Fi labo.
4. Ne pas activer le profil Ethernet clinique pendant ce cycle.
5. Claude poste l'heure de retour et le premier etat des services.
NO-GO immediat si le DGX ne revient pas sur le reseau labo ou si le dashboard reste inaccessible plus de 10 minutes apres retour reseau.
## 3. Check technique post-reboot
### 3.1 Services
Noms constates/attendus a verifier sur le DGX. Ne pas supposer qu'un service existe: si une unite est absente, le noter et verifier si sa fonction est couverte par un autre service.
- `rpa-streaming`
- `rpa-agent-chat`
- `rpa-vision-v3-dashboard`
- `rpa-vision-v3-vwb-frontend`
- `rpa-vision-v3-vwb-backend`
- `rpa-vision-v3-worker`
- `rpa-vision-v3-stream-worker` si installe, sinon documenter l'absence
- `rpa-vision-v3-api`
- `rpa-grounding` ou service grounder equivalent si present; sinon documenter le fallback
- `rpa-vision-v3-healthcheck.timer` si actif dans le deploiement final; sinon ne pas bloquer mais documenter
Attendu:
- services POC critiques actifs apres reboot;
- services POC critiques enabled si necessaires au demarrage automatique;
- pas de boucle restart;
- logs recents sans traceback bloquant;
- healthcheck timer actif ou decision documentee si non utilise;
- VWB frontend servi par le vrai frontend POC (`frontend_v4`), pas l'ancien `frontend/`.
- Ne jamais utiliser `git clean -xfd` sur DGX pendant la sequence POC: cela detruirait les donnees runtime non trackees.
### 3.2 Reseau et ports
| Port | Usage | Attendu labo |
|---|---|---|
| `5001` | dashboard / VWB integre | accessible via `192.168.1.45` |
| `5004` | agent-chat / ChatWindow Lea | accessible depuis VM Windows autorisee |
| `5005` | streaming/fleet/replay | accessible depuis VM Windows autorisee |
| `5002` | VWB backend si separe | conforme au deploiement DGX |
| `8000` | upload/API core si actif | conforme au deploiement DGX |
| `11434` | Ollama local DGX | local/interne sauf decision contraire |
Attendu:
- aucun appel navigateur produit vers `localhost` ou `127.0.0.1`;
- `5004/5005` repondent depuis la VM Windows;
- le dashboard charge toutes ses ressources depuis l'IP DGX;
- la carte Ethernet clinique reste inactive ou non connectee pendant le check labo.
### 3.3 Endpoints et fonctions serveur
| Controle | Attendu |
|---|---|
| Dashboard `5001` | page login/auth visible, pas 500 |
| Streaming `5005/health` | healthy |
| Streaming route protegee sans token | 401 |
| Fleet machines | VM Lea visible ou reenrolement documente |
| Enrolement VM Lea | `config.txt`/runtime pointe vers DGX, pas vers URL cloud |
| Replay next | repond correctement pour machine enrollee |
| Agent chat `5004` | online et session chat possible |
| VWB catalog/workflows | liste chargee |
| Anchors API | upload/thumbnail/original accessibles depuis IP DGX |
| Worker | pas de backlog bloquant, pas de crash loop |
| Modele/grounding | service disponible ou fallback documente |
## 4. Check Dom UI post-reboot
Dom ne fait que ces gestes:
| Etape | Geste Dom | GO | NO-GO |
|---|---|---|---|
| T1 Dashboard | Ouvrir l'URL DGX fournie | page login/dashboard visible | page blanche, site inaccessible, erreur 500 |
| T2 Chat Lea | Dans la VM, ouvrir "Discuter avec Lea", envoyer "Bonjour" | reponse en quelques secondes | pas connectee, erreur serveur, fermeture fenetre |
| T3 Bulles action | Demander une action simple | bulles/progression visibles | aucune progression visible pendant une action lancee |
| T4 Import VWB | Importer un workflow JSON Lea | succes, workflow visible | erreur, rien ne se passe |
| T5 Anchors | Ouvrir une etape avec ancre | image d'ancre/crop visible | image absente, plein ecran au lieu de crop |
| T6 Replay supervise | Lancer un replay safe/supervise | execution ou demande de confirmation | clic hors cible, blocage, absence de reaction |
| T7 Reconnexion apres reboot | Fermer/rouvrir Lea sur VM | reconnecte DGX sans reconfig manuelle | demande Python/outils externes ou config manuelle non prevue |
## 5. Criteres GO/NO-GO
GO livraison labo si:
- DGX revient apres reboot;
- services critiques actifs;
- dashboard accessible via IP DGX;
- VWB charge sans `localhost`;
- chat Lea fonctionne depuis VM;
- import workflow OK;
- anchors visibles;
- replay supervise OK ou comportement d'arret/confirmation conforme;
- installateur Lea autonome confirme;
- profil Ethernet clinique non active par erreur.
NO-GO livraison si un des points suivants echoue:
- DGX ne revient pas proprement apres reboot;
- dashboard/VWB inaccessible;
- `5004` ou `5005` indisponible depuis la VM;
- "Discuter avec Lea" ne fonctionne pas;
- installateur demande Python ou un outil externe;
- VWB appelle encore `localhost` cote navigateur;
- import/anchors/replay cassent;
- configuration Ethernet clinique activee accidentellement pendant les tests labo.
## 6. Preuves a archiver
Claude fournit:
- commit DGX courant;
- preuve backup/restore `workflows.db` si reset/deploy effectue;
- confirmation que `data/workflows` et les vrais chemins `visual_workflow_builder/backend/data/anchors`, `visual_workflow_builder/backend/data/anchor_images`, `visual_workflow_builder/backend/instance/workflows.db` sont presents apres deploy/reboot;
- liste services actifs/enabled;
- ports et endpoints avec resultats;
- resultat grep bundle no-localhost;
- resultat test VM Lea chat;
- resultat import/anchors/replay;
- chemin de l'artefact installateur Lea 1.0.1;
- incidents et rollback si besoin.
Qwen fournit:
- controle croise commits/fichiers parasites;
- matrice GO/NO-GO post-reboot;
- verification profil Ethernet clinique inactif;
- etat VM Windows 11 ARM DGX ou decision fallback VMware clinique.
Codex consolide:
- verdict final pret/pas pret;
- liste des anomalies restantes;
- decision de passage au check site ou retour correction.
## 7. Rollback minimal
Si VWB regresse:
- revenir au dernier commit POC valide connu;
- redeployer le bundle precedent;
- redemarrer uniquement dashboard/VWB backend;
- refaire T1, T4, T5.
Si agent-chat regresse:
- verifier service `rpa-agent-chat`;
- verifier env `5004`/feedback bus;
- redemarrer agent-chat puis streaming si necessaire;
- refaire T2/T3.
Si streaming/fleet regresse:
- verifier service `rpa-streaming`;
- verifier token/auth;
- verifier reenrolement VM;
- refaire T2/T6.
Si reseau clinique s'active par erreur:
- revenir au profil Wi-Fi labo;
- desactiver le profil Ethernet clinique;
- confirmer retour dashboard `192.168.1.45`;
- ne reprendre les tests qu'apres stabilisation.

View File

@@ -0,0 +1,97 @@
# Runbook — Test Lea live grandeur nature
- `Date`: 2026-06-08
- `Auteur`: Qwen (draft)
- `Statut`: DRAFT — en attente preflight vert + GO Dom
- **Interdit** : ❌ Replay autonome
---
## Prérequis
| # | Vérification | Commande / Action | Critère GO |
|---|---|---|---|
| 1 | Windows cible visible | `ping DESKTOP-58D5CAC_windows` | ✅ Répond |
| 2 | httpx OK côté Windows | `python -c "import httpx; print('OK')"` dans venv Windows | ✅ Import OK |
| 3 | Agent V1 en cours | Vérifier processus Windows agent V1 | ✅ Running |
| 4 | DGX Ollama accessible | `curl http://localhost:11434/api/tags` | ✅ Répond |
| 5 | Workflows acquis retrouvés | Dashboard → workflows chargés | ✅ ≥ 1 workflow visible |
| 6 | Dom présent physiquement | Devant le poste Windows | ✅ Confirmé |
---
## Exécution
### Étape 1 : Préflight (NON destructif)
```bash
# Sur le poste dev Linux
curl -X POST http://localhost:5005/api/v1/traces/stream/replay/preflight \
-H "Content-Type: application/json" \
-d '{"machine_id": "DESKTOP-58D5CAC_windows"}'
```
**Attendu** : `workflow_known: true`, aucun blocage G1-G6.
### Étape 2 : Capture Shadow supervisée
1. Ouvrir le dashboard Lea
2. Démarrer un apprentissage supervisé
3. Scénario safe : Notepad → Enregistrer → Explorateur → Easily Assure
4. **Ne pas** déclencher de replay autonome
5. Observer et collecter les preuves en temps réel
### Étape 3 : Collecte des preuves
| Preuve | Où la trouver | Critère |
|---|---|---|
| `live_events.jsonl` | `data/training/live_events.jsonl` | Non vide, timestamps cohérents |
| `learn_*.json` | `agent_chat/state/learn_*.json` | Présent, state non vide |
| Shadow understanding | `agent_chat/state/` | Compréhension non vide |
| Screenshots avant/après | Captures manuelles | Chaque action documentée |
| Rapports preflight | Sortie curl étape 1 | `workflow_known: true` |
### Étape 4 : Arrêt propre
1. Stopper l'apprentissage via le dashboard
2. Vérifier que les fichiers de state sont cohérents
3. Ne pas lancer de replay automatique
---
## Critères GO/NOGO
### GO (tous doivent être verts)
| ID | Critère |
|---|---|
| G1 | Préflight `workflow_known: true` |
| G2 | Windows cible joignable |
| G3 | `httpx` import OK côté Windows |
| G4 | DGX Ollama accessible (`localhost:11434`) |
| G5 | Workflows acquis retrouvés (≥ 1) |
| G6 | Dom confirme présent physiquement |
### NOGO (un seul suffit)
| ID | Critère |
|---|---|
| N1 | Préflight KO |
| N2 | Windows injoignable |
| N3 | `httpx` absent |
| N4 | DGX Ollama inaccessible |
| N5 | Dom absent devant Windows |
| N6 | Replay autonome demandé |
---
## Rollback
1. Arrêter proprement l'apprentissage
2. Ne pas supprimer les fichiers de state
3. Documenter le point d'arrêt
4. Rapporter à Codex/Claude
---
*Ce runbook est un draft. Il sera validé après preflight vert et GO Dom.*

View File

@@ -0,0 +1,208 @@
diff --git a/tests/unit/test_dashboard_routes.py b/tests/unit/test_dashboard_routes.py
index 3f8f0528c..69cc1b2fb 100644
--- a/tests/unit/test_dashboard_routes.py
+++ b/tests/unit/test_dashboard_routes.py
@@ -212,6 +212,58 @@ class TestDashboardRoutes:
data = resp.get_json()
assert 'workflows' in data
+ def test_workflows_list_reads_vwb_db(self, client, monkeypatch, tmp_path):
+ """Régression red-gate : /api/workflows reflète la base VWB v3, pas 0.
+
+ Avant correctif l'endpoint globait un store JSON vide et renvoyait
+ toujours total:0. On construit une DB VWB minimale (schéma canonique
+ workflows + steps) et on vérifie que l'endpoint expose le compte réel.
+ """
+ import sqlite3
+ from pathlib import Path
+
+ db_path = tmp_path / "instance" / "workflows.db"
+ db_path.parent.mkdir(parents=True, exist_ok=True)
+ conn = sqlite3.connect(str(db_path))
+ conn.execute(
+ "CREATE TABLE workflows (id VARCHAR(64) PRIMARY KEY, name VARCHAR(255), "
+ "description TEXT, created_at DATETIME, updated_at DATETIME, "
+ "is_active BOOLEAN, source VARCHAR(64), review_status VARCHAR(32))"
+ )
+ conn.execute(
+ "CREATE TABLE steps (id VARCHAR(64) PRIMARY KEY, workflow_id VARCHAR(64), "
+ "action_type VARCHAR(64))"
+ )
+ conn.execute(
+ "INSERT INTO workflows VALUES (?,?,?,?,?,?,?,?)",
+ ("wf_aiva", "Urgence_aiva_demo", "demo", "2026-06-01", "2026-06-18",
+ 1, "manual", ""),
+ )
+ conn.execute(
+ "INSERT INTO workflows VALUES (?,?,?,?,?,?,?,?)",
+ ("wf_learned", "Learned_flow", "", "2026-06-02", "2026-06-17",
+ 1, "learned_import", "pending"),
+ )
+ # 3 steps pour wf_aiva → nodes_count attendu = 3
+ for i in range(3):
+ conn.execute(
+ "INSERT INTO steps VALUES (?,?,?)", (f"s{i}", "wf_aiva", "click")
+ )
+ conn.commit()
+ conn.close()
+
+ monkeypatch.setattr(dashboard_app, "VWB_DB_PATH", Path(db_path))
+
+ resp = client.get('/api/workflows')
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data['total'] == 2, f"attendu 2 workflows, obtenu {data['total']}"
+ names = {w['name'] for w in data['workflows']}
+ assert 'Urgence_aiva_demo' in names
+ aiva = next(w for w in data['workflows'] if w['name'] == 'Urgence_aiva_demo')
+ assert aiva['nodes_count'] == 3
+ assert aiva['source'] == 'manual'
+
def test_sessions_list(self, client):
"""L'API sessions retourne la liste."""
resp = client.get('/api/agent/sessions')
diff --git a/web_dashboard/app.py b/web_dashboard/app.py
index 7ee00c811..aec1edaf9 100644
--- a/web_dashboard/app.py
+++ b/web_dashboard/app.py
@@ -189,6 +189,20 @@ SESSIONS_PATH = DATA_PATH / "sessions"
WORKFLOWS_PATH = DATA_PATH / "workflows"
LOGS_PATH = BASE_PATH / "logs"
+# Source canonique des workflows (décision produit D3) : la base VWB v3
+# (SQLAlchemy/SQLite) que Léa lit déjà au runtime. Chemin absolu robuste (PAS la
+# DB fantôme vide à la racine du repo `instance/workflows.db`, schéma obsolète,
+# ni l'ancien store JSON `data/training/workflows/` créé vide sur DGX).
+# Surchargeable via RPA_VWB_DB_PATH pour les déploiements atypiques.
+def _resolve_vwb_db_path() -> Path:
+ override = os.getenv("RPA_VWB_DB_PATH", "").strip()
+ if override:
+ return Path(override).expanduser()
+ return BASE_PATH / "visual_workflow_builder" / "backend" / "instance" / "workflows.db"
+
+
+VWB_DB_PATH = _resolve_vwb_db_path()
+
# StorageManager
storage = StorageManager(base_path=str(DATA_PATH))
@@ -261,7 +275,9 @@ def system_status():
"""Statut du système."""
try:
sessions_count = len(list(SESSIONS_PATH.glob('*'))) if SESSIONS_PATH.exists() else 0
- workflows_count = len(list(WORKFLOWS_PATH.glob('*.json'))) if WORKFLOWS_PATH.exists() else 0
+ # Source canonique D3 : base VWB v3 (même comptage que /api/workflows),
+ # pas l'ancien store JSON `data/training/workflows/` créé vide sur DGX.
+ workflows_count = len(_load_workflows_from_vwb_db())
dependencies_ok = True
try:
@@ -785,36 +801,83 @@ def rename_session_workflow(session_id):
# API Workflows
# =============================================================================
+def _load_workflows_from_vwb_db() -> list:
+ """Charge les workflows depuis la base VWB v3 (source canonique D3).
+
+ Lit directement le SQLite que Léa interroge au runtime (cf.
+ `agent_chat/app.py` → `GET /api/v3/session/state`). On compte les `steps`
+ par workflow pour `nodes_count` (pas de notion d'`edges` en DAG linéaire :
+ `edges_count` = max(steps-1, 0)). Robuste à l'absence de la DB ou des
+ colonnes `source`/`review_status` (DB ancienne) : retourne [] sans planter.
+ """
+ import sqlite3
+
+ if not VWB_DB_PATH.exists():
+ return []
+
+ workflows = []
+ conn = sqlite3.connect(str(VWB_DB_PATH))
+ try:
+ conn.row_factory = sqlite3.Row
+ # Colonnes disponibles (la DB fantôme/ancienne n'a pas source/review_status)
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(workflows)")}
+ has_source = 'source' in cols
+ has_review = 'review_status' in cols
+
+ select_cols = ['id', 'name', 'description', 'created_at', 'updated_at']
+ if has_source:
+ select_cols.append('source')
+ if has_review:
+ select_cols.append('review_status')
+
+ # Nombre de steps par workflow (= nodes du DAG)
+ step_counts = {
+ row[0]: row[1]
+ for row in conn.execute(
+ "SELECT workflow_id, COUNT(*) FROM steps GROUP BY workflow_id"
+ )
+ }
+
+ rows = conn.execute(
+ f"SELECT {', '.join(select_cols)} FROM workflows ORDER BY updated_at DESC"
+ ).fetchall()
+
+ for row in rows:
+ wf_id = row['id']
+ nodes_count = step_counts.get(wf_id, 0)
+ workflows.append({
+ 'workflow_id': wf_id,
+ 'name': row['name'] or wf_id,
+ 'description': row['description'] or '',
+ 'nodes_count': nodes_count,
+ 'edges_count': max(nodes_count - 1, 0),
+ 'learning_state': 'OBSERVATION',
+ 'created_at': str(row['created_at'] or ''),
+ 'updated_at': str(row['updated_at'] or ''),
+ 'execution_count': 0,
+ 'source': row['source'] if has_source else 'manual',
+ 'review_status': row['review_status'] if has_review else '',
+ 'file_path': f"vwb_db://{wf_id}",
+ })
+ finally:
+ conn.close()
+
+ return workflows
+
+
@app.route('/api/workflows')
def list_workflows():
- """Liste tous les workflows."""
+ """Liste tous les workflows depuis la base VWB v3 (source canonique D3).
+
+ Avant ce correctif, l'endpoint globait `data/training/workflows/*.json`
+ (ancien store JSON, créé vide sur DGX) et renvoyait toujours `total: 0`,
+ rendant la surface « ce que Léa sait » faussement vide. On lit désormais la
+ même base SQLite que Léa au runtime.
+ """
try:
- workflows = []
hide_unnamed = request.args.get('hide_unnamed', 'true').lower() == 'true'
- if not WORKFLOWS_PATH.exists():
- WORKFLOWS_PATH.mkdir(parents=True, exist_ok=True)
- return jsonify({'workflows': [], 'total': 0, 'hidden_unnamed': 0})
-
- for wf_file in WORKFLOWS_PATH.glob('*.json'):
- try:
- with open(wf_file, 'r') as f:
- wf_data = json.load(f)
-
- workflows.append({
- 'workflow_id': wf_data.get('workflow_id', wf_file.stem),
- 'name': wf_data.get('name', wf_file.stem),
- 'description': wf_data.get('description', ''),
- 'nodes_count': len(wf_data.get('nodes', [])),
- 'edges_count': len(wf_data.get('edges', [])),
- 'learning_state': wf_data.get('learning_state', 'OBSERVATION'),
- 'created_at': wf_data.get('created_at', ''),
- 'updated_at': wf_data.get('updated_at', ''),
- 'execution_count': wf_data.get('execution_count', 0),
- 'file_path': str(wf_file)
- })
- except Exception as e:
- print(f"Erreur lecture workflow {wf_file}: {e}")
+ workflows = _load_workflows_from_vwb_db()
# Filtrer les workflows "Unnamed" si demandé
if hide_unnamed:

View File

@@ -0,0 +1,310 @@
diff --git a/agent_v0/server_v1/api_stream.py b/agent_v0/server_v1/api_stream.py
index 547aeb299..aa620853b 100644
--- a/agent_v0/server_v1/api_stream.py
+++ b/agent_v0/server_v1/api_stream.py
@@ -835,15 +835,56 @@ def _get_worker_queue_status() -> Dict[str, Any]:
components_ready = bool(components) and all(bool(v) for v in components.values())
health_status = (health or {}).get("status")
running = bool(health) and not health_stale and health_status != "stopped"
+
+ # Distinction VEILLE (armé, lazy) vs DÉGRADÉ (vrai échec).
+ #
+ # Les composants lourds (ScreenAnalyzer/CLIP/FAISS/StateEmbedding) sont
+ # chargés en lazy par run_worker : le processor n'est instancié qu'au
+ # premier _process_session (cf. run_worker._get_processor / _process_session).
+ # Un worker neuf qui n'a jamais reçu de session écrit donc status="healthy"
+ # avec tous les composants à false — c'est l'état NORMAL « en veille », pas
+ # une panne. L'étiqueter "degraded" fait lire une panne là où il n'y en a pas.
+ #
+ # Signal retenu pour « init jamais tentée » : TOUS les composants à false ET
+ # sessions_processed == 0 ET sessions_failed == 0. Justification : run_worker
+ # n'appelle _get_processor() (donc l'init lazy) que dans _process_session, qui
+ # incrémente toujours exactement un compteur (processed / failed / skipped).
+ # Tant que processed == 0 ET failed == 0, aucune session n'a déclenché une
+ # init suivie d'un traitement — le worker est armé en attente. Un simple skip
+ # (dossier/shots absents) passe quand même par _get_processor() : les
+ # composants se chargent, donc tous-à-false devient faux et on n'entre pas ici.
+ # run_worker._health_components() écrit toujours les 4 clés (jamais un dict
+ # vide), d'où le test sur les VALEURS et non sur la présence des clés.
+ # Si run_worker a lui-même forcé status="degraded" (VLM + ScreenAnalyzer
+ # absent, cf. run_worker._write_health), c'est un VRAI échec : on le conserve.
+ stats = (health or {}).get("stats") or {}
+ init_attempted = bool(stats.get("sessions_processed", 0)) or bool(
+ stats.get("sessions_failed", 0)
+ )
+ components_all_false = bool(components) and not any(
+ bool(v) for v in components.values()
+ )
+ armed = (
+ running
+ and not components_ready
+ and health_status == "healthy"
+ and components_all_false # aucun composant lourd encore chargé
+ and not init_attempted
+ )
+
status = health_status or "unknown"
if not running:
status = "stale" if health else "unknown"
+ elif armed:
+ # En veille : worker sain, composants chargés à la 1re session.
+ status = "idle"
elif not components_ready:
status = "degraded"
return {
"running": running,
"status": status,
+ "armed": armed,
"queue_length": len(queue),
"queue": queue,
"replay_lock_active": REPLAY_LOCK_FILE.exists(),
@@ -858,11 +899,29 @@ def _get_worker_queue_status() -> Dict[str, Any]:
"components": components,
"components_ready": components_ready,
"processing_ready": running and not REPLAY_LOCK_FILE.exists() and components_ready,
- "stats": (health or {}).get("stats") or {},
+ "status_hint": _worker_status_hint(status, armed),
+ "stats": stats,
"note": "Le worker VLM tourne dans un process séparé (agent_v0.server_v1.run_worker).",
}
+def _worker_status_hint(status: str, armed: bool) -> str:
+ """Message humain pour le statut worker (consommé par le dashboard)."""
+ if armed or status == "idle":
+ return "En veille — composants chargés à la 1re session."
+ if status == "degraded":
+ return "Worker apprentissage dégradé — init des composants en échec."
+ if status == "stale":
+ return "Health file périmé (> 180s) — worker peut-être arrêté."
+ if status == "stopped":
+ return "Worker arrêté."
+ if status == "busy":
+ return "Traitement d'une session en cours."
+ if status == "healthy":
+ return "Worker prêt — composants chargés."
+ return "État worker inconnu."
+
+
# =========================================================================
# Compteur d'analyses en cours par session (pour attendre avant finalize)
# =========================================================================
diff --git a/tests/integration/test_stream_processor.py b/tests/integration/test_stream_processor.py
index 660187901..344e614cb 100644
--- a/tests/integration/test_stream_processor.py
+++ b/tests/integration/test_stream_processor.py
@@ -1289,3 +1289,158 @@ class TestAPIEndpoints:
assert len(workflows) == 1
assert workflows[0]["workflow_id"] == "wf_api_001"
assert workflows[0]["nodes"] == 2
+
+
+class TestWorkerStatusTruthfulness:
+ """Truthfulness du statut worker exposé par _get_worker_queue_status.
+
+ Distingue VEILLE (armé, lazy : worker neuf qui n'a jamais traité de
+ session, composants chargés à la 1re session) de DÉGRADÉ (init tentée
+ et en échec). Un worker en veille ne doit JAMAIS être étiqueté 'degraded'.
+ """
+
+ # Même contrainte que TestAPIEndpoints : api_stream fail-closed à l'import
+ # si RPA_API_TOKEN absent.
+ _TEST_API_TOKEN = "test_token_for_worker_status_0123456789abcdef"
+
+ @pytest.fixture(autouse=True)
+ def _ensure_api_token(self, monkeypatch):
+ monkeypatch.setenv("RPA_API_TOKEN", self._TEST_API_TOKEN)
+
+ @pytest.fixture
+ def status_env(self, tmp_path, monkeypatch):
+ """Isole les fichiers worker (health/queue/lock) sur tmp_path."""
+ from agent_v0.server_v1 import api_stream
+
+ health_file = tmp_path / "_worker_health.json"
+ queue_file = tmp_path / "_worker_queue.txt"
+ lock_file = tmp_path / "_replay_active.lock"
+ monkeypatch.setattr(api_stream, "WORKER_HEALTH_FILE", health_file)
+ monkeypatch.setattr(api_stream, "WORKER_QUEUE_FILE", queue_file)
+ monkeypatch.setattr(api_stream, "REPLAY_LOCK_FILE", lock_file)
+ return api_stream, health_file
+
+ @staticmethod
+ def _write_health(health_file, **overrides):
+ """Écrit un health file frais (mtime récent => non stale)."""
+ payload = {
+ "pid": 1234,
+ "started_at": "2026-06-18T10:00:00",
+ "last_cycle": "2026-06-18T10:00:30",
+ "current_session": None,
+ "queue_length": 0,
+ "components": {
+ "screen_analyzer": False,
+ "clip_embedder": False,
+ "faiss_manager": False,
+ "state_embedding_builder": False,
+ },
+ "stats": {
+ "sessions_processed": 0,
+ "sessions_failed": 0,
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 0,
+ },
+ "status": "healthy",
+ }
+ payload.update(overrides)
+ health_file.write_text(json.dumps(payload), encoding="utf-8")
+
+ def test_fresh_worker_is_idle_not_degraded(self, status_env):
+ """Worker neuf : healthy, 0 session, tous composants false
+ => statut 'idle' (en veille / armé), PAS 'degraded'."""
+ api_stream, health_file = status_env
+ self._write_health(health_file) # défaut = état neuf
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["running"] is True
+ assert status["status"] == "idle", status
+ assert status["armed"] is True
+ assert status["components_ready"] is False
+ # processing_ready reste False tant que les composants ne sont pas chargés
+ assert status["processing_ready"] is False
+ assert "veille" in status["status_hint"].lower()
+
+ def test_worker_init_failed_is_degraded(self, status_env):
+ """Init tentée et en échec : run_worker force status='degraded'
+ (VLM + ScreenAnalyzer absent) => on conserve 'degraded'."""
+ api_stream, health_file = status_env
+ self._write_health(
+ health_file,
+ status="degraded", # forcé par run_worker._write_health
+ components={
+ "screen_analyzer": False,
+ "clip_embedder": True,
+ "faiss_manager": True,
+ "state_embedding_builder": False,
+ },
+ stats={
+ "sessions_processed": 0,
+ "sessions_failed": 1, # une session a tenté l'init et échoué
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 0,
+ },
+ )
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["running"] is True
+ assert status["status"] == "degraded", status
+ assert status["armed"] is False
+ assert status["processing_ready"] is False
+ assert "dégradé" in status["status_hint"].lower()
+
+ def test_worker_partial_components_after_attempt_is_degraded(self, status_env):
+ """Composants partiels après tentative de traitement (sessions_failed>0),
+ sans status forcé par le worker => 'degraded' (pas 'idle')."""
+ api_stream, health_file = status_env
+ self._write_health(
+ health_file,
+ status="healthy",
+ components={
+ "screen_analyzer": True,
+ "clip_embedder": True,
+ "faiss_manager": False, # un composant manquant
+ "state_embedding_builder": True,
+ },
+ stats={
+ "sessions_processed": 0,
+ "sessions_failed": 2,
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 0,
+ },
+ )
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["status"] == "degraded", status
+ assert status["armed"] is False
+
+ def test_worker_ready_after_processing_is_healthy(self, status_env):
+ """Worker ayant traité au moins une session, tous composants chargés
+ => 'healthy' et processing_ready=True."""
+ api_stream, health_file = status_env
+ self._write_health(
+ health_file,
+ status="healthy",
+ components={
+ "screen_analyzer": True,
+ "clip_embedder": True,
+ "faiss_manager": True,
+ "state_embedding_builder": True,
+ },
+ stats={
+ "sessions_processed": 3,
+ "sessions_failed": 0,
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 42,
+ },
+ )
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["status"] == "healthy", status
+ assert status["armed"] is False
+ assert status["components_ready"] is True
+ assert status["processing_ready"] is True
diff --git a/web_dashboard/templates/index.html b/web_dashboard/templates/index.html
index c96cc8bf4..aeb0e7fa8 100644
--- a/web_dashboard/templates/index.html
+++ b/web_dashboard/templates/index.html
@@ -2838,13 +2838,23 @@
]);
const processingReady = processing && processing.processing_ready === true;
- const processingDegraded = processing && !processing.error && !processingReady;
+ // « En veille » (armé/lazy) ≠ « dégradé » : un worker neuf sans
+ // session a tous ses composants à false par design (chargement à la
+ // 1re session), ce n'est PAS une panne. Seul status==='degraded'
+ // (init tentée et en échec) est une vraie alerte.
+ const processingArmed = processing && (processing.armed === true || processing.status === 'idle');
+ const processingDegraded = processing && !processing.error && processing.status === 'degraded';
+ const statusHint = (processing && processing.status_hint) || '';
statusEl.innerHTML = processingDegraded
? '<span style="color:#f59e0b;">⚠️</span>'
- : '<span style="color:#22c55e;">✅</span>';
+ : processingArmed
+ ? '<span style="color:#3b82f6;">⏸️</span>'
+ : '<span style="color:#22c55e;">✅</span>';
statusEl.title = processingDegraded
- ? 'Streaming en ligne, worker apprentissage dégradé'
- : 'Serveur streaming en ligne';
+ ? `Streaming en ligne, worker apprentissage dégradé${statusHint ? ' — ' + statusHint : ''}`
+ : processingArmed
+ ? `Streaming en ligne, worker en veille${statusHint ? ' — ' + statusHint : ''}`
+ : 'Serveur streaming en ligne';
document.getElementById('streamActiveSessions').textContent = data.active_sessions || 0;
document.getElementById('streamTotalEvents').textContent = data.total_events || 0;
@@ -2862,14 +2872,20 @@
if (data.server_version) rows.push({label: 'Version serveur', value: data.server_version});
if (processing && !processing.error) {
const status = processing.status || 'unknown';
+ const workerIcon = processingReady ? '✅' : (processingArmed ? '⏸️' : '⚠️');
rows.push({
label: 'Worker apprentissage',
- value: processingReady ? `✅ ${status}` : `⚠️ ${status}`
+ value: `${workerIcon} ${status}`
});
rows.push({
label: 'Composants intelligence',
- value: processing.components_ready ? 'prêts' : 'non prêts'
+ value: processing.components_ready
+ ? 'prêts'
+ : (processingArmed ? 'en veille (chargés à la 1re session)' : 'non prêts')
});
+ if (statusHint) {
+ rows.push({label: 'Détail worker', value: statusHint});
+ }
if (processing.queue_length !== undefined) {
rows.push({label: 'Queue apprentissage', value: processing.queue_length});
}

View File

@@ -0,0 +1,318 @@
diff --git a/visual_workflow_builder/backend/app.py b/visual_workflow_builder/backend/app.py
index 7bdae57b0..c3a285cc0 100644
--- a/visual_workflow_builder/backend/app.py
+++ b/visual_workflow_builder/backend/app.py
@@ -28,6 +28,109 @@ load_dotenv() # fallback .env dans cwd (n'écrase pas les vars déjà définies
# Initialize Flask app
app = Flask(__name__)
+# ============================================================
+# HTTP Basic Auth LAN (cohérent avec le dashboard 5001)
+# ============================================================
+# Le VWB (backend 5002) était exposé au LAN SANS authentification. On ajoute
+# un middleware before_request qui exige un header Authorization: Basic <b64>
+# pour toute requête NON-loopback (LAN), avec les MÊMES credentials que le
+# dashboard : DASHBOARD_USER / DASHBOARD_PASSWORD (dans .env.local).
+#
+# GARDE-FOU CRITIQUE — exemption loopback :
+# Le dashboard (agent_chat/app.py `_fetch_vwb_workflows`) et les healthchecks
+# appellent ce backend en boucle locale (http://localhost:5002 → 127.0.0.1).
+# Exiger l'auth en loopback CASSERAIT l'intégration dashboard↔VWB. On exempte
+# donc 127.0.0.1 / ::1 (et ::ffff:127.0.0.1) de toute auth.
+#
+# Différence assumée avec le dashboard (fail-closed) : ici on NE crashe PAS si
+# DASHBOARD_PASSWORD est absent. On log un warning et on laisse passer le LAN
+# (mode POC dev/dégradé). En clinique, DASHBOARD_PASSWORD est défini dans
+# .env.local (chargé ci-dessus, lignes 24-26) → l'auth LAN est effective.
+import base64 as _base64
+import hmac as _hmac
+
+_VWB_AUTH_USER = os.getenv("DASHBOARD_USER", "lea").strip()
+_VWB_AUTH_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "").strip()
+# Désactivation explicite (dev/tests, parité avec le dashboard).
+_VWB_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in (
+ "1", "true", "yes",
+)
+
+# Adresses considérées comme loopback (server-to-server, jamais challengées).
+_VWB_LOOPBACK_ADDRS = {"127.0.0.1", "::1", "::ffff:127.0.0.1"}
+
+# Paths publics (pas d'auth) — healthchecks systemd / NPM / smokes.
+_VWB_PUBLIC_PATHS = {"/health", "/api/health"}
+
+if not _VWB_AUTH_PASSWORD and not _VWB_AUTH_DISABLED:
+ logging.getLogger("vwb.auth").warning(
+ "[SECURITE] DASHBOARD_PASSWORD non defini : l'auth Basic LAN du VWB "
+ "(5002) est INACTIVE (le LAN passe sans credentials). Definir "
+ "DASHBOARD_PASSWORD dans .env.local pour l'activer (cible clinique)."
+ )
+
+
+def _vwb_auth_ok(header_value: str) -> bool:
+ """Valide le header Authorization Basic. Comparaison constant-time.
+
+ Logique identique au dashboard (`web_dashboard/app.py::_dashboard_auth_ok`).
+ """
+ if not header_value or not header_value.lower().startswith("basic "):
+ return False
+ try:
+ decoded = _base64.b64decode(header_value[6:].strip()).decode("utf-8")
+ except (ValueError, UnicodeDecodeError):
+ return False
+ if ":" not in decoded:
+ return False
+ user, _, password = decoded.partition(":")
+ user_ok = _hmac.compare_digest(user, _VWB_AUTH_USER)
+ pwd_ok = _hmac.compare_digest(password, _VWB_AUTH_PASSWORD)
+ return user_ok and pwd_ok
+
+
+@app.before_request
+def _vwb_basic_auth_middleware():
+ """Middleware d'auth HTTP Basic LAN sur le backend VWB (port 5002).
+
+ - Bypass total si DASHBOARD_AUTH_DISABLED=true (dev/tests).
+ - Bypass total si DASHBOARD_PASSWORD absent (mode POC degrade, warning emis
+ au demarrage) — on ne casse pas le service faute de secret.
+ - Loopback (127.0.0.1 / ::1) : JAMAIS challenge (proxy dashboard, healthcheck).
+ - Preflight CORS (OPTIONS) : laisse passer (le navigateur n'envoie pas
+ l'en-tete Authorization au preflight).
+ - Paths publics (_VWB_PUBLIC_PATHS) : healthchecks externes.
+ - Sinon (requete LAN) : header Authorization: Basic <b64> obligatoire, sinon 401.
+ """
+ from flask import request, Response
+
+ # Dev / tests / mode degrade sans secret : bypass total
+ if _VWB_AUTH_DISABLED or not _VWB_AUTH_PASSWORD:
+ return None
+
+ # Preflight CORS : pas d'auth (le navigateur n'envoie pas les credentials)
+ if request.method == "OPTIONS":
+ return None
+
+ # Exemption loopback (server-to-server : dashboard, healthcheck)
+ if (request.remote_addr or "") in _VWB_LOOPBACK_ADDRS:
+ return None
+
+ # Paths publics (healthchecks externes)
+ if (request.path or "/") in _VWB_PUBLIC_PATHS:
+ return None
+
+ if _vwb_auth_ok(request.headers.get("Authorization", "")):
+ return None
+
+ # Pas authentifie — challenge 401 avec WWW-Authenticate
+ return Response(
+ '{"error": "authentication required"}',
+ status=401,
+ mimetype="application/json",
+ headers={"WWW-Authenticate": 'Basic realm="RPA Vision V3 VWB"'},
+ )
+
# ============================================================
# Logging — fichier rotatif + console (idempotent)
# ============================================================
diff --git a/visual_workflow_builder/backend/instance/workflows.db b/visual_workflow_builder/backend/instance/workflows.db
index db6eabd62..b7e181cbe 100644
Binary files a/visual_workflow_builder/backend/instance/workflows.db and b/visual_workflow_builder/backend/instance/workflows.db differ
diff --git a/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py b/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py
new file mode 100644
index 000000000..f4bff4d9d
--- /dev/null
+++ b/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py
@@ -0,0 +1,195 @@
+"""
+Tests de l'auth HTTP Basic LAN du backend VWB (port 5002).
+
+Le VWB etait expose au LAN SANS authentification. Le middleware
+`_vwb_basic_auth_middleware` ajoute un challenge 401 sur toute requete
+NON-loopback, avec les MEMES credentials que le dashboard
+(DASHBOARD_USER / DASHBOARD_PASSWORD).
+
+Controles cles :
+- Loopback (127.0.0.1) sans credentials -> 200 (proxy dashboard / healthcheck).
+- LAN (REMOTE_ADDR non loopback) sans credentials -> 401 + WWW-Authenticate.
+- LAN avec mauvais mot de passe -> 401.
+- LAN avec bons credentials -> passage (pas de 401).
+- /health public meme en LAN.
+- DASHBOARD_AUTH_DISABLED=true -> bypass total.
+- DASHBOARD_PASSWORD absent -> auth inactive (mode POC degrade, pas de crash).
+"""
+from __future__ import annotations
+
+import base64
+import importlib
+import os
+import sys
+from pathlib import Path
+
+import pytest
+
+# Le backend VWB s'importe en tant que module top-level `app`
+# (cf. tests/conftest.py : `from app import app, db`). On ajoute le repertoire
+# backend au path pour pouvoir le recharger avec les variables d'env voulues.
+_BACKEND_DIR = Path(__file__).resolve().parent.parent
+if str(_BACKEND_DIR) not in sys.path:
+ sys.path.insert(0, str(_BACKEND_DIR))
+
+# Adresse LAN simulee (non loopback)
+_LAN_ADDR = "192.168.1.50"
+_LAN_ENV = {"REMOTE_ADDR": _LAN_ADDR}
+
+
+def _basic_auth_header(user: str, password: str) -> str:
+ token = base64.b64encode(f"{user}:{password}".encode()).decode()
+ return f"Basic {token}"
+
+
+def _reload_app():
+ """Recharge le module `app` pour relire les constantes d'auth depuis l'env."""
+ if "app" in sys.modules:
+ return importlib.reload(sys.modules["app"])
+ return importlib.import_module("app")
+
+
+@pytest.fixture
+def auth_enabled_client(monkeypatch):
+ """Client VWB avec auth LAN active (DASHBOARD_USER/PASSWORD definis)."""
+ monkeypatch.setenv("DASHBOARD_USER", "lea")
+ monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
+ monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
+ mod = _reload_app()
+ mod.app.config["TESTING"] = True
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
+ with mod.app.test_client() as c:
+ with mod.app.app_context():
+ mod.db.create_all()
+ yield c
+ mod.db.drop_all()
+
+
+@pytest.fixture
+def auth_disabled_client(monkeypatch):
+ """Client VWB avec auth desactivee (DASHBOARD_AUTH_DISABLED=true)."""
+ monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
+ monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
+ mod = _reload_app()
+ mod.app.config["TESTING"] = True
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
+ with mod.app.test_client() as c:
+ with mod.app.app_context():
+ mod.db.create_all()
+ yield c
+ mod.db.drop_all()
+
+
+@pytest.fixture
+def no_password_client(monkeypatch):
+ """Client VWB sans DASHBOARD_PASSWORD (mode POC degrade : auth inactive)."""
+ monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
+ monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
+ mod = _reload_app()
+ mod.app.config["TESTING"] = True
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
+ with mod.app.test_client() as c:
+ with mod.app.app_context():
+ mod.db.create_all()
+ yield c
+ mod.db.drop_all()
+
+
+class TestVwbBasicAuth:
+ """Auth HTTP Basic LAN sur le backend VWB (5002)."""
+
+ def test_loopback_no_creds_passes(self, auth_enabled_client):
+ """Requete loopback (127.0.0.1) sans creds -> PAS de 401.
+
+ Garde-fou critique : le dashboard proxifie en loopback. La requete
+ ne doit jamais etre challengee (200, ou autre code applicatif != 401).
+ """
+ resp = auth_enabled_client.get("/api/v3/session/state")
+ assert resp.status_code != 401, (
+ f"Loopback ne doit jamais etre challenge (got {resp.status_code})"
+ )
+
+ def test_lan_no_creds_returns_401(self, auth_enabled_client):
+ """Requete LAN (non loopback) sans creds -> 401 + WWW-Authenticate."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state", environ_base=_LAN_ENV
+ )
+ assert resp.status_code == 401
+ assert "WWW-Authenticate" in resp.headers
+ assert "Basic" in resp.headers["WWW-Authenticate"]
+
+ def test_lan_wrong_password_returns_401(self, auth_enabled_client):
+ """Requete LAN avec mauvais mot de passe -> 401."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": _basic_auth_header("lea", "wrong")},
+ )
+ assert resp.status_code == 401
+
+ def test_lan_wrong_user_returns_401(self, auth_enabled_client):
+ """Requete LAN avec mauvais utilisateur -> 401."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": _basic_auth_header("intruder", "secret-test-pwd")},
+ )
+ assert resp.status_code == 401
+
+ def test_lan_valid_credentials_pass(self, auth_enabled_client):
+ """Requete LAN avec bons creds -> PAS de 401 (auth franchie)."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": _basic_auth_header("lea", "secret-test-pwd")},
+ )
+ assert resp.status_code != 401, (
+ f"Bons creds doivent franchir l'auth (got {resp.status_code})"
+ )
+
+ def test_lan_malformed_header_returns_401(self, auth_enabled_client):
+ """Requete LAN avec header mal forme (Bearer) -> 401."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": "Bearer tototoken"},
+ )
+ assert resp.status_code == 401
+
+ def test_lan_health_is_public(self, auth_enabled_client):
+ """/health reste public meme en LAN (healthcheck externe)."""
+ resp = auth_enabled_client.get("/health", environ_base=_LAN_ENV)
+ assert resp.status_code == 200
+
+ def test_lan_options_preflight_not_blocked(self, auth_enabled_client):
+ """Preflight CORS (OPTIONS) en LAN -> pas de 401 (CORS preserve)."""
+ resp = auth_enabled_client.open(
+ "/api/v3/session/state", method="OPTIONS", environ_base=_LAN_ENV
+ )
+ assert resp.status_code != 401
+
+ def test_auth_disabled_bypass_lan(self, auth_disabled_client):
+ """DASHBOARD_AUTH_DISABLED=true -> LAN passe sans creds."""
+ resp = auth_disabled_client.get(
+ "/api/v3/session/state", environ_base=_LAN_ENV
+ )
+ assert resp.status_code != 401
+
+ def test_no_password_degraded_lan_passes(self, no_password_client):
+ """DASHBOARD_PASSWORD absent -> mode POC degrade : LAN passe (pas de crash)."""
+ resp = no_password_client.get(
+ "/api/v3/session/state", environ_base=_LAN_ENV
+ )
+ assert resp.status_code != 401
+
+
+@pytest.fixture(autouse=True)
+def _restore_module(monkeypatch):
+ """Restaure le module `app` en mode auth desactivee apres chaque test,
+ pour ne pas contaminer les autres tests VWB (qui importent `app`)."""
+ yield
+ monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
+ monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
+ monkeypatch.delenv("DASHBOARD_USER", raising=False)
+ if "app" in sys.modules:
+ importlib.reload(sys.modules["app"])

View File

@@ -0,0 +1,83 @@
# Registre des décisions — 2026-06-08
- `Date`: 2026-06-08
- `Auteur`: Qwen (compilation)
- `Statut`: actif, source de vérité jusqu'à contre-ordre Dom
---
## Décisions tranchées
### D-01 : DGX = Option A (court terme)
- **Cible** : `/home/aivanov/ai/rpa_vision_v3`, user `aivanov`
- **Tranché par** : Dom (15:15)
- **Référence** : `active/2026-06-08_1515_decisions-dom-go-operationnels.md`
- **Branch** : `poc-dgx` poussée sur Gitea (`6d34b3cb6`)
- **Option B** (`/opt/rpa_vision_v3`, user `rpa`) : reportée post-POC
### D-02 : WP-A dashboard fail-closed — GO
- **Ce que ça fait** : Dashboard refuse de démarrer si `DASHBOARD_PASSWORD` absent ET auth non désactivée. Mot de passe par défaut supprimé.
- **Commit** : `549ea0631`
- **Tests** : 11/11 passés
- **Tranché par** : Dom (15:15) + QG Qwen (15:48)
### D-03 : WP-B verrou enrôlement — GO
- **Ce que ça fait** : `RPA_FLEET_ENROLL_LOCKED=true` bloque enrôlement nouveau `machine_id`. Ferme contournement "poste révoqué + nouveau ID + token global".
- **Commit** : `f18de016d`
- **Tests** : 6/6 passés
- **Tranché par** : Dom (15:15) + QG Qwen (15:48)
### D-04 : P1.g GPU merge — GO
- **Ce que ça fait** : `resolve_device(auto/cuda/cpu)` avec garde-fou VRAM (`min_free_gb=2`, `max_total_gb=6`), fallback CPU.
- **Commit** : `0e215da84`
- **Tests** : 15/15 passés, smoke OK (`auto → cuda`, `cpu → cpu`)
- **Tranché par** : Dom (15:15) + QG Qwen (15:25)
- **Rollback** : `RPA_VISION_DEVICE=cpu`, `RPA_EASYOCR_GPU=0`
### D-05 : Grounder candidat = Qwen3-VL-4B-Instruct via vLLM
- **Bench** : 87.5% accuracy, 1.1s latence, 1 dangereux/16, Apache-2.0
- **Tranché par** : Claude (15:45) + QG Qwen (16:10)
- **Note** : Pas d'activation runtime sans GO Dom + bench sur écrans réels
### D-06 : Grounding gemma4:26b supervisé
- **Bench** : 69% accuracy, **0 dangereux**, 14.4s OCR
- **Tranché par** : Claude (15:45) + QG Qwen (16:10)
- **Usage** : Grounding supervisé (pas autonome)
### D-07 : UI-TARS gate vision
- **Ce que ça fait** : `has_vision_capability()` via `/api/show` — skip propre si modèle aveugle + `logger.warning`
- **Commit** : `d00fe7b00`
- **Tranché par** : Claude (12:25) + QG Qwen (12:28)
- **Contexte** : UI-TARS cassé sur DGX (aveugle, mmproj non chargé) → 500 silencieux corrigé
### D-08 : Trained artifacts V2 — GO avec réserves
- **Paquet** : ~306 Mo, 6107 fichiers (anchors ajoutés, screen_states retirés)
- **Anti-secret** : CLEAN (0 token, 0 identité patient, 0 machine_id réel)
- **Rewrite** : 2 colonnes (`image_path` + `thumbnail_path`) `/home/dom/``/home/aivanov/`
- **Revue visuelle** : Dom confirme captures 100% fictives (mockup `urgence.labs`)
- **Tranché par** : Claude (16:08) + Codex (16:06) + QG Qwen (16:10, 16:50)
- **Transfert** : Bloqué en attente GO Dom effectif
### D-09 : Lea live — GO quand preflight vert
- **Condition** : Dom devant Windows + preflight G1-G6 verts
- **Scope** : Capture Shadow supervisée, Notepad/Explorateur/Easily lecture seule
- **Interdit** : ❌ Replay autonome
- **Tranché par** : Dom (15:15)
### D-10 : Code mort — NO-GO suppression
- **Ce qui est interdit** : Suppression, déplacement, archive, marquage `DEPRECATED` massif
- **Autorisé** : Inventaire read-only, documentation
- **Tranché par** : Dom (16:37) + Codex (16:37) + Claude (16:35)
- **Référence** : `active/2026-06-08_1637_decision-piste2-no-go-code-mort.md`
---
## Décisions en attente
| Sujet | En attente de | Impact |
|---|---|---|
| Unification `.service` files | Conception Codex | Migration DGX propre |
| Transfert trained artifacts DGX | GO Dom effectif + tar créé | Clone DGX complet |
| Node.js sur DGX | Dom/Claude | VWB frontend 3002 |
| Benchmark GPU P1.g | Claude (en cours) | Activation large P1.g |
| Scan anti-code-mort | Attente (read-only) | Détection automatique "UI-TARS bis" |

View File

@@ -0,0 +1,145 @@
# Registre des decisions — 2026-06-09
- `Date`: 2026-06-09
- `Auteur`: Codex
- `Statut`: actif
---
## Decisions tranchees
### D-2026-06-09-01 : Codex orchestre activement Claude et Qwen
- **Tranche par** : Dom, 2026-06-09
- **Decision** : Codex est l'orchestrateur actif du projet. Tant que Claude et Qwen ont des loops de coordination actifs, Codex doit leur fournir une prochaine tache actionnable.
- **Exception** : Codex peut ne pas distribuer une execution seulement s'il attend explicitement un feu vert de Dom ou un QG bloquant.
- **Implication** : en cas de blocage sur execution, Codex distribue quand meme des taches non destructives : preparation, audit read-only, QG, rollback, plan de tests, registre decisions.
- **Persistance** : regle ajoutee a `docs/coordination/ROLES.md`.
### D-2026-06-09-02 : Dashboard DGX — GO partiel, produit incomplet
- **Tranche par** : QG Qwen + Codex
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1120_claude-to-codex_RESULTAT-DASHBOARD-DGX-E2E.md`
- `docs/coordination/inbox_codex/2026-06-09_1130_qwen-to-codex-claude-dom_QG-DASHBOARD-DGX-E2E-GO-PARTIEL.md`
- **Decision** : securite dashboard DGX validee ; dashboard produit non valide tant que P0 workflows non servis et ZIP agent absent ne sont pas corriges.
- **Suite autorisee** : preparation Lea live read-only ; preparation corrections P0 sans execution.
- **Bloquant multi-TIM** : WP-C token par poste.
### D-2026-06-09-03 : Branching stable = `poc-prod`
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
- **Decision** : branche stable POC/NVP/prod = `poc-prod`.
- **Schema retenu** : `main` racine, `dev` integration quotidienne, `poc-prod` stable protegee promue depuis `dev` apres QG.
- **Conservation** : `poc-dgx` conserve comme snapshot DGX.
- **Reste ouvert** : chantier blobs ~15 G, sort de `master`, droits de push/protection sur `poc-prod`.
### D-2026-06-09-04 : Docker — rester clone + venv + systemd pour le POC
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1410_claude-to-codex_AUDIT-DOCKER-VS-CLONE-SYSTEMD.md`
- `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
- **Decision court terme** : ne pas dockeriser pour le POC ; conserver clone + venv + systemd.
- **Decision moyen terme** : Docker hybride progressif post-POC, avec Ollama et agent RPA hors conteneur.
### D-2026-06-09-05 : GO execution P0-1/P0-2 dashboard DGX
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
- **P0-1** : appliquer `DATABASE_URL` absolu pour VWB, restart service, verifier `/api/workflows/` = 23.
- **P0-2** : build `deploy/Lea_v1.0.0.zip` sur DGX pour usage POC interne.
- **Garde-fou** : paquet agent non distribuable multi-TIM tant que WP-C token par poste n'est pas implemente et QG.
### D-2026-06-09-06 : M2 cible dev Linux, Ollama via tunnel DGX
- **Tranche par** : QG Qwen corrige a 15:12 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1420_claude-to-codex_RESULTAT-M2-VERIF-SERVEUR-DEV-LINUX-READONLY.md`
- `docs/coordination/inbox_codex/2026-06-09_1425_claude-to-codex_RESULTAT-M2-OLLAMA-ENV-READONLY.md`
- `docs/coordination/inbox_codex/2026-06-09_1512_qwen-to-codex-claude-dom_QG-M2-CORRECTION-OLLAMA-TUNNEL-GO.md`
- **Decision** : M2 cible le serveur dev Linux `192.168.1.40`, pas le DGX directement.
- **Ollama** : acces via tunnel SSH local `127.0.0.1:11434` vers DGX ; Qwen indique tunnel relance et modeles disponibles.
- **Garde-fou** : execution live Shadow uniquement avec Dom devant Windows + GO Codex. Tunnel non persistant = P1 a stabiliser avant demo multi-machine.
### D-2026-06-09-07 : WP-C peut commencer apres P0 dashboard
- **Tranche par** : QG Qwen a 15:05 et 15:06 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1505_qwen-to-codex-claude-dom_QG-PREP-P0-DASHBOARD-WPC-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_1506_qwen-to-codex-claude-dom_QG-POST-P0-BRANCHES-DOCKER.md`
- **Decision QG** : WP-C token par poste peut commencer en parallele M2 apres P0 dashboard.
- **Garde-fou** : WP-C reste bloquant multi-TIM ; tests P0 requis : build ZIP sans token global + enroll poste unique.
### D-2026-06-09-08 : P0 dashboard corrige par symlink workflow store
- **Tranche par** : Dom, relaye et execute par Claude a 20:25 CEST.
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_2025_claude-to-codex-qwen_RESULTAT-P0-DASHBOARD-CORRECTIONS.md`
- **Rectificatif** : le diagnostic `DATABASE_URL` etait incomplet/errone ; la route VWB `/api/workflows/` lit les JSON via `WorkflowDatabase("data/workflows")`, pas SQLAlchemy.
- **Cause racine** : divergence de `WorkingDirectory` dev vs DGX, donc chemin relatif `data/workflows` resolu au mauvais endroit sur DGX.
- **Correction POC** : symlink `data/workflows` -> `visual_workflow_builder/backend/data/workflows`.
- **Verification** : VWB 5002 `/api/workflows/` = 39 workflows.
- **Rollback** : `rm data/workflows && mkdir data/workflows`.
- **Note** : `DATABASE_URL` laisse en place, neutre pour cette route, utile aux composants SQLAlchemy.
### D-2026-06-09-09 : DETTE-015 double store workflows
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_2035_claude-to-codex-qwen_INFO-DETTE-015-store-workflows.md`
- **Decision** : enregistrer la dette double store workflows en P2 post-POC.
- **Docs** :
- `docs/DETTE_TECHNIQUE.md`
- `docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md`
- **Cible post-POC** : unifier sur la DB SQLAlchemy existante sous flag, TDD, sans refactor avant la demo.
### D-2026-06-09-10 : Qwen indisponible, Codex orchestre en mode degrade
- **Tranche par** : Dom, 2026-06-09.
- **Decision** : Qwen est considere temporairement indisponible. Codex ne lui route plus de nouvelles taches tant que la panne n'est pas levee.
- **Implication** : Codex assure l'interim de coordination et demande a Claude des self-checks/read-only/handoffs, sans executer les actions qui exigent un QG formel si elles sont structurelles ou risquées.
- **Garde-fou** : pas de code WP-C, pas de live M2, pas de multi-TIM sans GO explicite Dom/Codex et, si possible, QG a posteriori quand Qwen revient.
### D-2026-06-09-11 : Qwen revenu, reprise QG normale
- **Tranche par** : Dom/Codex, 2026-06-09 20:47 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_2025_qwen-to-codex-claude-dom_QG-TUNNEL-PERSISTANT-M2-READINESS-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2026_qwen-to-codex-claude-dom_QG-WPC-CARTO-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2027_qwen-to-codex-claude-dom_ACK-MESSAGES-FIN-JOURNEE.md`
- **Decision** : Qwen est revenu ; Codex reprend le mode normal avec QG Qwen.
- **Action** : Qwen doit rendre le QG post-P0 dashboard sur le resultat Claude 20:25 ; Claude peut avancer WP-C Patch 1 local/TDD uniquement.
### D-2026-06-09-12 : Dashboard post-P0 — GO final
- **Tranche par** : QG Qwen a 20:50 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_QG-POST-P0-DASHBOARD-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_ACK-POST-P0.md`
- **Decision** : P0-1 et P0-2 resolus ; dashboard produit debloque pour M2.
- **Verification Qwen** : VWB 5002 `/api/workflows/` = 39, symlink OK, tunnel Ollama actif.
- **Garde-fou** : ZIP agent POC interne uniquement ; multi-TIM reste NOGO avant WP-C.
- **Point P1** : verifier et retirer `DATABASE_URL` si present dans env systemd/service pour eviter dette inutile avant GO M2.
### D-2026-06-09-13 : WP-C Patch 1 GO, Patch 2 autorise local/TDD
- **Tranche par** : QG Qwen a 21:10 CEST + Codex.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_2058_claude-to-codex_RESULTAT-WPC-PATCH1-MIGRATION-TDD.md`
- `docs/coordination/inbox_codex/2026-06-09_2104_claude-to-codex_INFO-WPC-PATCH1-COMMITE.md`
- `docs/coordination/inbox_codex/2026-06-09_2110_qwen-to-codex-claude-dom_QG-WPC-PATCH1-GO.md`
- **Patch 1** : GO, commit local `f7f692641`, non pousse, non deploye DGX.
- **DATABASE_URL** : garder ; P1 post-POC, pas de cleanup avant M2.
- **Patch 2 autorise** : generation token a l'enroll, local/TDD uniquement, pas de branchement auth, pas de ZIP, pas de deploiement DGX.
---
## Decisions en attente
| Sujet | En attente de | Impact |
|---|---|---|
| WP-C Patch 2 local/TDD | Resultat Claude + QG Qwen | Generation token enroll seulement |
| Branching details : blobs/master/protection | QG Qwen + GO Dom | Nettoyage et protection stable |
| Execution M2 Lea live | Dom devant Windows + GO Codex | Capture Shadow supervisee |
| Push/deploiement WP-C Patch 1+ | GO Dom/Codex separe | Integration distante |
— Codex

View File

@@ -0,0 +1,62 @@
# Registre des decisions — 2026-06-10
- `Date`: 2026-06-10
- `Auteur`: Codex
- `Statut`: actif
---
## Decisions tranchees
### D-2026-06-10-01 : Plan de continuite si contexte Codex bas/reset
- **Tranche par** : Dom, relaye par Codex a 09:18 CEST.
- **Contexte** : Dom signale qu'il reste tres peu de contexte Codex et qu'un reset est prevu vers 14:12.
- **Decision** : Codex doit poser un plan de relais pour permettre a Claude et Qwen d'avancer sans dependance forte a son contexte.
- **Claude** : peut avancer WP-C Patch 2 local/TDD seulement et produire preuves/resultat.
- **Qwen** : garde le fil, rend QG, signale contradictions, tient active/registre si necessaire.
- **Dom** : reste arbitre pour live, push, deploiement, branches et extension de scope.
- **Refs** :
- `docs/handoffs/2026-06-10_handoff_codex_context-low-reset-1412.md`
- `docs/coordination/active/2026-06-10_0918_continuite-codex-context-reset.md`
- `docs/coordination/inbox_claude/2026-06-10_0918_codex-to-claude_RELANCE-WPC-PATCH2-CONTINUITE.md`
- `docs/coordination/inbox_qwen/2026-06-10_0918_codex-to-qwen_QG-CONTINUITE-CONTEXT-RESET.md`
### D-2026-06-10-02 : WP-C Patch 1-3 GO, arret avant Patch 4 runtime
- **Tranche par** : QG Qwen + Codex, 2026-06-10.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-10_1425_qwen-to-codex-claude-dom_QG-WPC-PATCH2-GO.md`
- `docs/coordination/inbox_codex/2026-06-10_1435_qwen-to-codex-claude-dom_QG-WPC-PATCH3-GO.md`
- `docs/coordination/active/2026-06-10_1437_wpc-patch1-3-go-stop-patch4.md`
- **Decision** : WP-C Patch 1, Patch 2 et Patch 3 sont GO, locaux, non pousses, non deployes.
- **Etat commits** :
- Patch 1 : `f7f692641`.
- Patch 2 : `9fb2c7bfe`.
- Patch 3 : `b20d17882`.
- **Garde-fou** : Patch 4 est le premier impact runtime (`api_stream.py::_verify_token` derriere flag `RPA_FLEET_PER_AGENT_TOKEN`) et reste interdit sans decision Dom explicite.
- **Autorise** : preparation read-only, plan TDD, plan rollback, audit risques.
- **Interdit** : Patch 4 code, push, deploiement, ZIP/package, M2 live, multi-TIM, secret en clair.
### D-2026-06-10-03 : WP-C arrete pour POC, dette auth globale acceptee
- **Tranche par** : Dom, relaye/QG par Qwen a 14:50 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-10_1450_qwen-to-codex-claude-dom_DECISION-WPC-ABANDON-DETTE.md`
- `docs/coordination/active/2026-06-10_1440_anti-doublon-wpc-verdict.md`
- `docs/DETTE_TECHNIQUE.md` (`DETTE-016`)
- **Decision** : le token global + `machine_id` auto-declare est suffisant pour le POC controle.
- **Consequence** : WP-C token par poste est arrete pour le POC ; Patch 4 runtime est annule.
- **Etat commits** : Patch 1-3 restent locaux, inertes, non pousses, non deployes.
- **Dette** : `DETTE-016` creee en P2/ACCEPTED pour le gap "auth token global sans verification cryptographique par poste".
- **Garde-fou** : reouvrir WP-C ou une solution equivalente uniquement avant distribution multi-TIM elargie, exposition reseau non maitrisee ou besoin de revocation par poste non contournable.
## Decisions en attente
| Sujet | En attente de | Impact |
|---|---|---|
| M2 Lea live | Dom devant Windows + GO Codex | Execution supervisee |
| Branches/blobs/protection | QG + GO Dom | Hygiene repo post-POC |
| Reouverture WP-C post-POC | Decision Dom explicite | Seulement si multi-TIM elargi / exposition reseau / revocation forte |
— Codex

View File

@@ -0,0 +1,105 @@
# Revue globale post-panne DGX — 2026-06-20
- `Pilote`: Codex
- `Contributeurs`: Claude, Qwen
- `Contexte`: coupure électrique à 02:07 CEST
- `Verdict`: **GO labo supervisé / NO-GO fonctionnement autonome et clinique**
## Résumé
Le DGX, les services, les données et la VM Windows ont survécu ou ont été récupérés. L'accès Dom à la VM est rétabli. La panne révèle cependant que l'accès graphique et le démarrage complet de Léa ne sont pas encore autonomes: le tunnel SSH et le mot de passe VNC sont volatils, et aucun agent Windows n'a encore prouvé une reconnexion après la panne.
## Checklist consolidée
| Domaine | Contrôle | État | Preuve / observation |
|---|---|---:|---|
| Coordination | Watcher Codex/Claude/Qwen | OK | service utilisateur persistant `enabled+active` |
| Réseau | DGX joignable sur `192.168.1.45` | OK | IP WiFi statique, SSH et services accessibles |
| Réseau | Exclusion/réservation `.45` dans le DHCP box | À FAIRE | risque de conflit futur |
| Réseau | Ethernet clinique inactif | OK | interface sans carrier |
| Réseau | Ethernet clinique `autoconnect=off` | KO | profil encore `autoconnect=yes` |
| Sécurité | 5900/5902/3389/22220/8000 fermés depuis le LAN | OK | nmap: filtered/closed; loopback conservé |
| Services | Services système RPA | OK | 10 units `enabled`; services critiques actifs, zéro erreur/restart |
| Dashboard | API et UI | OK | auth 401 sans creds; UI réelle rendue; Socket.IO/API 200 |
| Dashboard | Fallback user concurrent | KO | service user disabled mais actif en crash-loop, 167+ restarts, conflit 5001 |
| Dashboard | Monitoring 5003 / Session Cleaner 5006 | DÉGRADÉ | affichés arrêtés; non bloquants POC mais classification à clarifier |
| Dashboard | Liens navigateur vers `localhost` | DÉGRADÉ | liens 8000/5002/3002/5004/5005 incorrects depuis un poste distant |
| Dashboard | Backups/restore points | DÉGRADÉ | dashboard annonce 0 backup et aucun point de restauration |
| VWB | Backend/frontend | OK | health 200; UI réelle rendue; catalogue/session APIs 200 |
| VWB | Cohérence catalogue | DÉGRADÉ | DB=24 workflows; sélecteur UI=22; review=8 |
| VWB | Workflows appris | DÉGRADÉ | plusieurs entrées importables à 0 nœud/0 transition |
| Données | `workflows.db` | OK | SQLite `integrity_check=ok`, 24 workflows |
| Données | Workflows fichiers | OK | symlink runtime valide, 42 fichiers |
| Données | Anchors | OK | 468 fichiers anchors + 47 anchor images |
| FAISS | Index et métadonnées | OK | 13 666 vecteurs, test HTTP 200 `success=true`, index sain |
| Worker | Daemon et queue | OK | heartbeat post-panne; queue total/pending/failed=0 |
| Agent chat | Service | OK | status online, 79 workflows |
| Streaming | Service | OK | `/health=200`, version 1.0.0 |
| Upload API | Service 8000 | OK | `rpa-vision-v3-api` actif+enabled, 401 loopback attendu |
| Ollama/grounder | Services | OK/DÉGRADÉ | process/services présents; fonction modèle non exercée dans ce smoke |
| VM | Autostart QEMU | OK après recovery | user service active+enabled, linger, 0 restart |
| VM | Boot Windows | OK | framebuffer Windows lock screen, 8,6 Go RSS |
| VM | VNC serveur loopback | OK | RFB 3.8 sur `127.0.0.1:5902` |
| VM | Tunnel poste Dom | RÉTABLI MAIS VOLATIL | tunnel local actif; Remmina connecté; perdu au reboot poste |
| VM | Mot de passe VNC | RÉTABLI MAIS VOLATIL | auth de bout en bout validée; absent du script d'autostart |
| VM | SSH guest 22220 | KO | hostfwd présent mais aucune bannière guest |
| VM | Guest agent QEMU | KO/NON PROUVÉ | socket présent, aucune réponse `guest-ping` |
| Léa Windows | Reconnexion agent post-panne | KO/NON PROUVÉ | aucun `last_seen` fleet postérieur à la panne |
| Léa Windows | `config.txt` live vers `.45` | NON TESTÉ | le template build ne prouve pas la configuration installée |
| Git DGX | Branche | OK | `poc-dgx` |
| Git DGX | Reproductibilité déploiement | DÉGRADÉ | HEAD `ec1fb81`, patch auth `app.py` + DB modifiés, cible `cf81ce4c7` non intégrée |
| Installateur | Artefact autonome 1.0.1 | DÉGRADÉ | seul `deploy/Lea_v1.0.0.zip` visible dans ce checkout |
| Frontend | VWB servi par Vite dev | POC SEULEMENT | port 3002 LAN; à remplacer par build/proxy pour clinique |
## Cause de l'absence d'accès VM
1. Le poste Dom a redémarré: le tunnel SSH local a disparu.
2. Le VNC QEMU est volontairement loopback-only et filtré depuis le LAN.
3. QEMU redémarre avec `password=on`, mais aucun script ne réinjecte le mot de passe.
Correctif runtime appliqué: tunnel `localhost:5902` recréé, mot de passe VNC reposé sans exposition, authentification validée, Remmina connecté.
## Tests fonctionnels nécessitant Dom dans Windows
- [ ] Se connecter à la session Windows.
- [ ] Vérifier que Léa démarre automatiquement; sinon la lancer.
- [ ] Vérifier le `config.txt` live vers le DGX `.45`.
- [ ] Confirmer un nouveau `last_seen` de la machine dans Fleet.
- [ ] Ouvrir « Discuter avec Léa » et envoyer `Bonjour`.
- [ ] Vérifier les bulles/progressions d'une action simple.
- [ ] Importer un workflow JSON dans VWB.
- [ ] Ouvrir une étape avec anchor et confirmer le crop.
- [ ] Exécuter un replay supervisé sans action destructive.
## Actions prioritaires
### P0 — autonomie après reboot
1. Stopper proprement le fallback `rpa-vision-v3-dashboard-user` en crash-loop; garder le service système.
2. Rendre le tunnel VM persistant sur le poste Dom (`systemd --user` ou `autossh`).
3. Rendre l'auth VNC persistante via un secret local protégé, ou retirer le password QEMU et s'appuyer sur SSH+loopback.
4. Après login Windows, prouver le démarrage/re-enrôlement Léa et corriger le `config.txt` live si nécessaire.
### P1 — installation fiable
5. Exclure/réserver `.45` dans le DHCP de la box.
6. Passer le profil Ethernet clinique à `autoconnect=no` tant que non autorisé.
7. Réconcilier Git DGX avec backup de `workflows.db`; ne pas utiliser `reset --hard` sans procédure validée.
8. Produire/identifier l'installateur Léa 1.0.1 attendu.
9. Corriger les liens dashboard `localhost` et distinguer clairement services critiques/optionnels.
10. Mettre en place un vrai backup/restore point post-déploiement.
### P2 — dette produit
11. Diagnostiquer guest SSH/QEMU guest agent.
12. Réconcilier les 24 workflows DB avec les 22 affichés et traiter les 8 reviews/entrées à 0 nœud.
13. Remplacer Vite dev par un build statique/proxy avant clinique.
## Preuves visuelles
- `output/playwright/dgx-post-panne-dashboard-20260620-0241.png`
- `output/playwright/dgx-post-panne-vwb-20260620-0241.png`
## Divergences de revue résolues
La première matrice Qwen confondait `5006` avec l'upload API, inspectait les unités user au lieu des unités système et utilisait des chemins de données non runtime. Les contrôles Codex/Claude ont confirmé: upload API sur 8000, services système persistants, 42 fichiers workflows et 468+47 fichiers anchors. Les constats Qwen conservés sont le guest SSH KO, le fallback dashboard en crash-loop et le worktree DGX non reproductible.

View File

@@ -0,0 +1,14 @@
[Unit]
Description=RPA Vision coordination inbox watcher
After=default.target
[Service]
Type=simple
WorkingDirectory=/home/dom/ai/rpa_vision_v3
Environment=COORD_LOOP_INTERVAL=15
ExecStart=/home/dom/ai/rpa_vision_v3/docs/coordination/coordination_loop.sh watch 15
Restart=always
RestartSec=5
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,21 @@
[Unit]
Description=RPA Vision V3 - Web Dashboard (Flask) user fallback
After=default.target network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/aivanov/ai/rpa_vision_v3
EnvironmentFile=/home/aivanov/ai/rpa_vision_v3/.env.local
Environment=PYTHONUNBUFFERED=1
Environment=ENVIRONMENT=production
Environment=RPA_SERVICE_NAME=rpa-vision-v3-dashboard-user
Environment=RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock
Environment=RPA_GROUNDING_IMG_DIR=/run/rpa
ExecStart=/home/aivanov/ai/rpa_vision_v3/venv_v3/bin/python3 web_dashboard/app.py
Restart=on-failure
RestartSec=3
TimeoutStopSec=30
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,69 @@
# Template — Quality Gate (QG)
- `De`: [Agent QG]
- `A`: [Destinataire]
- `Copie`: [CC]
- `Date`: [YYYY-MM-DD HH:MM Europe/Paris]
- `Statut`: QG
- `Répond à`: [Fichier de référence]
---
## Contexte
[1-2 lignes décrivant ce qui est évalué]
---
## Critères d'acceptation
| # | Critère | Requis | Résultat | Verdict |
|---|---|---|---|---|
| 1 | [Description du critère] | ✅ | [Résultat observé] | GO/NO-GO |
| 2 | ... | | | |
| 3 | ... | | | |
---
## Vérifications directes
| Test | Commande / Méthode | Résultat attendu | Résultat observé | Verdict |
|---|---|---|---|---|
| Tests unitaires | `pytest ...` | X/Y passés | [Résultat] | ✅/❌ |
| Smoke test | [Commande] | [Attendu] | [Observé] | ✅/❌ |
| Code review | [Fichiers] | [Critère] | [Observé] | ✅/❌ |
---
## Réserves
| # | Réserve | Impact | Action requise |
|---|---|---|---|
| R1 | [Description] | [Faible/Moyen/Haut] | [Action] |
---
## Verdict global
**[GO / GO avec réserves / NO-GO]**
[1-2 lignes de justification]
---
## Prochaines étapes
| Étape | Owner | Deadline |
|---|---|---|
| [Action] | [Qui] | [Quand] |
---
## Stop conditions
- [Condition qui bloquerait le GO]
- [Autre condition]
---
*Template réutilisable — copier et adapter pour chaque QG.*

View File

@@ -0,0 +1,228 @@
# Handoff Codex — fin journee 2026-06-08 / reprise 2026-06-09
- `Date`: 2026-06-08 18:17 CEST
- `Auteur`: Codex
- `Objet`: reprise demain apres cycle DGX, securite, dashboard, branches, tests reels
- `Statut`: source de reprise operationnelle
## Resume executif
La journee a bascule le projet d'un etat fragile/local vers un socle DGX utilisable en local-only.
Acquis valides :
- DGX Option A deploye : `/home/aivanov/ai/rpa_vision_v3`, branche `poc-dgx`.
- Venv ARM DGX OK, torch CUDA OK sur GB10 `sm_121`.
- Donnees entrainees utiles transferees : 23 workflows, 199 ancres, FAISS 512-dim `ntotal=13666`.
- Services DGX 6/6 UP, `disabled`, bind `127.0.0.1`, pas d'exposition externe.
- Secrets generes sur DGX, non publies, `.env.local` chmod 600.
- WP-A/WP-B securite valides : dashboard fail-closed + verrou reenrolement.
- P1.g GPU valide local RTX 5070 et DGX GB10, gain environ 90 %, overlap 100 %, 0 OOM.
- Correctif GB10 memory-unified livre : `auto -> cuda` OK.
- Correctif bind securite livre : default `127.0.0.1` via `RPA_BIND_HOST`.
Non acquis / a reprendre :
- Dashboard fonctionnel bout-en-bout non encore teste comme workflow produit.
- Test Lea grandeur nature non execute.
- Replay controle multi-machine non prouve.
- Token par poste WP-C non implemente.
- Branching Gitea propre dev vs POC/NVP/prod non encore tranche.
- Dockerisation non tranchee : clone+venv+systemd fonctionne, Docker a etudier.
## Sources de verite a lire demain
### Etat DGX final
- `docs/coordination/inbox_codex/2026-06-08_1745_claude-to-codex-qwen_RECAP-FINAL-dgx-operationnel.md`
- `docs/coordination/inbox_codex/2026-06-08_1752_qwen-to-codex-claude_QG-RECAP-FINAL-DGX.md`
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-DGX-CLONE-VENV-ARM.md`
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-ARTIFACTS-V2-TRANSFERT-DGX.md`
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-DGX-COMPAT-RUNTIME.md`
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-BENCH-GPU-DGX-GB10.md`
### Decisions et roles
- `docs/coordination/registre/2026-06-08_decisions.md`
- `docs/coordination/ROLES.md`
- `docs/coordination/active/2026-06-08_1637_decision-piste2-no-go-code-mort.md`
- `docs/coordination/active/2026-06-08_1729_relance-missions-dgx-systemd-gb10.md`
### Lea live / QG
- `docs/coordination/RUNBOOK-LEA-LIVE-DRAFT.md`
- `docs/coordination/templates/TEMPLATE-QG.md`
- `docs/coordination/inbox_codex/2026-06-08_PLAN-LEA-LIVE-GRANDEUR-NATURE.md`
## Etat technique detaille
### DGX
- Host : `aivanov@192.168.1.45`.
- Repo : `/home/aivanov/ai/rpa_vision_v3`.
- Branche : `poc-dgx`.
- Commit final recape : `09f65cecb` cote local/Gitea `poc-dgx`.
- Services UP local-only : `rpa-streaming` 5005, `dashboard` 5001, `api` 8000, `vwb-backend` 5002, `stream-worker`, `worker`.
- Services `disabled` : oui.
- Exposition externe : non, bind `127.0.0.1`.
- VWB frontend 3002 : non inclus, Node absent / a trancher.
- Grounding service dedie : non finalise.
### Securite
- WP-A : dashboard refuse demarrage sans `DASHBOARD_PASSWORD`.
- WP-B : verrou reenrolement via `RPA_FLEET_ENROLL_LOCKED`.
- `.env.local` DGX genere sur place, valeurs non consignees.
- Auth disabled interdit en contexte DGX.
- Reste : WP-C token par poste, a mettre en P0 apres tests reels.
### Donnees entrainees
- Archive V2 transferee et verifiee.
- `workflows.db` : 23 workflows.
- `visual_anchors` : 199.
- Anchors : 468 PNG, 398 references effectives OK.
- FAISS : dim 512, `ntotal=13666`.
- Exclusions confirmees : `live_sessions`, `sessions`, `uploads`, `screenshots`, `.env*`, secrets.
### GPU / modeles
- P1.g device policy validee.
- RTX 5070 local : GO Qwen.
- DGX GB10 : GO avec correction auto, puis QG final GO.
- `RPA_VISION_DEVICE=auto` OK apres correctif memory-unified.
- EasyOCR + ultralytics + poids YOLO icon_detect installes sur DGX.
- UI-TARS reste non active pour sante/replay : bench dangereux.
- Qwen3-VL-4B via vLLM reste candidat grounder, pas cable runtime large.
- `gemma4:26b` reste candidat supervise/judge.
## Branching Gitea a trancher demain
Constat actuel :
- `poc-dgx` existe et contient le snapshot operationnel DGX.
- `main` est en retard/ambigu pour le POC.
- Plusieurs branches historiques existent.
Proposition Codex :
- `dev` : branche de developpement active, integration quotidienne.
- `poc/nvp/prod` ou `poc-prod` : branche stable POC/NVP/prod, protegee par QG.
- `poc-dgx` : conserver comme snapshot DGX du 2026-06-08, puis soit merger dans `poc/nvp/prod`, soit le garder comme tag/branche de release.
Decision requise :
- nom exact de la branche stable : `poc/nvp/prod`, `poc-prod`, ou autre ;
- regle de promotion : dev -> QG -> stable ;
- qui peut pousser sur stable.
## Docker vs clone
Etat :
- Clone + venv + systemd fonctionne maintenant et doit rester le chemin POC court terme.
- Docker n'est pas tranche.
- Un document existe : `docs/ROADMAP_DOCKERISATION.md`.
Position Codex :
- Court terme : garder clone/venv/systemd pour ne pas casser le POC.
- Moyen terme : etudier Docker pour services applicatifs seulement, avec volumes externes pour data/secrets/modeles.
- Ollama/GPU/DGX ARM doivent etre traites explicitement, pas caches dans un Dockerfile premature.
Decision demain :
- lancer un audit Docker read-only ;
- produire deux options : `clone+systemd durci` vs `docker compose applicatif`.
## Dashboard — priorite demain
Le dashboard est critique et doit etre teste comme produit, pas seulement comme process UP.
Checklist minimale :
- login Basic avec `DASHBOARD_PASSWORD` ;
- fail-closed si secret absent ;
- onglet Fleet visible ;
- liste agents ;
- creation/enrolement agent ;
- revoke ;
- `RPA_FLEET_ENROLL_LOCKED` ;
- generation paquet agent ;
- proxy dashboard -> streaming ;
- pas de fuite token dans UI/logs/ZIP ;
- VWB backend accessible local-only ;
- actions dashboard realistes : ouvrir workflow, voir donnees entrainees, lancer preflight.
QG attendu :
- Qwen doit verifier securite et parcours ;
- Claude execute les tests avec captures/logs ;
- Codex tranche les corrections.
## Tests reels — priorite produit
Objectif demain :
1. Verifier dashboard DGX local-only.
2. Preflight Lea live.
3. Dom devant Windows.
4. Capture supervisee safe Notepad/Explorateur/Easily lecture seule.
5. Preuves attendues :
- `live_events.jsonl`;
- `agent_chat/state/learn_*.json`;
- shadow understanding non vide ;
- workflow genere ;
- logs/captures coherents ;
- preflight replay non destructif.
6. Replay controle uniquement, pas autonome.
7. Ensuite seulement, multi-machine.
Stop conditions :
- pas de replay autonome ;
- pas d'action metier destructive ;
- pas d'exposition externe ;
- pas de service `enable` sans decision ;
- pas de secret affiche ;
- pas de suppression code mort.
## Missions demain proposees
### Codex
- Reprendre ce handoff.
- Valider branches Gitea avec Dom.
- Orchestrer dashboard tests.
- Trancher Docker vs clone : lancer audit read-only.
- Maintenir QG strict avant promotion stable.
### Claude
- Executer test dashboard DGX bout-en-bout.
- Preparer puis executer preflight Lea live.
- Produire rapport preuves.
- Bench grounder Qwen3-VL-4B sur Easily reel si preconditions OK.
- Ne pas toucher code mort.
### Qwen
- QG dashboard securite/parcours.
- QG Lea live preflight.
- QG branches/procedure promotion.
- QG Docker vs clone.
- Maintenir registre decisions.
## Etat git local a ne pas oublier
Le repo local reste charge en fichiers non suivis de coordination, graphify et artefacts d'agents.
Important :
- ne pas faire de cleanup destructif ;
- ne pas commit les secrets ;
- ne pas commit `data/` massif ;
- `poc-dgx` est la branche operationnelle Gitea ;
- le handoff present est une source de reprise, pas un commit applicatif.
— Codex

View File

@@ -0,0 +1,157 @@
# Handoff Codex — fin journee 2026-06-09 / reprise 2026-06-10
- `Date`: 2026-06-09 21:18 CEST
- `Auteur`: Codex
- `Statut`: source de reprise operationnelle
- `Objet`: fin de cycle dashboard DGX, M2 Lea live, WP-C, branches, Docker
## Resume executif
La journee a transforme le socle DGX/local-only en POC produit beaucoup plus proche du live.
Acquis valides :
- Dashboard DGX securite : GO.
- Dashboard produit : GO post-P0 Qwen.
- P0-1 workflows : corrige par symlink `data/workflows` -> `visual_workflow_builder/backend/data/workflows`, VWB 5002 sert 39 workflows.
- P0-2 paquet agent : ZIP genere, download HTTP 200, usage POC interne seulement.
- M2 Lea live : cible technique validee = serveur dev Linux `192.168.1.40:5005`, agent Windows pointe deja dessus.
- Tunnel Ollama DGX : rendu persistant via `systemd --user`, QG GO, 10 modeles accessibles via `127.0.0.1:11434`.
- Branching : branche stable decidee = `poc-prod`.
- Deploiement POC court terme : clone + venv + systemd, pas Docker.
- WP-C : Patch 1 local/TDD fait, commit local `f7f692641`, QG GO, non pousse, non deploye.
Non acquis / a reprendre :
- M2 Lea live non execute : attend Dom devant Windows + GO Codex au moment T.
- WP-C Patch 2 autorise a 21:06 local/TDD seulement ; pas encore de resultat lu au moment du handoff.
- WP-C complet non implemente ; multi-TIM reste NOGO.
- Push/deploiement WP-C non decide.
- Branches `main/dev/poc-prod` non creees/poussees ; blobs ~15 G non traites ; `master` non tranche.
- DETTE-015 workflow store : documentee, migration post-POC seulement.
## Sources de verite recentes
### Dashboard / P0
- `docs/coordination/inbox_codex/2026-06-09_2025_claude-to-codex-qwen_RESULTAT-P0-DASHBOARD-CORRECTIONS.md`
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_QG-POST-P0-DASHBOARD-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2035_claude-to-codex-qwen_INFO-DETTE-015-store-workflows.md`
- `docs/DETTE_TECHNIQUE.md`
- `docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md`
### M2 Lea live
- `docs/coordination/inbox_codex/2026-06-09_1546_claude-to-codex_PREP-M2-EXECUTION-CHECKLIST.md`
- `docs/coordination/inbox_codex/2026-06-09_1605_claude-to-codex-qwen_TUNNEL-OLLAMA-PERSISTANT-FAIT.md`
- `docs/coordination/inbox_codex/2026-06-09_1710_claude-to-codex_RESULTAT-TUNNEL-M2-STABILITE.md`
- `docs/coordination/inbox_codex/2026-06-09_2025_qwen-to-codex-claude-dom_QG-TUNNEL-PERSISTANT-M2-READINESS-GO.md`
### WP-C
- `docs/coordination/inbox_codex/2026-06-09_1715_claude-to-codex_CARTO-WPC-TOKEN-PAR-POSTE-READONLY.md`
- `docs/coordination/inbox_codex/2026-06-09_2040_claude-to-codex_PLAN-WPC-TDD-EXECUTABLE.md`
- `docs/coordination/inbox_codex/2026-06-09_2058_claude-to-codex_RESULTAT-WPC-PATCH1-MIGRATION-TDD.md`
- `docs/coordination/inbox_codex/2026-06-09_2104_claude-to-codex_INFO-WPC-PATCH1-COMMITE.md`
- `docs/coordination/inbox_codex/2026-06-09_2110_qwen-to-codex-claude-dom_QG-WPC-PATCH1-GO.md`
- `docs/coordination/inbox_claude/2026-06-09_2106_codex-to-claude_GO-WPC-PATCH2-local-TDD-only.md`
- `docs/coordination/inbox_qwen/2026-06-09_2106_codex-to-qwen_ATTENTE-QG-WPC-patch2.md`
### Branches / Docker
- `docs/coordination/inbox_codex/2026-06-09_1405_claude-to-codex_AUDIT-M3-branches-gitea-readonly.md`
- `docs/coordination/inbox_codex/2026-06-09_1410_claude-to-codex_AUDIT-DOCKER-VS-CLONE-SYSTEMD.md`
- `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
## Decisions actees
### Dashboard DGX
- Securite dashboard : GO.
- Produit dashboard : GO post-P0.
- P0-1 vrai fix : symlink workflows, pas `DATABASE_URL`.
- `DATABASE_URL` : Qwen recommande de le garder ; il aide SQLAlchemy a lire la bonne DB, meme s'il ne corrige pas `/api/workflows/`.
- DETTE-015 : double store workflows documente, migration post-POC uniquement.
### M2 Lea live
- Cible M2 : serveur dev Linux `192.168.1.40:5005`, pas DGX directement.
- Ollama : tunnel local `127.0.0.1:11434` vers DGX, service `ollama-tunnel.service` user, GO Qwen.
- M2 live : techniquement pret, mais execution uniquement avec Dom devant Windows + GO Codex au moment T.
### Branches / Docker
- Stable = `poc-prod`.
- `poc-dgx` conserve comme snapshot/deploy DGX.
- Court terme POC = clone + venv + systemd.
- Docker = post-POC, hybride/progressif.
### WP-C
- WP-C reste bloquant multi-TIM.
- Patch 1 : commit local `f7f692641`, GO Qwen, non pousse, non deploye.
- Patch 2 : GO Codex donne a Claude a 21:06, local/TDD seulement.
## Etat technique detaille
### DGX
- DGX reste local-only.
- Services DGX operationnels selon les tests du jour.
- VWB 5002 `/api/workflows/` = 39 apres symlink.
- Paquet agent `deploy/Lea_v1.0.0.zip` existe et sert HTTP 200, mais contient encore le token global.
- Pas de distribution multi-TIM.
### M2
- Agent Windows gele pointe `http://192.168.1.40:5005/api/v1`.
- Serveur dev Linux expose 5005 LAN, workflows visibles.
- Tunnel Ollama persistant local sur le dev.
- Checklist M2 prete.
- Live Shadow non execute.
### WP-C
- Patch 1 modifie :
- `agent_v0/server_v1/agent_registry.py`
- `tests/unit/test_wpc_migration.py`
- Tests annonces par Claude :
- `tests/unit/test_wpc_migration.py` : 3 passed
- `test_fleet_enroll_lock_wpb.py` + migration : 9 passed
- Commit local : `f7f692641 feat(wp-c): migration colonnes token par poste (patch 1, inerte)`
- Prochaine etape autorisee au moment du handoff : Patch 2 local/TDD seulement.
## Stop conditions maintenues
- Pas de live Shadow sans Dom devant Windows.
- Pas de replay autonome.
- Pas de multi-TIM avant WP-C.
- Pas de Patch 3+ sans QG.
- Pas de deploiement DGX WP-C sans decision.
- Pas de push Gitea sans decision.
- Pas de suppression code mort.
- Pas de secret en clair.
## Reprise demain
1. Lire ce handoff.
2. Lire les nouveaux messages `inbox_codex` apres 2026-06-09 21:18 CEST.
3. Si Claude a livre `RESULTAT-WPC-PATCH2-ENROLL-TOKEN-TDD`, demander/attendre QG Qwen.
4. Si Dom veut M2 live :
- verifier tunnel Ollama ;
- verifier dashboard/workflows ;
- verifier agent Windows running ;
- confirmer Dom devant Windows ;
- donner GO Codex uniquement au moment T.
5. Ne pas lancer multi-TIM.
6. Ne pas pousser `f7f692641` ni autres commits sans decision.
7. Reprendre branches `poc-prod` seulement apres arbitrage blobs/protection.
## Etat git a retenir
- Branche locale : `poc-dgx`.
- HEAD local : `f7f692641`, en avance sur `gitea/poc-dgx` (`09f65cecb`) d'au moins le Patch 1 WP-C.
- Commit `f7f692641` non pousse et non deploye DGX.
- Worktree charge en fichiers de coordination, graphify, docs et artefacts non suivis : ne pas nettoyer destructivement.
— Codex

View File

@@ -0,0 +1,66 @@
# Handoff Codex — contexte bas / relais avant reset 14:12
- `Date`: 2026-06-10 09:18 CEST
- `Auteur`: Codex
- `Statut`: source de reprise operationnelle
- `Declencheur`: Dom signale contexte Codex tres bas et reset prevu vers 14:12
## Etat courant confirme
- Branche locale : `poc-dgx`.
- HEAD local : `f7f692641 feat(wp-c): migration colonnes token par poste (patch 1, inerte)`.
- Worktree charge en docs/coordination, graphify, artefacts non suivis : ne pas nettoyer.
- Dashboard DGX securite + produit : GO.
- P0 workflows + ZIP : resolus.
- M2 Lea live : techniquement pret, cible `192.168.1.40:5005`, tunnel Ollama persistant GO, execution uniquement avec Dom devant Windows + GO Codex au moment T.
- WP-C Patch 1 : GO Qwen, commit local, non pousse, non deploye.
- WP-C Patch 2 : autorise local/TDD depuis 2026-06-09 21:06, non livre au moment de ce handoff.
- Multi-TIM : NOGO avant WP-C complet.
## Objectif de continuite
Permettre a Claude et Qwen d'avancer sans dependance forte au contexte Codex :
- Claude avance sur l'execution locale/TDD strictement bornee.
- Qwen garde le fil, rend les QG, signale les contradictions a Dom.
- Dom reste l'arbitre pour live Windows, push, deploiement, branches et toute extension de scope.
## Planning propose jusqu'au reset 14:12
| Fenetre | Owner | Action | Sortie attendue |
|---|---|---|---|
| Maintenant -> 10:30 | Claude | Relancer/executer WP-C Patch 2 local/TDD seulement | `RESULTAT-WPC-PATCH2-ENROLL-TOKEN-TDD` vers `inbox_codex/` |
| Des reception RESULTAT | Qwen | QG Patch 2 | `QG-WPC-PATCH2-GO` ou `NOGO` vers `inbox_codex/` |
| En parallele | Qwen | Tenir l'etat actif et verifier contradictions | Maj active/registre si necessaire |
| En parallele | Claude | Preparer read-only M2/checklist/rollback, sans live | Note courte si utile, aucune execution Windows |
| Si Codex indisponible | Qwen + Dom | Maintenir gates et arbitrer la suite | Pas de Patch 3/push/deploiement sans decision explicite |
## Autorisations pendant indisponibilite Codex
Autorise sans nouveau Codex :
- Claude : Patch 2 WP-C local/TDD uniquement, selon message `docs/coordination/inbox_claude/2026-06-09_2106_codex-to-claude_GO-WPC-PATCH2-local-TDD-only.md`.
- Claude : audits read-only, plans de rollback, plans de test, preparation M2 sans execution.
- Qwen : QG, audit, contradiction check, synthese, registre, mise a jour de pointeurs actifs.
Non autorise sans decision explicite Dom/Codex :
- Patch 3+ WP-C.
- Branchement auth runtime.
- Build/package ZIP modifie.
- Push Gitea ou creation/protection branches.
- Deploiement DGX.
- Live M2 / replay / Shadow sans Dom devant Windows.
- Multi-TIM.
- Suppression de code mort ou nettoyage destructif.
## Reprise Codex apres reset
1. Lire ce handoff.
2. Lire `docs/coordination/active/2026-06-10_0918_continuite-codex-context-reset.md`.
3. Lire les nouveaux `inbox_codex/` apres 2026-06-10 09:18 CEST.
4. Si Claude a livre Patch 2 : attendre/lire QG Qwen avant toute suite.
5. Si Qwen a rendu GO Patch 2 : demander decision Dom avant Patch 3, push ou deploiement.
6. Si Dom veut M2 live : verifier chaine complete et confirmer Dom devant Windows avant GO.
— Codex

View File

@@ -0,0 +1,113 @@
# Handoff Codex — fin journee 2026-06-10 / reprise 2026-06-11
- `Auteur`: Codex
- `Date cloture`: 2026-06-10 23:37 CEST
- `Reprise`: 2026-06-11
- `Mode demande par Dom`: bi-turbo, focus POC DGX
- `Statut`: pret reprise
## Cap demain
Priorite unique : **POC DGX a fond**, sans dispersion.
Deux pistes en parallele :
1. **Operationnel M2/DGX**
- smoke DGX apres nuit / eventuel reboot ;
- verification acces dashboard/Fleet depuis Windows ;
- M2 live supervise avec Dom devant Windows ;
- collecte preuves record -> replay -> apprentissage si disponible.
2. **Cadre QG / qualite**
- Claude finalise runbook M2 + Git safety readonly + smoke DGX ;
- Qwen relit runbook Claude et rend GO/NOGO ;
- Codex orchestre, verifie, tranche avec Dom.
## Etat valide au depart
### DGX
- 6 services systemd DGX : `enabled` + `active`.
- DGX = cible POC.
- DEV = machine de code/test, Ollama via tunnel vers DGX.
- `ollama-tunnel.service` actif/persistant cote DEV.
- VLM cible actee par Qwen : `Qwen3-VL-4B-Instruct` pour POC, avec tunnel Ollama.
- Dashboard expose temporairement en HTTP direct pour test : decision acceptee, reversible, pas cible clinique.
- Point a verifier demain : activation effective cote DGX depuis Windows (`restart dashboard` / `ufw allow 5001/tcp` si necessaire avec Dom).
### WP-C / securite agents
- WP-C token par poste arrete pour POC.
- `DETTE-016` creee en P2/ACCEPTED.
- Patch 4 runtime annule.
- Patch 1-3 locaux/inertes/non deployes.
- Multi-TIM POC controle : GO Dom via Fleet/dashboard existant.
- Multi-TIM elargi/post-POC : NOGO sans WP-C ou equivalent.
### Git
- Branche courante locale : `poc-dgx`.
- `poc-dgx` local est 3 commits devant `gitea/poc-dgx`.
- Les 3 commits devant sont WP-C inertes :
- `f7f692641` Patch 1 migration colonnes token ;
- `9fb2c7bfe` Patch 2 generation token enroll ;
- `b20d17882` Patch 3 verify_token registre.
- Branche archive locale creee : `archive/wpc-local-inerte-2026-06-10` -> `b20d17882`.
- **Interdit demain matin** : push `poc-dgx` tel quel.
## Messages / docs a relire en premier demain
1. `docs/coordination/active/2026-06-11_0000_pointeur-handoff-reprise-2026-06-11.md`
2. `docs/coordination/inbox_codex/2026-06-10_2345_qwen-to-codex-claude-dom_ACK-CADRE-TRAVAIL-POC-DGX.md`
3. `docs/coordination/inbox_codex/2026-06-10_2315_qwen-to-codex-claude-dom_QG-GATES-POST-WPC-ABANDON.md`
4. `docs/coordination/inbox_codex/2026-06-10_2316_qwen-to-codex-claude-dom_QG-M2-LIVE-READINESS.md`
5. `docs/AUDIT_GAPS_APPLI_100PCT_2026-06-10.md`
6. `docs/coordination/inbox_claude/2026-06-10_2330_qwen-to-claude_AVIS-GAPS-APPLI-100PCT.md`
7. `docs/coordination/active/2026-06-10_1540_repartition-post-wpc-dgx-m2.md`
## Actions immediates demain
1. Lire les nouveaux messages Claude/Qwen.
2. Verifier si Claude a depose :
- `RESULTAT-GIT-SAFETY-WPC-ARCHIVE-READONLY`
- `RUNBOOK-M2-LIVE-POC`
- `SMOKE-DGX-DEMAIN-MATIN`
3. Faire smoke DGX readonly :
- `systemctl is-enabled/is-active` des 6 services ;
- health `5005/5002` ;
- dashboard `5001` ;
- Ollama tags ;
- VWB workflows ;
- Fleet API sans afficher de token.
4. Verifier accessibilite Windows -> dashboard DGX.
5. Si Dom est devant Windows : GO/NOGO Codex pour M2 live.
## Stop conditions
- Secret en clair dans logs/docs/reponses.
- `RPA_AUTH_DISABLED=true`.
- Agent rouge / crash dashboard.
- Tentative Patch 4 runtime.
- Push/deploiement sans Dom.
- Rebase/reset/cherry-pick sans decision Dom/Codex.
## Risques techniques prioritaires connus
Depuis `AUDIT_GAPS_APPLI_100PCT_2026-06-10.md` et avis Qwen :
- `A1`: timeout HTTP client 5s pouvant perdre une action longue.
- `A2`: watchdog `_retry_pending` serveur absent.
- `P1`: DETTE-006/010 grounding Qwen3-VL/smart_resize a trancher.
- `A3`: ecran Windows verrouille non detecte.
Pour demain : ne pas coder ces points sans revalidation existing-first et decision Dom/Codex. Le premier objectif reste M2 live supervise.
## Definition de sortie demain matin
- DGX smoke OK.
- Runbook M2 valide QG.
- Dom devant Windows.
- Agent Windows visible actif dans Fleet.
- Un scenario record/replay documente avec preuves.
— Codex

View File

@@ -0,0 +1,72 @@
# Handoff - 2026-06-13 - Codex watcher + reprise POC VLM
- `Date`: 2026-06-13 08:45 CEST (DEV local)
- `Auteur`: Codex
- `Statut`: source de reprise operationnelle
## Pre-check watcher obligatoire
Au debut de la prochaine session, avant toute action :
1. `docs/coordination/coordination_loop.sh ensure`
2. Lire les messages pertinents pour l'agent courant dans `inbox_codex/`, `inbox_claude/`, `inbox_qwen/` et `active/`.
3. Apres traitement : `docs/coordination/coordination_loop.sh ack`
Si le watcher ne peut pas etre lance ou verifie, signaler le blocage avant de continuer.
Etat valide au handoff :
- service utilisateur `rpa-coordination-watcher.service` installe, active et enabled ;
- commande service : `docs/coordination/coordination_loop.sh watch 15` ;
- PID observe apres redemarrage : `1229002` ;
- file locale `.loop_state/unread_messages.tsv` nettoyee ;
- dernier `ensure` : `loop OK`, `0 pending`.
Commandes utiles :
- `docs/coordination/coordination_loop.sh service-status`
- `docs/coordination/coordination_loop.sh pending`
- `docs/coordination/coordination_loop.sh events`
- `docs/coordination/coordination_loop.sh service-install` si le service systemd utilisateur manque
- `docs/coordination/coordination_loop.sh service-stop` pour arreter/desactiver explicitement le service
## Etat courant
- Le watcher/loop a ete revu et consolide : queue unread persistante, digest lisible, trigger par message, dernier trigger, journal events, lock `flock` sur scan/baseline/ack, service systemd utilisateur persistant.
- La baseline coordination a ete reinitialisee apres nettoyage des faux positifs anciens.
- Le tri des baselines a ete durci en `LC_ALL=C sort -u` avant `comm`, pour eviter les faux nouveaux messages.
- `docs/coordination/README.md`, `docs/handoffs/README.md`, `docs/handoffs/TEMPLATE_HANDOFF.md` et `AGENTS.md` imposent maintenant `ensure` en debut de session.
## POC VLM
- Commit local courant : `5c5ce747b feat(grounding): cablage Qwen3-VL-4B/vLLM (RPA_GROUNDING_ENGINE, defaut off)`.
- Commit non pousse.
- Cablage par env `RPA_GROUNDING_ENGINE=qwen3vl_vllm`.
- Defaut OFF : aucun impact runtime si l'env n'est pas posee.
- Cross-review terminee : Codex + Qwen OK gates/securite, Claude a committe localement.
## Prochaines actions
1. Reprise session : lancer `docs/coordination/coordination_loop.sh ensure`.
2. Lire toute nouvelle coordination, puis `ack`.
3. Validation E2E DGX en one-shot du mode `RPA_GROUNDING_ENGINE=qwen3vl_vllm` contre `rpa-vllm-grounder` local-only `127.0.0.1:8001`.
4. Ne pas rendre l'activation permanente dans les services POC tant que Dom n'a pas donne GO.
5. Documenter les resultats E2E dans `docs/coordination/active/` et prevenir Claude/Qwen.
## Garde-fous
- DGX POC prioritaire ; DEV sert a valider avant passage DGX.
- Pas de push, pas de deploy, pas de changement auth/reseau, pas d'activation runtime permanente sans GO Dom.
- Pas de secret dans les handoffs/messages.
- Decisions importantes a faire remonter a Dom via Claude, canal remote.
- Donnees cliniques interdites.
## References
- `docs/coordination/coordination_loop.sh`
- `docs/coordination/systemd/rpa-coordination-watcher.service`
- `docs/coordination/README.md`
- `docs/handoffs/README.md`
- `.remember/remember.md`
- `docs/coordination/inbox_codex/2026-06-13_1006_claude-to-codex-qwen_COMMIT-CABLAGE-ET-HANDOFF-SESSION2.md`
- `docs/coordination/inbox_codex/2026-06-13_1000_qwen-to-codex-claude_ACK-CABLAGE-GATES-SECURITE.md`

View File

@@ -0,0 +1,111 @@
# Handoff Codex avant reboot serveur — Lea chat / multi-utilisateur / DETTE-019
Date: 2026-06-15 09:51 Europe/Paris
Auteur: Codex
Contexte: Dom va redemarrer la machine. Reprise attendue apres reboot.
## Consigne obligatoire a la reprise
Avant toute coordination:
```bash
docs/coordination/coordination_loop.sh ensure
```
Puis lire tous les messages pertinents pour Codex et terminer par:
```bash
docs/coordination/coordination_loop.sh ack
```
Le watcher doit etre operationnel pour Codex, Claude et Qwen avant de continuer.
## Etat coordination
- Watcher OK avant handoff.
- Dernier message lu: `docs/coordination/inbox_codex/2026-06-15_0947_claude-to-qwen-codex_DETTE019-DEPLOYEE-PREUVE-RUNTIME.md`.
- Ack coordination a faire juste apres creation de ce handoff.
## Etat Lea chat diagnostique par Codex
Constats non destructifs:
- `agent_chat` local 5004 repond:
- `/api/status` => `online`
- 130 workflows detectes
- `POST /api/chat` => 200 pour `statut`
- `POST /api/chat` => 200 pour `qu'est-ce que tu sais faire ?`
- `streaming` local 5005 repond `/health` => `healthy`.
- `POST /api/learn/start` fonctionne. Une session diagnostic a ete creee puis annulee proprement.
- `tests/unit/test_chat_interface.py -q` => 34 passed.
- `virsh list --all` montrait `win11` en `shut off`.
- SocketIO:
- sans Origin: OK
- `Origin: http://localhost:5004`: OK
- `Origin: http://192.168.1.40:5004`: OK
- `Origin: http://127.0.0.1:5004`: FAIL HTTP 400
Hypothese principale:
- Le backend texte fonctionne, mais les echanges d'actions visibles dans la ChatWindow sont probablement coupes car le process `agent_chat` actuel n'a pas `LEA_FEEDBACK_BUS` dans son environnement.
- Dans `agent_chat/app.py`, `_emit_lea()` est no-op si `LEA_FEEDBACK_BUS` est false/absent.
- La ChatWindow native attend ces events `lea:*` pour afficher les bulles d'action: `action_started`, `action_progress`, `done`, `paused`, `resumed`.
## Travail donne a Claude et Qwen
Claude:
- Fichier: `docs/coordination/inbox_claude/2026-06-15_0942_codex-to-claude-qwen_PRIORITES-LEA-CHAT-ACTIONS-MULTIUSER.md`
- Mission: diagnostic runtime VM/Lea, config `RPA_SERVER_URL`, `RPA_AGENT_CHAT_URL`, `RPA_MACHINE_ID`, `LEA_FEEDBACK_BUS`, tests ChatWindow et bus action apres reboot VM.
Qwen:
- Fichier: `docs/coordination/inbox_qwen/2026-06-15_0942_codex-to-qwen-claude_QG-LEA-CHAT-ACTIONS-MULTIUSER.md`
- Mission: gates GO/NOGO chat/action/multi-utilisateur.
Trace active:
- `docs/coordination/active/2026-06-15_0942_priorites-lea-chat-interface-multiuser.md`
## Priorites proposees a la reprise
P0 - Depuis la VM Windows apres redemarrage:
- Verifier que `RPA_SERVER_URL` pointe vers `http://<serveur>:5005/api/v1`.
- Verifier que `RPA_AGENT_CHAT_URL` pointe vers `http://<serveur>:5004`.
- Verifier que `RPA_MACHINE_ID` est unique.
- Tester `/health` 5005, `/api/status` 5004, puis `POST /api/chat`.
- Lancer Lea et verifier le log `LeaServerClient initialise : chat=... stream_url=...`.
P1 - Echanges d'actions:
- Decider si `LEA_FEEDBACK_BUS=1` est requis pour la session.
- Si oui, activer/verifier cote `agent_chat` et cote Windows.
- Preuve minimale: voir `lea:action_started`, `lea:action_progress`, `lea:done` dans la ChatWindow pour un replay safe.
P2 - Multi-utilisateur:
- Deux agents avec `RPA_MACHINE_ID` distincts.
- Deux sessions chat distinctes.
- Un ordre/replay cible la bonne machine.
- Pas de transcript/action visible sur l'autre machine.
## DETTE-019
Dernier message Claude lu:
- `DETTE-019` est deployee runtime POC DGX.
- Push `33c1e2e0d` fait.
- DGX HEAD `33c1e2e`.
- `workflows.db` preservee, backup `.bak-predeploy-dette019-20260615`.
- `rpa-streaming` DGX redemarre, service actif, health 200.
- Preuve runtime: 5 cibles texte score 0.90, cas douteux `0013` rejete par `rejected_low_score_grounding`.
- `DETTE-018` reste ouverte.
## Ce que Codex n'a pas fait
- Pas de modification code applicatif.
- Pas de restart service.
- Pas de modification VM.
- Pas de token expose dans ce handoff.

23
docs/handoffs/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Handoffs - regle de reprise par defaut
Tout handoff ou prompt de reprise doit commencer par le pre-check coordination.
## Pre-check watcher obligatoire
Avant de reprendre le travail :
1. Verifier/lancer/scanner le watcher :
`docs/coordination/coordination_loop.sh ensure`
2. Lire les messages pertinents pour l'agent courant dans `inbox_codex/`,
`inbox_claude/`, `inbox_qwen/` et `active/`.
3. Apres traitement, vider la file locale :
`docs/coordination/coordination_loop.sh ack`
Si le watcher ne peut pas etre lance ou verifie, le handoff de reprise doit le
signaler comme blocage avant toute autre action.
Cette regle vaut pour Codex, Claude et Qwen.

View File

@@ -0,0 +1,25 @@
# Handoff - YYYY-MM-DD
- `Date`:
- `Auteur`:
- `Statut`: source de reprise operationnelle
## Pre-check watcher obligatoire
Au debut de la prochaine session, avant toute action :
1. `docs/coordination/coordination_loop.sh ensure`
2. Lire les messages pertinents pour l'agent courant.
3. Apres traitement : `docs/coordination/coordination_loop.sh ack`
Si le watcher ne peut pas etre lance ou verifie, signaler le blocage.
## Etat courant
## Decisions actives
## Prochaines actions
## Garde-fous
## References

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""PP-OCRv5 CPU baseline bench — dry-run 1 capture.
Compare docTR vs EasyOCR vs PP-OCRv5 (CPU-only paddlepaddle).
Label obligatoire : baseline CPU, non verdict GPU.
Metrics:
- text accuracy (field-level exact match)
- word bbox center error (px) vs docTR reference
- latency cold/warm (s)
- peak memory (MB)
"""
import time
import tracemalloc
import json
import sys
from pathlib import Path
# ── Config ──
TEST_IMAGE = Path("/home/dom/ai/rpa_vision_v3/data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260318T010719_62a058/shots/shot_0172_full.png")
EASILY_IMAGE = Path("/home/dom/ai/rpa_vision_v3/output/playwright/easily_dryrun_2026-05-26/landing_wide.png")
RESULTS_JSON = Path("/home/dom/ai/rpa_vision_v3/scripts/bench_ppocrv5_results.json")
ENGINES = ["ppocrv5_cpu", "doctr", "easyocr"]
def bench_ppocrv5_cpu(img_path: Path) -> dict:
"""Run PP-OCRv5 CPU on image, return results dict."""
from paddleocr import PaddleOCR
tracemalloc.start()
ocr = PaddleOCR(
use_textline_orientation=True,
lang="fr",
return_word_box=True,
)
mem_init = tracemalloc.get_traced_memory()[1] / 1024 / 1024
# Cold run
t0 = time.perf_counter()
result_cold = ocr.ocr(str(img_path))
t_cold = time.perf_counter() - t0
# Warm run
t0 = time.perf_counter()
result_warm = ocr.ocr(str(img_path))
t_warm = time.perf_counter() - t0
mem_peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
tracemalloc.stop()
# Parse results — PaddleOCR v3.4 returns list of pages
texts = []
bboxes = []
if result_cold and result_cold[0]:
for line in result_cold[0]:
if line is None:
continue
bbox_raw = line[0] # [[x1,y1],[x2,y2],[x3,y3],[x4,y4]]
text = line[1][0] # recognized text
confidence = line[1][1]
# Compute center
xs = [pt[0] for pt in bbox_raw]
ys = [pt[1] for pt in bbox_raw]
cx = sum(xs) / len(xs)
cy = sum(ys) / len(ys)
texts.append({"text": text, "confidence": confidence})
bboxes.append({"bbox": bbox_raw, "center": (cx, cy), "text": text})
return {
"engine": "ppocrv5_cpu",
"image": str(img_path),
"cold_latency_s": round(t_cold, 3),
"warm_latency_s": round(t_warm, 3),
"mem_init_MB": round(mem_init, 1),
"mem_peak_MB": round(mem_peak, 1),
"num_detections": len(texts),
"texts": texts,
"bboxes": bboxes,
"paddle_version": "3.4.0",
"paddlepaddle_version": "3.3.1",
"device": "cpu",
"cuda_available_driver": True,
"cuda_compiled_paddle": False,
"label": "baseline CPU, non verdict GPU",
}
def bench_doctr(img_path: Path) -> dict:
"""Run docTR CPU on image."""
from doctr.models import ocr_predictor
tracemalloc.start()
predictor = ocr_predictor(pretrained=True)
mem_init = tracemalloc.get_traced_memory()[1] / 1024 / 1024
from doctr.io import DocumentFile
doc = DocumentFile.from_images(str(img_path))
t0 = time.perf_counter()
result = predictor(doc)
t_cold = time.perf_counter() - t0
t0 = time.perf_counter()
result2 = predictor(doc)
t_warm = time.perf_counter() - t0
mem_peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
tracemalloc.stop()
texts = []
bboxes = []
for page in result.pages:
for block in page.blocks:
for line in block.lines:
for word in line.words:
texts.append({"text": word.value, "confidence": word.confidence})
# docTR bbox in relative coords (0-1)
bbox = word.geometry
# Convert relative to pixel
import PIL.Image
with PIL.Image.open(img_path) as im:
w, h = im.size
cx = (bbox[0][0] + bbox[1][0]) / 2 * w
cy = (bbox[0][1] + bbox[1][1]) / 2 * h
bboxes.append({
"bbox_relative": [(bbox[0][0], bbox[0][1]), (bbox[1][0], bbox[1][1])],
"center_px": (round(cx, 1), round(cy, 1)),
"text": word.value,
})
return {
"engine": "doctr",
"image": str(img_path),
"cold_latency_s": round(t_cold, 3),
"warm_latency_s": round(t_warm, 3),
"mem_init_MB": round(mem_init, 1),
"mem_peak_MB": round(mem_peak, 1),
"num_detections": len(texts),
"texts": texts,
"bboxes": bboxes,
"version": "1.0.1",
"device": "cpu",
"label": "baseline CPU",
}
def bench_easyocr(img_path: Path) -> dict:
"""Run EasyOCR CPU on image."""
import easyocr
tracemalloc.start()
reader = easyocr.Reader(["fr"], gpu=False)
mem_init = tracemalloc.get_traced_memory()[1] / 1024 / 1024
t0 = time.perf_counter()
result = reader.readtext(str(img_path))
t_cold = time.perf_counter() - t0
t0 = time.perf_counter()
result2 = reader.readtext(str(img_path))
t_warm = time.perf_counter() - t0
mem_peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
tracemalloc.stop()
texts = []
bboxes = []
for detection in result:
bbox_raw = detection[0] # list of [x,y] points
text = detection[1]
confidence = detection[2]
xs = [pt[0] for pt in bbox_raw]
ys = [pt[1] for pt in bbox_raw]
cx = sum(xs) / len(xs)
cy = sum(ys) / len(ys)
texts.append({"text": text, "confidence": confidence})
bboxes.append({"bbox": bbox_raw, "center_px": (round(cx, 1), round(cy, 1)), "text": text})
return {
"engine": "easyocr",
"image": str(img_path),
"cold_latency_s": round(t_cold, 3),
"warm_latency_s": round(t_warm, 3),
"mem_init_MB": round(mem_init, 1),
"mem_peak_MB": round(mem_peak, 1),
"num_detections": len(texts),
"texts": texts,
"bboxes": bboxes,
"version": "1.7.2",
"device": "cpu",
"label": "baseline CPU",
}
def main():
# Check image exists
img = TEST_IMAGE if TEST_IMAGE.exists() else EASILY_IMAGE
if not img.exists():
print(f"ERROR: No test image found. Tried {TEST_IMAGE} and {EASILY_IMAGE}")
sys.exit(1)
print(f"Bench image: {img}")
print(f"Image size: ...")
import PIL.Image
with PIL.Image.open(img) as im:
w, h = im.size
print(f" {w}x{h}, mode={im.mode}")
all_results = {}
# ── PP-OCRv5 CPU ──
print("\n=== PP-OCRv5 CPU ===")
try:
r = bench_ppocrv5_cpu(img)
all_results["ppocrv5_cpu"] = r
print(f" Cold: {r['cold_latency_s']}s | Warm: {r['warm_latency_s']}s | Detections: {r['num_detections']}")
print(f" Memory: init {r['mem_init_MB']}MB | peak {r['mem_peak_MB']}MB")
except Exception as e:
print(f" FAILED: {e}")
all_results["ppocrv5_cpu"] = {"error": str(e)}
# ── docTR ──
print("\n=== docTR CPU ===")
try:
r = bench_doctr(img)
all_results["doctr"] = r
print(f" Cold: {r['cold_latency_s']}s | Warm: {r['warm_latency_s']}s | Detections: {r['num_detections']}")
print(f" Memory: init {r['mem_init_MB']}MB | peak {r['mem_peak_MB']}MB")
except Exception as e:
print(f" FAILED: {e}")
all_results["doctr"] = {"error": str(e)}
# ── EasyOCR ──
print("\n=== EasyOCR CPU ===")
try:
r = bench_easyocr(img)
all_results["easyocr"] = r
print(f" Cold: {r['cold_latency_s']}s | Warm: {r['warm_latency_s']}s | Detections: {r['num_detections']}")
print(f" Memory: init {r['mem_init_MB']}MB | peak {r['mem_peak_MB']}MB")
except Exception as e:
print(f" FAILED: {e}")
all_results["easyocr"] = {"error": str(e)}
# Save JSON
with open(RESULTS_JSON, "w") as f:
json.dump(all_results, f, indent=2, default=str)
print(f"\nResults saved to {RESULTS_JSON}")
# ── Synthesis table ──
print("\n=== Synthesis ===")
print(f"{'Engine':<15} {'Cold(s)':<10} {'Warm(s)':<10} {'Det':<6} {'Mem(MB)':<10} {'Label'}")
for eng, r in all_results.items():
if "error" in r:
print(f"{eng:<15} FAILED")
continue
print(f"{eng:<15} {r['cold_latency_s']:<10} {r['warm_latency_s']:<10} {r['num_detections']:<6} {r['mem_peak_MB']:<10} {r.get('label', '')}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="/home/aivanov/ai/rpa_vision_v3"
VENV="$ROOT/venv_v3"
WIN_IP="${WIN_IP:-192.168.1.11}"
ENV_FILE="$ROOT/.env.local"
SERVICE_FILE="/etc/systemd/system/rpa-agent-chat.service"
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "ERROR: run as root with sudo" >&2
exit 1
fi
if [[ ! -d "$ROOT" || ! -x "$VENV/bin/python3" || ! -f "$ROOT/agent_chat/app.py" ]]; then
echo "ERROR: DGX RPA tree or venv missing under $ROOT" >&2
exit 1
fi
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: missing $ENV_FILE" >&2
exit 1
fi
ts="$(date +%Y%m%d_%H%M%S)"
cp -a "$ENV_FILE" "$ENV_FILE.bak_m2_r1_$ts"
python3 - "$ENV_FILE" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
lines = path.read_text().splitlines()
seen = False
out = []
for line in lines:
if line.startswith("RPA_BIND_HOST="):
out.append("RPA_BIND_HOST=0.0.0.0")
seen = True
else:
out.append(line)
if not seen:
out.append("RPA_BIND_HOST=0.0.0.0")
path.write_text("\n".join(out) + "\n")
PY
install -m 0644 /dev/stdin "$SERVICE_FILE" <<UNIT
[Unit]
Description=RPA Vision V3 - Agent Chat (Flask/SocketIO, port 5004)
After=network-online.target rpa-streaming.service
Wants=network-online.target
Requires=rpa-streaming.service
[Service]
Type=simple
User=aivanov
Group=aivanov
WorkingDirectory=$ROOT
EnvironmentFile=$ENV_FILE
Environment="PYTHONUNBUFFERED=1"
Environment="ENVIRONMENT=production"
Environment="RPA_SERVICE_NAME=rpa-agent-chat"
Environment="PYTHONPATH=$ROOT"
Environment="AGENT_CHAT_ENABLE_OWL=0"
Environment="AGENT_CHAT_ENABLE_UI_DETECTION=0"
ExecStart=$VENV/bin/python3 -m agent_chat.app
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
KillMode=mixed
NoNewPrivileges=true
PrivateTmp=true
StandardOutput=journal
StandardError=journal
SyslogIdentifier=rpa-agent-chat
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now rpa-agent-chat.service
systemctl restart rpa-streaming.service
ensure_iptables_rule() {
if ! iptables -C INPUT "$@" 2>/dev/null; then
iptables -A INPUT "$@"
fi
}
ensure_iptables_rule -i lo -p tcp -m multiport --dports 5004,5005 -j ACCEPT
ensure_iptables_rule -p tcp -s "$WIN_IP" -m multiport --dports 5004,5005 -j ACCEPT
ensure_iptables_rule -p tcp -m multiport --dports 5004,5005 -j DROP
echo "--- services ---"
systemctl is-active rpa-agent-chat.service rpa-streaming.service
echo "--- ports ---"
ss -ltnp | grep -E ':(5004|5005)\b' || true
echo "--- local health ---"
curl -sS -m 5 http://127.0.0.1:5005/health
echo
curl -sS -m 5 -o /dev/null -w 'agent_chat_status=%{http_code}\n' http://127.0.0.1:5004/api/status
echo "--- firewall rules ---"
iptables -S INPUT | grep -E 'dports 5004,5005|dport (5004|5005)' || true

View File

@@ -23,6 +23,7 @@ from agent_v0.server_v1.replay_engine import (
_edge_to_normalized_actions,
_resolve_runtime_vars,
_resolve_runtime_vars_in_str,
_coerce_action_coords,
)
@@ -192,11 +193,185 @@ class TestCompilerGapLiteralFloats:
assert action["x_pct"] == 0.30
assert action["y_pct"] == 0.50
def test_navigate_action_type_unknown(self):
"""navigate action type is NOT handled by _edge_to_normalized_actions —
falls into the else branch logging "Type d'action inconnu"."""
edge = _FakeEdge(_FakeAction("navigate", parameters={"target": "login"}))
def test_navigate_action_type_handled(self):
"""navigate action type IS now handled by _edge_to_normalized_actions —
produces a normalized action dict with type='navigate' and parameters."""
edge = _FakeEdge(_FakeAction("navigate", parameters={"action": "login"}))
actions = _edge_to_normalized_actions(edge, params={})
# navigate produces empty actions — not compiled at all
assert actions == []
assert len(actions) == 1
action = actions[0]
assert action["type"] == "navigate"
assert "parameters" in action
assert action["parameters"]["action"] == "login"
assert action["parameters"]["login_coords_var"] == "navigate_login_coords"
assert action["parameters"]["password_coords_var"] == "navigate_password_coords"
assert action["parameters"]["submit_coords_var"] == "navigate_submit_coords"
class TestNavigateBranchNonRegression:
"""Non-regression tests for the navigate branch in _edge_to_normalized_actions.
These verify the D1 fix: navigate action type now produces a proper
normalized dict that the server-side dispatch can route to
_handle_navigate_action.
"""
def test_navigate_default_params(self):
"""Navigate with minimal params fills defaults."""
edge = _FakeEdge(_FakeAction("navigate", parameters={}))
actions = _edge_to_normalized_actions(edge, params={})
assert len(actions) == 1
action = actions[0]
assert action["type"] == "navigate"
assert action["parameters"]["action"] == "login"
assert action["parameters"]["login_coords_var"] == "navigate_login_coords"
assert action["parameters"]["password_coords_var"] == "navigate_password_coords"
assert action["parameters"]["submit_coords_var"] == "navigate_submit_coords"
def test_navigate_custom_vars(self):
"""Navigate with custom coords_var names propagates them."""
edge = _FakeEdge(
_FakeAction(
"navigate",
parameters={
"login_coords_var": "login_pos",
"password_coords_var": "pwd_pos",
"submit_coords_var": "btn_pos",
},
)
)
actions = _edge_to_normalized_actions(edge, params={})
assert len(actions) == 1
params = actions[0]["parameters"]
assert params["login_coords_var"] == "login_pos"
assert params["password_coords_var"] == "pwd_pos"
assert params["submit_coords_var"] == "btn_pos"
def test_navigate_login_config_overrides(self):
"""Navigate forwards login_config keys to parameters."""
edge = _FakeEdge(
_FakeAction(
"navigate",
parameters={
"login_field": "username",
"password_field": "pass",
"submit_button": "connexion",
"context": "DPI urgences",
},
)
)
actions = _edge_to_normalized_actions(edge, params={})
assert len(actions) == 1
params = actions[0]["parameters"]
assert params["login_field"] == "username"
assert params["password_field"] == "pass"
assert params["submit_button"] == "connexion"
assert params["context"] == "DPI urgences"
def test_navigate_base_fields_present(self):
"""Navigate action retains edge_id, from_node, to_node, action_id."""
edge = _FakeEdge(_FakeAction("navigate", parameters={"action": "login"}))
actions = _edge_to_normalized_actions(edge, params={})
action = actions[0]
assert "edge_id" in action
assert "from_node" in action
assert "to_node" in action
assert "action_id" in action
assert action["edge_id"] == "edge_coords_gap"
assert action["from_node"] == "node_src"
assert action["to_node"] == "node_dst"
def test_navigate_no_x_y_pct(self):
"""Navigate action does NOT include x_pct/y_pct — coords come from handler."""
edge = _FakeEdge(_FakeAction("navigate", parameters={"action": "login"}))
actions = _edge_to_normalized_actions(edge, params={})
action = actions[0]
assert "x_pct" not in action
assert "y_pct" not in action
# ── Test P1-C: _coerce_action_coords ──────────────────────────────────
class TestCoerceActionCoords:
"""P1-C coercion helper: cast x_pct/y_pct strings to floats after
_resolve_runtime_vars template resolution.
Chain: navigate → variables → _resolve_runtime_vars → strings →
_coerce_action_coords → floats. Fail-safe on unresolved/invalid.
"""
def test_float_idempotent(self):
"""Float values pass through unchanged — existing mouse_click actions unaffected."""
action = {"type": "click", "x_pct": 0.15, "y_pct": 0.07}
result = _coerce_action_coords(action)
assert result["x_pct"] == 0.15
assert result["y_pct"] == 0.07
assert result["type"] == "click"
def test_string_to_float_conversion(self):
"""Resolved template strings "0.35" → 0.35 (float) after _resolve_runtime_vars."""
action = {"type": "click", "x_pct": "0.35", "y_pct": "0.07"}
result = _coerce_action_coords(action)
assert result["x_pct"] == 0.35
assert isinstance(result["x_pct"], float)
assert result["y_pct"] == 0.07
assert isinstance(result["y_pct"], float)
assert result["type"] == "click"
def test_unresolved_template_pause_for_human(self):
"""Unresolved {{var.field}} template → pause_for_human, never fallback 0.0."""
action = {"type": "click", "x_pct": "{{navigate_login_coords.x_pct}}", "y_pct": 0.07}
result = _coerce_action_coords(action)
assert result["type"] == "pause_for_human"
assert result["safety_level"] == "high"
assert "coords_var non résolu" in result["_skip_reason"]
assert "{{navigate_login_coords.x_pct}}" in result["_skip_reason"]
def test_invalid_string_pause_for_human(self):
"""Non-convertible string "abc" → pause_for_human, no fallback coords."""
action = {"type": "click", "x_pct": "abc", "y_pct": 0.07}
result = _coerce_action_coords(action)
assert result["type"] == "pause_for_human"
assert result["safety_level"] == "high"
assert "coords invalide" in result["_skip_reason"]
assert "abc" in result["_skip_reason"]
def test_no_coords_keys_unchanged(self):
"""Action without x_pct/y_pct passes through unchanged."""
action = {"type": "navigate", "parameters": {"action": "login"}}
result = _coerce_action_coords(action)
assert result == action
def test_full_chain_resolve_then_coerce(self):
"""Full chain: _resolve_runtime_vars → _coerce_action_coords → floats."""
variables = {
"navigate_login_coords": {
"x_pct": 0.15,
"y_pct": 0.35,
"method": "ocr_anchor",
}
}
action = {
"type": "click",
"x_pct": "{{navigate_login_coords.x_pct}}",
"y_pct": "{{navigate_login_coords.y_pct}}",
}
# Step 1: resolve templates (produces strings)
resolved = _resolve_runtime_vars(action, variables)
assert resolved["x_pct"] == "0.15" # string after resolver
assert resolved["y_pct"] == "0.35" # string after resolver
# Step 2: coerce strings to floats
coerced = _coerce_action_coords(resolved)
assert coerced["x_pct"] == 0.15 # float after coercion
assert isinstance(coerced["x_pct"], float)
assert coerced["y_pct"] == 0.35
assert isinstance(coerced["y_pct"], float)
assert coerced["type"] == "click"

View File

@@ -362,8 +362,14 @@ def import_core_workflow_to_db(
dict {created: bool, workflow_id: str, signature: str, warnings: list}.
`created=False` quand un workflow de même trajectoire existait déjà.
Note (non-wiring) : cette unité n'est PAS branchée au worker live ni à la
route HTTP existante ; voir le rapport de câblage R1.
Sémantique : **create-or-skip** (choix acté Dom 2026-07-02). Si un workflow
de même signature de trajectoire existe déjà, on le RÉUTILISE tel quel — on ne
le met PAS à jour. Rationale : le workflow validé (revue humaine) fait foi ;
un ré-apprentissage automatique ne doit pas écraser une version validée. Si un
refresh explicite devient nécessaire, ce sera un chantier séparé (create-or-update).
Wiring : branché au worker live via `stream_processor._maybe_import_to_vwb`
(depuis c82829f2b, 29/06), sous gate `RPA_R1_AUTO_IMPORT` (défaut OFF).
"""
# Imports paresseux : garde le module léger et évite un import core/DB au load.
from core.execution.trajectory_signature import workflow_trajectory_signature