Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29bf3370b4 | ||
|
|
6b5d847615 | ||
|
|
145c64e67f | ||
|
|
6cb29a28ce | ||
|
|
df5cd3be99 | ||
|
|
4125ab8160 | ||
|
|
edd0b4823f | ||
|
|
d4e3a73e89 | ||
|
|
9d93c3fe3d | ||
|
|
39f7947da9 | ||
|
|
855cc0333e | ||
|
|
2cac1cad0a |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(ssh root@10.0.0.1 \"ls -la /root/ARIA-AGENT/aria-shared/logs/\")"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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/",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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() {
|
||||||
|
|||||||
@@ -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() }));
|
||||||
|
|||||||
@@ -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,5 +1,6 @@
|
|||||||
.env
|
.env
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
.buildenv/
|
||||||
*.spec
|
*.spec
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
+54
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
Executable
+25
@@ -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"
|
||||||
@@ -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:
|
||||||
|
|||||||
Executable
+75
@@ -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)"
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user