ARIA-AGENT/diagnostic/server.js

1700 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
/**
* ARIA Diagnostic Server
*
* Leichtgewichtiges Diagnose-Tool:
* - Verbindet sich direkt mit OpenClaw Gateway (ws://127.0.0.1:18789)
* - Fuehrt den vollstaendigen Handshake durch
* - Sendet Testnachrichten und zeigt Antworten
* - Zeigt Verbindungsstatus aller Komponenten
*
* Laeuft im selben Netzwerk wie aria-core (network_mode: service:aria)
*/
const http = require("http");
const { WebSocket, WebSocketServer } = require("ws");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
// ── Konfiguration ───────────────────────────────────────
const HTTP_PORT = parseInt(process.env.DIAG_PORT || "3001", 10);
const GATEWAY_URL = process.env.ARIA_CORE_WS || "ws://127.0.0.1:18789";
const GATEWAY_TOKEN = process.env.ARIA_AUTH_TOKEN || "";
const RVS_HOST = process.env.RVS_HOST || "";
const RVS_PORT = process.env.RVS_PORT || "443";
const RVS_TLS = process.env.RVS_TLS || "true";
const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true";
const RVS_TOKEN = process.env.RVS_TOKEN || "";
const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456";
// ── State ───────────────────────────────────────────────
const state = {
gateway: { status: "disconnected", lastError: null, handshakeOk: false },
rvs: { status: "disconnected", lastError: null },
proxy: { status: "unknown", lastError: null },
};
let activeSessionKey = "aria-diag-v3";
const logs = [];
let gatewayWs = null;
let rvsWs = null;
let reqIdCounter = 0;
const browserClients = new Set();
// ── Pipeline Tracking ──────────────────────────────────
let pipelineActive = false;
let pipelineStartTime = 0;
function plog(message, level) {
const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : "";
const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` };
logs.push(entry);
if (logs.length > 500) logs.shift();
console.log(`[PIPELINE] ${entry.message}`);
broadcast({ type: "log", entry });
}
let pipelineTimeout = null;
function pipelineStart(method, text) {
// Falls noch eine Pipeline laeuft, beenden
if (pipelineActive) pipelineEnd(false, "Abgebrochen (neue Nachricht)");
pipelineActive = true;
pipelineStartTime = Date.now();
if (pipelineTimeout) clearTimeout(pipelineTimeout);
pipelineTimeout = setTimeout(() => {
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 60s");
}, 60000);
plog(`━━━ Pipeline Start: ${method} ━━━`);
plog(`Nachricht: "${text}"`);
}
function pipelineEnd(ok, detail) {
if (!pipelineActive) return;
if (pipelineTimeout) { clearTimeout(pipelineTimeout); pipelineTimeout = null; }
const elapsed = Date.now() - pipelineStartTime;
if (ok) {
plog(`>>> Fertig (${elapsed}ms): ${detail}`);
} else {
plog(`>>> FEHLER (${elapsed}ms): ${detail}`, "error");
}
plog(`━━━ Pipeline Ende ━━━`);
pipelineActive = false;
}
// ── Auto-Restart bei Netzwerk-Namespace-Verlust ──────
// Bei network_mode: "service:aria" verliert dieser Container
// den Netzwerkzugriff wenn aria-core neustartet.
// Nach MAX_GATEWAY_FAILURES aufeinanderfolgenden Fehlern → process.exit
// Docker restart: unless-stopped startet uns mit neuem Namespace neu.
const MAX_GATEWAY_FAILURES = 6; // 6 × 5s = 30s
let gatewayFailCount = 0;
function checkGatewayHealth() {
if (state.gateway.status === "connected") {
gatewayFailCount = 0;
return;
}
gatewayFailCount++;
if (gatewayFailCount >= MAX_GATEWAY_FAILURES) {
log("error", "server", `Gateway ${MAX_GATEWAY_FAILURES}x nicht erreichbar — Neustart (Netzwerk-Namespace veraltet?)`);
// Kurze Verzoegerung damit die Log-Nachricht noch gesendet wird
setTimeout(() => process.exit(1), 500);
}
}
function nextReqId() {
return `diag-${++reqIdCounter}`;
}
function log(level, source, message) {
const entry = { ts: new Date().toISOString(), level, source, message };
logs.push(entry);
if (logs.length > 500) logs.shift();
console.log(`[${entry.ts}] [${level}] [${source}] ${message}`);
// An alle Browser-Clients senden
broadcast({ type: "log", entry });
}
function broadcast(msg) {
const data = JSON.stringify(msg);
for (const client of browserClients) {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
}
}
function broadcastState() {
broadcast({ type: "state", state });
}
// ── OpenClaw Gateway Verbindung ─────────────────────────
async function connectGateway() {
if (gatewayWs) {
try { gatewayWs.close(); } catch (_) {}
gatewayWs = null;
}
state.gateway.status = "connecting";
state.gateway.handshakeOk = false;
broadcastState();
log("info", "gateway", `Verbinde: ${GATEWAY_URL}`);
try {
const ws = new WebSocket(GATEWAY_URL);
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error("Verbindungs-Timeout (10s)"));
}, 10000);
ws.on("open", () => {
clearTimeout(timeout);
resolve();
});
ws.on("error", (err) => {
clearTimeout(timeout);
reject(err);
});
});
log("info", "gateway", "TCP-Verbindung hergestellt — warte auf Challenge");
// Schritt 1: Auf Challenge warten
const challengeRaw = await waitForMessage(ws, 10000);
const challenge = JSON.parse(challengeRaw);
if (challenge.type !== "event" || challenge.event !== "connect.challenge") {
throw new Error(`Unerwartete erste Nachricht: ${challengeRaw.slice(0, 200)}`);
}
const nonce = challenge.payload?.nonce || "";
log("info", "gateway", `Challenge empfangen (nonce: ${nonce.slice(0, 8)}...)`);
// Schritt 2: Connect Request senden
const connectReq = {
type: "req",
id: nextReqId(),
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "cli",
version: "0.0.1",
platform: "linux",
mode: "cli",
},
role: "operator",
scopes: ["operator.read", "operator.write"],
caps: [],
commands: [],
permissions: {},
auth: GATEWAY_TOKEN ? { token: GATEWAY_TOKEN } : {},
locale: "de-DE",
userAgent: "aria-diagnostic/0.0.1",
},
};
ws.send(JSON.stringify(connectReq));
log("info", "gateway", "Connect-Request gesendet");
// Schritt 3: hello-ok warten
const responseRaw = await waitForMessage(ws, 10000);
const response = JSON.parse(responseRaw);
if (response.type === "res" && response.ok) {
log("info", "gateway", "Handshake erfolgreich — hello-ok!");
state.gateway.status = "connected";
state.gateway.handshakeOk = true;
state.gateway.lastError = null;
gatewayFailCount = 0;
} else {
const error = typeof response.error === "string"
? response.error
: JSON.stringify(response.error || response).slice(0, 300);
throw new Error(`Handshake fehlgeschlagen: ${error}`);
}
gatewayWs = ws;
broadcastState();
// Nachrichten-Loop — RAW logging fuer Debugging
ws.on("message", (raw) => {
try {
const rawStr = raw.toString();
log("debug", "gateway", `RAW <<< ${rawStr.slice(0, 300)}`);
const msg = JSON.parse(rawStr);
handleGatewayMessage(msg);
} catch (err) {
log("error", "gateway", `Parse-Fehler: ${err.message}`);
}
});
ws.on("close", (code, reason) => {
log("warn", "gateway", `Verbindung geschlossen (Code: ${code}, Reason: ${reason || "-"})`);
state.gateway.status = "disconnected";
state.gateway.handshakeOk = false;
gatewayWs = null;
broadcastState();
checkGatewayHealth();
// Auto-Reconnect nach 5s
setTimeout(connectGateway, 5000);
});
ws.on("error", (err) => {
log("error", "gateway", `WebSocket-Fehler: ${err.message}`);
state.gateway.lastError = err.message;
broadcastState();
});
} catch (err) {
log("error", "gateway", `Fehler: ${err.message}`);
state.gateway.status = "error";
state.gateway.lastError = err.message;
state.gateway.handshakeOk = false;
gatewayWs = null;
broadcastState();
checkGatewayHealth();
// Retry nach 5s
setTimeout(connectGateway, 5000);
}
}
// Extrahiert Text aus OpenClaw chat-Event message.content Array
function extractChatText(payload) {
try {
const content = payload.message?.content;
if (Array.isArray(content)) {
return content
.filter(c => c.type === "text")
.map(c => c.text || "")
.join("");
}
if (typeof payload.message === "string") return payload.message;
return payload.text || "";
} catch { return ""; }
}
// Deduplizierung: nur ein chat_final pro runId broadcasten
const seenFinalRuns = new Set();
function handleGatewayMessage(msg) {
if (msg.type === "res") {
const status = msg.ok ? "OK" : `FEHLER: ${JSON.stringify(msg.error).slice(0, 100)}`;
log("info", "gateway", `Response [${msg.id}]: ${status}`);
if (pipelineActive) {
if (msg.ok) plog(`Gateway ACK [${msg.id}] — Nachricht angenommen`);
else plog(`Gateway NACK [${msg.id}] — ${JSON.stringify(msg.error).slice(0, 100)}`, "error");
}
broadcast({ type: "response", msg });
return;
}
if (msg.type === "event") {
const event = msg.event || "?";
const payload = msg.payload || {};
// ── agent Events: Streaming-Deltas vom LLM ──
if (event === "agent") {
const data = payload.data || {};
const delta = data.delta || "";
if (delta && payload.stream === "assistant") {
broadcast({ type: "chat_delta", delta, payload });
}
// agent Events nicht einzeln loggen (zu viele)
return;
}
// ── chat Events: Snapshots mit state=delta|final ──
if (event === "chat") {
const state = payload.state || "";
const text = extractChatText(payload);
if (state === "final") {
const runId = payload.runId || "";
if (runId && seenFinalRuns.has(runId)) return; // Duplikat
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
broadcast({ type: "chat_final", text, payload });
return;
}
if (state === "delta") {
// Periodischer Snapshot — nicht einzeln loggen
return;
}
if (state === "error") {
const error = payload.error || text || "Unbekannt";
log("error", "gateway", `Chat-Fehler: ${error}`);
if (pipelineActive) pipelineEnd(false, error);
broadcast({ type: "chat_error", error, payload });
return;
}
log("debug", "gateway", `chat state=${state}`);
return;
}
// ── Legacy event names (chat:delta, chat:final, chat:error) ──
if (event === "chat:delta") {
const delta = payload.delta || payload.text || "";
if (delta) broadcast({ type: "chat_delta", delta, payload });
return;
}
if (event === "chat:final") {
const runId = payload.runId || "";
if (runId && seenFinalRuns.has(runId)) return; // Duplikat
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
const text = extractChatText(payload) || payload.text || "";
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
broadcast({ type: "chat_final", text, payload });
return;
}
if (event === "chat:error") {
const error = payload.error || payload.message || "Unbekannt";
log("error", "gateway", `Chat-Fehler: ${error}`);
if (pipelineActive) pipelineEnd(false, error);
broadcast({ type: "chat_error", error, payload });
return;
}
// ── Andere Events (tick, health, presence) ──
if (event === "tick" || event === "health") return; // Noise
log("debug", "gateway", `Event: ${event}`);
}
}
function sendToGateway(text, isPipeline) {
if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) {
log("error", "gateway", "Nicht verbunden — kann nicht senden");
if (isPipeline) pipelineEnd(false, "Gateway nicht verbunden");
return false;
}
const reqId = nextReqId();
const msg = {
type: "req",
id: reqId,
method: "chat.send",
params: {
sessionKey: activeSessionKey,
message: text,
idempotencyKey: crypto.randomUUID(),
},
};
const payload = JSON.stringify(msg);
log("debug", "gateway", `RAW >>> ${payload}`);
gatewayWs.send(payload);
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
return true;
}
// ── RVS Verbindung (optional) ───────────────────────────
function connectRVS(forcePlain) {
if (!RVS_HOST || !RVS_TOKEN) {
log("info", "rvs", "Nicht konfiguriert — ueberspringe");
state.rvs.status = "not_configured";
broadcastState();
return;
}
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws (wenn erlaubt)
const useTls = RVS_TLS === "true" && !forcePlain;
const proto = useTls ? "wss" : "ws";
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
state.rvs.status = "connecting";
broadcastState();
log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);
const ws = new WebSocket(url);
ws.on("open", () => {
log("info", "rvs", `Verbunden (${proto})`);
state.rvs.status = "connected";
state.rvs.lastError = null;
rvsWs = ws;
broadcastState();
});
ws.on("message", (raw) => {
try {
const msg = JSON.parse(raw.toString());
if (msg.type === "chat" && msg.payload) {
const sender = msg.payload.sender || "?";
log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
}
broadcast({ type: "rvs_chat", msg });
} else if (msg.type === "heartbeat") {
// ignorieren
} else {
log("debug", "rvs", `Nachricht: ${JSON.stringify(msg).slice(0, 150)}`);
}
} catch {}
});
ws.on("close", () => {
log("warn", "rvs", "Verbindung geschlossen");
state.rvs.status = "disconnected";
rvsWs = null;
broadcastState();
setTimeout(() => connectRVS(), 5000);
});
ws.on("error", (err) => {
log("error", "rvs", `Fehler (${proto}): ${err.message}`);
state.rvs.lastError = err.message;
broadcastState();
// TLS Fallback: wenn wss fehlschlaegt und Fallback erlaubt → ws versuchen
if (useTls && RVS_TLS_FALLBACK === "true") {
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
ws.removeAllListeners();
try { ws.close(); } catch (_) {}
connectRVS(true);
}
});
}
function sendToRVS(text, isPipeline) {
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) {
log("error", "rvs", "Nicht verbunden");
if (isPipeline) pipelineEnd(false, "RVS nicht verbunden");
return false;
}
rvsWs.send(JSON.stringify({
type: "chat",
payload: { text, sender: "diagnostic" },
timestamp: Date.now(),
}));
log("info", "rvs", `Gesendet via RVS: "${text}"`);
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
return true;
}
// ── Claude Proxy Test ────────────────────────────────────
async function testProxy(prompt) {
state.proxy.status = "testing";
state.proxy.lastError = null;
broadcastState();
log("info", "proxy", `Teste Proxy: ${PROXY_URL}`);
try {
// Schritt 1: Erreichbarkeit pruefen
const healthUrl = `${PROXY_URL}/v1/models`;
log("info", "proxy", `Rufe ab: ${healthUrl}`);
const modelsRes = await fetch(healthUrl, {
headers: { "Authorization": "Bearer not-needed" },
signal: AbortSignal.timeout(10000),
});
if (!modelsRes.ok) {
throw new Error(`Models-Endpoint: HTTP ${modelsRes.status} ${modelsRes.statusText}`);
}
const modelsData = await modelsRes.json();
const models = (modelsData.data || []).map(m => m.id).filter(Boolean);
log("info", "proxy", `Proxy erreichbar — ${models.length} Model(s) verfuegbar`);
// Modellnamen loggen + OpenClaw-Config Hinweis
if (models.length > 0) {
log("info", "proxy", `Modelle: ${models.join(", ")}`);
log("info", "proxy", `Fuer docker-compose.yml (DEFAULT_MODEL): ${models.map(m => m.replace("openai/", "")).join(" | ")}`);
}
// Schritt 1b: Auth-Dateien im Proxy-Container pruefen
try {
const authInfo = await dockerExec("aria-proxy", "echo '--- /root/.config/claude/ ---' && ls -la /root/.config/claude/ 2>&1 && echo '--- /root/.claude/ ---' && ls -la /root/.claude/ 2>&1 && echo '--- Credential-Dateien ---' && find /root/.config/claude /root/.claude -name '*.json' -o -name '*credential*' -o -name '*auth*' -o -name '*token*' 2>/dev/null | head -20");
log("info", "proxy", `Auth-Dateien im Container:\n${authInfo}`);
broadcast({ type: "proxy_auth", info: authInfo });
} catch (authErr) {
log("warn", "proxy", `Auth-Check fehlgeschlagen: ${authErr.message}`);
}
// Schritt 2: Chat Completion testen (kurzer Prompt)
const testPrompt = prompt || "Antworte mit genau einem Wort: Ping";
log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);
const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer not-needed",
},
body: JSON.stringify({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: testPrompt }],
max_tokens: 200,
}),
signal: AbortSignal.timeout(30000),
});
if (!chatRes.ok) {
const errBody = await chatRes.text().catch(() => "");
throw new Error(`Chat-Completion: HTTP ${chatRes.status}${errBody.slice(0, 300)}`);
}
const chatData = await chatRes.json();
const reply = chatData.choices?.[0]?.message?.content || "(leer)";
log("info", "proxy", `Antwort: "${reply.slice(0, 200)}"`);
state.proxy.status = "connected";
state.proxy.lastError = null;
state.proxy.models = models;
broadcastState();
broadcast({ type: "proxy_result", ok: true, reply, models });
} catch (err) {
log("error", "proxy", `Fehler: ${err.message}`);
state.proxy.status = "error";
state.proxy.lastError = err.message;
broadcastState();
broadcast({ type: "proxy_result", ok: false, error: err.message });
}
}
// ── Claude Login im Proxy-Container ─────────────────────
// ── Interaktives Terminal (xterm.js ↔ Docker Exec) ──────
const net = require("net");
function attachTerminal(clientWs, containerName, cmd) {
const createBody = JSON.stringify({
AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true,
Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd],
});
const createReq = http.request({
socketPath: "/var/run/docker.sock",
path: `/containers/${containerName}/exec`,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) },
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
if (res.statusCode !== 201) {
clientWs.send(JSON.stringify({ type: "term_error", error: `Exec create failed: HTTP ${res.statusCode}` }));
return;
}
const execId = JSON.parse(data).Id;
// Exec starten — raw TCP socket fuer bidirektionalen Stream
// Docker Exec mit Tty macht ein HTTP Upgrade auf raw stream
const startBody = JSON.stringify({ Detach: false, Tty: true });
const sock = net.connect({ path: "/var/run/docker.sock" }, () => {
// Raw HTTP Request senden (Upgrade-artig)
const req = `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${Buffer.byteLength(startBody)}\r\n\r\n${startBody}`;
sock.write(req);
});
let headersParsed = false;
let headerBuf = "";
sock.on("data", (chunk) => {
if (!headersParsed) {
// HTTP Response Header parsen
headerBuf += chunk.toString("utf-8");
const headerEnd = headerBuf.indexOf("\r\n\r\n");
if (headerEnd === -1) return; // Noch nicht komplett
const headers = headerBuf.slice(0, headerEnd);
const remaining = chunk.slice(chunk.length - (headerBuf.length - headerEnd - 4));
headersParsed = true;
if (!headers.includes("200") && !headers.includes("101")) {
clientWs.send(JSON.stringify({ type: "term_error", error: `Exec start failed: ${headers.split("\r\n")[0]}` }));
sock.end();
return;
}
log("info", "proxy", "Terminal-Session gestartet");
clientWs.send(JSON.stringify({ type: "term_ready" }));
clientWs._termSock = sock;
// Verbleibende Daten nach dem Header
if (remaining.length > 0) {
clientWs.send(JSON.stringify({ type: "term_data", data: remaining.toString("base64") }));
}
return;
}
// Tty=true: Rohdaten direkt durchreichen
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ type: "term_data", data: chunk.toString("base64") }));
}
});
sock.on("end", () => {
log("info", "proxy", "Terminal-Session beendet");
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ type: "term_exit" }));
}
clientWs._termSock = null;
setTimeout(() => checkProxyAuth(), 1000);
});
sock.on("error", (err) => {
log("error", "proxy", `Terminal-Socket-Fehler: ${err.message}`);
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ type: "term_error", error: err.message }));
}
});
// Wenn Browser-Client disconnected, Socket schliessen
clientWs.on("close", () => {
if (sock && !sock.destroyed) sock.end();
});
});
});
createReq.on("error", (err) => {
clientWs.send(JSON.stringify({ type: "term_error", error: err.message }));
});
createReq.write(createBody);
createReq.end();
}
function handleTermInput(clientWs, data) {
if (clientWs._termSock && !clientWs._termSock.destroyed) {
clientWs._termSock.write(Buffer.from(data, "base64"));
}
}
// Credentials manuell einfuegen (von einem Rechner wo Claude eingeloggt ist)
async function writeProxyCredentials(credentialsJson) {
try {
// Validieren
const parsed = JSON.parse(credentialsJson);
if (!parsed.claudeAiOauth && !parsed.oauth) {
throw new Error("Ungueltig: Kein OAuth-Objekt gefunden. Erwartet 'claudeAiOauth' oder 'oauth' Key.");
}
log("info", "proxy", "Schreibe Credentials in Proxy-Container...");
// Escaped fuer Shell — Einfache Anfuehrungszeichen im JSON escapen
const escaped = credentialsJson.replace(/'/g, "'\\''");
// In beide moegliche Speicherorte schreiben
await dockerExec("aria-proxy", `mkdir -p /root/.config/claude && echo '${escaped}' > /root/.config/claude/.credentials.json && mkdir -p /root/.claude && echo '${escaped}' > /root/.claude/credentials.json`);
log("info", "proxy", "Credentials geschrieben!");
broadcast({ type: "login_status", status: "done" });
broadcast({ type: "login_output", text: "Credentials erfolgreich geschrieben! Proxy muss neu gestartet werden." });
// Auth pruefen
setTimeout(() => checkProxyAuth(), 500);
} catch (err) {
log("error", "proxy", `Credentials schreiben fehlgeschlagen: ${err.message}`);
broadcast({ type: "login_status", status: "error", error: err.message });
}
}
async function checkProxyAuth() {
try {
log("info", "proxy", "Pruefe Auth-Dateien im Proxy-Container...");
// Breit suchen: Claude Code speichert Credentials je nach Version an verschiedenen Orten
const authInfo = await dockerExec("aria-proxy", `
echo '=== /root/.config/claude/ ===' &&
ls -la /root/.config/claude/ 2>&1 &&
echo '' &&
echo '=== /root/.claude/ ===' &&
ls -la /root/.claude/ 2>&1 &&
echo '' &&
echo '=== /root/.claude/auth/ ===' &&
ls -la /root/.claude/auth/ 2>&1 &&
echo '' &&
echo '=== Credentials-Dateien (rekursiv) ===' &&
find /root/.config/claude /root/.claude -name '*.json' -o -name '*credential*' -o -name '*auth*' -o -name '*token*' -o -name '*oauth*' -o -name '*session*' 2>/dev/null | head -20 &&
echo '' &&
echo '=== .credentials.json ===' &&
cat /root/.config/claude/.credentials.json 2>/dev/null || echo '(nicht in .config/claude/)' &&
echo '' &&
echo '=== /root/.claude/credentials.json ===' &&
cat /root/.claude/credentials.json 2>/dev/null || echo '(nicht in .claude/)' &&
echo '' &&
echo '=== /root/.claude/auth/*.json ===' &&
cat /root/.claude/auth/*.json 2>/dev/null || echo '(keine auth/*.json)'
`.trim());
log("info", "proxy", `Auth-Dateien:\n${authInfo}`);
broadcast({ type: "proxy_auth", info: authInfo });
} catch (err) {
log("error", "proxy", `Auth-Check fehlgeschlagen: ${err.message}`);
broadcast({ type: "proxy_auth", info: null, error: err.message });
}
}
// ── OpenClaw Agent-Auth pruefen ──────────────────────────
async function checkCoreAuth() {
try {
log("info", "gateway", "Pruefe OpenClaw Agent-Konfiguration...");
const info = await dockerExec("aria-core", `
echo '=== Agent-Verzeichnis ===' &&
ls -la /home/node/.openclaw/agents/main/agent/ 2>&1 &&
echo '' &&
echo '=== auth-profiles.json ===' &&
cat /home/node/.openclaw/agents/main/agent/auth-profiles.json 2>/dev/null || echo '(nicht vorhanden)' &&
echo '' &&
echo '=== Umgebungsvariablen ===' &&
echo "OPENAI_BASE_URL=$OPENAI_BASE_URL" &&
echo "OPENAI_API_KEY=$(echo $OPENAI_API_KEY | head -c 15)..." &&
echo "DEFAULT_MODEL=$DEFAULT_MODEL" &&
echo '' &&
echo '=== OpenClaw Version ===' &&
openclaw --version 2>/dev/null || echo '(openclaw CLI nicht gefunden)' &&
echo '' &&
echo '=== Agents Liste ===' &&
openclaw agents list 2>/dev/null || echo '(Befehl fehlgeschlagen)'
`.trim());
log("info", "gateway", `OpenClaw Config:\n${info}`);
broadcast({ type: "core_auth", info });
} catch (err) {
log("error", "gateway", `Core-Auth-Check fehlgeschlagen: ${err.message}`);
broadcast({ type: "core_auth", info: null, error: err.message });
}
}
// ── Docker Container Logs ────────────────────────────────
const CONTAINER_MAP = {
gateway: "aria-core",
proxy: "aria-proxy",
bridge: "aria-bridge",
};
function fetchDockerLogs(tab, tail) {
const containerName = CONTAINER_MAP[tab];
if (!containerName) return;
const lines = parseInt(tail, 10) || 100;
const dockerPath = `/containers/${containerName}/logs?stdout=true&stderr=true&tail=${lines}&timestamps=true`;
return new Promise((resolve, reject) => {
const req = http.request(
{ socketPath: "/var/run/docker.sock", path: dockerPath, method: "GET" },
(res) => {
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => {
// Docker log stream hat 8-byte Header pro Frame (stream type + size)
const raw = Buffer.concat(chunks);
const logLines = [];
let offset = 0;
while (offset < raw.length) {
if (offset + 8 > raw.length) break;
const size = raw.readUInt32BE(offset + 4);
if (offset + 8 + size > raw.length) break;
const line = raw.slice(offset + 8, offset + 8 + size).toString("utf-8").trimEnd();
if (line) logLines.push(line);
offset += 8 + size;
}
resolve(logLines);
});
}
);
req.on("error", (err) => reject(err));
req.end();
});
}
async function handleDockerLogs(ws, tab, tail) {
const containerName = CONTAINER_MAP[tab];
if (!containerName) {
ws.send(JSON.stringify({ type: "docker_logs", tab, error: `Unbekannter Tab: ${tab}` }));
return;
}
try {
log("info", "server", `Lade Docker-Logs: ${containerName} (tail ${tail || 100})`);
const lines = await fetchDockerLogs(tab, tail);
ws.send(JSON.stringify({ type: "docker_logs", tab, container: containerName, lines }));
} catch (err) {
log("error", "server", `Docker-Logs Fehler (${containerName}): ${err.message}`);
ws.send(JSON.stringify({ type: "docker_logs", tab, error: err.message }));
}
}
// ── Docker Exec (Befehl in Container ausfuehren) ────────
function dockerExec(containerName, cmd) {
return new Promise((resolve, reject) => {
const createBody = JSON.stringify({
AttachStdout: true, AttachStderr: true,
Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd],
});
const createReq = http.request({
socketPath: "/var/run/docker.sock",
path: `/containers/${containerName}/exec`,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) },
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
if (res.statusCode !== 201) return reject(new Error(`Exec create: HTTP ${res.statusCode}${data.slice(0, 200)}`));
const execId = JSON.parse(data).Id;
// Exec starten
const startBody = JSON.stringify({ Detach: false });
const startReq = http.request({
socketPath: "/var/run/docker.sock",
path: `/exec/${execId}/start`,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(startBody) },
}, (sRes) => {
const chunks = [];
sRes.on("data", (c) => chunks.push(c));
sRes.on("end", () => {
// Docker multiplexed stream: 8-byte header pro Frame
const raw = Buffer.concat(chunks);
const lines = [];
let offset = 0;
while (offset < raw.length) {
if (offset + 8 > raw.length) { lines.push(raw.slice(offset).toString("utf-8").trim()); break; }
const size = raw.readUInt32BE(offset + 4);
if (size === 0 || offset + 8 + size > raw.length) { lines.push(raw.slice(offset).toString("utf-8").trim()); break; }
const line = raw.slice(offset + 8, offset + 8 + size).toString("utf-8").trimEnd();
if (line) lines.push(line);
offset += 8 + size;
}
resolve(lines.join("\n"));
});
});
startReq.on("error", reject);
startReq.write(startBody);
startReq.end();
});
});
createReq.on("error", reject);
createReq.write(createBody);
createReq.end();
});
}
// ── Hilfsfunktionen ─────────────────────────────────────
function waitForMessage(ws, timeoutMs) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Timeout (${timeoutMs}ms)`));
}, timeoutMs);
ws.once("message", (data) => {
clearTimeout(timeout);
resolve(data.toString());
});
});
}
// ── HTTP Server + WebSocket fuer Browser ────────────────
const htmlPath = path.join(__dirname, "index.html");
const server = http.createServer((req, res) => {
if (req.url === "/" || req.url === "/index.html") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(fs.readFileSync(htmlPath, "utf-8"));
} else if (req.url === "/api/state") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ state, logs: logs.slice(-100) }));
} else {
res.writeHead(404);
res.end("Not Found");
}
});
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
browserClients.add(ws);
// Initialen State + letzte Logs senden
ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) }));
ws.on("message", (raw) => {
try {
const msg = JSON.parse(raw.toString());
if (msg.action === "test_gateway") {
pipelineStart("Gateway", msg.text || "aria lebst du noch?");
sendToGateway(msg.text || "aria lebst du noch?", true);
} else if (msg.action === "test_rvs") {
pipelineStart("RVS", msg.text || "aria lebst du noch?");
sendToRVS(msg.text || "aria lebst du noch?", true);
} else if (msg.action === "reconnect_gateway") {
connectGateway();
} else if (msg.action === "reconnect_rvs") {
connectRVS();
} else if (msg.action === "test_proxy") {
testProxy(msg.text);
} else if (msg.action === "check_proxy_auth") {
checkProxyAuth();
} else if (msg.action === "proxy_login") {
attachTerminal(ws, "aria-proxy", "claude login");
} else if (msg.action === "core_terminal") {
// Interaktive Shell in aria-core (fuer openclaw agents, etc.)
attachTerminal(ws, "aria-core", msg.cmd || "sh");
} else if (msg.action === "check_core_auth") {
checkCoreAuth();
} else if (msg.action === "term_input") {
handleTermInput(ws, msg.data);
} else if (msg.action === "write_credentials") {
writeProxyCredentials(msg.credentials);
} else if (msg.action === "docker_logs") {
handleDockerLogs(ws, msg.tab, msg.tail);
} else if (msg.action === "live_ssh_start") {
startLiveSSH(ws);
} else if (msg.action === "live_ssh_input") {
if (ws._sshSock) ws._sshSock.write(msg.data);
} else if (msg.action === "live_ssh_close") {
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
} else if (msg.action === "check_desktop") {
checkDesktopAvailable(ws);
} else if (msg.action === "list_sessions") {
handleListSessions(ws);
} else if (msg.action === "read_session") {
handleReadSession(ws, msg.sessionPath);
} else if (msg.action === "delete_session") {
handleDeleteSession(ws, msg.sessionPath);
} else if (msg.action === "set_active_session") {
handleSetActiveSession(ws, msg.sessionKey);
} else if (msg.action === "get_active_session") {
ws.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
} else if (msg.action === "create_session") {
handleCreateSession(ws, msg.sessionName);
} else if (msg.action === "restart_session") {
handleRestartSession(ws);
} else if (msg.action === "list_brain") {
handleListBrain(ws);
} else if (msg.action === "read_brain_file") {
handleReadBrainFile(ws, msg.filename);
// ── Einstellungen ──
} else if (msg.action === "list_permissions") {
handleListPermissions(ws);
} else if (msg.action === "save_permissions") {
handleSavePermissions(ws, msg.allowedTools);
} else if (msg.action === "get_model") {
handleGetModel(ws);
} else if (msg.action === "set_model") {
handleSetModel(ws, msg.model);
} else if (msg.action === "get_openclaw_config") {
handleGetOpenClawConfig(ws);
}
} catch {}
});
ws.on("close", () => {
browserClients.delete(ws);
});
});
// ── Live SSH ─────────────────────────────────────────────
function startLiveSSH(clientWs) {
// Bestehende Session schliessen
if (clientWs._sshSock) {
try { clientWs._sshSock.end(); } catch (_) {}
clientWs._sshSock = null;
}
const createBody = JSON.stringify({
AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true,
Cmd: ["ssh", "-tt", "aria-wohnung"],
});
const createReq = http.request({
socketPath: "/var/run/docker.sock",
path: "/containers/aria-core/exec",
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) },
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
if (res.statusCode !== 201) {
clientWs.send(JSON.stringify({ type: "live_ssh_error", error: `Exec create failed: HTTP ${res.statusCode}` }));
return;
}
const execId = JSON.parse(data).Id;
const startBody = JSON.stringify({ Detach: false, Tty: true });
const sock = net.connect({ path: "/var/run/docker.sock" }, () => {
const req = `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${Buffer.byteLength(startBody)}\r\n\r\n${startBody}`;
sock.write(req);
});
let headersParsed = false;
let headerBuf = "";
sock.on("data", (chunk) => {
if (!headersParsed) {
headerBuf += chunk.toString("utf-8");
const headerEnd = headerBuf.indexOf("\r\n\r\n");
if (headerEnd === -1) return;
const headers = headerBuf.slice(0, headerEnd);
const remaining = chunk.slice(chunk.length - (headerBuf.length - headerEnd - 4));
headersParsed = true;
if (!headers.includes("200") && !headers.includes("101")) {
clientWs.send(JSON.stringify({ type: "live_ssh_error", error: `SSH start failed` }));
sock.end();
return;
}
log("info", "server", "Live SSH-Session gestartet");
clientWs.send(JSON.stringify({ type: "live_ssh_connected" }));
clientWs._sshSock = sock;
if (remaining.length > 0) {
clientWs.send(JSON.stringify({ type: "live_ssh_data", data: remaining.toString("base64") }));
}
return;
}
// Stream-Daten ans Frontend
try {
clientWs.send(JSON.stringify({ type: "live_ssh_data", data: chunk.toString("base64") }));
} catch (_) { sock.end(); }
});
sock.on("close", () => {
clientWs._sshSock = null;
try { clientWs.send(JSON.stringify({ type: "live_ssh_closed" })); } catch (_) {}
log("info", "server", "Live SSH-Session beendet");
});
sock.on("error", (err) => {
clientWs._sshSock = null;
try { clientWs.send(JSON.stringify({ type: "live_ssh_error", error: err.message })); } catch (_) {}
});
});
});
createReq.on("error", (err) => {
clientWs.send(JSON.stringify({ type: "live_ssh_error", error: `Docker: ${err.message}` }));
});
createReq.end(createBody);
}
function checkDesktopAvailable(clientWs) {
// Pruefen ob VNC auf der VM laeuft (Port 5900/5901)
const checkSock = net.connect({ host: "host.docker.internal", port: 5901 }, () => {
checkSock.end();
clientWs.send(JSON.stringify({
type: "desktop_status",
available: true,
url: `http://${clientWs._socket?.remoteAddress || "localhost"}:6080/vnc.html?autoconnect=true`,
message: "VNC Desktop verfuegbar",
}));
});
checkSock.on("error", () => {
clientWs.send(JSON.stringify({
type: "desktop_status",
available: false,
message: "Kein VNC-Server auf aria-wohnung gefunden (Port 5901)",
}));
});
checkSock.setTimeout(3000, () => {
checkSock.destroy();
clientWs.send(JSON.stringify({
type: "desktop_status",
available: false,
message: "VNC-Server nicht erreichbar (Timeout)",
}));
});
}
// ── Session Viewer ──────────────────────────────────────
const SESSIONS_DIR = "/home/node/.openclaw/agents/main/sessions";
async function handleListSessions(clientWs) {
try {
log("info", "server", "Lade Sessions aus aria-core...");
// sessions.json als Index lesen + Datei-Details holen
const raw = await dockerExec("aria-core", `
cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}' &&
echo '===FILE_DETAILS===' &&
for f in ${SESSIONS_DIR}/*.jsonl; do
[ -f "$f" ] || continue
name=$(basename "$f")
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
size=$(du -h "$f" 2>/dev/null | cut -f1)
modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
echo "FILE:$name|LINES:$lines|SIZE:$size|MODIFIED:$modified"
done
`.trim());
const parts = raw.split("===FILE_DETAILS===");
let sessionsIndex = {};
try { sessionsIndex = JSON.parse(parts[0].trim()); } catch {}
// Datei-Details parsen
const fileDetails = {};
if (parts[1]) {
for (const line of parts[1].trim().split("\n")) {
if (!line.startsWith("FILE:")) continue;
const segs = {};
for (const seg of line.split("|")) {
const idx = seg.indexOf(":");
if (idx > 0) segs[seg.slice(0, idx)] = seg.slice(idx + 1);
}
if (segs.FILE) fileDetails[segs.FILE] = segs;
}
}
// Sessions zusammenbauen: Index + Datei-Info kombinieren
const sessions = [];
// Aus sessions.json die Session-Keys und IDs
const indexEntries = Array.isArray(sessionsIndex) ? sessionsIndex
: Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions
: Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) }));
for (const entry of indexEntries) {
const id = entry.id || entry.sessionId || "";
const rawKey = entry.key || entry.sessionKey || entry.name || id;
// "agent:main:aria-diagnostic" → "aria-diagnostic"
const key = rawKey.replace(/^agent:main:/, "");
const filename = `${id}.jsonl`;
const details = fileDetails[filename] || {};
// updatedAt aus sessions.json (ms) ist genauer als stat
const updatedAt = entry.updatedAt || 0;
const model = entry.model || "";
sessions.push({
path: `${SESSIONS_DIR}/${filename}`,
sessionKey: key,
sessionId: id,
lines: parseInt(details.LINES) || 0,
size: details.SIZE || "?",
modified: updatedAt ? Math.floor(updatedAt / 1000) : (parseInt(details.MODIFIED) || 0),
model,
});
// Aus fileDetails entfernen (fuer Waisen-Check)
delete fileDetails[filename];
}
// Dateien die nicht im Index stehen (Waisen / Reset-Files)
for (const [filename, details] of Object.entries(fileDetails)) {
const id = filename.replace(".jsonl", "");
sessions.push({
path: `${SESSIONS_DIR}/${filename}`,
sessionKey: id.slice(0, 12) + "...",
sessionId: id,
lines: parseInt(details.LINES) || 0,
size: details.SIZE || "?",
modified: parseInt(details.MODIFIED) || 0,
orphan: true,
});
}
sessions.sort((a, b) => b.modified - a.modified);
clientWs.send(JSON.stringify({ type: "sessions_list", sessions }));
log("info", "server", `${sessions.length} Session(s) gefunden`);
} catch (err) {
log("error", "server", `Sessions laden fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "sessions_list", sessions: [], error: err.message }));
}
}
async function handleReadSession(clientWs, sessionPath) {
if (!sessionPath || sessionPath.includes("..")) {
clientWs.send(JSON.stringify({ type: "session_detail", error: "Ungueltiger Pfad" }));
return;
}
try {
// Letzte 100 Zeilen der Session (JSONL)
const raw = await dockerExec("aria-core", `tail -100 '${sessionPath.replace(/'/g, "")}'`);
const messages = [];
for (const line of raw.split("\n")) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
messages.push(obj);
} catch {}
}
clientWs.send(JSON.stringify({ type: "session_detail", path: sessionPath, messages, raw: messages.length === 0 ? raw : undefined }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "session_detail", error: err.message }));
}
}
async function handleDeleteSession(clientWs, sessionPath) {
if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" }));
return;
}
try {
log("warn", "server", `Loesche Session: ${sessionPath}`);
const safePath = sessionPath.replace(/'/g, "");
// Session-ID aus Pfad extrahieren (UUID.jsonl)
const filename = safePath.split("/").pop();
const sessionId = filename.replace(".jsonl", "");
// 1. JSONL-Datei loeschen
await dockerExec("aria-core", `rm -f '${safePath}'`);
// 2. Eintrag aus sessions.json entfernen
try {
const sFile = `${SESSIONS_DIR}/sessions.json`;
const script = [
'const fs=require("fs");',
`const f="${sFile}",sid="${sessionId}";`,
'try{const d=JSON.parse(fs.readFileSync(f,"utf8"));',
'for(const k of Object.keys(d)){const v=d[k];',
'if(v&&(v.sessionId===sid||v.id===sid))delete d[k];}',
'fs.writeFileSync(f,JSON.stringify(d,null,2));}catch(e){}',
].join("");
const b64 = Buffer.from(script).toString("base64");
await dockerExec("aria-core", `echo ${b64} | base64 -d | node`);
} catch (e) {
log("warn", "server", `sessions.json Update fehlgeschlagen: ${e.message}`);
}
clientWs.send(JSON.stringify({ type: "session_deleted", ok: true, path: sessionPath }));
log("info", "server", "Session geloescht");
} catch (err) {
clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: err.message }));
}
}
// ── Session aktivieren ─────────────────────────────────
function handleSetActiveSession(clientWs, sessionKey) {
if (!sessionKey || typeof sessionKey !== "string") {
clientWs.send(JSON.stringify({ type: "active_session", ok: false, error: "Kein sessionKey" }));
return;
}
activeSessionKey = sessionKey;
log("info", "server", `Aktive Session: ${activeSessionKey}`);
// Allen Clients mitteilen
for (const c of browserClients) {
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
}
}
// ── Session erstellen ──────────────────────────────────
async function handleCreateSession(clientWs, sessionName) {
if (!sessionName || typeof sessionName !== "string" || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: "Ungueltiger Name (nur a-z, 0-9, -, _)" }));
return;
}
try {
// Session wird automatisch erstellt wenn man die erste Nachricht sendet
activeSessionKey = sessionName;
log("info", "server", `Neue Session erstellt und aktiviert: ${sessionName}`);
// Allen Clients mitteilen
for (const c of browserClients) {
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
}
clientWs.send(JSON.stringify({ type: "session_created", ok: true, sessionKey: sessionName }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: err.message }));
}
}
// ── Session neu starten (Container Restart) ────────────
async function handleRestartSession(clientWs) {
try {
log("info", "server", "Starte aria-core Container neu (Session Restart)...");
clientWs.send(JSON.stringify({ type: "session_restarted", status: "restarting" }));
// Container neu starten via Docker API
const http = require("http");
await new Promise((resolve, reject) => {
const req = http.request({
socketPath: "/var/run/docker.sock",
path: "/containers/aria-core/restart?t=5",
method: "POST",
}, (res) => {
let body = "";
res.on("data", d => body += d);
res.on("end", () => {
if (res.statusCode === 204 || res.statusCode === 200) resolve();
else reject(new Error(`Restart fehlgeschlagen: HTTP ${res.statusCode} ${body}`));
});
});
req.on("error", reject);
req.end();
});
log("info", "server", "aria-core Container neu gestartet");
// Warten bis Gateway wieder erreichbar (max 30s)
for (let i = 0; i < 15; i++) {
await new Promise(r => setTimeout(r, 2000));
try {
await dockerExec("aria-core", "echo ok");
log("info", "server", "aria-core ist wieder erreichbar");
clientWs.send(JSON.stringify({
type: "session_restarted", status: "ok",
info: "aria-core neu gestartet — Permissions werden bei der naechsten Nachricht geladen",
}));
// Aktive Session beibehalten
for (const c of browserClients) {
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
}
return;
} catch {}
}
clientWs.send(JSON.stringify({ type: "session_restarted", status: "timeout", error: "aria-core antwortet nicht nach 30s" }));
} catch (err) {
log("error", "server", `Session Restart fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "session_restarted", status: "error", error: err.message }));
}
}
// ── Brain Viewer ────────────────────────────────────────
async function handleListBrain(clientWs) {
try {
log("info", "server", "Lade Brain-Dateien...");
const raw = await dockerExec("aria-core", `
for f in /home/node/.openclaw/workspace/memory/*; do
[ -f "$f" ] || continue
name=$(basename "$f")
size=$(du -h "$f" 2>/dev/null | cut -f1)
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
# Frontmatter extrahieren (erste 10 Zeilen)
head10=$(head -10 "$f" 2>/dev/null | tr '\\n' '|')
echo "FILE:$name|SIZE:$size|LINES:$lines|MODIFIED:$modified|HEAD:$head10"
done
`.trim());
const files = [];
for (const line of raw.split("\n")) {
if (!line.startsWith("FILE:")) continue;
const parts = {};
for (const seg of line.split("|")) {
const idx = seg.indexOf(":");
if (idx > 0) {
const key = seg.slice(0, idx);
const val = seg.slice(idx + 1);
// HEAD hat mehrere |, also nur die bekannten Keys parsen
if (["FILE", "SIZE", "LINES", "MODIFIED"].includes(key)) {
parts[key] = val;
}
}
}
if (!parts.FILE || parts.FILE === "*") continue;
// Frontmatter-Info aus HEAD extrahieren
let description = "";
let memType = "";
const headPart = line.slice(line.indexOf("|HEAD:") + 6);
if (headPart) {
const headLines = headPart.split("|");
for (const hl of headLines) {
if (hl.startsWith("description:")) description = hl.replace("description:", "").trim();
if (hl.startsWith("type:")) memType = hl.replace("type:", "").trim();
}
}
files.push({
name: parts.FILE,
size: parts.SIZE || "?",
lines: parseInt(parts.LINES) || 0,
modified: parseInt(parts.MODIFIED) || 0,
description,
memType,
});
}
files.sort((a, b) => b.modified - a.modified);
clientWs.send(JSON.stringify({ type: "brain_list", files }));
log("info", "server", `${files.length} Brain-Datei(en) gefunden`);
} catch (err) {
log("error", "server", `Brain laden fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "brain_list", files: [], error: err.message }));
}
}
async function handleReadBrainFile(clientWs, filename) {
// Path Traversal verhindern
if (!filename || filename.includes("..") || filename.includes("/")) {
clientWs.send(JSON.stringify({ type: "brain_content", error: "Ungueltiger Dateiname" }));
return;
}
try {
const content = await dockerExec("aria-core",
`cat '/home/node/.openclaw/workspace/memory/${filename.replace(/'/g, "")}'`);
clientWs.send(JSON.stringify({ type: "brain_content", filename, content }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "brain_content", filename, error: err.message }));
}
}
// ── Einstellungen: Tool-Berechtigungen ──────────────────
const OPENCLAW_SETTINGS_PATHS = [
"/home/node/.openclaw/settings.json",
"/home/node/.openclaw/agents/main/agent/settings.json",
"/home/node/.openclaw/config.json",
"/home/node/.claude/settings.json", // Claude Code User-Level
"/home/node/.openclaw/workspace/.claude/settings.json", // Claude Code Projekt-Level
];
async function findSettingsFile() {
// Pruefen welche Settings-Dateien existieren
try {
const raw = await dockerExec("aria-core", `
for f in ${OPENCLAW_SETTINGS_PATHS.join(" ")}; do
[ -f "$f" ] && echo "FOUND:$f"
done
`.trim());
for (const line of raw.split("\n")) {
if (line.startsWith("FOUND:")) return line.slice(6);
}
} catch {}
// Default: erster Pfad (wird erstellt)
return OPENCLAW_SETTINGS_PATHS[0];
}
async function handleListPermissions(clientWs) {
try {
log("info", "server", "Lade Tool-Berechtigungen...");
// Alle moeglichen Settings-Dateien pruefen (Debug-Info)
let allPaths = "";
try {
allPaths = await dockerExec("aria-core", `
for f in ${OPENCLAW_SETTINGS_PATHS.join(" ")}; do
if [ -f "$f" ]; then
echo "EXISTS: $f ($(wc -c < "$f") bytes)"
else
echo "MISSING: $f"
fi
done
`.trim());
} catch {}
const settingsPath = await findSettingsFile();
let settings = {};
let rawContent = "";
let info = "";
try {
rawContent = await dockerExec("aria-core", `cat '${settingsPath}' 2>/dev/null || echo '{}'`);
settings = JSON.parse(rawContent.trim() || "{}");
info = `Geladen aus: ${settingsPath}`;
} catch (e) {
info = `Settings-Datei nicht lesbar (${settingsPath}) — Default-Berechtigungen`;
}
// OpenClaw/Claude Code Format: allowedTools ist ein Array von Tool-Namen
// Wenn leer/nicht vorhanden: alle Tools erlaubt
const allowedTools = settings.allowedTools || [];
const permissions = settings.permissions || {};
clientWs.send(JSON.stringify({
type: "permissions_list",
allowedTools,
permissions,
settingsPath,
info,
debug: { allPaths: allPaths.trim(), rawKeys: Object.keys(settings).join(", ") },
}));
log("info", "server", `Berechtigungen geladen (${allowedTools.length} Tools explizit erlaubt) aus ${settingsPath}`);
if (allPaths) log("info", "server", `Settings-Dateien:\n${allPaths}`);
} catch (err) {
log("error", "server", `Berechtigungen laden fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "permissions_list", error: err.message, allowedTools: [], permissions: {} }));
}
}
async function handleSavePermissions(clientWs, allowedTools) {
if (!Array.isArray(allowedTools)) {
clientWs.send(JSON.stringify({ type: "permissions_saved", ok: false, error: "Ungueltige Daten" }));
return;
}
try {
log("info", "server", `Speichere ${allowedTools.length} Tool-Berechtigungen in alle Settings-Pfade`);
// In ALLE moeglichen Pfade schreiben, in BEIDEN Formaten:
// 1. allowedTools: ["Bash", "Read", ...] — OpenClaw Format
// 2. permissions.allow: ["Bash(*)", "Read(*)", ...] — Claude Code Format
const script = [
'const fs=require("fs");const path=require("path");',
`const paths=${JSON.stringify(OPENCLAW_SETTINGS_PATHS)};`,
`const tools=${JSON.stringify(allowedTools)};`,
// Claude Code Format: Tool-Namen mit (*) Glob-Pattern
'const ccAllow=tools.map(t=>t+"(*)");',
'const ok=[];const fail=[];',
'for(const f of paths){try{',
'let s={};try{s=JSON.parse(fs.readFileSync(f,"utf8"));}catch(e){}',
's.allowedTools=tools;',
'if(!s.permissions)s.permissions={};',
's.permissions.allow=ccAllow;',
's.permissions.deny=[];',
'const dir=path.dirname(f);',
'try{fs.mkdirSync(dir,{recursive:true});}catch(e){}',
'fs.writeFileSync(f,JSON.stringify(s,null,2));',
'ok.push(f);',
'}catch(e){fail.push(f+": "+e.message);}}',
// Verify: nur erfolgreiche Pfade zuruecklesen
'const result={ok:[],fail:fail,verified:{}};',
'for(const f of ok){',
'try{const d=JSON.parse(fs.readFileSync(f,"utf8"));',
'result.verified[f]={allowedTools:d.allowedTools||[],permsAllow:(d.permissions&&d.permissions.allow)||[]};}',
'catch(e){result.fail.push(f+": verify-read "+e.message);}',
'}',
'result.ok=ok;',
'process.stdout.write(JSON.stringify(result));',
].join("");
const b64 = Buffer.from(script).toString("base64");
const verifyRaw = await dockerExec("aria-core", `echo ${b64} | base64 -d | node`);
// Verify: gespeicherte Daten pruefen
let verifyResult = {};
let verified = false;
let verifyDetails = [];
try {
verifyResult = JSON.parse(verifyRaw.trim());
// Mindestens ein Pfad muss erfolgreich sein
verified = verifyResult.ok && verifyResult.ok.length > 0;
for (const f of (verifyResult.fail || [])) {
verifyDetails.push(`FEHLER: ${f}`);
}
for (const [fpath, data] of Object.entries(verifyResult.verified || {})) {
const ok = data.allowedTools.length === allowedTools.length
&& allowedTools.every(t => data.allowedTools.includes(t));
const ccOk = data.permsAllow.length === allowedTools.length;
verifyDetails.push(`${fpath}: allowedTools=${ok?"OK":"FEHLER"} permissions.allow=${ccOk?"OK":"FEHLER"} (${data.allowedTools.length}/${data.permsAllow.length} Tools)`);
if (!ok) verified = false;
}
} catch (e) {
verifyDetails.push(`Parse-Fehler: ${e.message} — Raw: ${verifyRaw.substring(0, 200)}`);
}
log("info", "server", `Verify:\n${verifyDetails.join("\n")}`);
if (!verified) {
clientWs.send(JSON.stringify({
type: "permissions_saved", ok: false,
error: `Verify fehlgeschlagen:\n${verifyDetails.join("\n")}`,
}));
return;
}
const okCount = (verifyResult.ok || []).length;
const failCount = (verifyResult.fail || []).length;
const infoMsg = `${allowedTools.length} Tools gespeichert (${okCount} Pfade OK` + (failCount ? `, ${failCount} fehlgeschlagen` : '') + ')';
clientWs.send(JSON.stringify({
type: "permissions_saved", ok: true,
info: infoMsg,
details: verifyDetails,
needsRestart: true,
}));
log("info", "server", infoMsg);
} catch (err) {
log("error", "server", `Berechtigungen speichern fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "permissions_saved", ok: false, error: err.message }));
}
}
// ── Einstellungen: Model ────────────────────────────────
async function handleGetModel(clientWs) {
try {
const raw = await dockerExec("aria-core", `echo $DEFAULT_MODEL`);
clientWs.send(JSON.stringify({ type: "model_info", model: raw.trim(), info: "Aktuelles Model (ENV)" }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "model_info", error: err.message }));
}
}
async function handleSetModel(clientWs, model) {
if (!model || typeof model !== "string") {
clientWs.send(JSON.stringify({ type: "model_info", error: "Kein Model angegeben" }));
return;
}
try {
// Model in Settings speichern (OpenClaw liest das)
const settingsPath = await findSettingsFile();
const script = [
'const fs=require("fs");',
`const f="${settingsPath}";`,
'let s={};try{s=JSON.parse(fs.readFileSync(f,"utf8"));}catch(e){}',
`s.model=${JSON.stringify(model)};`,
`const dir=f.substring(0,f.lastIndexOf("/"));`,
'try{fs.mkdirSync(dir,{recursive:true});}catch(e){}',
'fs.writeFileSync(f,JSON.stringify(s,null,2));',
].join("");
const b64 = Buffer.from(script).toString("base64");
await dockerExec("aria-core", `echo ${b64} | base64 -d | node`);
clientWs.send(JSON.stringify({ type: "model_info", model, info: `Model auf "${model}" gesetzt (Neustart noetig)` }));
log("info", "server", `Model gesetzt: ${model}`);
} catch (err) {
clientWs.send(JSON.stringify({ type: "model_info", error: err.message }));
}
}
// ── Einstellungen: OpenClaw Config ──────────────────────
async function handleGetOpenClawConfig(clientWs) {
try {
const raw = await dockerExec("aria-core", `
echo '=== Umgebungsvariablen ==='
echo "DEFAULT_MODEL=$DEFAULT_MODEL"
echo "RATE_LIMIT_PER_USER=$RATE_LIMIT_PER_USER"
echo "OPENCLAW_GATEWAY_TOKEN=$(echo $OPENCLAW_GATEWAY_TOKEN | head -c 8)..."
echo "OPENCLAW_GATEWAY_BIND=$OPENCLAW_GATEWAY_BIND"
echo ""
echo '=== Settings-Dateien ==='
for f in ${OPENCLAW_SETTINGS_PATHS.join(" ")}; do
if [ -f "$f" ]; then
echo "--- $f ---"
cat "$f"
echo ""
fi
done
echo ""
echo '=== Agent-Verzeichnis ==='
ls -la /home/node/.openclaw/agents/main/agent/ 2>&1
echo ""
echo '=== Workspace ==='
ls -la /home/node/.openclaw/workspace/ 2>&1
`.trim());
clientWs.send(JSON.stringify({ type: "openclaw_config", config: raw }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "openclaw_config", error: err.message }));
}
}
// ── Start ───────────────────────────────────────────────
server.listen(HTTP_PORT, "0.0.0.0", () => {
log("info", "server", `Diagnostic Server laeuft auf http://0.0.0.0:${HTTP_PORT}`);
log("info", "server", `Gateway: ${GATEWAY_URL}`);
log("info", "server", `Token: ${GATEWAY_TOKEN ? GATEWAY_TOKEN.slice(0, 8) + "..." : "(keiner)"}`);
log("info", "server", `RVS: ${RVS_HOST ? `${RVS_HOST}:${RVS_PORT}` : "(nicht konfiguriert)"}`);
log("info", "server", `Proxy: ${PROXY_URL}`);
// Verbindungen aufbauen
connectGateway();
connectRVS();
});