feat: journée 17 avril — tests E2E validés, dashboard fleet+audit, VWB bridge, cleaner C2
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 14s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped

Pipeline E2E complet validé :
  Capture VM → streaming → serveur → cleaner → replay → audit trail
  Mode apprentissage supervisé fonctionne (Léa échoue → humain → reprise)

Dashboard :
  - Cleanup 14→10 onglets (RCE supprimée)
  - Fleet : enregistrer/révoquer agents, tokens, ZIP pré-configuré téléchargeable
  - Audit trail MVP (/audit) : filtres, tableau, export CSV, conformité AI Act/RGPD
  - Formulaire Fleet simplifié (nom + email, machine_id auto)

VWB bridge Léa→VWB :
  - Compound décomposés en N steps (saisie + raccourci visibles)
  - Layout serpentin 3 colonnes (plus colonne verticale)
  - Badge OS 🪟/🐧, filtre OS retiré (admin Linux voit Windows)
  - Fix import SQLite readonly

Cleaner intelligent :
  - Descriptions lisibles (UIA/C2) + détection doublons
  - Logique C2 : UIElement identifié = jamais parasite
  - Patterns parasites resserrés
  - Message Léa : "Je n'y arrive pas, montrez-moi comment faire"

Config agent (INC-1 à INC-7) :
  - SERVER_URL + SERVER_BASE unifiés
  - RPA_OLLAMA_HOST séparé
  - allow_redirects=False sur POST
  - Middleware réécriture URL serveur

CI Gitea : fix token + Flask-SocketIO + ruff propre
Fleet endpoints : /agents/enroll|uninstall|fleet + agent_registry SQLite
Backup : script quotidien workflows.db + audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-17 17:46:40 +02:00
parent 2fa864b5c7
commit 4f61741420
27 changed files with 5088 additions and 1543 deletions

View File

@@ -37,7 +37,8 @@ CORE_ACTION_TO_VWB = {
"mouse_click": "click_anchor",
"text_input": "type_text",
"key_press": "keyboard_shortcut",
"compound": "click_anchor", # Sera décomposé en sous-étapes
"key_combo": "keyboard_shortcut",
"compound": "click_anchor", # Décomposé en N étapes séparées par le bridge
"wait": "wait_for_anchor",
"scroll": "scroll_to_anchor",
"unknown": "click_anchor",
@@ -133,76 +134,103 @@ def convert_learned_to_vwb_steps(
if to_node and to_node not in visited:
queue.append(to_node)
# Convertir chaque edge en Step VWB
# Convertir chaque edge en Step(s) VWB
# Les actions compound sont décomposées en N steps séparés
steps = []
for idx, edge in enumerate(ordered_edges):
for edge in ordered_edges:
action = edge.get("action", {})
action_type = action.get("type", "unknown")
action_params = action.get("parameters", {})
target = action.get("target", {})
# Déterminer le type VWB
vwb_action_type = CORE_ACTION_TO_VWB.get(action_type, "click_anchor")
# Construire les paramètres VWB
vwb_params = {}
if action_type == "mouse_click":
# Extraire la position en pourcentage si disponible
by_position = target.get("by_position")
if by_position:
vwb_params["x_pct"] = by_position[0] if isinstance(by_position, list) else 0
vwb_params["y_pct"] = by_position[1] if isinstance(by_position, list) else 0
button = action_params.get("button", "left")
if button == "double":
vwb_action_type = "double_click_anchor"
elif button == "right":
vwb_action_type = "right_click_anchor"
elif action_type == "text_input":
vwb_params["text"] = action_params.get("text", "")
elif action_type == "key_press":
keys = action_params.get("keys", [])
if not keys and action_params.get("key"):
keys = [action_params["key"]]
vwb_params["keys"] = keys
elif action_type == "compound":
# Stocker les sous-étapes dans les paramètres pour référence
vwb_params["compound_steps"] = action_params.get("steps", [])
warnings.append(
f"Étape {idx + 1} : action compound décomposée — vérifier manuellement"
)
# Ajouter des infos de ciblage pour la review humaine
if target.get("by_role"):
vwb_params["target_role"] = target["by_role"]
if target.get("by_text"):
vwb_params["target_text"] = target["by_text"]
# Construire le label
from_node = edge.get("from_node", "")
to_node = edge.get("to_node") or edge.get("target_node", "")
from_name = nodes_by_id.get(from_node, {}).get("name", from_node)
to_name = nodes_by_id.get(to_node, {}).get("name", to_node)
label = _build_step_label(vwb_action_type, vwb_params, from_name, to_name)
step = {
"action_type": vwb_action_type,
"order": idx,
"position_x": 400,
"position_y": 80 + idx * 120,
"parameters": vwb_params,
"label": label,
# Métadonnées d'origine pour traçabilité
"metadata": {
"core_edge_id": edge.get("edge_id", ""),
"core_from_node": from_node,
"core_to_node": to_node,
},
edge_meta = {
"core_edge_id": edge.get("edge_id", ""),
"core_from_node": from_node,
"core_to_node": to_node,
}
steps.append(step)
if action_type == "compound":
# --- Décomposer les compound en N étapes VWB séparées ---
sub_steps = action_params.get("steps", [])
if not sub_steps:
warnings.append(
f"Action compound sans sous-étapes (edge {edge.get('edge_id', '?')})"
)
continue
for sub_idx, sub in enumerate(sub_steps):
sub_type = sub.get("type", "unknown")
sub_vwb_type, sub_params = _convert_compound_substep(
sub_type, sub, target
)
label = _build_step_label(sub_vwb_type, sub_params, from_name, to_name)
steps.append({
"action_type": sub_vwb_type,
"order": len(steps),
"position_x": 0, # sera recalculé par _compute_layout
"position_y": 0,
"parameters": sub_params,
"label": label,
"metadata": {
**edge_meta,
"compound_sub_index": sub_idx,
"compound_total": len(sub_steps),
},
})
warnings.append(
f"Compound décomposé en {len(sub_steps)} étapes VWB séparées "
f"(edge {edge.get('edge_id', '?')})"
)
else:
# --- Action simple (non-compound) ---
vwb_action_type = CORE_ACTION_TO_VWB.get(action_type, "click_anchor")
vwb_params = {}
if action_type == "mouse_click":
by_position = target.get("by_position")
if by_position:
vwb_params["x_pct"] = by_position[0] if isinstance(by_position, list) else 0
vwb_params["y_pct"] = by_position[1] if isinstance(by_position, list) else 0
button = action_params.get("button", "left")
if button == "double":
vwb_action_type = "double_click_anchor"
elif button == "right":
vwb_action_type = "right_click_anchor"
elif action_type == "text_input":
vwb_params["text"] = action_params.get("text", "")
elif action_type in ("key_press", "key_combo"):
vwb_action_type = "keyboard_shortcut"
keys = action_params.get("keys", [])
if not keys and action_params.get("key"):
keys = [action_params["key"]]
vwb_params["keys"] = keys
# Ajouter des infos de ciblage pour la review humaine
if target.get("by_role"):
vwb_params["target_role"] = target["by_role"]
if target.get("by_text"):
vwb_params["target_text"] = target["by_text"]
label = _build_step_label(vwb_action_type, vwb_params, from_name, to_name)
steps.append({
"action_type": vwb_action_type,
"order": len(steps),
"position_x": 0,
"position_y": 0,
"parameters": vwb_params,
"label": label,
"metadata": edge_meta,
})
# Appliquer le layout serpentin à tous les steps
_compute_layout(steps)
if not steps and nodes:
# Pas d'edges mais des nodes → créer des étapes basiques depuis les nodes
@@ -212,18 +240,91 @@ def convert_learned_to_vwb_steps(
steps.append({
"action_type": "click_anchor",
"order": idx,
"position_x": 400,
"position_y": 80 + idx * 120,
"position_x": 0,
"position_y": 0,
"parameters": {
"window_title": (node.get("template", {}).get("window", {}) or {}).get("title_pattern", ""),
},
"label": f"Écran : {node_name}",
"metadata": {"core_node_id": node.get("node_id", "")},
})
_compute_layout(steps)
return workflow_meta, steps, warnings
def _convert_compound_substep(
sub_type: str, sub: Dict[str, Any], parent_target: Dict[str, Any]
) -> Tuple[str, Dict[str, Any]]:
"""
Convertit une sous-étape compound en (vwb_action_type, vwb_params).
Gère les types : mouse_click, text_input, key_combo, key_press.
"""
vwb_params: Dict[str, Any] = {}
if sub_type == "mouse_click":
vwb_type = "click_anchor"
pos = sub.get("pos")
if pos and isinstance(pos, (list, tuple)) and len(pos) >= 2:
vwb_params["x_pct"] = pos[0]
vwb_params["y_pct"] = pos[1]
button = sub.get("button", "left")
if button == "double":
vwb_type = "double_click_anchor"
elif button == "right":
vwb_type = "right_click_anchor"
# Hériter les infos de ciblage du parent
if parent_target.get("by_role"):
vwb_params["target_role"] = parent_target["by_role"]
if parent_target.get("by_text"):
vwb_params["target_text"] = parent_target["by_text"]
elif sub_type == "text_input":
vwb_type = "type_text"
vwb_params["text"] = sub.get("text", "")
elif sub_type in ("key_combo", "key_press"):
vwb_type = "keyboard_shortcut"
keys = sub.get("keys", [])
if not keys and sub.get("key"):
keys = [sub["key"]]
vwb_params["keys"] = keys
else:
# Type inconnu — fallback sur click_anchor
vwb_type = CORE_ACTION_TO_VWB.get(sub_type, "click_anchor")
return vwb_type, vwb_params
def _compute_layout(
steps: List[Dict[str, Any]],
cols: int = 3,
cell_w: int = 280,
cell_h: int = 140,
margin_x: int = 60,
margin_y: int = 40,
) -> List[Dict[str, Any]]:
"""
Disposition en grille serpentin (zigzag) pour lisibilité humaine.
Lignes paires : gauche → droite
Lignes impaires : droite → gauche
Modifie les steps en place et les retourne.
"""
for idx, step in enumerate(steps):
row = idx // cols
col = idx % cols
# Serpentin : lignes impaires inversées
if row % 2 == 1:
col = cols - 1 - col
step["position_x"] = margin_x + col * (cell_w + margin_x)
step["position_y"] = margin_y + row * (cell_h + margin_y)
return steps
def _build_step_label(
action_type: str, params: Dict[str, Any], from_name: str, to_name: str
) -> str: