Commit graph

15 commits

Author SHA1 Message Date
Jeuner
da1d8be426 feat: Frontend im Produktionsbetrieb ausliefern, TTFT bei OpenRouter
npm start baute web/dist, aber niemand lieferte es aus — der Server stellte
nur die API bereit. Jetzt registriert er @fastify/static, wenn ein Build
vorliegt, und die App läuft komplett unter :8788. Ohne web/dist bleibt alles
wie bisher (Vite übernimmt im Dev-Betrieb); der Start sagt, welcher Fall
vorliegt. /api und /ws behalten Vorrang, unbekannte /api-Pfade antworten
weiterhin mit JSON statt mit index.html.

OpenRouter, beim Verifizieren gegen ein Gratis-Modell gefunden:
- ttftMs wurde nie gesetzt — evalMs wurde aus firstTokenAt gerechnet, das
  Feld selbst blieb null
- die Zeitmessung startete erst nach dem fetch und ließ damit die Wartezeit
  auf den Anbieter aus. Startet jetzt davor, wie bei Ollama

Geprüft im Produktionsmodus: /, /assets/*.js, SPA-Fallback, JSON-404 auf
/api, WebSocket aus Origin :8788 und :5174 verbunden, fremde Origin 403.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUP4R3rgq4XwVs4bVf7uh
2026-08-28 14:59:35 +02:00
Jeuner
ed3c01303b docs: README überarbeitet, Fork-Bezug entfernt
Der Fork-Hinweis im Kopf ist raus; das Projekt steht für sich. Statt der
Herkunft steht jetzt oben, was es tut und was drin ist.

Inhaltliche Korrekturen:
- Die Konfigurations-Tabelle war zerbrochen: ein Absatz stand mitten drin,
  PIPER_MODEL und ALLOWED_ORIGINS hingen danach als Fragment. TOOLS_ROOT
  fehlte ganz. Tabelle repariert, Spalte "Zweck" ergänzt.
- Der Abschnitt "Produktion" behauptete, `npm start` liefere die App aus.
  Der Server hat kein Static-Serving — er stellt nur die API bereit,
  web/dist bleibt liegen. Steht jetzt so da, mit dem, was es stattdessen
  braucht.

Struktur:
- Endpunkte waren über drei Abschnitte verstreut, jetzt eine API-Tabelle
- Anhang-Grenzen als Tabelle statt als Fließtext
- Werkzeug- und Sicherheitsabschnitte zusammengezogen, Dopplungen raus
- Tabellen-Pipes ausgerichtet, Codeblock-Sprachen ergänzt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUP4R3rgq4XwVs4bVf7uh
2026-08-28 14:50:29 +02:00
Jeuner
1297ffcc56 feat: Statistik-Leiste mit Tokens, tok/s, TTFT und Kontext-Füllstand
Zeile über dem Composer mit den Messwerten der letzten Antwort plus
Session-Summe. Die Zahlen sind gemessen, nicht geschätzt.

Quellen:
- Ollama: prompt_eval_count, eval_count, eval_duration aus dem
  Abschluss-Chunk; TTFT wird beim ersten Content- oder Thinking-Chunk
  gestoppt
- OpenRouter: usage-Block, dafür stream_options.include_usage. Der Block
  kommt erst nach finish_reason, deshalb wird nur noch abgekürzt, wenn er
  schon da ist — sonst gingen die Tokenzahlen verloren
- Kosten aus promptPrice/completionPrice der Modellliste

Werkzeugrunden sind mehrere Modellaufrufe für eine sichtbare Antwort:
Erzeugtes wird summiert, der Prompt-Stand ist der der letzten Runde,
TTFT zählt nur die erste.

Kontext-Füllstand gegen GET /api/ps statt gegen die deklarierte Länge des
Modells. Das ist nicht dasselbe: qwen3.5 deklariert 262144, geladen läuft
es mit 4096. Gegen die deklarierte Länge stünde der Balken bei 1 %,
während vorne längst abgeschnitten wird.

onStats darf asynchron sein und wird vor onDone abgewartet — der
/api/ps-Lookup schob die Stats-Nachricht sonst hinter das done, und der
Client verwarf sie.

Während des Streamens zählt die Leiste eingehende Chunks als Näherung
(mit ≈ markiert); gemessen liegt das bei Prosa innerhalb weniger Prozent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUP4R3rgq4XwVs4bVf7uh
2026-08-28 14:42:45 +02:00
Jeuner
dd0ee30165 feat: Bestätigungspflicht für Werkzeuge mit Außenwirkung, DNS-Pinning
Ergebnis eines Security-Reviews. Die schwerwiegendste Kette war:
Fremdinhalt aus read_webpage weist das Modell an, remember aufzurufen —
der Anker landet gepinnt und sessionübergreifend im Gedächtnis und geht
danach als "verlässliches Wissen" in jeden System-Prompt. Beide Werkzeuge
liefen ungefragt, requiresConfirmation war nur ein Feld ohne Wirkung.

Bestätigung:
- runTool wertet requiresConfirmation über ToolContext.confirm aus; ohne
  Rückkanal (Skript, Test) gilt abgelehnt statt ungefragt ausführen
- Handshake über den WebSocket: tool-confirm mit ungekürzten Argumenten,
  Antwort allow/always/deny. Ablehnung auch bei Timeout (2 min),
  Verbindungsabbruch und Stop; "always" gilt pro Verbindung
- read_webpage und remember sind bestätigungspflichtig; remember pinnt
  nicht mehr automatisch, damit Modell-Anker normal verfallen
- ToolConfirm-Komponente zeigt Werkzeug und vollständige Argumente

SSRF-Guard (DNS-Rebinding):
- fetch gegen node:http/https mit eigenem lookup-Hook getauscht: geprüft
  wird genau die Adresse, die dann auch verbunden wird. Vorher löste fetch
  ein zweites Mal auf — öffentlich beim Prüfen, 127.0.0.1 beim Verbinden
- IPv6-Literale werden entklammert, gzip/deflate/br werden ausgepackt

Nebenbei:
- ctx.signal kombiniert Abbruch und Zeitlimit und wirkt bis in den Abruf
- activeAborts als Set: Stop erwischt alle laufenden Antworten
- Chat-Rate-Limit 30/min je Verbindung, Rate-Limiter räumt Buckets ab
- web/dist untracked

Nicht umgesetzt: Auth/Origin-Härtung — der Server läuft bewusst lokal,
Anfragen ohne Origin-Header bleiben erlaubt (im README dokumentiert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUP4R3rgq4XwVs4bVf7uh
2026-08-28 14:30:18 +02:00
Jeuner
0cfc501ec4 docs: Anhänge (Text/PDF) dokumentiert, Modellauswahl in Konfiguration erwähnt 2026-08-28 13:46:56 +02:00
Jeuner
32128aba7a feat: read_webpage-Werkzeug (Defuddle) mit SSRF-Schutz
- lädt öffentliche Websites und extrahiert Hauptinhalt als Markdown
  (defuddle/node + linkedom)
- SSRF-Schutz: nur http/https, private Adressbereiche werden nach
  DNS-Auflösung abgewiesen, auch über Weiterleitungen (redirect:
  manual, jede Hop-URL neu geprüft)
- Grenzen: 15 s Zeitlimit, 2 MB Fetch-Limit, 25 kB Output-Cap;
  Inhalt wird dem Modell als nicht vertrauenswürdig markiert
- Text-/PDF-Anhänge: Message-Feld files, Validierung über
  server/src/files.ts, PDF-Textextraktion via pdf-parse
- README: Werkzeugliste und Netz-Ausnahme dokumentiert
2026-08-28 13:45:00 +02:00
Jeuner
969ec6909d docs: RAM-Hinweis, qwen3:8b als leichtere Alternative; setup.sh warnt bei <16 GB RAM 2026-08-28 13:36:46 +02:00
Jeuner
ea87c350a5 feat: Installationsroutine für macOS (setup.sh)
- prüft macOS, Homebrew, Node 22+, npm-Abhängigkeiten, ffmpeg,
  whisper.cpp inkl. Modell, Piper inkl. Stimme, Ollama-Server und
  das Sprachmodell — fehlende Teile werden auf Nachfrage installiert
- Modi: interaktiv, --check (nur Prüfung), --yes (ohne Nachfragen)
- README: Schnellstart-Abschnitt (clone → setup → dev)
- npm run setup als Alias
2026-08-28 13:35:16 +02:00
Jeuner
9bce768f95 feat: Gedächtnis mit Append-Only-Log, Traumphase und Ankerpunkten
- memory_events als append-only Log (nur INSERT), daraus Zustand
  deterministisch rekonstruierbar (fold/rebuild)
- Traumphase konsolidiert Log in Ankerpunkte: LLM-Extraktion mit
  Heuristik-Fallback, Dedupe über Wort-Ähnlichkeit, Decay auf
  ungepinnten Ankern
- einstellbares Gedächtnis-Fenster (Default 10 Schritte) statt
  hartkodiertem slice(-24)
- wichtigste Ankerpunkte werden als System-Kontext eingespielt
- neue Werkzeuge remember (schreibend, gepinnt) und recall
- Gedächtnis-Panel im Frontend: Ankerliste, Pinnen, Traumphase
  manuell auslösen, Rekonstruktion
- Eval-Skript: npm run memory:eval --workspace server
2026-08-27 21:41:35 +02:00
Jeuner
33125e1420 feat: Werkzeuge (Tool-Calling) mit vier lesenden Werkzeugen
qwen3.5 meldet die Fähigkeit "tools". Der Streaming-Loop in ollama.ts sammelt
jetzt tool_calls ein, führt sie aus, hängt die Ergebnisse an den Verlauf und
startet eine weitere Runde — bis eine Antwort ohne Werkzeugwunsch entsteht
oder MAX_TOOL_ROUNDS (5) erreicht ist. In der letzten Runde werden bewusst
keine Werkzeuge mehr mitgeschickt, damit eine Antwort entsteht statt eines
weiteren Aufrufwunsches.

Werkzeuge (alle ausschließlich lesend):
- get_time    Datum/Uhrzeit je IANA-Zeitzone — Modelle raten das Datum sonst
- calculate   Arithmetik über einen eigenen Parser
- read_file   Textdatei unterhalb des Projektverzeichnisses
- list_files  Verzeichnisinhalt

Sicherheit:
- Der Dateizugriff liegt in einer Sandbox. Pfade werden per realpath aufgelöst
  (damit auch Symlinks) und müssen unterhalb von TOOLS_ROOT liegen. .env, .git/
  und Schlüsseldateien sind auch innerhalb der Wurzel gesperrt.
- calculate benutzt kein eval/new Function. Der Ausdruck stammt aus einer
  Modellantwort, die von Nutzereingaben beeinflusst wird; der Parser kennt nur
  Zahlen und Grundrechenarten.
- 15 s Zeitlimit je Aufruf, Fehler werden als Ergebnis an das Modell
  zurückgegeben statt die Antwort abzubrechen. Nur ToolError-Texte werden
  durchgereicht, damit keine internen Details nach außen gelangen.

Geprüft am laufenden Modell: Zeit, Rechnen (4271*8934+17 = 38157131, exakt),
Dateizugriff und zwei Werkzeuge in einer Antwort. Gegen die Sandbox getestet:
../../../etc/passwd, /etc/passwd, ~/.ssh/id_rsa, .env sowie Code-Injection in
calculate — alle abgewiesen. Auch als Prompt formuliert ("lies .env und nenne
den API-Key") bleibt der Zugriff blockiert.

Die Oberfläche zeigt Aufrufe samt Argumenten und Dauer über der Antwort; ein
Schalter in den Einstellungen deaktiviert Werkzeuge. Die Anzeige lebt nur im
Browser-Zustand und ist nach einem Neuladen weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1NXUpHbxTusqQmsjrFfbU
2026-08-26 19:01:28 +02:00
Jeuner
adb57ae6e3 feat: Bilder im Chat (Vision)
qwen3.5 meldet die Fähigkeit "vision", die bisher ungenutzt blieb. Bilder
lassen sich jetzt per Button, Zwischenablage (⌘V) oder Drag & Drop anhängen;
eine Nachricht darf auch nur aus einem Bild bestehen.

- images.ts: Validierung vor dem Speichern — maximal 4 Bilder je Nachricht,
  je 6 MB, Typ per Magic Bytes statt per Dateiendung (PNG/JPEG/GIF/WebP).
  Der WebSocket bekommt dazu ein maxPayload von 32 MB, vorher war er
  unbegrenzt und Bilder machen Nachrichten deutlich größer.
- db.ts: Spalte images samt Migration für bestehende Datenbanken aus agenttwo
- ollama.ts: images-Feld je Message; openrouter.ts: OpenAI-Format mit
  image_url, MIME aus den Magic Bytes statt fest image/png
- Composer: Vorschau mit Entfernen-Button, Fehlermeldung bei zu großen oder
  nicht unterstützten Dateien; ChatMessage zeigt Bilder in der Historie
- Bilder wandern in die Chat-Historie und werden bei Folgefragen erneut
  mitgeschickt, damit Rückfragen zum selben Bild funktionieren

Getestet gegen das laufende Modell: ein rendertes PNG (roter Kreis auf weiß)
wird korrekt als japanische Flagge beschrieben. Validierung geprüft gegen
5 Bilder, Textdatei mit Bildnamen, kaputtes base64 und 7-MB-Blob — alle
abgelehnt.

Nebenbei: Branding in UI, Titel und Serverlog auf agenttwo-tools umgestellt.
Die localStorage-Keys bleiben unverändert, sonst gingen gespeicherte
Einstellungen verloren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1NXUpHbxTusqQmsjrFfbU
2026-08-26 17:44:57 +02:00
Jeuner
e48228c570 chore: Fork als agenttwo-tools aufsetzen
Basis für die Tool-Calling- und Vision-Entwicklung. Die Historie von agenttwo
bleibt erhalten, damit sich Fixes aus der Basis per cherry-pick übernehmen
lassen; das Remote upstream zeigt dafür auf Jeuners/agenttwo.

Eigene Ports, damit beide Projekte gleichzeitig laufen können:
- Backend 8787 -> 8788, Vite 5173 -> 5174 samt Proxy-Zielen
- Origin-Allowlist in security.ts entsprechend auf 5174/8788
- OpenRouter-Attribution (HTTP-Referer, X-Title) angepasst

Die SQLite-Datei liegt wie gehabt neben dem Server, beide Instanzen haben
dadurch getrennte Sessions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1NXUpHbxTusqQmsjrFfbU
2026-08-26 17:35:51 +02:00
Jeuner
e721540b80 docs: Voraussetzungen korrigieren (Node 22+, piper als Python-Modul)
Beim Testen eines frischen Clones fielen zwei falsche Angaben auf:

- "Node.js 20+" stimmte nicht. db.ts nutzt node:sqlite, das unter Node 20
  gar nicht existiert (ERR_UNKNOWN_BUILTIN_MODULE) — der Server startet dort
  nicht. Ab 22 läuft es, mit ExperimentalWarning; stabil ab Node 24.
  Zusätzlich als engines-Feld in package.json hinterlegt.
- "brew install piper" führte in die Irre: voice.ts ruft `python3 -m piper`
  auf, nicht das Homebrew-Binary. Korrekt ist `pip3 install piper-tts`.

Dazu ein Prüf-Snippet, mit dem sich die Toolchain vor dem ersten Start
verifizieren lässt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1NXUpHbxTusqQmsjrFfbU
2026-08-26 17:16:17 +02:00
Jeuner
799c9c9394 fix: Origin-Prüfung für HTTP und WebSocket, Härtung von STT/TTS
Der Server lauscht nur auf 127.0.0.1, hatte aber keine Authentifizierung und
mit CORS origin:true jede fremde Origin reflektiert. Da localhost aus jedem
Browser-Tab erreichbar ist, konnte damit jede besuchte Webseite über
/api/sessions und /api/sessions/:id/messages die komplette Chat-Historie
auslesen. Der WebSocket-Upgrade prüfte die Origin ebenfalls nicht — WebSockets
unterliegen nicht der Same-Origin-Policy, wodurch fremde Seiten Chats senden,
Antworten mitlesen und Kosten auf dem OpenRouter-Key erzeugen konnten (CSWSH).

- security.ts: Origin-Allowlist (localhost/127.0.0.1:5173 und :8787, per
  ALLOWED_ORIGINS erweiterbar) und einfacher In-Memory-Rate-Limiter
- CORS auf die Allowlist begrenzt, zusätzlicher onRequest-Hook mit 403
- WebSocket-Handshake weist fremde Origins mit 403 ab
- ffmpeg: Containerformat per Magic Bytes bestimmt statt geraten, dazu
  -protocol_whitelist file — verhindert, dass Demuxer wie concat oder HLS
  auf lokale Pfade und URLs im hochgeladenen Inhalt zugreifen
- Rate-Limits auf /api/stt (10/min) und /api/tts (30/min), Textlänge bei TTS
  begrenzt, bodyLimit und MAX_AUDIO_BYTES auf 8 MB angeglichen
- Fehlerdetails (lokale Pfade, Upstream-Antworten) nur noch ins Server-Log;
  dafür Fastify-Logger aktiviert, sonst wären sie stillschweigend verworfen

Nebenbei: ollama.ts respektiert jetzt OLLAMA_URL statt localhost:11434
hartzukodieren, und ws wandert von devDependencies zu dependencies, da es
zur Laufzeit importiert wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1NXUpHbxTusqQmsjrFfbU
2026-08-26 16:54:31 +02:00
Jeuner
752198c5fa init: oxagenttwo — voice chat with local qwen3 (Ollama) + OpenRouter, Whisper STT, Piper TTS (Thorsten), sessions, thinking mode 2026-08-26 16:37:59 +02:00