feat: conformité AI Act — divulgation IA, consentement, rétention, arrêt urgence

- Léa se présente comme "assistante basée sur l'intelligence artificielle"
- Dialog consentement avant enregistrement (capture écran/clavier)
- Rétention logs 180 jours (Article 12 + 26(6))
- Bouton ARRÊT D'URGENCE toujours visible (Article 14)
- Transparence mode autonome explicite (Article 50)
- Rapport conformité AI Act en français (docs/CONFORMITE_AI_ACT.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 16:57:43 +01:00
parent 353c2a347e
commit f340eab628
7 changed files with 424 additions and 23 deletions

View File

@@ -38,6 +38,11 @@ SCREENSHOT_QUALITY = 85
# Désactiver avec RPA_BLUR_SENSITIVE=false pour le développement/tests
BLUR_SENSITIVE = os.environ.get("RPA_BLUR_SENSITIVE", "true").lower() in ("true", "1", "yes")
# Retention des logs — minimum 6 mois (180 jours) requis par le Reglement IA
# (Article 12 — journalisation automatique, Article 26(6) — conservation minimum)
# Configurable via variable d'environnement pour permettre l'ajustement
LOG_RETENTION_DAYS = int(os.environ.get("RPA_LOG_RETENTION_DAYS", "180"))
# Monitoring
PERF_MONITOR_INTERVAL_S = 30
LOGS_DIR = BASE_DIR / "logs"

View File

@@ -14,7 +14,7 @@ import uuid
import time
import logging
import threading
from .config import SESSIONS_ROOT, AGENT_VERSION, SERVER_URL, MACHINE_ID
from .config import SESSIONS_ROOT, AGENT_VERSION, SERVER_URL, MACHINE_ID, LOG_RETENTION_DAYS
from .core.captor import EventCaptorV1
from .core.executor import ActionExecutorV1
from .network.streamer import TraceStreamer
@@ -51,7 +51,8 @@ class AgentV1:
self.session_dir = None
# Gestion du stockage local et nettoyage
self.storage = SessionStorage(SESSIONS_ROOT)
# Retention minimum 6 mois (Reglement IA, Article 12)
self.storage = SessionStorage(SESSIONS_ROOT, retention_days=LOG_RETENTION_DAYS)
threading.Thread(target=self._delayed_cleanup, daemon=True).start()
self.vision = None

View File

@@ -14,7 +14,16 @@ from datetime import datetime, timedelta
logger = logging.getLogger("session_storage")
class SessionStorage:
def __init__(self, base_dir: Path, max_size_gb: int = 5, retention_days: int = 1):
def __init__(self, base_dir: Path, max_size_gb: int = 5, retention_days: int = 180):
"""Gestionnaire de stockage local pour les sessions Agent V1.
Args:
base_dir: Dossier racine de stockage des sessions.
max_size_gb: Taille maximale du stockage local (Go).
retention_days: Duree de retention en jours. Defaut = 180 (6 mois),
minimum requis par le Reglement IA (Article 12 — journalisation
automatique, Article 26(6) — conservation des logs).
"""
self.base_dir = base_dir
self.max_size_bytes = max_size_gb * 1024 * 1024 * 1024
self.retention_days = retention_days

View File

@@ -247,9 +247,10 @@ class ChatWindow:
self._build_input_area(root)
self._build_resize_grip(root)
# Message d'accueil
# Message d'accueil — divulgation IA obligatoire (Article 50, Reglement IA)
self._add_lea_message(
"Bonjour ! Je suis L\u00e9a.\n"
"Bonjour ! Je suis L\u00e9a, une assistante bas\u00e9e sur "
"l'intelligence artificielle.\n"
"Je peux apprendre vos t\u00e2ches r\u00e9p\u00e9titives "
"et les refaire \u00e0 votre place.\n"
"Que puis-je faire pour vous ?"
@@ -915,11 +916,33 @@ class ChatWindow:
)
def _do_quick_record(self) -> None:
"""Demande le nom de la t\u00e2che et lance l'enregistrement."""
import tkinter as tk
from tkinter import simpledialog
"""Demande le consentement puis le nom de la tache et lance l'enregistrement.
# Creer un dialogue ephemere
Notification prealable obligatoire (Articles 13/14, Reglement IA) :
l'utilisateur doit etre informe de ce qui sera capture AVANT le demarrage.
"""
import tkinter as tk
from tkinter import simpledialog, messagebox
# --- Consentement prealable (Articles 13/14, Reglement IA) ---
consent_root = tk.Tk()
consent_root.withdraw()
consent_root.attributes('-topmost', True)
consent = messagebox.askyesno(
"Enregistrement — Information",
"\u26a0\ufe0f L'enregistrement va capturer votre \u00e9cran, "
"vos clics et vos frappes clavier pour apprendre cette t\u00e2che.\n\n"
"Les donn\u00e9es sensibles seront automatiquement flout\u00e9es.\n\n"
"Voulez-vous continuer ?",
parent=consent_root,
)
consent_root.destroy()
if not consent:
self._add_lea_message("Enregistrement annul\u00e9.")
return
# --- Dialogue de saisie du nom ---
tmp_root = tk.Tk()
tmp_root.withdraw()
tmp_root.attributes('-topmost', True)

View File

@@ -120,11 +120,17 @@ class NotificationManager:
# ------------------------------------------------------------------ #
def greet(self) -> bool:
"""Notification de bienvenue au démarrage."""
"""Notification de bienvenue au démarrage.
Inclut la divulgation IA obligatoire (Article 50, Règlement IA).
"""
return self.notify(
title=APP_NAME,
message="Bonjour ! Léa est prête.",
timeout=5,
message=(
"Bonjour ! Léa est prête. "
"Je suis une assistante basée sur l'intelligence artificielle."
),
timeout=7,
)
def session_started(self, workflow_name: str) -> bool:
@@ -152,11 +158,18 @@ class NotificationManager:
)
def replay_started(self, workflow_name: str, step_count: int) -> bool:
"""Notification de début de replay."""
"""Notification de début de replay.
Transparence obligatoire en mode autonome (Article 50, Règlement IA) :
l'utilisateur doit savoir qu'un système d'IA agit sur son écran.
"""
return self.notify(
title=APP_NAME,
message=f"Je m'en occupe ! '{workflow_name}' en cours...",
timeout=5,
message=(
f"Le système d'intelligence artificielle exécute la tâche "
f"'{workflow_name}' sur votre écran."
),
timeout=7,
)
def replay_step(self, current: int, total: int, description: str) -> bool:

View File

@@ -71,6 +71,23 @@ def _show_info(title: str, message: str) -> None:
root.destroy()
def _ask_consent(title: str, message: str) -> bool:
"""Dialogue de consentement Oui/Non via tkinter (sans PyQt5).
Utilise pour la notification prealable obligatoire (Articles 13/14,
Reglement IA) avant tout enregistrement.
"""
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.withdraw()
root.attributes('-topmost', True)
result = messagebox.askyesno(title, message, parent=root)
root.destroy()
return result
# ---------------------------------------------------------------------------
# SmartTrayV1
# ---------------------------------------------------------------------------
@@ -240,6 +257,13 @@ class SmartTrayV1:
visible=lambda _i: self._chat_window is not None,
),
pystray.Menu.SEPARATOR,
# --- Arret d'urgence (Article 14, Reglement IA — controle humain) ---
# Toujours visible, quel que soit l'etat de l'agent
item(
"\u26d4 ARR\u00caT D'URGENCE",
self._on_emergency_stop,
),
pystray.Menu.SEPARATOR,
# --- Utilitaires ---
item("\U0001f4c2 Mes fichiers", self._on_open_folder),
item("\u274c Quitter L\u00e9a", self._on_quit),
@@ -323,9 +347,23 @@ class SmartTrayV1:
self._update_icon()
def _on_start_session(self, _icon=None, _item=None) -> None:
"""Demande le nom de la t\u00e2che et demarre la session."""
"""Demande le consentement puis le nom de la tache et demarre la session.
Notification prealable obligatoire (Articles 13/14, Reglement IA) :
l'utilisateur doit etre informe de ce qui sera capture AVANT le demarrage.
"""
# Dialogue tkinter dans un thread dedie
def _dialog():
# --- Consentement prealable (Articles 13/14, Reglement IA) ---
if not _ask_consent(
"Enregistrement — Information",
"\u26a0\ufe0f L'enregistrement va capturer votre \u00e9cran, "
"vos clics et vos frappes clavier pour apprendre cette t\u00e2che.\n\n"
"Les donn\u00e9es sensibles seront automatiquement flout\u00e9es.\n\n"
"Voulez-vous continuer ?",
):
return
name = _ask_string(
"Nouvelle t\u00e2che",
"D\u00e9crivez la t\u00e2che \u00e0 apprendre :",
@@ -427,9 +465,11 @@ class SmartTrayV1:
with self._state_lock:
self._replay_active = True
self._update_icon()
# Transparence mode autonome (Article 50, Reglement IA)
self._notifier.notify(
"L\u00e9a",
f"Je m'en occupe ! '{workflow_name}' en cours...",
f"Le syst\u00e8me d'intelligence artificielle ex\u00e9cute la "
f"t\u00e2che '{workflow_name}' sur votre \u00e9cran.",
)
try:
@@ -459,6 +499,48 @@ class SmartTrayV1:
threading.Thread(target=_replay, daemon=True).start()
def _on_emergency_stop(self, _icon=None, _item=None) -> None:
"""Arret d'urgence — stoppe TOUTES les activites de l'agent immediatement.
Controle humain obligatoire (Article 14, Reglement IA).
Arrete l'enregistrement, le replay ET le heartbeat d'un seul clic.
Toujours accessible dans le menu, quel que soit l'etat de l'agent.
"""
logger.warning("ARRET D'URGENCE declenche par l'utilisateur")
# Arreter l'enregistrement si en cours
if self._shared_state is not None:
if self._shared_state.is_recording:
try:
self._shared_state.stop_recording()
except Exception as e:
logger.error("Erreur arret enregistrement d'urgence : %s", e)
# Arreter le replay si en cours
if self._shared_state.is_replay_active:
self._shared_state.set_replay_active(False)
else:
# Fallback sans etat partage
if self.is_recording:
try:
self.on_stop()
except Exception as e:
logger.error("Erreur arret session d'urgence : %s", e)
# Forcer l'etat local a l'arret
with self._state_lock:
self.is_recording = False
self.actions_count = 0
self._replay_active = False
self._update_icon()
# Notification
self._notifier.notify(
"\u26d4 Arr\u00eat d'urgence",
"Toutes les activit\u00e9s ont \u00e9t\u00e9 arr\u00eat\u00e9es.",
timeout=10,
)
def _on_open_folder(self, _icon=None, _item=None) -> None:
"""Ouvre le dossier des sessions dans l'explorateur de fichiers."""
from ..config import SESSIONS_ROOT
@@ -593,7 +675,12 @@ class SmartTrayV1:
self._update_icon()
if active:
self._notifier.notify("L\u00e9a", "Je m'en occupe...")
# Transparence mode autonome (Article 50, Reglement IA)
self._notifier.notify(
"L\u00e9a",
"Le syst\u00e8me d'intelligence artificielle ex\u00e9cute "
"une t\u00e2che sur votre \u00e9cran.",
)
else:
self._notifier.notify("L\u00e9a", "C'est fait !")
@@ -645,11 +732,8 @@ class SmartTrayV1:
def run(self) -> None:
"""Demarre le tray, les threads de fond, et entre dans la boucle principale."""
# Notification d'accueil (avec identifiant machine)
self._notifier.notify(
"L\u00e9a",
f"Bonjour ! L\u00e9a est pr\u00eate.",
)
# Notification d'accueil — divulgation IA (Article 50, Reglement IA)
self._notifier.greet()
# Enregistrer le hotkey global Ctrl+Shift+L (toggle chat)
self._start_hotkey()