feat: Werkzeug-Bestätigung, DNS-Pinning, Statistik-Leiste, Produktionsbetrieb (#1)

Ergebnis eines Security-Reviews des Werkzeug-Pfads plus die daraus
hervorgegangenen Verbesserungen.

Sicherheit:
- runTool wertet requiresConfirmation aus; Rückfrage über den WebSocket mit
  vollständigen Argumenten, Antwort allow/always/deny. Ohne Rückkanal gilt
  abgelehnt. read_webpage und remember sind bestätigungspflichtig
- remember pinnt nicht mehr automatisch, Modell-Anker verfallen normal
- SSRF-Guard mit gepinnter DNS-Auflösung (lookup-Hook statt fetch): geprüft
  wird genau die Adresse, die auch verbunden wird — schließt DNS-Rebinding
- ctx.signal kombiniert Abbruch und Zeitlimit bis in den Netzwerkabruf
- activeAborts als Set, Chat-Rate-Limit je Verbindung, Bucket-Cleanup

Statistik-Leiste über dem Composer: Tokens, tok/s, TTFT, Gesamtzeit und
Kontext-Füllstand, dazu die Session-Summe. Zahlen sind gemessen — bei Ollama
aus dem Abschluss-Chunk, bei OpenRouter aus dem usage-Block. Der Füllstand
rechnet gegen das tatsächlich genutzte Fenster aus /api/ps, nicht gegen die
deklarierte Länge des Modells.

Produktionsbetrieb: der Server liefert web/dist jetzt mit aus, npm start
genügt. /api und /ws behalten Vorrang.

README überarbeitet, Fork-Bezug entfernt, zerbrochene Konfigurationstabelle
repariert, API-Referenz ergänzt.

Nicht umgesetzt: Auth-/Origin-Härtung — der Server läuft bewusst lokal.
This commit is contained in:
Jeuner 2026-08-28 15:04:55 +02:00 committed by GitHub
parent 878acd9933
commit 8a489dfc5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1495 additions and 347 deletions

View file

@ -39,6 +39,9 @@ interface Bucket {
resetAt: number;
}
/** Ab dieser Größe wird aufgeräumt — reicht für jede realistische Nutzung. */
const MAX_BUCKETS = 1000;
/**
* Einfacher In-Memory-Zähler pro Zeitfenster. Bremst teure Endpunkte
* (Whisper läuft bis zu 180 s) gegen versehentliche oder böswillige Fluten.
@ -46,8 +49,16 @@ interface Bucket {
export function createRateLimiter(limit: number, windowMs: number) {
const buckets = new Map<string, Bucket>();
/** Abgelaufene Zähler wegräumen, damit die Map nicht unbegrenzt wächst. */
function sweep(now: number) {
for (const [key, bucket] of buckets) {
if (now >= bucket.resetAt) buckets.delete(key);
}
}
return function allow(key: string): boolean {
const now = Date.now();
if (buckets.size > MAX_BUCKETS) sweep(now);
const bucket = buckets.get(key);
if (!bucket || now >= bucket.resetAt) {