From 1140934e15c63082e03eaeab00a99013ae08a135 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 24 Sep 2026 19:40:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(host-agent):=20cross-platform=20=E2=80=94?= =?UTF-8?q?=20Linux,=20macOS=20UND=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eine Codebasis fuer alle drei Desktop-OS. Der Agent waehlt je OS automatisch: - exec: bash -lc (Linux/macOS) bzw. PowerShell (Windows) - Root/Admin: sudo (Unix) bzw. 'als Administrator starten' (Windows, kein sudo) - Screenshot: grim/scrot (Linux) · screencapture (macOS) · PowerShell/System. Drawing (Windows) - Root-Check: _is_admin() (os.geteuid Unix / IsUserAnAdmin Windows) statt hartem os.geteuid (crashte auf Windows) - info: user aus USER|USERNAME; getloadavg bleibt guarded Build: build.sh (Linux/Docker), build-native.sh (Linux/macOS), build-native.bat (Windows). PyInstaller cross-kompiliert nicht -> je OS bauen. README-Plattform- Tabelle ergaenzt. Co-Authored-By: Claude Opus 4.8 --- host-agent/README.md | 29 +++++++++++++-- host-agent/build-native.bat | 28 ++++++++++++++ host-agent/host_agent.py | 73 ++++++++++++++++++++++++++++--------- 3 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 host-agent/build-native.bat diff --git a/host-agent/README.md b/host-agent/README.md index 3404747..3172056 100644 --- a/host-agent/README.md +++ b/host-agent/README.md @@ -21,14 +21,35 @@ Geräte in einem LAN; der Host-Agent steuert *den Rechner, auf dem er läuft*. ARIA nutzt diese über die Brain-Tools `host_list` / `host_exec` / `host_read` / `host_write` / `host_info` / `host_screenshot`. -## Bauen (portable Binary) +## Plattformen +Eine Codebasis, läuft auf **Linux, macOS und Windows** (der Agent wählt Shell, +Screenshot-Methode und Root/Admin-Check je OS automatisch): + +| | exec | Root/Admin | Screenshot | +|---|---|---|---| +| **Linux** | `bash -lc` | sudo (`SUDO_PASSWORD`/`SUDO_NOPASSWD`) / root | grim (Wayland) · scrot/maim (X11) | +| **macOS** | `bash -lc` | sudo (wie Linux) | `screencapture` (Bordmittel) | +| **Windows** | PowerShell | Agent **als Administrator** starten (kein sudo) | PowerShell/System.Drawing (Bordmittel) | + +PyInstaller kann **nicht cross-kompilieren** — jede Binary wird auf ihrem OS gebaut. + +## Bauen + +**Linux (portabel, empfohlen)** — Docker-Container mit altem glibc: ```bash -./build.sh # braucht Docker; erzeugt dist/aria-host-agent (~15 MB) +./build.sh # -> dist/aria-host-agent (~15 MB, läuft auf vielen Distros) ``` -Gebaut wird in einem bullseye-Container (altes glibc), damit die Binary auf -möglichst vielen Distributionen läuft. +**Linux/macOS ohne Docker** — PyInstaller direkt (linkt gegen lokales glibc/OS): +```bash +./build-native.sh # -> dist/aria-host-agent +``` + +**Windows** — PyInstaller (Python 3 im PATH nötig): +```bat +build-native.bat REM -> dist\aria-host-agent.exe +``` ### Docker scheitert? (Live-ISO / overlayfs-Root) diff --git a/host-agent/build-native.bat b/host-agent/build-native.bat new file mode 100644 index 0000000..5fb719f --- /dev/null +++ b/host-agent/build-native.bat @@ -0,0 +1,28 @@ +@echo off +REM ARIA Host-Agent — Windows-Build mit PyInstaller (kein Docker). +REM Voraussetzung: Python 3 installiert und im PATH (python.org, "Add to PATH"). +REM +REM build-native.bat +REM +REM Ergebnis: dist\aria-host-agent.exe (Onefile). Danach .env danebenlegen +REM (siehe .env.example) und starten. Fuer Admin-Rechte die .exe per Rechtsklick +REM "Als Administrator ausfuehren". +setlocal +cd /d "%~dp0" + +python -m venv .buildenv || goto :err +call .buildenv\Scripts\activate.bat +python -m pip install --quiet --upgrade pip +python -m pip install --quiet pyinstaller -r requirements.txt || goto :err +pyinstaller --onefile --name aria-host-agent --collect-all psutil host_agent.py || goto :err +call deactivate + +echo. +echo Fertig: dist\aria-host-agent.exe +echo .env danebenlegen (siehe .env.example), dann starten (ggf. als Administrator). +goto :eof + +:err +echo. +echo FEHLER beim Bauen. Ist Python 3 installiert und im PATH? (python --version) +exit /b 1 diff --git a/host-agent/host_agent.py b/host-agent/host_agent.py index a9898ba..b2c0cd9 100644 --- a/host-agent/host_agent.py +++ b/host-agent/host_agent.py @@ -31,11 +31,28 @@ import re import shutil import socket import subprocess +import sys import time from pathlib import Path import websockets +# ─── Plattform-Weichen (Linux / macOS / Windows) ──────────────────── +IS_WINDOWS = os.name == "nt" +IS_MAC = sys.platform == "darwin" + + +def _is_admin() -> bool: + """root (Unix) bzw. Administrator (Windows).""" + try: + return os.geteuid() == 0 # Unix (Linux/macOS) + except AttributeError: + try: + import ctypes + return ctypes.windll.shell32.IsUserAnAdmin() != 0 # Windows + except Exception: + return False + logging.basicConfig( level=logging.INFO, format="%(asctime)s [host-agent] %(levelname)s %(message)s", @@ -180,8 +197,18 @@ def _window_text(text: str, params: dict) -> dict: # ─── Aktionen ─────────────────────────────────────────────────────── def _wrap_sudo(cmd: str, use_sudo: bool): - """Gibt (argv, stdin_data) oder (None, fehlertext) wenn sudo nicht moeglich.""" - if not use_sudo or os.geteuid() == 0: + """Baut die Argv (OS-abhaengige Shell) + optional Root/Admin-Rechte. + Gibt (argv, stdin_data) oder (None, fehlertext).""" + if IS_WINDOWS: + # PowerShell; kein sudo. Fuer Admin-Rechte muss der Agent SELBST als + # Administrator laufen (UAC) — dann hat 'sudo:true' bereits volle Rechte. + if use_sudo and not _is_admin(): + return None, ("Windows kennt kein sudo. Starte den Agent als " + "Administrator ('Als Administrator ausfuehren'), dann " + "laufen Kommandos mit vollen Rechten.") + return ["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd], None + # Unix: Linux + macOS (bash vorhanden; macOS-sudo verhaelt sich wie Linux) + if not use_sudo or _is_admin(): return ["bash", "-lc", cmd], None if SUDO_PASSWORD: return ["sudo", "-S", "-p", "", "bash", "-lc", cmd], SUDO_PASSWORD + "\n" @@ -266,7 +293,8 @@ def _do_info(params: dict) -> dict: "host": HOST_NAME, "hostname": socket.gethostname(), "os": platform.platform(), "kernel": platform.release(), "arch": platform.machine(), "python": platform.python_version(), - "user": os.environ.get("USER") or "", "is_root": os.geteuid() == 0, + "user": os.environ.get("USER") or os.environ.get("USERNAME") or "", + "is_root": _is_admin(), } try: import psutil @@ -306,24 +334,35 @@ def _do_info(params: dict) -> dict: def _do_screenshot(params: dict) -> dict: - """Bildschirmfoto via System-Tool (Wayland: grim; X11: scrot/maim/import/ - gnome-screenshot). Braucht eine aktive grafische Session (DISPLAY/WAYLAND).""" + """Bildschirmfoto — OS-abhaengig. Windows: PowerShell/System.Drawing; + macOS: screencapture; Linux: grim (Wayland) / scrot/maim/import (X11). + Braucht eine aktive grafische Session.""" import tempfile tmp = os.path.join(tempfile.gettempdir(), f"aria_shot_{int(time.time())}.png") - wayland = bool(os.environ.get("WAYLAND_DISPLAY")) candidates = [] - if wayland and shutil.which("grim"): - candidates.append(["grim", tmp]) - for tool, argv in (("scrot", ["scrot", "-o", tmp]), - ("maim", ["maim", tmp]), - ("gnome-screenshot", ["gnome-screenshot", "-f", tmp]), - ("import", ["import", "-window", "root", tmp])): - if shutil.which(tool): - candidates.append(argv) + if IS_WINDOWS: + ps = ("Add-Type -AssemblyName System.Windows.Forms,System.Drawing;" + "$b=[System.Windows.Forms.SystemInformation]::VirtualScreen;" + "$bmp=New-Object System.Drawing.Bitmap $b.Width,$b.Height;" + "$g=[System.Drawing.Graphics]::FromImage($bmp);" + "$g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$b.Size);" + f"$bmp.Save('{tmp}');$g.Dispose();$bmp.Dispose()") + candidates.append(["powershell", "-NoProfile", "-NonInteractive", "-Command", ps]) + elif IS_MAC: + candidates.append(["screencapture", "-x", tmp]) # -x = ohne Ton + else: + if os.environ.get("WAYLAND_DISPLAY") and shutil.which("grim"): + candidates.append(["grim", tmp]) + for tool, argv in (("scrot", ["scrot", "-o", tmp]), + ("maim", ["maim", tmp]), + ("gnome-screenshot", ["gnome-screenshot", "-f", tmp]), + ("import", ["import", "-window", "root", tmp])): + if shutil.which(tool): + candidates.append(argv) if not candidates: return {"ok": False, "error": - "Kein Screenshot-Tool gefunden. Installiere grim (Wayland) oder " - "scrot/maim (X11)."} + "Kein Screenshot-Tool gefunden. Linux: grim (Wayland) oder " + "scrot/maim (X11) installieren. (Windows/macOS nutzen Bordmittel.)"} last_err = "" for argv in candidates: try: @@ -466,7 +505,7 @@ class HostAgent: def main() -> None: logger.info("ARIA Host-Agent startet — id=%s name=%s control=%s root=%s", - HOST_ID, HOST_NAME, CONTROL_ENABLED, os.geteuid() == 0) + HOST_ID, HOST_NAME, CONTROL_ENABLED, _is_admin()) try: asyncio.run(HostAgent().run()) except KeyboardInterrupt: