@@ -3005,7 +3022,10 @@
? `
đź—‘ `
: '';
const histBadge = srcBadgeHtml(m.type, m.answeredBy || '', m.model || '', m.modelReason || '');
- const innerHtml = `${trashBtn}${linked}
${escapeHtml(m.meta)}${histBadge} — ${time}
`;
+ const proofBtn = (m.ts && m.type === 'received')
+ ? `
🔍 `
+ : '';
+ const innerHtml = `${trashBtn}${proofBtn}${linked}
${escapeHtml(m.meta)}${histBadge} — ${time}
`;
for (const b of boxes) {
const el = document.createElement('div');
el.className = `chat-msg ${m.type}`;
@@ -3661,7 +3681,10 @@
// Quell-Badge: welcher Backend die Antwort erzeugt hat (nur ARIA-Bubbles)
const srcBadge = srcBadgeHtml(type, (options && options.answeredBy) || '',
(options && options.model) || '', (options && options.modelReason) || '');
- const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}
${escapeHtml(meta)}${srcBadge} — ${new Date().toLocaleTimeString('de-DE')}
`;
+ const proofBtn = (backupTs && type === 'received')
+ ? `
🔍 `
+ : '';
+ const html = `${trashBtn}${proofBtn}${linked}${ttsBlock}${gpsBlock}
${escapeHtml(meta)}${srcBadge} — ${new Date().toLocaleTimeString('de-DE')}
`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
@@ -9535,6 +9558,47 @@
} catch (e) { btn.disabled = false; btn.textContent = 'In Katalog'; alert(e.message); }
}
+ // ── Belege: was hat ARIA fuer eine Antwort wirklich ausgefuehrt? ──
+ async function showAriaProof(ts, btn) {
+ const project = (btn && btn.closest('.chat-msg') && btn.closest('.chat-msg').dataset.projectId) || '';
+ const body = document.getElementById('proof-body');
+ document.getElementById('proof-modal').style.display = 'flex';
+ body.innerHTML = '
lade … ';
+ let j;
+ try { j = await (await fetch('/api/aria-proof?ts=' + ts + '&project=' + encodeURIComponent(project))).json(); }
+ catch (e) { body.innerHTML = '
âś— ' + escapeHtml(e.message) + ' '; return; }
+ if (!j.ok) { body.innerHTML = '
âś— ' + escapeHtml(j.error || 'Fehler') + ' '; return; }
+ const t = x => new Date(x).toLocaleTimeString('de-DE');
+ const head = '
Zeitraum ' + t(j.from) + ' – ' + t(j.to) +
+ (project ? ' · Projekt ' + escapeHtml(project) + '' : ' · Hauptchat') + '
';
+ if (!j.toolCount) {
+ const before = j.logSince && new Date(j.to).toISOString().slice(0, 10) < j.logSince;
+ body.innerHTML = head + '
' +
+ (before ? 'FĂĽr diese Antwort gibt es noch kein Protokoll (aufgezeichnet wird seit ' + escapeHtml(j.logSince) + ').'
+ : 'Keine Befehle ausgefĂĽhrt. ARIA hat fĂĽr diese Antwort nichts nachgesehen (kein Bash/ssh, kein Lesen). ' +
+ 'Sagt sie darin „nachgesehen" oder nennt sie einen aktuellen Stand, stammt das aus dem Verlauf oder ihrem Gedächtnis.') +
+ '
Nicht enthalten: ARIAs eigene Werkzeuge wie Gedächtnis-Suche oder Timer — die stehen im Brain-Log.
';
+ return;
+ }
+ const prettyInput = (name, input) => {
+ try { const o = JSON.parse(input); if (name === 'Bash' && o.command) return o.command;
+ if (o.file_path) return o.file_path + (o.pattern ? ' · ' + o.pattern : '');
+ if (o.pattern) return o.pattern + (o.path ? ' in ' + o.path : '');
+ return JSON.stringify(o, null, 1); } catch { return input || ''; }
+ };
+ body.innerHTML = head + j.requests.map(r => r.tools.map(x =>
+ '
' +
+ '
' + escapeHtml(x.name || '?') + (x.isError ? ' · Fehler' : '') + ' ' +
+ '' + t(x.ts) + '
' +
+ '
' + escapeHtml(prettyInput(x.name, x.input)) + ' ' +
+ (x.result != null ? '
Ausgabe' +
+ (x.resultTruncatedBytes ? ' (gekĂĽrzt, ' + x.resultTruncatedBytes + ' Zeichen mehr)' : '') + ' ' +
+ '' + escapeHtml(x.result) + ' '
+ : '
(keine Ausgabe protokolliert)
') +
+ '
').join('')).join('') +
+ '
Passwörter/Tokens sind im Protokoll geschwärzt. Nicht enthalten: Gedächtnis-Suche/Timer (Brain-Werkzeuge).
';
+ }
+
function openCtxInfo() { document.getElementById('ctx-info-modal').style.display = 'flex'; }
// Kontextfenster einer Karte aendern = dasselbe Modell mit anderem -c neu
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 81bd39f..b396a11 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -554,6 +554,52 @@ async function refreshLlmCatalogFromHF() {
return { models: merged, added };
}
+// ── Belege: Werkzeug-Aufrufe zu einer ARIA-Antwort ─────────────────
+const ARIA_TOOL_LOG_DIR = "/shared/logs/aria-tools";
+function ariaProof(toTs, project) {
+ // Beginn = Stefans letzte Nachricht davor im selben Kontext (sonst 30 min)
+ let fromTs = toTs - 30 * 60 * 1000;
+ try {
+ const lines = fs.readFileSync("/shared/config/chat_backup.jsonl", "utf-8").split("\n");
+ for (let i = lines.length - 1; i >= 0; i--) {
+ if (!lines[i]) continue;
+ let e; try { e = JSON.parse(lines[i]); } catch { continue; }
+ if (!e || !(e.ts < toTs) || e.role !== "user") continue;
+ if ((e.project_id || "") !== project) continue;
+ fromTs = Math.max(fromTs, e.ts - 2000);
+ break;
+ }
+ } catch (_) { }
+ const days = new Set([new Date(fromTs).toISOString().slice(0, 10), new Date(toTs).toISOString().slice(0, 10)]);
+ const reqs = new Map();
+ for (const d of days) {
+ let txt = "";
+ try { txt = fs.readFileSync(`${ARIA_TOOL_LOG_DIR}/${d}.jsonl`, "utf-8"); } catch { continue; }
+ for (const line of txt.split("\n")) {
+ if (!line) continue;
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
+ if (!ev.requestId) continue;
+ const r = reqs.get(ev.requestId) || { requestId: ev.requestId, project: null, start: 0, end: 0, tools: [], byId: {} };
+ if (ev.kind === "start") { r.start = ev.ts; r.project = ev.projectId || ""; r.model = ev.model || ""; }
+ else if (ev.kind === "end") r.end = ev.ts;
+ else if (ev.kind === "tool_use") {
+ const t = { id: ev.id, name: ev.name, input: ev.input, inputTruncatedBytes: ev.inputTruncatedBytes || 0, ts: ev.ts };
+ r.tools.push(t); if (ev.id) r.byId[ev.id] = t;
+ } else if (ev.kind === "tool_result") {
+ const t = ev.id && r.byId[ev.id];
+ if (t) { t.result = ev.content; t.isError = !!ev.isError; t.resultTruncatedBytes = ev.truncatedBytes || 0; t.doneTs = ev.ts; }
+ }
+ reqs.set(ev.requestId, r);
+ }
+ }
+ const hit = [...reqs.values()]
+ .filter(r => r.start && r.project === project && r.start >= fromTs && r.start <= toTs + 5000)
+ .sort((a, b) => a.start - b.start)
+ .map(({ byId, ...r }) => r);
+ const logSince = (() => { try { return fs.readdirSync(ARIA_TOOL_LOG_DIR).filter(f => /\.jsonl$/.test(f)).sort()[0]?.slice(0, 10) || ""; } catch { return ""; } })();
+ return { from: fromTs, to: toTs, requests: hit, toolCount: hit.reduce((n, r) => n + r.tools.length, 0), logSince };
+}
+
// ── Gerade im Trend: ladbare GGUF-Modelle von HuggingFace ───────────
// Fuer das Fenster "Bestenlisten" im Modell-Katalog. "Trend" ist nicht
// "das Beste" — viele Community-Mischungen. Darum standardmaessig nur
@@ -2388,6 +2434,17 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message, models: loadLlmCatalog() }));
});
+ } else if (req.url.startsWith("/api/aria-proof") && req.method === "GET") {
+ // Belege zu einer ARIA-Antwort: alle Werkzeug-Aufrufe (Bash/ssh, Read, …),
+ // die ARIA im Proxy zwischen Stefans Frage und dieser Antwort gemacht hat.
+ // Quelle: /shared/logs/aria-tools/
.jsonl (proxy-patches/routes.js).
+ const u = new URL(req.url, "http://x");
+ const to = Number(u.searchParams.get("ts")) || 0;
+ const project = u.searchParams.get("project") || "";
+ res.writeHead(200, { "Content-Type": "application/json" });
+ if (!to) { res.end(JSON.stringify({ ok: false, error: "ts fehlt" })); return; }
+ try { res.end(JSON.stringify({ ok: true, ...ariaProof(to, project) })); }
+ catch (e) { res.end(JSON.stringify({ ok: false, error: e.message })); }
} else if (req.url.startsWith("/api/llm-discover") && req.method === "GET") {
const u = new URL(req.url, "http://x");
discoverGguf(u.searchParams.get("cat") || "chat", u.searchParams.get("trusted") !== "0")
diff --git a/proxy-patches/routes.js b/proxy-patches/routes.js
index c628647..4b55ec7 100644
--- a/proxy-patches/routes.js
+++ b/proxy-patches/routes.js
@@ -118,7 +118,51 @@ function _emitToolEvent(toolName, projectId) {
* kind: "start" | "text" | "tool_use" | "tool_result" | "end"
*/
function _emitStreamEvent(requestId, kind, fields) {
- _postJson(STREAM_HOOK_URL, { requestId, kind, ts: Date.now(), ...fields });
+ const ev = { requestId, kind, ts: Date.now(), ...fields };
+ _postJson(STREAM_HOOK_URL, ev);
+ _persistToolEvent(ev);
+}
+
+// ── Werkzeug-Protokoll (Beleg) ────────────────────────────────
+// Was ARIA SELBST im Proxy ausfuehrt (Bash/ssh, Read, Edit, …) war bisher
+// nur live sichtbar. Jetzt zusaetzlich dauerhaft: eine JSONL-Datei pro Tag
+// unter /shared/logs/aria-tools/. Die Diagnostic zeigt pro ARIA-Antwort die
+// zugehoerigen Aufrufe ("Belege"). Nur start/tool_use/tool_result/end —
+// keine Text-Deltas. Fail-open: ein Schreibfehler stoert ARIA nie.
+const TOOL_LOG_DIR = process.env.ARIA_TOOL_LOG_DIR || "/shared/logs/aria-tools";
+const TOOL_LOG_KINDS = new Set(["start", "tool_use", "tool_result", "end"]);
+const TOOL_LOG_KEEP_DAYS = 30;
+let _toolLogCleaned = "";
+// Offensichtliche Geheimnisse nicht dauerhaft ablegen (Passwoerter, Tokens,
+// API-Keys, private Schluessel) — Ausgaben wie "cat .env" landen sonst im Log.
+const SECRET_PATTERNS = [
+ [/(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)/g, "$1 *** $2"],
+ [/((?:pass(?:word|wd)?|token|secret|api[_-]?key|auth|credential)[\w-]*["']?\s*[=:]\s*["']?)[^\s"',;]{4,}/gi, "$1***"],
+ [/\b(hf_|ghp_|gho_|github_pat_|sk-|xox[bp]-|glpat-)[A-Za-z0-9_-]{8,}/g, "$1***"],
+ [/(https?:\/\/[^\s:@\/]+:)[^\s@\/]+@/g, "$1***@"],
+];
+function _redact(v) {
+ if (typeof v !== "string") return v;
+ for (const [re, rep] of SECRET_PATTERNS) v = v.replace(re, rep);
+ return v;
+}
+function _persistToolEvent(ev) {
+ if (!TOOL_LOG_KINDS.has(ev.kind)) return;
+ try {
+ ev = { ...ev, input: _redact(ev.input), content: _redact(ev.content) };
+ const day = new Date(ev.ts).toISOString().slice(0, 10);
+ fs.mkdirSync(TOOL_LOG_DIR, { recursive: true });
+ fs.appendFile(`${TOOL_LOG_DIR}/${day}.jsonl`, JSON.stringify(ev) + "\n", () => {});
+ if (_toolLogCleaned !== day) { // einmal am Tag: alte Tage weg
+ _toolLogCleaned = day;
+ const cutoff = new Date(Date.now() - TOOL_LOG_KEEP_DAYS * 86400000).toISOString().slice(0, 10);
+ for (const f of fs.readdirSync(TOOL_LOG_DIR)) {
+ if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f) && f.slice(0, 10) < cutoff) {
+ try { fs.unlinkSync(`${TOOL_LOG_DIR}/${f}`); } catch (_) { }
+ }
+ }
+ }
+ } catch (_) { /* fail-open */ }
}
function _truncate(str, max) {