Commit Graph
12 Commits
Author SHA1 Message Date
Richard b5bf97ae74 feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar 2026-08-10 10:48:34 +02:00
RichardandClaude Opus 5 61a93ffa58 Umsetzungsplaene fuer vier neue Agenten-Tools angelegt
Aus Richards Ideensammlung, nach Sichtung des vorhandenen Codes:

- FileRW-Papierkorb + Cleanup-Job: delete verschiebt nach .trash statt
  endgueltig zu loeschen, Aufraeumen ueber IToolJobProvider/ToolJobScheduler.
  Macht Loeschen reversibel und erlaubt damit, FileRW.delete im Staging von
  Approve auf Auto herunterzustufen.
- AgentInspector: lesende Aufsicht ueber andere Agenten. Beleg-basiert
  (Audit/Receipts/Taskboard) statt datei-basiert; harte Allowlist, damit
  AgentSettings.json mit den API-Keys nicht in einen LLM-Kontext geraet.
- AgentEditor-Haertung: Identity/Soul-Aenderungen ueber die vorhandene
  StagingPolicy freigabepflichtig machen, Selbstbearbeitung sperren,
  Audit + restore ergaenzen. Das Tool selbst existiert bereits.
- WebSearch: Suche als eigenes Tool, Lesen bleibt bei WebFetch hinter der
  Domain-Whitelist. Kein agent-reach (Klartext-Cookies, Fremdprozess).
  Trennung Rechercheagent / handelnder Agent gegen Prompt Injection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:13:09 +02:00
RichardandClaude Opus 4.8 1157d28588 Konfig- und Zustandsdateien atomar schreiben
File.WriteAllText kuerzt die Zieldatei zuerst auf null und fuellt sie dann. Bricht
der Vorgang dazwischen ab, ist der alte Inhalt weg und der neue unvollstaendig.

Das ist im Betrieb bereits eingetreten: In der Instanz TradingTeam lag eine
TokenUsage.json.corrupt_..., die die Fehlerbehandlung beiseitegelegt hatte. Der
Verbrauch bis dahin war verloren.

AtomicFile schreibt in eine Nebendatei, erzwingt das Schreiben auf die Platte und
ersetzt dann. Umgestellt sind ChatHistory, ChatContext, alle Instanz- und
Agentenkonfigurationen, Identity und Soul, die App-Einstellungen sowie der
Stock-Index.

Zum Ersetzen wurde das Windows-Verhalten gemessen statt vermutet. Mit einem Leser,
der die Zieldatei geoeffnet haelt:

  Freigabe des Lesers      File.Move   File.Replace
  Read                     scheitert   scheitert
  ReadWrite                scheitert   scheitert
  ReadWrite | Delete       scheitert   funktioniert

File.Move verlangt die Zieldatei exklusiv und scheitert deshalb immer, sobald
jemand sie geoeffnet hat. Daher File.Replace — und ein Lesehelfer
AtomicFile.ReadAllText, der das Loeschen freigibt, damit unsere eigenen Leser
keinen Schreiber blockieren. Die Leser in InstanceDirectoryManager und beim Laden
der Chatverlaeufe nutzen ihn jetzt.

Zusaetzlich ein Schloss je Zieldatei: Zwei gleichzeitige Schreibvorgaenge auf
dieselbe Datei sind ohnehin ein Rennen, ohne Serialisierung scheitern sie aber
zusaetzlich mit "Zugriff verweigert". Fuer fremde Leser wie Virenscanner bleibt
eine Wiederholung mit Wartezeit.

366 Tests gruen (218 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:47:38 +02:00
RichardandClaude Opus 4.8 4747835fa1 K1: Langzeitgedaechtnis fuer Agenten
Geplante Agenten begannen bei jedem Cron-Lauf bei null. Ein Agent, der alle 30
Minuten lief, wusste nichts von seinem letzten Durchgang — er rief dieselben
Quellen ab, zog dieselben Schluesse und konnte keine Entwicklung ueber Zeit
verfolgen. Das war zugleich die groesste Faehigkeitsluecke und eine dauerhafte
Token-Verschwendung.

Speicher-Fundament

SqliteStorage buendelt den Zugang zur Instanz-Datenbank und aktiviert WAL,
busy_timeout und Connection-Pooling. Vorher oeffnete jeder Aufruf eine Verbindung
ohne diese Einstellungen; bei mehreren gleichzeitig schreibenden Agenten gab das
"database is locked". Das sah nach einer Grenze von SQLite aus, war aber nur
fehlende Konfiguration. Zwei Tests decken das gezielt ab.

Gedaechtnis

Typisierte Tabelle statt JSON in einer Wert-Spalte — nur so laesst sich filtern,
sortieren und spaeter auswerten. Das Schema ist schlicht gehalten, damit eine
MySQL-Variante spaeter dieselbe Struktur mit wenigen Dialektunterschieden
bekommen kann.

Der wichtigste Teil ist der optionale Schluessel: Erneutes Merken darunter
aktualisiert den Eintrag, statt einen zweiten anzulegen. Ohne das wuechse das
Gedaechtnis eines halbstuendlich laufenden Agenten um 48 Eintraege pro Tag zur
selben Sache. Beobachtungen ohne Schluessel sammeln sich weiterhin an, wenn ein
Verlauf entstehen soll.

Der Abruf sortiert nach Wichtigkeit, dann Aktualitaet — wesentlich, weil das
Ergebnis begrenzt wird und bei einer Kappung das Wichtigste ueberleben muss.
Zusaetzlich greift eine Zeichenobergrenze, damit ein Abruf den Kontext nicht
sprengt.

Die Trennung privat/geteilt ist absichtlich dieselbe wie beim FileRW-Tool, damit
das Konzept fuer Agenten wiedererkennbar bleibt.

Beim Testen fiel auf, dass das Maskieren der LIKE-Platzhalter falsch war: Die
Zeichen wurden entfernt statt maskiert, wodurch eine Suche nach einem
Prozentzeichen zu einem leeren Muster und damit zu einem Treffer auf alles wurde.
Jetzt mit ESCAPE-Klausel.

338 Tests gruen (190 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 10:44:39 +02:00
RichardandClaude Opus 4.8 8604fa30c7 S7: Zugangsdaten in Konfigurationsdateien verschluesseln
OpenRouter-Schluessel, Datenbank-Verbindungszeichenfolgen samt Passwort,
Mail-Zugangsdaten und das Telegram-2FA-Passwort lagen im Klartext in
AgentSettings.json und InstanceSettings.json. Wer die Dateien lesen konnte — ein
Backup, eine Dateifreigabe, ein versehentlicher Commit — hatte alle Zugaenge.

SecretProtector nutzt DPAPI im Benutzerkontext: Die Werte lassen sich nur vom
selben Windows-Benutzer auf demselben Rechner lesen. Das schuetzt gegen
Weitergabe der Datei, nicht gegen einen Angreifer, der bereits als dieser
Benutzer laeuft — fuer einen lokal laufenden Dienst die angemessene Stufe.

Verschluesselte Werte tragen ein Praefix. Dadurch bleibt Klartext aus bestehenden
Konfigurationen lesbar und wird beim naechsten Speichern automatisch uebernommen;
vorhandene Installationen laufen ohne Zutun weiter.

Ein Wert, der sich nicht entschluesseln laesst — etwa nach Benutzer- oder
Rechnerwechsel — wird gemeldet statt stillschweigend als Klartext
durchgereicht. Sonst ginge ein unbrauchbarer Schluessel an die API und der
Fehler waere schwer zuzuordnen.

ConfigSecrets entscheidet anhand der Feldnamen, welche Werte betroffen sind. Das
ist noetig, weil die Tool-Konfiguration ein freies Woerterbuch ist. Beim Speichern
werden die Werte nur fuer den Schreibvorgang verschluesselt und danach wieder
entschluesselt, damit die laufende Instanz weiterarbeiten kann.

309 Tests gruen (161 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 10:21:08 +02:00
RichardandClaude Opus 4.8 cbb8ac22bc SSRF-Schutz, Pfadpruefung und Retry-Logik
S5 — Die Domain-Whitelist wurde nur auf die Ausgangs-URL angewendet, HttpClient
folgte Weiterleitungen aber selbst. Eine erlaubte Domain konnte damit auf
beliebige interne Adressen weiterleiten: Router, NAS, der Git-Server im LAN,
Cloud-Metadatendienste. Der Agent haette deren Inhalt zurueckgeliefert.

UrlGuard prueft Schema, private und lokale Netzbereiche sowie die Whitelist.
AllowAutoRedirect ist abgeschaltet; Weiterleitungen werden einzeln aufgeloest und
JEDER Zwischenschritt erneut geprueft, begrenzt auf fuenf Spruenge.

Nebenbei behoben: Die alte www-Behandlung ersetzte die Zeichenfolge ueber den
ganzen Hostnamen, aus mywww.example.com wurde myexample.com. Und die
Subdomain-Pruefung achtet jetzt auf den Punkt, sodass example.com.attacker.net
nicht mehr als Treffer fuer example.com durchgeht.

S6 — Die Pfadpruefung in FileRW verglich nur Zeichenketten-Praefixe. Ohne
abschliessenden Verzeichnistrenner erlaubte ein Root wie Agent-X\Workspace damit
auch Zugriffe auf Agent-X\Workspace-Backup. WorkspacePath vergleicht jetzt auf
Verzeichnisgrenzen und lehnt zusaetzlich absolute Pfade, UNC-Freigaben und
Alternate Data Streams ab.

Das Dateisystem wird in den Tests bewusst nicht abstrahiert — sie sollen die
echte Windows-Pfadsemantik pruefen. Eine Abstraktion wuerde genau die
Fehlerklasse verstecken, um die es geht.

B12 — Der Client warf bei jedem Nicht-2xx sofort; ein einzelnes HTTP 429 beendete
damit einen kompletten geplanten Lauf, obwohl Rate-Limits und kurze 5xx bei
OpenRouter Normalbetrieb sind. RetryPolicy wiederholt 408/425/429/5xx mit
exponentiellem Backoff und Streuung, respektiert ein Retry-After des Servers und
deckelt die Wartezeit. Dauerhafte Fehler wie 401 werden nicht wiederholt.

Die Wartefunktion ist injizierbar, damit die Tests nicht wirklich warten.

264 Tests gruen (116 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 00:24:03 +02:00
RichardandClaude Opus 4.8 e5067cae70 Sicherheitsluecken S1, S2 und S3 schliessen
Neues Testprojekt tests/ClawdDotNet.Tools.Tests. Die Angriffsfaelle aus der
Bestandsaufnahme bleiben darin dauerhaft als Testfaelle dokumentiert — zusammen
mit Gegenproben, damit die Fixes nicht zu streng werden und legitime Nutzung
blockieren.

S3 — DirectAPI gab die abgerufene URL als Quelle an das Modell zurueck, samt
API-Schluessel im Query-String. Der Schluessel landete damit im
Konversationskontext, wurde bei jedem Folgeschritt erneut gesendet, in
ChatContext.json geschrieben und in die Logs uebernommen. UrlSanitizer maskiert
sensible Query-Parameter; auch die Fehlermeldungen sind betroffen und werden
bereinigt.

S2 — Die Kanal-/Video-Angabe wurde ungeprueft in eine Argument-Zeichenkette fuer
yt-dlp interpoliert. UseShellExecute=false verhindert Shell-Metazeichen, nicht
aber Options-Injection: yt-dlp kennt die Option --exec, die beliebige Befehle
ausfuehrt. Kritisch, weil der Agent untrusted Inhalte verarbeitet — eine
Prompt-Injection darin konnte ihn dazu bringen, genau so einen Wert zu setzen.

YouTubeUrl validiert Handles und URLs gegen die zulaessigen YouTube-Hosts und
lehnt alles ab, was mit einem Bindestrich beginnt. Die Argumente gehen jetzt
einzeln ueber ProcessStartInfo.ArgumentList, die Adresse steht hinter dem
Optionsende-Trenner. Der ffmpeg-Aufruf wurde ebenso umgestellt.

S1 — Die Tabellen-Whitelist suchte den erlaubten Namen als Teilzeichenkette
irgendwo im Statement, auch in Kommentaren. Bei einer Freigabe fuer prices
genuegte deshalb ein DELETE auf users mit einem Kommentar, der prices enthielt,
um eine beliebige Tabelle zu loeschen. Umgekehrt galten harmlose Abfragen, die
ein Schluesselwort nur als Wert enthielten, faelschlich als Schreibzugriff.

SqlGuard entfernt zuerst Kommentare und String-Literale, lehnt mehrere Statements
ab, bestimmt die Operation am ersten Schluesselwort und extrahiert Tabellennamen
gezielt hinter FROM/JOIN/INTO/UPDATE/TABLE — inklusive kommagetrennter Listen mit
Aliassen. JEDE referenzierte Tabelle muss freigegeben sein, nicht irgendeine.
Ohne Whitelist wird nichts durchgelassen. Die MongoDB-Pruefung vergleicht den
Collection-Namen jetzt exakt statt per Teilzeichenkette.

178 Tests gruen (91 Core, 87 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:49:17 +02:00
RichardandClaude Opus 4.8 69b5704add Prompt-Caching, Tool-Ergebnis-Kappung und Kostenerfassung
T1 — Prompt-Caching. Bisher wurde bei jedem Schritt eines Runs der komplette
Prompt neu berechnet, inklusive Tool-Definitionen und System-Prompt, die sich nie
aendern. Bei zehn Schritten und einem 15k-Praefix sind das 150.000 statt 15.000
Eingabe-Tokens.

ChatMessage bekommt dafuer einen eigenen JsonConverter: Der Inhalt geht weiterhin
als String raus, bei gesetztem CacheBreakpoint jedoch als Blockarray mit
cache_control. Beim Lesen werden beide Formate akzeptiert, damit bestehende
ChatContext.json weiter geladen werden koennen.

PromptCache setzt zwei Breakpoints: einen auf den System-Prompt (deckt
Tool-Definitionen und System-Prompt ab) und einen rollierenden auf die letzte
Nachricht mit Inhalt. Vorherige Markierungen werden vorher entfernt, damit sie
sich nicht ansammeln. Aktivierung ueber promptCaching: auto (Default, aktiv fuer
Modelle mit Unterstuetzung), on oder off.

Der wichtigste Test dazu prueft die Praefix-Stabilitaet: Der System-Prompt muss
ueber alle Schritte zeichengleich serialisiert werden. Ein einziger Zeitstempel
darin wuerde den Cache still verwerfen — die Kosten blieben unveraendert, ohne
dass es irgendwo auffiele.

T9 — Usage liest prompt_tokens_details.cached_tokens; die Zahl wird bis in
AgentRunResult durchgereicht. Ohne sie liesse sich die Wirkung nicht belegen.

T2 — Tool-Ergebnisse werden jetzt zentral in ExecuteToolCallAsync gekappt
(maxToolResultChars, Default 16.000). Bisher konnte ein einzelner WebFetch mit
dem 512-KB-Standardlimit rund 130.000 Tokens in EINER Antwort erzeugen; die
Compaction griff erst danach, bezahlt war der Request laengst.

T3 — Die Zusammenfassung beim Kompaktieren laeuft ueber ein konfigurierbares
summaryModel (Default gemini-2.5-flash) statt ueber das teure Agentenmodell.

B4 — AgentRunResult fuehrt Prompt- und Completion-Tokens getrennt; die
Kostenanzeige schaetzte bisher 50/50, real liegt das Verhaeltnis eher bei 95:5.
Die veraltete Preistabelle bleibt offen.

Neue Einstellungen sind im PropertyGrid sichtbar und werden vom AgentEditor bei
neuen Agenten mitgeschrieben.

Alle 91 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:32:17 +02:00
RichardandClaude Opus 4.8 6bbe9f9a80 B2 beheben: Chat-Laeufe pro Agent serialisieren
Der Konversationskontext _chatContexts[agentId] ist eine geteilte List<ChatMessage>.
Nur der Lookup lief unter Lock, alle Add-Aufrufe im Schleifenkoerper waren
ungeschuetzt. Da WebView, ToolJob-Wakeups und AgentComm denselben Agenten
gleichzeitig ansprechen koennen, verschraenkten sich ihre Nachrichten zu einer
ungueltigen Tool-Sequenz, die die API mit HTTP 400 ablehnt.

ChatAsync laeuft jetzt hinter einem SemaphoreSlim(1,1) pro Agent; verschiedene
Agenten bleiben unabhaengig. Die Timeout-Uhr startet erst nach dem Eintritt,
damit Wartezeit in der Warteschlange den Lauf nicht aufzehrt.

Zwei Folgeprobleme mit demselben Ursprung:

- _runningChats hielt nur EINE CancellationTokenSource je Agent; der zweite Lauf
  ueberschrieb den ersten. AbortChat brach dadurch nur einen ab, der andere lief
  bis ins Run-Timeout. Jetzt eine Liste, die auch wartende Laeufe erfasst.
- ExecuteToolCallAsync fing OperationCanceledException mit ab und gab sie als
  Tool-Ergebnis zurueck, wodurch der Abbruch erst einen Schritt spaeter griff.
  Cancellation wird nun durchgereicht.

Ausserdem: send_message an den eigenen Agenten wird abgelehnt — es waere mit dem
neuen Gate in einen Deadlock gelaufen.

Neu: GetChatContext(agentId) als Momentaufnahme des Kontexts, fuer Diagnose und
Kontextgroessen-Anzeige.

Build-Fix: Das WinForms-Projekt globbt **/*.cs und kompilierte dadurch die
Test-Quellen mit. tests\** wird jetzt wie src\** ausgeschlossen.

Alle 49 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:55:04 +02:00
RichardandClaude Opus 4.8 eae13771cf B1, B14 und B3 beheben: Compaction-Paarung und Token-Semantik
B1 — Die Compaction behielt blind die letzten 6 Nachrichten. Fiel diese Grenze
mitten in eine Tool-Sequenz, entstand eine tool-Antwort ohne zugehoerigen
assistant-tool_call; die API lehnt das mit HTTP 400 ab. FindSafeTailStart
verschiebt die Grenze jetzt rueckwaerts auf eine Blockgrenze.

B14 — Bei Konversationen mit hoechstens 6 Nachrichten enthielt der Tail auch die
system-Nachricht, die anschliessend ein zweites Mal angehaengt wurde. Ergebnis
war ein doppelter System-Prompt und eine duplizierte Konversation — die
Compaction vergroesserte den Kontext, statt ihn zu verkleinern. Der Tail beginnt
nun grundsaetzlich hinter dem System-Prompt; liegt davor nichts Nennenswertes,
wird die Kompaktierung uebersprungen. Gefunden durch den Property-Test.

Nebeneffekt: Zusammengefasst wird nur noch der Teil, der tatsaechlich wegfaellt.
Der Tail bleibt woertlich erhalten und musste bisher doppelt bezahlt werden.

B3 — maxTokens zaehlte kumulativ ueber alle Schritte, wurde aber wie eine
Kontextgrenze konfiguriert. Da jeder Schritt den vollen Kontext erneut sendet,
brach ein Chat mit 20k Kontext nach vier Schritten ab. Aufgeteilt in
maxCumulativeTokens (Kostenbudget, Default 500k) und maxContextTokens
(Kontextgroesse). Alte Konfigurationen werden beim Laden migriert, die
Fehlermeldungen unterscheiden jetzt Schritt- und Kostenlimit.

Alle 31 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:15:50 +02:00
RichardandClaude Opus 4.8 667cecce25 Testfundament aufbauen und Bestandsaufnahme dokumentieren
IChatCompletionClient aus OpenRouterClient extrahiert, damit AgentEngine und
ContextCompactor ohne echte API-Aufrufe testbar sind.

Neues Testprojekt tests/ClawdDotNet.Core.Tests (xUnit, Shouldly, NSubstitute,
FsCheck) mit:
- FakeChatClient (programmierbare Antwortfolgen, Deep-Copy der Requests)
- ContextInvariants (prueft die API-Regeln fuer tool_call-Paarung)
- Conversation-Builder fuer gueltige Testkonversationen
- 26 Tests: Compaction, LoopGuard, 2 Property-Tests

10 Tests sind bewusst rot — sie reproduzieren die Bugs B1, B3 und B14 aus der
Bestandsaufnahme und werden mit den Fixes gruen.

Ausserdem: fehlende Tool-Projekte in slnx ergaenzt, Test-Pakete im
packageSourceMapping der NuGet.Config eingetragen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:07:54 +02:00
RichardandClaude Opus 4.8 2fed388c99 Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git.
- .NET 10 WinForms Anwendung (Multi-Agent / Tool-System)
- .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:21:46 +02:00