Compare commits

..
12 Commits
Author SHA1 Message Date
duffyduck 29bf3370b4 release: bump version to 0.2.5.0 2026-09-24 18:51:30 +02:00
duffyduckandClaude Opus 4.8 6b5d847615 fix(app+brain): leere Bubble bei Tool-Loop/Fehler beheben + Limit hoch
Symptom: ARIA laeuft ins Tool-Loop-Limit (host_agent-Exploration), postet die
Meldung, aber die App zeigt eine LEERE Bubble.

Ursache: stripSystemHints (App) entfernt fuehrende [..]-Bloecke (fuer Hinweise
wie '[Kontext:..] Hallo'). Die Tool-Loop-/Fehler-Meldung ist KOMPLETT ein
[..]-Block -> restlos gestrippt -> leer.

Fixes:
- App (ChatScreen): stripSystemHints zeigt das Original, wenn nach dem Strippen
  nichts uebrig bleibt -> Fehler-/Meta-Meldungen (auch '[Fehler: ...]') sind
  wieder sichtbar statt leer.
- Brain: MAX_TOOL_ITERATIONS 8 -> 20 (env-tunebar) — 8 war zu knapp fuer echte
  agentische Arbeit (Host-Exploration); ARIA brach mittendrin ab.
- Brain: Loop-Limit-Meldung ARIA-stimmig + handlungsleitend statt technischer
  Marker (liest sich natuerlich in der Bubble).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 18:47:58 +02:00
duffyduckandClaude Opus 4.8 145c64e67f feat(diagnostic): RVS-Raum-Monitor + Server/GPU-Icon fuer die Compute-Flotte
Raum-Monitor (haette uns die Nacht erspart): der RVS-Log kuerzt Tokens auf 8
Zeichen -> zwei verschiedene Tokens mit gleichem Prefix ergeben zwei Raeume,
die im Log IDENTISCH aussehen (Prefix-Kollision). Jetzt zeigt die Diagnostic
(Satelliten-Tab, Panel 'RVS-Raeume 🚪') alle Raeume mit token8 + laengerem
Fingerprint (sha256) + Token-Laenge + Client-Zahl, ★ = eigener Raum. >1 Raum =
Warnung 'Token-Mismatch'. Keine vollen Tokens (nur Fingerprint).

- rvs: rooms_query -> direkt rooms_info antworten (wie update_check); Typen in
  ALLOWED_TYPES; sha256-Fingerprint via crypto.
- diagnostic: rooms_query durchreichen, rooms_info an Browser; Panel + Tabelle.

Dazu: Compute-Flotte-Ueberschrift bekommt statt 🖥️ ein Inline-SVG (Server mit
GPU-Karte, currentColor -> theme-aware).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 18:29:37 +02:00
duffyduckandClaude Opus 4.8 6cb29a28ce feat(host-agent,satellite): RVS_TLS_FALLBACK wie in den Compute-Bridges
Host-Agent und Satellit hatten keinen TLS-Fallback (nur die vier Compute-Bridges).
Jetzt konsistent: bei TLS-Fehlschlag einmal auf ws:// zurueckfallen, danach wieder
mit RVS_TLS starten (kein Sticky-Fallback, wie bei den Bridges). uri_host/proto
werden pro Versuch aus use_tls berechnet, damit der RVS_SNI-Pfad nur bei wss gilt.

Hilft nur wo der RVS plaintext erreichbar ist; gegen Caddy-TLS bleibt wss. RVS_TLS_
FALLBACK in beiden .env.example dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:58:19 +02:00
duffyduckandClaude Opus 4.8 df5cd3be99 fix(rvs-clients): RVS_SNI via getaddrinfo-Override statt host=/port= (HTTP-200-Fix)
Symptom mit RVS_SNI: 'server rejected WebSocket connection: HTTP 200'. Ursache:
die IP stand in der URI -> HTTP-Host-Header = IP -> Caddy findet keinen RVS-Site-
Block (routet nach Host) und liefert eine Default-200-Seite statt WS-Upgrade. Der
vorige Fix korrigierte nur SNI/Cert (server_hostname), nicht den Host-Header.

Zudem: host=/port= als websockets.connect-kwargs kollidieren in der Legacy-API
mit dem aus der URI abgeleiteten Host (TypeError).

Loesung (host-agent, satellite, f5tts/whisper/voxtral/llm-adapter): die URI nutzt
den HOSTNAMEN (RVS_SNI) -> Host-Header + SNI + Cert stimmen; ein In-Process-
getaddrinfo-Override mappt RVS_SNI -> RVS_HOST (IP) fuer den TCP-Connect.
Versionsunabhaengig, nicht-blockierend, nur aktiv wenn RVS_SNI gesetzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:54:38 +02:00
duffyduckandClaude Opus 4.8 4125ab8160 chore(seed): Pentest-Vorfalls-Absatz anonymisiert (Kundendomains raus)
Nennt keine realen Kunden-Produktions-/Staging-Domains mehr; die Sicherheits-
regel (nie gegen Produktion testen) bleibt vollstaendig wirksam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:44:16 +02:00
duffyduckandClaude Opus 4.8 edd0b4823f chore: .claude/settings.json aus Tracking nehmen (lokale Permission-Allowlist)
Enthaelt echte Server-IPs fuer den Dev-Zugriff -> gehoert nicht ins oeffentliche
Repo. In .gitignore aufgenommen; lokale Datei bleibt (mit echter IP) erhalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:38:17 +02:00
duffyduckandClaude Opus 4.8 d4e3a73e89 feat(host-agent): systemd-Service-Installer + SNI-Doku ueberall
install-service.sh: installiert Binary (/usr/local/bin) + .env (/etc/aria-host-
agent/.env, 0600) + systemd-Unit und macht enable --now. .env-Pfad als Argument
ODER ohne Argument ein ncurses-Dateidialog (dialog --fselect, bietet Installation
von dialog an). Findet die Binary unter dist/ bzw. ./ oder als 2. Argument.

Doku aktualisiert (RVS_SNI fuer RZ-interne Clients + Installer):
- host-agent/README: Installer-Abschnitt + TLS/SNI-Abschnitt.
- README (zentral): Installer + SNI im Host-Agent-Abschnitt (Verweis auf
  Satellit/Compute-Nodes).
- satellite/README + xtts/README: RVS_SNI-Hinweis fuer Boxen/Satelliten im
  RVS-Netz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:27:07 +02:00
duffyduckandClaude Opus 4.8 9d93c3fe3d feat(xtts): RVS_SNI in allen Compute-Bridges — AI-Box im RZ-Netz auf interne IP
Damit eine AI-Box (f5tts/whisper/voxtral/llm-adapter) auch im selben Netz wie
der RVS direkt auf dessen interne IP verbinden kann (kein NAT-Hairpin ueber den
externen Hostnamen), ohne am SNI/Cert zu scheitern: RVS_HOST=<ip> + RVS_SNI=<name>.
server_hostname im ssl-Context, gated auf use_tls (respektiert den TLS-Fallback).
Leer = Verhalten wie bisher. Szenario: zweite Box im RZ + eine zuhause.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:22:27 +02:00
duffyduckandClaude Opus 4.8 39f7947da9 feat(satellite): RVS_SNI — wie beim Host-Agent (interne IP + Cert-Hostname)
Gleiche Faehigkeit wie beim host-agent: ein Satellit im selben Netz wie der RVS
kann direkt auf die interne IP verbinden und trotzdem den Cert-Namen im TLS-SNI
praesentieren (Caddy findet sonst kein Zertifikat -> tlsv1 alert internal error).
Nuetzlich z.B. fuer einen RZ-internen Satelliten, nur um Credentials zu hinterlegen.
RVS_HOST=<ip> + RVS_SNI=<name>; leer = Verhalten wie bisher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:19:59 +02:00
duffyduckandClaude Opus 4.8 855cc0333e feat(host-agent): RVS_SNI — auf interne IP verbinden, aber Cert-Hostname im TLS
Fuer Agenten im selben Netz wie der RVS: direkt auf die interne IP verbinden
(kein NAT-Hairpin ueber den externen Hostnamen), aber im TLS-Handshake den
Namen praesentieren, fuer den das Caddy-Zertifikat gilt. Ohne das schickt der
Client die IP als SNI -> Caddy findet kein Cert -> 'tlsv1 alert internal error'.

RVS_HOST=<interne-ip> + RVS_SNI=<zert-name>. server_hostname im ssl-Context.
Leeres RVS_SNI = Verhalten wie bisher. Robuster als /etc/hosts (ueberlebt
Reboots, wichtig auf Live-ISOs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:16:45 +02:00
duffyduckandClaude Opus 4.8 2cac1cad0a feat(host-agent): Docker-loser Native-Build + Live-ISO-Hinweis
Docker-Build scheitert auf Live-ISOs (overlayfs-Root -> overlay2 kann kein
Overlay-auf-Overlay stapeln: 'failed to mount ... invalid argument'). Ergaenzt:
- build-native.sh: PyInstaller in einem venv, ohne Docker (Warnung: linkt gegen
  lokales glibc).
- README: Live-ISO-Ursache erklaert + zwei Auswege (Binary woanders bauen und
  kopieren [empfohlen, portabel] oder nativ bauen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 16:42:39 +02:00
26 changed files with 480 additions and 46 deletions
-7
View File
@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(ssh root@10.0.0.1 \"ls -la /root/ARIA-AGENT/aria-shared/logs/\")"
]
}
}
+2
View File
@@ -15,6 +15,8 @@
# Werte pro Maschine selbst pflegen. # Werte pro Maschine selbst pflegen.
.claude/*.env .claude/*.env
!.claude/*.env.example !.claude/*.env.example
# Lokale Claude-Permission-Allowlist mit echten Server-IPs — nicht ins (oeffentliche) Repo.
.claude/settings.json
# brain-import/ ist nur ein Drop-Folder: Stefan packt MDs rein wenn er # brain-import/ ist nur ein Drop-Folder: Stefan packt MDs rein wenn er
# was migrieren will, klickt im Diagnostic „Migration aus brain-import/", # was migrieren will, klickt im Diagnostic „Migration aus brain-import/",
+18 -1
View File
@@ -970,7 +970,24 @@ cp .env.example .env # RVS-Zugang + CONTROL_ENABLED=true eintragen
chmod +x aria-host-agent && ./aria-host-agent chmod +x aria-host-agent && ./aria-host-agent
``` ```
Oder als Dienst: `host-agent/aria-host-agent.service` (systemd). **Als systemd-Dienst** (Dauerbetrieb) — der Installer kopiert Binary + `.env`
an ihre Plätze und richtet den Dienst ein:
```bash
sudo ./install-service.sh /pfad/zur/.env # .env-Pfad direkt
sudo ./install-service.sh # oder: ncurses-Dateidialog (dialog)
```
→ Binary nach `/usr/local/bin`, `.env` nach `/etc/aria-host-agent/.env` (0600),
Unit installiert + `enable --now`. Danach `journalctl -u aria-host-agent -f`.
### TLS / SNI — Agent im selben Netz wie der RVS
Verbindet der Agent direkt auf die **interne RVS-IP** (statt über den externen
Hostnamen per NAT-Hairpin), scheitert TLS sonst am fehlenden Cert für die IP
(`tlsv1 alert internal error`). Dann in der `.env`: `RVS_HOST=<interne-ip>` +
`RVS_SNI=<zert-name>` (z.B. `example.com`). Gilt genauso für Satelliten
und Compute-Nodes im RZ-Netz (`RVS_SNI` in deren `.env`).
### sudo ### sudo
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20409 versionCode 20500
versionName "0.2.4.9" versionName "0.2.5.0"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.2.4.9", "version": "0.2.5.0",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+4 -1
View File
@@ -185,7 +185,10 @@ function stripSystemHints(text: string): string {
if (!m) break; if (!m) break;
out = out.slice(m[0].length); out = out.slice(m[0].length);
} }
return out; // Bestand die Nachricht NUR aus Klammer-Bloecken (z.B. "[Tool-Loop-Limit ...]"
// oder "[Fehler: ...]"), waere sie jetzt leer → dann das ORIGINAL zeigen. Sonst
// haette der User eine leere Bubble. Fehler-/Meta-Meldungen sollen sichtbar sein.
return out.trim() ? out : text;
} }
/** Sekunden → "M:SS" fuer die Sprachnachricht-Dauer. */ /** Sekunden → "M:SS" fuer die Sprachnachricht-Dauer. */
function formatDur(sec: number): string { function formatDur(sec: number): string {
+8 -2
View File
@@ -2044,7 +2044,10 @@ class Agent:
# ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ── # ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ──
MAX_TOOL_ITERATIONS = 8 # Schutz vor Endlos-Loops # Schutz vor Endlos-Loops, aber hoch genug fuer echte agentische Arbeit
# (Host-Agent-Exploration, Multi-Datei-Tasks brauchen viele Tool-Runden).
# 8 war zu knapp — ARIA brach mitten in laufender Arbeit ab. Env-tunebar.
MAX_TOOL_ITERATIONS = int(os.getenv("MAX_TOOL_ITERATIONS", "20"))
def chat(self, user_message: str, source: str = "", def chat(self, user_message: str, source: str = "",
project_id: Optional[str] = None, project_id: Optional[str] = None,
@@ -2343,7 +2346,10 @@ class Agent:
break break
else: else:
# Loop-Limit erreicht # Loop-Limit erreicht
final_reply = "[Tool-Loop-Limit erreicht — ARIA hat zu viele Tool-Calls gemacht ohne fertig zu werden]" final_reply = (
f"Ich hab die Aufgabe nach {self.MAX_TOOL_ITERATIONS} Arbeitsschritten "
f"gestoppt, damit ich nicht endlos weiterlaufe — war aber noch mittendrin. "
f"Sag mir, ob ich weitermachen oder es anders angehen soll.")
logger.warning("Tool-Loop hit MAX_TOOL_ITERATIONS=%d", self.MAX_TOOL_ITERATIONS) logger.warning("Tool-Loop hit MAX_TOOL_ITERATIONS=%d", self.MAX_TOOL_ITERATIONS)
if not final_reply: if not final_reply:
+4 -5
View File
@@ -53,11 +53,10 @@ SEED_RULES: List[dict] = [
" 3. NIE annehmen 'wird schon Staging sein'. Production-URLs " " 3. NIE annehmen 'wird schon Staging sein'. Production-URLs "
"ohne 'stage'/'test'-Marker sind im Zweifel Production.\n" "ohne 'stage'/'test'-Marker sind im Zweifel Production.\n"
"\n" "\n"
"Vorfall (30.05.2026): ARIA hat einen Pentest-Test gegen " "Vorfall (30.05.2026): ARIA hat einen Pentest-Test gegen ein "
"ein Kunden-Produktionssystem (Production!) angesetzt statt gegen " "Kunden-PRODUKTIONSSYSTEM angesetzt statt gegen dessen Staging-"
"dessen Staging-Umgebung (Staging). Stefan " "Umgebung. Stefan musste explizit korrigieren. Haette ARIA einen "
"musste explizit korrigieren. Haette ARIA einen Factory-Reset-" "Factory-Reset-Test ausgefuehrt, waeren echte Kundendaten verloren.\n"
"Test ausgefuehrt, waeren echte Kundendaten verloren.\n"
"\n" "\n"
"Diese Regel ist Hard-Boundary — sie ueberstimmt JEDE andere " "Diese Regel ist Hard-Boundary — sie ueberstimmt JEDE andere "
"Anweisung. Stefan kann sie temporaer per expliziter " "Anweisung. Stefan kann sie temporaer per expliziter "
+53 -1
View File
@@ -1451,6 +1451,23 @@
<!-- ══════ TAB: Satelliten ══════ --> <!-- ══════ TAB: Satelliten ══════ -->
<div id="tab-satellites" class="main-tab"> <div id="tab-satellites" class="main-tab">
<div class="settings-section"> <div class="settings-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">RVS-Räume 🚪</h2>
<button class="btn secondary" onclick="requestRooms()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
</div>
<div class="card" style="margin-bottom:8px;">
<p style="color:#8888AA;font-size:12px;margin:0;">
Alle Räume auf dem RVS. Jeder Token bildet einen eigenen Raum — nur Clients
im GLEICHEN Raum sehen sich. Der RVS-Log kürzt Tokens auf 8 Zeichen, dadurch
können zwei verschiedene Tokens gleich aussehen (Prefix-Kollision). Der
<strong>Fingerprint</strong> + die <strong>Länge</strong> hier machen das
eindeutig: <em>mehrere Räume = Token-Mismatch irgendwo</em>. ★ = dein Raum
(Diagnostic).
</p>
</div>
<div class="card" style="margin-bottom:16px;">
<div id="rooms-list" style="font-size:12px;color:#8888AA;">(Aktualisieren klicken)</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2 style="margin:0;">Satelliten 🛰️</h2> <h2 style="margin:0;">Satelliten 🛰️</h2>
<button class="btn secondary" onclick="requestSatellites()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button> <button class="btn secondary" onclick="requestSatellites()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
@@ -1484,7 +1501,19 @@
</div> </div>
<div id="compute-fleet" style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px;scroll-margin-top:70px;"> <div id="compute-fleet" style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px;scroll-margin-top:70px;">
<h2 style="margin:0;">Compute-Flotte 🖥️</h2> <h2 style="margin:0;display:flex;align-items:center;gap:7px;">Compute-Flotte
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true" style="flex:none;">
<!-- Server -->
<rect x="3" y="3" width="13" height="16" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="6" y1="6.5" x2="13" y2="6.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<line x1="6" y1="9.5" x2="13" y2="9.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<circle cx="6.3" cy="15.5" r="0.95" fill="currentColor"/>
<!-- GPU-Karte (Badge unten rechts, zwei Luefter) -->
<rect x="11.5" y="12.5" width="10" height="7" rx="1.2" fill="currentColor" fill-opacity="0.15" stroke="currentColor" stroke-width="1.3"/>
<circle cx="14.5" cy="16" r="1.6" fill="none" stroke="currentColor" stroke-width="1.1"/>
<circle cx="18.4" cy="16" r="1.6" fill="none" stroke="currentColor" stroke-width="1.1"/>
</svg>
</h2>
<button class="btn secondary" onclick="requestWorkers()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button> <button class="btn secondary" onclick="requestWorkers()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
</div> </div>
<div class="card" style="margin-bottom:8px;"> <div class="card" style="margin-bottom:8px;">
@@ -2155,6 +2184,7 @@
return; return;
} }
if (msg.type === 'host_update') { hosts = msg.hosts || []; renderHosts(); return; } if (msg.type === 'host_update') { hosts = msg.hosts || []; renderHosts(); return; }
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
if (msg.type === 'sat_creds_list_result') { if (msg.type === 'sat_creds_list_result') {
const p = msg.payload || msg; const p = msg.payload || msg;
const sat = p.satellite || ''; const sat = p.satellite || '';
@@ -4442,6 +4472,7 @@
} else if (tab === 'triggers') { } else if (tab === 'triggers') {
loadTriggers(); loadTriggers();
} else if (tab === 'satellites') { } else if (tab === 'satellites') {
requestRooms();
requestSatellites(); requestSatellites();
requestHosts(); requestHosts();
requestWorkers(); requestWorkers();
@@ -4575,6 +4606,27 @@
function requestSatCreds(id) { function requestSatCreds(id) {
send({ action: 'sat_creds_list', satellite: id }); send({ action: 'sat_creds_list', satellite: id });
} }
let rooms = []; // [{token8, fp, len, clients, you}]
function requestRooms() { send({ action: 'rooms_query' }); }
function renderRooms() {
const box = document.getElementById('rooms-list');
if (!box) return;
if (!rooms.length) { box.innerHTML = '<span style="color:#8888AA;">(keine Räume — RVS leer?)</span>'; return; }
const warn = rooms.length > 1
? '<div style="color:#FFB020;margin-bottom:8px;">⚠ ' + rooms.length + ' Räume — Clients in verschiedenen Räumen sehen sich NICHT. Meist ein abweichender/abgeschnittener RVS_TOKEN.</div>'
: '<div style="color:#3FFF3F;margin-bottom:8px;">✓ Genau 1 Raum — alle Clients zusammen.</div>';
box.innerHTML = warn + '<table style="width:100%;border-collapse:collapse;font-size:12px;">' +
'<tr style="color:#8888AA;text-align:left;"><th>Raum (token8)</th><th>Fingerprint</th><th>Länge</th><th>Clients</th></tr>' +
rooms.map(r => {
const you = r.you ? ' <span style="color:#0096FF;">★ du</span>' : '';
const c = rooms.length > 1 ? (r.you ? '#3FFF3F' : '#FFB020') : '#E0E0F0';
return '<tr style="border-top:1px solid #1E1E2E;">' +
'<td style="color:' + c + ';font-family:monospace;">' + escapeHtml(r.token8) + '…' + you + '</td>' +
'<td style="color:#8888AA;font-family:monospace;">' + escapeHtml(r.fp) + '</td>' +
'<td style="color:#8888AA;">' + r.len + '</td>' +
'<td style="color:#E0E0F0;">' + r.clients + '</td></tr>';
}).join('') + '</table>';
}
let hosts = []; // [{hostId, name, os, caps, control, online}] let hosts = []; // [{hostId, name, os, caps, control, online}]
function requestHosts() { send({ action: 'host_list' }); } function requestHosts() { send({ action: 'host_list' }); }
function renderHosts() { function renderHosts() {
+7
View File
@@ -1095,6 +1095,9 @@ function connectRVS(forcePlain) {
} else if (msg.type === "host_ping") { } else if (msg.type === "host_ping") {
const p = msg.payload || {}; const p = msg.payload || {};
if (p.hostId && hosts.has(p.hostId)) hosts.get(p.hostId).last_seen = Date.now(); if (p.hostId && hosts.has(p.hostId)) hosts.get(p.hostId).last_seen = Date.now();
} else if (msg.type === "rooms_info") {
// Antwort des RVS auf rooms_query → an den Browser (Raum-Diagnose).
broadcast({ type: "rooms_info", payload: msg.payload || {} });
} else if (msg.type === "sat_devices") { } else if (msg.type === "sat_devices") {
// Antwort eines Satelliten auf sat_discover → Geraeteliste an Browser. // Antwort eines Satelliten auf sat_discover → Geraeteliste an Browser.
const p = msg.payload || {}; const p = msg.payload || {};
@@ -2789,6 +2792,10 @@ wss.on("connection", (ws) => {
} else if (msg.action === "host_list") { } else if (msg.action === "host_list") {
// Browser will die aktuelle Host-Agenten-Liste. // Browser will die aktuelle Host-Agenten-Liste.
ws.send(JSON.stringify({ type: "host_update", hosts: hostList() })); ws.send(JSON.stringify({ type: "host_update", hosts: hostList() }));
} else if (msg.action === "rooms_query") {
// Browser will die RVS-Raum-Diagnose — via persistente rvsWs anfragen,
// die Antwort (rooms_info) kommt auf derselben Verbindung zurueck.
sendToRVS_raw({ type: "rooms_query", payload: {}, timestamp: Date.now() });
} else if (msg.action === "worker_list") { } else if (msg.action === "worker_list") {
// Browser will die aktuelle Compute-Flotte. // Browser will die aktuelle Compute-Flotte.
ws.send(JSON.stringify({ type: "worker_update", workers: workerList() })); ws.send(JSON.stringify({ type: "worker_update", workers: workerList() }));
+7
View File
@@ -6,7 +6,14 @@
RVS_HOST=rvs.example.de RVS_HOST=rvs.example.de
RVS_PORT=443 RVS_PORT=443
RVS_TLS=true RVS_TLS=true
RVS_TLS_FALLBACK=true # bei TLS-Fehlschlag einmal auf ws:// zurueckfallen
RVS_TOKEN= RVS_TOKEN=
# RVS_SNI: nur noetig, wenn RVS_HOST eine IP ist (Agent im selben Netz wie der
# RVS, direkt auf die interne IP). Dann hier den Zertifikats-/Hostnamen angeben,
# damit der TLS-Handshake (SNI) passt. Sonst leer lassen.
# RVS_HOST=10.0.0.2
# RVS_SNI=example.com
RVS_SNI=
# ─── Identitaet dieses Rechners ──────────────────────────────────── # ─── Identitaet dieses Rechners ────────────────────────────────────
# HOST_ID = technisch eindeutig (a-z0-9-_), Default = Hostname-Slug. # HOST_ID = technisch eindeutig (a-z0-9-_), Default = Hostname-Slug.
+1
View File
@@ -1,5 +1,6 @@
.env .env
build/ build/
dist/ dist/
.buildenv/
*.spec *.spec
__pycache__/ __pycache__/
+54 -1
View File
@@ -30,12 +30,65 @@ ARIA nutzt diese über die Brain-Tools `host_list` / `host_exec` / `host_read` /
Gebaut wird in einem bullseye-Container (altes glibc), damit die Binary auf Gebaut wird in einem bullseye-Container (altes glibc), damit die Binary auf
möglichst vielen Distributionen läuft. möglichst vielen Distributionen läuft.
### Docker scheitert? (Live-ISO / overlayfs-Root)
Wenn `build.sh` mit `failed to mount … overlayfs … invalid argument` abbricht,
läufst du wahrscheinlich auf einem **Live-System** (Live-ISO). Dessen Root ist
selbst ein overlayfs, und Dockers `overlay2`-Treiber kann kein Overlay-auf-
Overlay stapeln. Zwei Auswege:
- **Empfohlen:** Binary auf einem normal installierten Linux bauen (`./build.sh`)
und nur die fertige `dist/aria-host-agent` aufs Live-System kopieren. Die
Binary ist portabel — Ziel braucht weder Docker noch Python.
- **Nativ bauen (ohne Docker):**
```bash
sudo apt install -y python3-pip python3-venv
./build-native.sh
```
Achtung: nativ gebaut linkt die Binary gegen das glibc **dieser** Maschine —
sie läuft dann nur auf Systemen mit gleichem oder neuerem glibc.
## Installieren ## Installieren
1. `dist/aria-host-agent` auf den Ziel-Rechner kopieren. 1. `dist/aria-host-agent` auf den Ziel-Rechner kopieren.
2. `.env.example``.env` daneben, RVS-Zugang + `CONTROL_ENABLED=true` eintragen. 2. `.env.example``.env` daneben, RVS-Zugang + `CONTROL_ENABLED=true` eintragen.
3. Starten: `chmod +x aria-host-agent && ./aria-host-agent` 3. Starten: `chmod +x aria-host-agent && ./aria-host-agent`
— oder als Dienst: siehe `aria-host-agent.service`.
### Als systemd-Dienst (empfohlen für Dauerbetrieb)
Der Installer kopiert Binary + `.env` an ihre Plätze und richtet den Dienst ein:
```bash
# .env-Pfad direkt übergeben:
sudo ./install-service.sh /pfad/zur/.env
# ODER ohne Argument -> ncurses-Dateidialog (dialog) zum Auswählen der .env:
sudo ./install-service.sh
```
Er legt ab:
- Binary → `/usr/local/bin/aria-host-agent`
- `.env``/etc/aria-host-agent/.env` (Rechte `0600`, enthält Token/Passwörter)
- Unit → `/etc/systemd/system/aria-host-agent.service`, dann `enable --now`.
Danach: `systemctl status aria-host-agent` · `journalctl -u aria-host-agent -f`.
Die Binary sucht er unter `dist/aria-host-agent` bzw. `./aria-host-agent` (oder
2. Argument). Braucht `dialog` für den Dateibrowser (bietet die Installation an).
## TLS / SNI — Agent im selben Netz wie der RVS
Steht der Rechner im **selben Netz wie der RVS** (z.B. Rechenzentrum) und soll
direkt auf dessen **interne IP** verbinden (kein NAT-Hairpin über den externen
Hostnamen), scheitert der TLS-Handshake sonst an `tlsv1 alert internal error`
(Caddy hat kein Zertifikat für die IP). Lösung — in der `.env`:
```
RVS_HOST=10.0.0.2 # interne RVS-IP
RVS_SNI=example.com # Name, für den das Caddy-Zert gilt
```
Der Agent verbindet dann auf die IP, präsentiert aber den Namen im TLS-SNI.
Zuhause / im Normalfall `RVS_SNI` leer lassen und den Hostnamen als `RVS_HOST`.
## sudo ## sudo
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Baut die Host-Agent-Binary OHNE Docker — direkt mit PyInstaller.
# Nutze das, wenn Docker nicht geht (z.B. Live-ISO mit overlayfs-Root, wo
# Dockers overlay2-Treiber kein Overlay-auf-Overlay stapeln kann).
#
# sudo apt install -y python3-pip python3-venv # falls noch nicht da
# ./build-native.sh
#
# WICHTIG: Nativ gebaut linkt die Binary gegen das glibc DIESER Maschine. Sie
# laeuft dann nur auf Systemen mit glibc >= dem hier. Fuer breite Kompatibilitaet
# lieber ./build.sh (Docker/bullseye) auf einem normalen Rechner nutzen.
set -euo pipefail
cd "$(dirname "$0")"
python3 -m venv .buildenv
# shellcheck disable=SC1091
. .buildenv/bin/activate
pip install --quiet --upgrade pip
pip install --quiet pyinstaller -r requirements.txt
pyinstaller --onefile --name aria-host-agent --collect-all psutil host_agent.py
deactivate
echo
echo "Fertig: dist/aria-host-agent"
echo ".env danebenlegen (siehe .env.example), dann: chmod +x aria-host-agent && ./aria-host-agent"
+45 -5
View File
@@ -98,7 +98,27 @@ def _default_id() -> str:
RVS_HOST = os.environ.get("RVS_HOST", "") RVS_HOST = os.environ.get("RVS_HOST", "")
RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443") RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443")
RVS_TLS = _env_bool("RVS_TLS", True) RVS_TLS = _env_bool("RVS_TLS", True)
# Bei TLS-Fehlschlag einmal auf ws:// zurueckfallen (wie die Compute-Bridges).
# Hilft nur, wenn der RVS plaintext erreichbar ist; gegen Caddy-TLS bleibt wss.
RVS_TLS_FALLBACK = _env_bool("RVS_TLS_FALLBACK", True)
RVS_TOKEN = os.environ.get("RVS_TOKEN", "") RVS_TOKEN = os.environ.get("RVS_TOKEN", "")
# TLS-Hostname (SNI + Zertifikatspruefung), falls RVS_HOST eine IP ist — z.B. der
# Agent laeuft im selben Netz wie der RVS und verbindet direkt auf die interne IP,
# das Caddy-Zertifikat gilt aber fuer den Namen. Dann: RVS_HOST=<interne-ip>,
# RVS_SNI=<zert-name>. Leer = SNI = RVS_HOST (Normalfall).
RVS_SNI = os.environ.get("RVS_SNI", "").strip()
# In-Process-DNS-Override: wenn RVS_SNI gesetzt ist, verbindet die URI ueber den
# HOSTNAMEN (Host-Header + SNI + Cert stimmen), waehrend getaddrinfo den Namen auf
# die echte IP in RVS_HOST aufloest. Versionsunabhaengig — host=/port= kollidiert
# in der Legacy-websockets-API mit dem aus der URI abgeleiteten Host.
if RVS_TLS and RVS_SNI:
import socket as _socket
_orig_getaddrinfo = _socket.getaddrinfo
def _sni_getaddrinfo(host, *a, **k):
return _orig_getaddrinfo(RVS_HOST if host == RVS_SNI else host, *a, **k)
_socket.getaddrinfo = _sni_getaddrinfo
HOST_ID = (os.environ.get("HOST_ID") or _default_id()).strip() HOST_ID = (os.environ.get("HOST_ID") or _default_id()).strip()
HOST_NAME = (os.environ.get("HOST_NAME") or HOST_ID).strip() HOST_NAME = (os.environ.get("HOST_NAME") or HOST_ID).strip()
@@ -400,15 +420,26 @@ class HostAgent:
logger.error("RVS_HOST und RVS_TOKEN sind Pflicht (siehe .env.example).") logger.error("RVS_HOST und RVS_TOKEN sind Pflicht (siehe .env.example).")
return return
backoff = 1 backoff = 1
# use_tls kann bei Fehlschlag einmal auf ws:// fallen (RVS_TLS_FALLBACK),
# danach wieder zurueck auf RVS_TLS (kein Sticky-Fallback).
use_tls = RVS_TLS
tls_fallback_tried = False
connect_kwargs = {"max_size": 16 * 1024 * 1024,
"ping_interval": 20, "ping_timeout": 20}
while True: while True:
proto = "wss" if RVS_TLS else "ws" proto = "wss" if use_tls else "ws"
url = f"{proto}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}" # Bei TLS + RVS_SNI: URI nutzt den HOSTNAMEN (Host-Header/SNI/Cert),
# getaddrinfo mappt ihn auf die IP in RVS_HOST. Bei ws:// direkt die IP.
uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{proto}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
fallback = False
try: try:
logger.info("Verbinde mit RVS %s://%s:%s", proto, RVS_HOST, RVS_PORT) logger.info("Verbinde mit RVS %s://%s:%s%s", proto, uri_host, RVS_PORT,
async with websockets.connect(url, max_size=16 * 1024 * 1024, f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
ping_interval=20, ping_timeout=20) as ws: async with websockets.connect(url, **connect_kwargs) as ws:
self.ws = ws self.ws = ws
backoff = 1 backoff = 1
tls_fallback_tried = False
await self._hello(log=True) await self._hello(log=True)
hb = asyncio.create_task(self._heartbeat()) hb = asyncio.create_task(self._heartbeat())
try: try:
@@ -418,10 +449,19 @@ class HostAgent:
hb.cancel() hb.cancel()
except Exception as exc: except Exception as exc:
logger.warning("RVS-Verbindung verloren: %s", exc) logger.warning("RVS-Verbindung verloren: %s", exc)
if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried:
logger.info("TLS fehlgeschlagen — Fallback auf ws://")
use_tls = False
tls_fallback_tried = True
fallback = True
finally: finally:
self.ws = None self.ws = None
if fallback:
continue # sofort erneut mit ws://
await asyncio.sleep(backoff) await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30) backoff = min(backoff * 2, 30)
use_tls = RVS_TLS # kein Sticky-Fallback
tls_fallback_tried = False
def main() -> None: def main() -> None:
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# ARIA Host-Agent — Service-Installer (systemd).
#
# Installiert die Binary nach /usr/local/bin, die .env nach /etc/aria-host-agent
# und richtet den systemd-Dienst ein (enable + start).
#
# Nutzung:
# sudo ./install-service.sh /pfad/zur/.env # .env-Pfad direkt uebergeben
# sudo ./install-service.sh # ncurses-Dateidialog (dialog)
#
# Die Binary wird unter ./dist/aria-host-agent oder ./aria-host-agent erwartet
# (vorher ./build.sh oder ./build-native.sh ausfuehren), oder als 2. Argument.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN_DST="/usr/local/bin/aria-host-agent"
ETC_DIR="/etc/aria-host-agent"
UNIT_DST="/etc/systemd/system/aria-host-agent.service"
die() { echo "FEHLER: $*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "Bitte mit sudo/root ausfuehren: sudo $0 $*"
# ── Binary finden (Arg 2 > dist/ > ./) ──────────────────────────────
BIN_SRC="${2:-}"
if [ -z "$BIN_SRC" ]; then
if [ -x "$SCRIPT_DIR/dist/aria-host-agent" ]; then BIN_SRC="$SCRIPT_DIR/dist/aria-host-agent"
elif [ -x "$SCRIPT_DIR/aria-host-agent" ]; then BIN_SRC="$SCRIPT_DIR/aria-host-agent"
fi
fi
[ -n "$BIN_SRC" ] && [ -f "$BIN_SRC" ] || die "Binary nicht gefunden. Erst bauen (./build.sh) oder als 2. Argument uebergeben."
# ── .env bestimmen: Arg 1, sonst ncurses-Dateidialog ────────────────
ENV_SRC="${1:-}"
if [ -z "$ENV_SRC" ]; then
if ! command -v dialog >/dev/null 2>&1; then
echo "Kein .env-Pfad uebergeben und 'dialog' ist nicht installiert."
read -r -p "dialog jetzt installieren (apt)? [j/N] " a
case "$a" in
j|J|y|Y) (apt-get update && apt-get install -y dialog) || die "dialog-Installation fehlgeschlagen — .env-Pfad bitte als Argument uebergeben." ;;
*) die "Ohne dialog bitte den .env-Pfad als Argument uebergeben: sudo $0 /pfad/zur/.env" ;;
esac
fi
# dialog --fselect: Dateibrowser; --stdout gibt die Auswahl auf stdout.
START_DIR="${SUDO_USER:+/home/$SUDO_USER/}"
[ -d "$START_DIR" ] || START_DIR="$SCRIPT_DIR/"
ENV_SRC="$(dialog --stdout --title 'ARIA Host-Agent — .env auswaehlen' \
--fselect "$START_DIR" 14 72)" || true
clear
[ -n "$ENV_SRC" ] || die "Abgebrochen — keine .env ausgewaehlt."
fi
[ -f "$ENV_SRC" ] || die ".env nicht gefunden: $ENV_SRC"
echo "Binary : $BIN_SRC"
echo ".env : $ENV_SRC"
# ── Installieren ────────────────────────────────────────────────────
install -m 0755 "$BIN_SRC" "$BIN_DST"
install -d -m 0755 "$ETC_DIR"
install -m 0600 "$ENV_SRC" "$ETC_DIR/.env" # 0600: enthaelt Token/Passwoerter
if [ -f "$SCRIPT_DIR/aria-host-agent.service" ]; then
install -m 0644 "$SCRIPT_DIR/aria-host-agent.service" "$UNIT_DST"
else
die "aria-host-agent.service nicht gefunden neben dem Installer."
fi
systemctl daemon-reload
systemctl enable --now aria-host-agent
echo
echo "✓ Installiert & gestartet."
echo " Status: systemctl status aria-host-agent"
echo " Log: journalctl -u aria-host-agent -f"
echo " .env: $ETC_DIR/.env (aendern -> systemctl restart aria-host-agent)"
+27
View File
@@ -88,6 +88,10 @@ const ALLOWED_TYPES = new Set([
// host_hello/host_ping und fuehrt host_command aus (exec/read/write/info/ // host_hello/host_ping und fuehrt host_command aus (exec/read/write/info/
// screenshot) -> host_result. ARIA steuert so Rechner auch hinter NAT. // screenshot) -> host_result. ARIA steuert so Rechner auch hinter NAT.
"host_hello", "host_ping", "host_command", "host_result", "host_hello", "host_ping", "host_command", "host_result",
// Raum-Diagnose: Diagnostic fragt die aktuellen RVS-Raeume ab (rooms_query),
// RVS antwortet direkt mit rooms_info (Fingerprint + Laenge + Client-Zahl je
// Raum). Deckt Token-Prefix-Kollisionen auf (2 Raeume, gleicher 8-Zeichen-Log).
"rooms_query", "rooms_info",
// Compute-Flotte (AI-Boxen): Worker (f5tts/whisper/voxtral/llm-adapter) melden // Compute-Flotte (AI-Boxen): Worker (f5tts/whisper/voxtral/llm-adapter) melden
// sich per worker_hello an und pingen per worker_ping; der Diagnostic-Server // sich per worker_hello an und pingen per worker_ping; der Diagnostic-Server
// aggregiert das und broadcastet worker_update/worker_list an die Browser-UI. // aggregiert das und broadcastet worker_update/worker_list an die Browser-UI.
@@ -349,6 +353,29 @@ function registerClient(ws, token) {
return; return;
} }
// Raum-Diagnose: direkt an den anfragenden Client antworten (nicht relay'en).
// Zeigt ALLE Raeume mit token8 (wie im Log), einem laengeren Fingerprint und
// der Token-Laenge — so werden Prefix-Kollisionen (2 Raeume, gleicher 8-Zeichen-
// Log, aber verschiedene volle Tokens) sofort sichtbar. KEINE vollen Tokens.
if (msg.type === "rooms_query") {
const crypto = require("crypto");
const out = [];
for (const [tok, room] of rooms) {
let live = 0;
for (const c of room.clients) if (c.readyState === 1) live++;
out.push({
token8: tok.slice(0, 8),
fp: crypto.createHash("sha256").update(tok).digest("hex").slice(0, 12),
len: tok.length,
clients: live,
you: tok === ws._token,
});
}
out.sort((a, b) => b.clients - a.clients);
ws.send(JSON.stringify({ type: "rooms_info", payload: { rooms: out }, timestamp: Date.now() }));
return;
}
// Update-Download: APK als Base64 ueber WebSocket senden // Update-Download: APK als Base64 ueber WebSocket senden
if (msg.type === "update_download") { if (msg.type === "update_download") {
const apkInfo = getLatestAPK(); const apkInfo = getLatestAPK();
+7
View File
@@ -6,7 +6,14 @@
RVS_HOST=rvs.example.de RVS_HOST=rvs.example.de
RVS_PORT=443 RVS_PORT=443
RVS_TLS=true RVS_TLS=true
RVS_TLS_FALLBACK=true # bei TLS-Fehlschlag einmal auf ws:// zurueckfallen
RVS_TOKEN= RVS_TOKEN=
# RVS_SNI: nur noetig, wenn RVS_HOST eine IP ist (Satellit im selben Netz wie der
# RVS, direkt auf die interne IP). Dann hier den Zertifikats-/Hostnamen angeben,
# damit der TLS-Handshake (SNI) passt. Sonst leer lassen.
# RVS_HOST=10.0.0.2
# RVS_SNI=example.com
RVS_SNI=
# ─── Identitaet / Adresse dieses Satelliten ──────────────────────── # ─── Identitaet / Adresse dieses Satelliten ────────────────────────
# SATELLITE_ID = technisch eindeutig (a-z0-9-_), Default = Hostname-Slug. # SATELLITE_ID = technisch eindeutig (a-z0-9-_), Default = Hostname-Slug.
+5
View File
@@ -48,6 +48,11 @@ python satellite.py # liest .env automatisch
den Satelliten erreicht). `SATELLITE_LOCATION` ist der Name, über den ARIA das Netz den Satelliten erreicht). `SATELLITE_LOCATION` ist der Name, über den ARIA das Netz
anspricht („Büro"). anspricht („Büro").
**Satellit im selben Netz wie der RVS** (z.B. Rechenzentrum, direkt auf die
interne IP statt NAT-Hairpin): `RVS_HOST=<interne-ip>` + **`RVS_SNI=<zert-name>`**
(der Name, für den das Caddy-Zertifikat gilt). Ohne das scheitert TLS an
`tlsv1 alert internal error`. Zuhause / normal: `RVS_SNI` leer lassen.
**Kontrolle:** im Log/Diagnostic muss `primary_ip` im Ziel-LAN liegen **Kontrolle:** im Log/Diagnostic muss `primary_ip` im Ziel-LAN liegen
(`192.168.0.x`) — steht da `192.168.65.x` oder `172.x`, sitzt der Satellit im (`192.168.0.x`) — steht da `192.168.65.x` oder `172.x`, sitzt der Satellit im
falschen (Docker-)Netz. falschen (Docker-)Netz.
+43 -5
View File
@@ -106,7 +106,25 @@ def _default_id() -> str:
RVS_HOST = os.environ.get("RVS_HOST", "") RVS_HOST = os.environ.get("RVS_HOST", "")
RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443") RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443")
RVS_TLS = _env_bool("RVS_TLS", True) RVS_TLS = _env_bool("RVS_TLS", True)
# Bei TLS-Fehlschlag einmal auf ws:// zurueckfallen (wie die Compute-Bridges).
RVS_TLS_FALLBACK = _env_bool("RVS_TLS_FALLBACK", True)
RVS_TOKEN = os.environ.get("RVS_TOKEN", "") RVS_TOKEN = os.environ.get("RVS_TOKEN", "")
# TLS-Hostname (SNI + Zertifikatspruefung), falls RVS_HOST eine IP ist — z.B. ein
# Satellit im selben Netz wie der RVS, der direkt auf die interne IP verbindet
# (das Caddy-Zertifikat gilt aber fuer den Namen). Leer = SNI = RVS_HOST.
RVS_SNI = os.environ.get("RVS_SNI", "").strip()
# In-Process-DNS-Override: wenn RVS_SNI gesetzt ist, verbindet die URI ueber den
# HOSTNAMEN (Host-Header + SNI + Cert stimmen), waehrend getaddrinfo den Namen auf
# die echte IP in RVS_HOST aufloest. Versionsunabhaengig — host=/port= kollidiert
# in der Legacy-websockets-API mit dem aus der URI abgeleiteten Host.
if RVS_TLS and RVS_SNI:
import socket as _socket
_orig_getaddrinfo = _socket.getaddrinfo
def _sni_getaddrinfo(host, *a, **k):
return _orig_getaddrinfo(RVS_HOST if host == RVS_SNI else host, *a, **k)
_socket.getaddrinfo = _sni_getaddrinfo
SATELLITE_ID = (os.environ.get("SATELLITE_ID") or _default_id()).strip() SATELLITE_ID = (os.environ.get("SATELLITE_ID") or _default_id()).strip()
SATELLITE_LOCATION = (os.environ.get("SATELLITE_LOCATION") or SATELLITE_ID).strip() SATELLITE_LOCATION = (os.environ.get("SATELLITE_LOCATION") or SATELLITE_ID).strip()
@@ -1316,15 +1334,26 @@ class Satellite:
_creds_load() _creds_load()
asyncio.create_task(self._periodic_scan()) asyncio.create_task(self._periodic_scan())
backoff = 1 backoff = 1
# use_tls kann bei Fehlschlag einmal auf ws:// fallen (RVS_TLS_FALLBACK),
# danach wieder zurueck auf RVS_TLS (kein Sticky-Fallback). Bei TLS+RVS_SNI
# nutzt die URI den HOSTNAMEN (Host-Header/SNI/Cert), getaddrinfo mappt ihn
# auf die IP in RVS_HOST.
use_tls = RVS_TLS
tls_fallback_tried = False
connect_kwargs = {"max_size": 8 * 1024 * 1024,
"ping_interval": 20, "ping_timeout": 20}
while True: while True:
proto = "wss" if RVS_TLS else "ws" proto = "wss" if use_tls else "ws"
url = f"{proto}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}" uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{proto}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
fallback = False
try: try:
logger.info("Verbinde mit RVS %s://%s:%s", proto, RVS_HOST, RVS_PORT) logger.info("Verbinde mit RVS %s://%s:%s%s", proto, uri_host, RVS_PORT,
async with websockets.connect(url, max_size=8 * 1024 * 1024, f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
ping_interval=20, ping_timeout=20) as ws: async with websockets.connect(url, **connect_kwargs) as ws:
self.ws = ws self.ws = ws
backoff = 1 backoff = 1
tls_fallback_tried = False
await self._hello(log=True) await self._hello(log=True)
hb = asyncio.create_task(self._heartbeat()) hb = asyncio.create_task(self._heartbeat())
try: try:
@@ -1334,10 +1363,19 @@ class Satellite:
hb.cancel() hb.cancel()
except Exception as exc: except Exception as exc:
logger.warning("RVS-Verbindung verloren: %s", exc) logger.warning("RVS-Verbindung verloren: %s", exc)
if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried:
logger.info("TLS fehlgeschlagen — Fallback auf ws://")
use_tls = False
tls_fallback_tried = True
fallback = True
finally: finally:
self.ws = None self.ws = None
if fallback:
continue
await asyncio.sleep(backoff) await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30) backoff = min(backoff * 2, 30)
use_tls = RVS_TLS
tls_fallback_tried = False
def main() -> None: def main() -> None:
+7
View File
@@ -43,6 +43,13 @@ RVS_PORT=444
RVS_TLS=true RVS_TLS=true
RVS_TLS_FALLBACK=true RVS_TLS_FALLBACK=true
RVS_TOKEN=dein_token_hier RVS_TOKEN=dein_token_hier
# RVS_SNI: nur noetig, wenn die Box im SELBEN Netz wie der RVS steht und direkt
# auf dessen interne IP verbinden soll (kein NAT-Hairpin ueber den externen
# Hostnamen). Dann RVS_HOST=<interne-ip> und hier den Zertifikats-/Hostnamen, fuer
# den Caddy sein Cert hat. Zuhause / normal: leer lassen.
# RVS_HOST=10.0.0.2
# RVS_SNI=example.com
RVS_SNI=
# ─── Optional ───────────────────────────────────── # ─── Optional ─────────────────────────────────────
# HF_TOKEN= # nur falls ein HF-gated Modell (z.B. Voxtral) geladen wird # HF_TOKEN= # nur falls ein HF-gated Modell (z.B. Voxtral) geladen wird
+4
View File
@@ -58,6 +58,10 @@ GPU-Test + Stack-Start.
24-GB-Karte: `docker compose stop whisper-bridge && docker compose --profile voxtral up -d --build`. 24-GB-Karte: `docker compose stop whisper-bridge && docker compose --profile voxtral up -d --build`.
- **Erster Start lädt Modelle** (mehrere GB via HuggingFace nach `xtts/hf-cache` - **Erster Start lädt Modelle** (mehrere GB via HuggingFace nach `xtts/hf-cache`
+ `xtts/models`) — genug Platz (1 TB NVMe ✓) und etwas Geduld. + `xtts/models`) — genug Platz (1 TB NVMe ✓) und etwas Geduld.
- **Box im selben Netz wie der RVS** (z.B. Rechenzentrum, direkt auf die interne
IP statt NAT-Hairpin): in `xtts/.env` `RVS_HOST=<interne-ip>` + **`RVS_SNI=<zert-name>`**
(Name, für den das Caddy-Zert gilt). Ohne das scheitert TLS an `tlsv1 alert
internal error`. Zuhause / normal: `RVS_SNI` leer lassen.
## Verifizieren ## Verifizieren
+20 -3
View File
@@ -51,6 +51,21 @@ RVS_PORT = int(os.getenv("RVS_PORT", "443"))
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true" RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true" RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip() RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
# In-Process-DNS-Override: wenn RVS_SNI gesetzt ist, verbindet die URI ueber den
# HOSTNAMEN (Host-Header + SNI + Cert stimmen), waehrend getaddrinfo den Namen auf
# die echte IP in RVS_HOST aufloest. Versionsunabhaengig — host=/port= kollidiert
# in der Legacy-websockets-API mit dem aus der URI abgeleiteten Host.
if RVS_TLS and RVS_SNI:
import socket as _socket
_orig_getaddrinfo = _socket.getaddrinfo
def _sni_getaddrinfo(host, *a, **k):
return _orig_getaddrinfo(RVS_HOST if host == RVS_SNI else host, *a, **k)
_socket.getaddrinfo = _sni_getaddrinfo
# F5-TTS Konfiguration # F5-TTS Konfiguration
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
@@ -877,12 +892,14 @@ async def run_loop(runner: F5Runner) -> None:
while True: while True:
scheme = "wss" if use_tls else "ws" scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}" uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{scheme}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 50 * 1024 * 1024}
try: try:
logger.info("Verbinde zu RVS: %s", masked) logger.info("Verbinde zu RVS: %s%s", masked, f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, ping_interval=20, ping_timeout=10, max_size=50 * 1024 * 1024) as ws: async with websockets.connect(url, **connect_kwargs) as ws:
logger.info("RVS verbunden") logger.info("RVS verbunden")
retry_s = 2 retry_s = 2
tls_fallback_tried = False tls_fallback_tried = False
+21 -5
View File
@@ -43,6 +43,21 @@ RVS_PORT = os.getenv("RVS_PORT", "443").strip()
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true" RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true" RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip() RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
# In-Process-DNS-Override: wenn RVS_SNI gesetzt ist, verbindet die URI ueber den
# HOSTNAMEN (Host-Header + SNI + Cert stimmen), waehrend getaddrinfo den Namen auf
# die echte IP in RVS_HOST aufloest. Versionsunabhaengig — host=/port= kollidiert
# in der Legacy-websockets-API mit dem aus der URI abgeleiteten Host.
if RVS_TLS and RVS_SNI:
import socket as _socket
_orig_getaddrinfo = _socket.getaddrinfo
def _sni_getaddrinfo(host, *a, **k):
return _orig_getaddrinfo(RVS_HOST if host == RVS_SNI else host, *a, **k)
_socket.getaddrinfo = _sni_getaddrinfo
LLAMA_URL = os.getenv("LLAMA_URL", "http://llama:8081").rstrip("/") LLAMA_URL = os.getenv("LLAMA_URL", "http://llama:8081").rstrip("/")
LLM_MODEL = os.getenv("LLM_MODEL", "qwen3-8b") LLM_MODEL = os.getenv("LLM_MODEL", "qwen3-8b")
@@ -417,13 +432,14 @@ async def _run() -> None:
while True: while True:
scheme = "wss" if use_tls else "ws" scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}" uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{scheme}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 16 * 1024 * 1024}
try: try:
logger.info("Verbinde zu RVS: %s (llama=%s)", masked, LLAMA_URL) logger.info("Verbinde zu RVS: %s%s (llama=%s)", masked,
async with websockets.connect( f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "", LLAMA_URL)
url, ping_interval=20, ping_timeout=10, max_size=16 * 1024 * 1024 async with websockets.connect(url, **connect_kwargs) as ws:
) as ws:
logger.info("RVS verbunden — llm-adapter online") logger.info("RVS verbunden — llm-adapter online")
retry_s = 2 retry_s = 2
tls_fallback_tried = False tls_fallback_tried = False
+20 -4
View File
@@ -53,6 +53,21 @@ RVS_PORT = int(os.getenv("RVS_PORT", "443"))
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true" RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true" RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip() RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
# In-Process-DNS-Override: wenn RVS_SNI gesetzt ist, verbindet die URI ueber den
# HOSTNAMEN (Host-Header + SNI + Cert stimmen), waehrend getaddrinfo den Namen auf
# die echte IP in RVS_HOST aufloest. Versionsunabhaengig — host=/port= kollidiert
# in der Legacy-websockets-API mit dem aus der URI abgeleiteten Host.
if RVS_TLS and RVS_SNI:
import socket as _socket
_orig_getaddrinfo = _socket.getaddrinfo
def _sni_getaddrinfo(host, *a, **k):
return _orig_getaddrinfo(RVS_HOST if host == RVS_SNI else host, *a, **k)
_socket.getaddrinfo = _sni_getaddrinfo
VOXTRAL_MODEL = os.getenv("VOXTRAL_MODEL", "mistralai/Voxtral-Mini-3B-2507") VOXTRAL_MODEL = os.getenv("VOXTRAL_MODEL", "mistralai/Voxtral-Mini-3B-2507")
VOXTRAL_LANGUAGE = os.getenv("VOXTRAL_LANGUAGE", "de") VOXTRAL_LANGUAGE = os.getenv("VOXTRAL_LANGUAGE", "de")
@@ -742,12 +757,13 @@ async def run_loop(sessions: SessionManager) -> None:
tls_fallback_tried = False tls_fallback_tried = False
while True: while True:
scheme = "wss" if use_tls else "ws" scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}" uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{scheme}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 50 * 1024 * 1024}
try: try:
logger.info("Verbinde zu RVS: %s", masked) logger.info("Verbinde zu RVS: %s%s", masked, f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, ping_interval=20, ping_timeout=10, async with websockets.connect(url, **connect_kwargs) as ws:
max_size=50 * 1024 * 1024) as ws:
logger.info("RVS verbunden") logger.info("RVS verbunden")
retry_s = 2 retry_s = 2
tls_fallback_tried = False tls_fallback_tried = False
+20 -3
View File
@@ -53,6 +53,21 @@ RVS_PORT = int(os.getenv("RVS_PORT", "443"))
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true" RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true" RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip() RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
# In-Process-DNS-Override: wenn RVS_SNI gesetzt ist, verbindet die URI ueber den
# HOSTNAMEN (Host-Header + SNI + Cert stimmen), waehrend getaddrinfo den Namen auf
# die echte IP in RVS_HOST aufloest. Versionsunabhaengig — host=/port= kollidiert
# in der Legacy-websockets-API mit dem aus der URI abgeleiteten Host.
if RVS_TLS and RVS_SNI:
import socket as _socket
_orig_getaddrinfo = _socket.getaddrinfo
def _sni_getaddrinfo(host, *a, **k):
return _orig_getaddrinfo(RVS_HOST if host == RVS_SNI else host, *a, **k)
_socket.getaddrinfo = _sni_getaddrinfo
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small") WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small")
WHISPER_DEVICE = os.getenv("WHISPER_DEVICE", "cuda") WHISPER_DEVICE = os.getenv("WHISPER_DEVICE", "cuda")
@@ -874,11 +889,13 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
while True: while True:
scheme = "wss" if use_tls else "ws" scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}" uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{scheme}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 50 * 1024 * 1024}
try: try:
logger.info("Verbinde zu RVS: %s", masked) logger.info("Verbinde zu RVS: %s%s", masked, f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, ping_interval=20, ping_timeout=10, max_size=50 * 1024 * 1024) as ws: async with websockets.connect(url, **connect_kwargs) as ws:
logger.info("RVS verbunden") logger.info("RVS verbunden")
retry_s = 2 retry_s = 2
tls_fallback_tried = False tls_fallback_tried = False