feat(host-agent): cross-platform — Linux, macOS UND Windows

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 19:40:17 +02:00
co-authored by Claude Opus 4.8
parent 29bf3370b4
commit 1140934e15
3 changed files with 109 additions and 21 deletions
+25 -4
View File
@@ -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` / ARIA nutzt diese über die Brain-Tools `host_list` / `host_exec` / `host_read` /
`host_write` / `host_info` / `host_screenshot`. `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 ```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 **Linux/macOS ohne Docker** — PyInstaller direkt (linkt gegen lokales glibc/OS):
möglichst vielen Distributionen läuft. ```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) ### Docker scheitert? (Live-ISO / overlayfs-Root)
+28
View File
@@ -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
+49 -10
View File
@@ -31,11 +31,28 @@ import re
import shutil import shutil
import socket import socket
import subprocess import subprocess
import sys
import time import time
from pathlib import Path from pathlib import Path
import websockets 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( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [host-agent] %(levelname)s %(message)s", format="%(asctime)s [host-agent] %(levelname)s %(message)s",
@@ -180,8 +197,18 @@ def _window_text(text: str, params: dict) -> dict:
# ─── Aktionen ─────────────────────────────────────────────────────── # ─── Aktionen ───────────────────────────────────────────────────────
def _wrap_sudo(cmd: str, use_sudo: bool): def _wrap_sudo(cmd: str, use_sudo: bool):
"""Gibt (argv, stdin_data) oder (None, fehlertext) wenn sudo nicht moeglich.""" """Baut die Argv (OS-abhaengige Shell) + optional Root/Admin-Rechte.
if not use_sudo or os.geteuid() == 0: 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 return ["bash", "-lc", cmd], None
if SUDO_PASSWORD: if SUDO_PASSWORD:
return ["sudo", "-S", "-p", "", "bash", "-lc", cmd], SUDO_PASSWORD + "\n" 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(), "host": HOST_NAME, "hostname": socket.gethostname(),
"os": platform.platform(), "kernel": platform.release(), "os": platform.platform(), "kernel": platform.release(),
"arch": platform.machine(), "python": platform.python_version(), "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: try:
import psutil import psutil
@@ -306,13 +334,24 @@ def _do_info(params: dict) -> dict:
def _do_screenshot(params: dict) -> dict: def _do_screenshot(params: dict) -> dict:
"""Bildschirmfoto via System-Tool (Wayland: grim; X11: scrot/maim/import/ """Bildschirmfoto — OS-abhaengig. Windows: PowerShell/System.Drawing;
gnome-screenshot). Braucht eine aktive grafische Session (DISPLAY/WAYLAND).""" macOS: screencapture; Linux: grim (Wayland) / scrot/maim/import (X11).
Braucht eine aktive grafische Session."""
import tempfile import tempfile
tmp = os.path.join(tempfile.gettempdir(), f"aria_shot_{int(time.time())}.png") tmp = os.path.join(tempfile.gettempdir(), f"aria_shot_{int(time.time())}.png")
wayland = bool(os.environ.get("WAYLAND_DISPLAY"))
candidates = [] candidates = []
if wayland and shutil.which("grim"): 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]) candidates.append(["grim", tmp])
for tool, argv in (("scrot", ["scrot", "-o", tmp]), for tool, argv in (("scrot", ["scrot", "-o", tmp]),
("maim", ["maim", tmp]), ("maim", ["maim", tmp]),
@@ -322,8 +361,8 @@ def _do_screenshot(params: dict) -> dict:
candidates.append(argv) candidates.append(argv)
if not candidates: if not candidates:
return {"ok": False, "error": return {"ok": False, "error":
"Kein Screenshot-Tool gefunden. Installiere grim (Wayland) oder " "Kein Screenshot-Tool gefunden. Linux: grim (Wayland) oder "
"scrot/maim (X11)."} "scrot/maim (X11) installieren. (Windows/macOS nutzen Bordmittel.)"}
last_err = "" last_err = ""
for argv in candidates: for argv in candidates:
try: try:
@@ -466,7 +505,7 @@ class HostAgent:
def main() -> None: def main() -> None:
logger.info("ARIA Host-Agent startet — id=%s name=%s control=%s root=%s", 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: try:
asyncio.run(HostAgent().run()) asyncio.run(HostAgent().run())
except KeyboardInterrupt: except KeyboardInterrupt: