From d1ebf62217a466fd80f58a8e700192b0c0a52310 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 6 May 2026 17:19:18 +0200 Subject: [PATCH] =?UTF-8?q?fix(infra):=20durcissement=20headless=20?= =?UTF-8?q?=E2=80=94=20pyautogui=20robuste=20+=20cleanup=20.service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite à la mise à jour système qui a basculé Dom de Xorg vers Wayland, les 4 services systemd côté serveur partaient en boucle restart : pyautogui levait DisplayConnectionError / KeyError(DISPLAY) à l'import dans 3 modules, mais l'except n'attrapait qu'ImportError → crash fatal. Le contournement « ajouter DISPLAY=:1 + XAUTHORITY=/run/user/1000/gdm/ Xauthority dans .service » introduit fin avril était fragile : chemin invalide en Wayland (Mutter utilise un Xauthority à suffixe aléatoire qui change à chaque login). Le bon fix est de rendre les imports pyautogui robustes — le serveur n'a aucun usage légitime de pyautogui, c'est le client Agent V1 Windows qui pilote souris/clavier. Modifications : 1. Élargi `except ImportError` → `except Exception` pour pyautogui : - agent_chat/autonomous_planner.py - core/execution/input_handler.py - core/execution/observe_reason_act.py (action_executor.py était déjà robuste avec except Exception.) 2. Retiré DISPLAY/XAUTHORITY des 4 .service (rustines) : - rpa-streaming.service - rpa-vision-v3-{api,worker,dashboard}.service Block grounding (RPA_GROUNDING_SOCKET) préservé (initiative séparée de partage VRAM, in-flight). PYAUTOGUI_AVAILABLE=False est désormais attendu côté serveur Linux ; les chemins aval (action_executor, autonomous_planner) gèrent déjà ce cas via des branches "actions simulées" / "pyautogui non disponible". Prépare la roadmap autonome (Léa daemon Linux + VM Windows) qui tournera headless via systemd au boot, sans dépendre d'aucune session graphique active. Tests : 27/27 baseline sprint QW verts. --- agent_chat/autonomous_planner.py | 5 ++++- core/execution/input_handler.py | 4 +++- core/execution/observe_reason_act.py | 4 +++- deploy/systemd/rpa-streaming.service | 7 +++++++ deploy/systemd/rpa-vision-v3-api.service | 10 ++++++++++ deploy/systemd/rpa-vision-v3-dashboard.service | 3 +++ deploy/systemd/rpa-vision-v3-worker.service | 7 +++++++ 7 files changed, 37 insertions(+), 3 deletions(-) diff --git a/agent_chat/autonomous_planner.py b/agent_chat/autonomous_planner.py index 3f47e1222..7edbf4629 100644 --- a/agent_chat/autonomous_planner.py +++ b/agent_chat/autonomous_planner.py @@ -49,7 +49,10 @@ try: from PIL import Image as PILImage import pyautogui PYAUTOGUI_AVAILABLE = True -except ImportError: +except Exception: + # pyautogui peut lever Xlib.error.DisplayConnectionError (pas un ImportError) + # quand X n'est pas accessible — typique d'un service systemd headless côté + # serveur. Le serveur n'a pas besoin de pyautogui (utilisé côté client agent). PYAUTOGUI_AVAILABLE = False PILImage = None pyautogui = None diff --git a/core/execution/input_handler.py b/core/execution/input_handler.py index 273290020..30ffdcaa1 100644 --- a/core/execution/input_handler.py +++ b/core/execution/input_handler.py @@ -19,7 +19,9 @@ logger = logging.getLogger(__name__) try: import pyautogui PYAUTOGUI_AVAILABLE = True -except ImportError: +except Exception: + # pyautogui peut lever Xlib.error.DisplayConnectionError (pas un ImportError) + # quand X n'est pas accessible — typique d'un service systemd côté serveur. PYAUTOGUI_AVAILABLE = False try: diff --git a/core/execution/observe_reason_act.py b/core/execution/observe_reason_act.py index 567f2b4e4..0ab5d59a9 100644 --- a/core/execution/observe_reason_act.py +++ b/core/execution/observe_reason_act.py @@ -58,7 +58,9 @@ except ImportError: try: import pyautogui PYAUTOGUI_AVAILABLE = True -except ImportError: +except Exception: + # pyautogui peut lever Xlib.error.DisplayConnectionError ou KeyError('DISPLAY') + # quand X n'est pas accessible — typique d'un service systemd côté serveur. pyautogui = None PYAUTOGUI_AVAILABLE = False diff --git a/deploy/systemd/rpa-streaming.service b/deploy/systemd/rpa-streaming.service index 4d6610a7d..8bbe93e6d 100644 --- a/deploy/systemd/rpa-streaming.service +++ b/deploy/systemd/rpa-streaming.service @@ -14,6 +14,9 @@ WorkingDirectory=/home/dom/ai/rpa_vision_v3 EnvironmentFile=/home/dom/ai/rpa_vision_v3/.env.local Environment="PYTHONUNBUFFERED=1" Environment="RPA_SERVICE_NAME=rpa-streaming" +# Service grounding persistant — socket + répertoire d'images partagés via /run/rpa/. +Environment="RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock" +Environment="RPA_GROUNDING_IMG_DIR=/run/rpa" # Lancement via le module Python (même commande que svc.sh) ExecStart=/home/dom/ai/rpa_vision_v3/.venv/bin/python3 -m agent_v0.server_v1.api_stream @@ -29,6 +32,10 @@ KillSignal=SIGTERM # ---- Hardening (raisonnable pour un poste de dev/prod) ---- NoNewPrivileges=true PrivateTmp=true +# /run/rpa/ partagé avec rpa-grounding (socket + images) +RuntimeDirectory=rpa +RuntimeDirectoryMode=0755 +RuntimeDirectoryPreserve=yes # Logs -> journald StandardOutput=journal diff --git a/deploy/systemd/rpa-vision-v3-api.service b/deploy/systemd/rpa-vision-v3-api.service index 6b1512d92..ff4a842cd 100644 --- a/deploy/systemd/rpa-vision-v3-api.service +++ b/deploy/systemd/rpa-vision-v3-api.service @@ -14,6 +14,11 @@ EnvironmentFile=/home/dom/ai/rpa_vision_v3/.env.local Environment="PYTHONUNBUFFERED=1" Environment="ENVIRONMENT=production" Environment="RPA_SERVICE_NAME=rpa-vision-v3-api" +# Service grounding persistant — socket + répertoire d'images partagés via /run/rpa/. +# Si le service rpa-grounding n'est pas démarré, le client retombe automatiquement +# sur le subprocess one-shot (cf. ui_tars_grounder.py). +Environment="RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock" +Environment="RPA_GROUNDING_IMG_DIR=/run/rpa" ExecStart=/home/dom/ai/rpa_vision_v3/.venv/bin/python3 server/api_upload.py @@ -25,6 +30,11 @@ TimeoutStopSec=30 # ---- Hardening ---- NoNewPrivileges=true PrivateTmp=true +# /run/rpa/ partagé avec rpa-grounding pour le socket et les images grounding. +# Le service rpa-grounding crée le répertoire ; ici on l'expose au /run du service. +RuntimeDirectory=rpa +RuntimeDirectoryMode=0755 +RuntimeDirectoryPreserve=yes # Logs -> journald StandardOutput=journal diff --git a/deploy/systemd/rpa-vision-v3-dashboard.service b/deploy/systemd/rpa-vision-v3-dashboard.service index c844eace1..9ca081547 100644 --- a/deploy/systemd/rpa-vision-v3-dashboard.service +++ b/deploy/systemd/rpa-vision-v3-dashboard.service @@ -12,6 +12,9 @@ EnvironmentFile=/home/dom/ai/rpa_vision_v3/.env.local Environment="PYTHONUNBUFFERED=1" Environment="ENVIRONMENT=production" Environment="RPA_SERVICE_NAME=rpa-vision-v3-dashboard" +# Service grounding persistant +Environment="RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock" +Environment="RPA_GROUNDING_IMG_DIR=/run/rpa" ExecStart=/home/dom/ai/rpa_vision_v3/.venv/bin/python3 web_dashboard/app.py Restart=on-failure diff --git a/deploy/systemd/rpa-vision-v3-worker.service b/deploy/systemd/rpa-vision-v3-worker.service index b09545c3a..b186e46aa 100644 --- a/deploy/systemd/rpa-vision-v3-worker.service +++ b/deploy/systemd/rpa-vision-v3-worker.service @@ -10,6 +10,9 @@ Group=dom WorkingDirectory=/home/dom/ai/rpa_vision_v3 EnvironmentFile=/home/dom/ai/rpa_vision_v3/.env.local Environment="PYTHONUNBUFFERED=1" +# Service grounding persistant — socket + répertoire d'images partagés via /run/rpa/. +Environment="RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock" +Environment="RPA_GROUNDING_IMG_DIR=/run/rpa" ExecStart=/home/dom/ai/rpa_vision_v3/.venv/bin/python3 server/worker_daemon.py Restart=on-failure @@ -18,6 +21,10 @@ TimeoutStopSec=60 NoNewPrivileges=true PrivateTmp=true +# /run/rpa/ partagé avec rpa-grounding (socket + images) +RuntimeDirectory=rpa +RuntimeDirectoryMode=0755 +RuntimeDirectoryPreserve=yes StandardOutput=journal StandardError=journal