Files
ClawdDotNet/docs/Bestandsaufnahme-2026-07.md
T
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

554 lines
28 KiB
Markdown
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.
# ClawdDotNet — Bestandsaufnahme (Juli 2026)
Vollständiges Review von Core-Engine, Security-Layer, Tools, Scheduling und UI.
Stand: Commit `92e50d3`.
**Kurzfassung:** Die Architektur ist gut — die Trennung Core/Tools/UI, das `IAgentTool`-Interface,
der Tool-Job-Mechanismus und das Instanz-Konzept tragen. Die Probleme liegen fast alle in
drei Bereichen: (1) Sicherheitsprüfungen, die als String-Vergleiche implementiert sind und
deshalb umgehbar sind, (2) fehlende Nebenläufigkeits-Absicherung im geteilten Chat-Kontext,
(3) kein Prompt-Caching — der mit Abstand größte Kostenhebel.
---
## 1. Kritische Sicherheitslücken (P0)
### S1 — DatabaseTool: Tabellen-Whitelist ist vollständig umgehbar
[`DatabaseTool.cs:218`](../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L218)
```csharp
return allowed.Any(t => t != null && inputLower.Contains(t));
```
Die Prüfung sucht den erlaubten Tabellennamen *irgendwo* im SQL-String — auch in einem
Kommentar oder String-Literal. Bei `allowedTables: ["prices"]` und `accessLevel: ReadWrite`:
```sql
DELETE FROM users -- prices
```
`IsWriteAttempt` → true (ReadWrite erlaubt), `IsTableAllowed` → true (enthält "prices").
Die `users`-Tabelle wird gelöscht. Gleiches gilt für jede beliebige andere Tabelle.
Zusätzlich:
- `IsWriteAttempt`/`IsAdminAttempt` ([Z.189/195](../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L189))
sind ebenfalls Substring-Prüfungen → False Positives (`SELECT * FROM prices WHERE note='update'`
wird als Schreibzugriff blockiert).
- Mehrere Statements pro Aufruf sind nicht unterbunden (`;`-Verkettung).
- Query-Ergebnisse haben kein `LIMIT` → eine Tabelle mit 100k Zeilen landet komplett im Kontext.
**Fix:** Echtes SQL-Parsing (z.B. via `Microsoft.SqlServer.TransactSql.ScriptDom` für MSSQL bzw.
einem generischen Tokenizer), oder — deutlich robuster — den freien SQL-Zugriff durch
vordefinierte, parametrisierte Named Queries ersetzen. Freies SQL vom LLM ist grundsätzlich
schwer abzusichern.
### S2 — SocialMediaManager: Kommando-Injection über yt-dlp
[`SocialMediaManagerTool.cs:442`](../src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs#L442), auch Z.484 und Z.490
```csharp
var psi = new ProcessStartInfo(ytDlpPath, $"--print \"%(id)s\" --playlist-end 1 {channelUrl}")
```
`channelUrl` stammt aus dem Agenten-Input und wird ungequotet in die Argumentliste interpoliert.
`UseShellExecute = false` verhindert Shell-Metazeichen, aber **nicht** Options-Injection:
yt-dlp kennt `--exec`, das beliebige Befehle nach dem Download ausführt.
Ein Wert wie `--exec "cmd /c ..." https://youtube.com/...` führt zu Codeausführung.
Kritisch wird das, weil der Agent untrusted Content verarbeitet (YouTube-Titel, X-Posts,
Mails) — eine Prompt-Injection in abgerufenen Inhalten kann den Agenten dazu bringen,
genau so einen "URL"-Parameter zu setzen.
**Fix:** `ProcessStartInfo.ArgumentList` verwenden (quotet korrekt), zusätzlich die URL gegen
eine Regex validieren (`^https://(www\.)?(youtube\.com|youtu\.be)/…`) und `--`-Separator
vor dem URL-Argument setzen.
### S3 — DirectAPI: API-Keys landen im Modell-Kontext
[`DirectAPITool.cs:83`](../src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs#L83), auch Z.99, 154, 177, 263
```csharp
var url = $"https://api.twelvedata.com/quote?symbol={symbol}&apikey={apiKey}";
return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value); // url → "source"
```
Die vollständige URL inklusive `apikey=` wird als `source` an das Modell zurückgegeben. Damit
landet der Key:
- im Chat-Kontext → wird bei **jedem** Folge-Request erneut an OpenRouter gesendet,
- in `ChatContext.json` auf der Platte,
- in den Logs.
**Fix:** Key als Header senden statt als Query-Parameter (wo möglich), und in `source`
grundsätzlich eine bereinigte URL zurückgeben (Query-String entfernen oder `apikey` maskieren).
### S4 — PermissionGate ist faktisch wirkungslos
[`PermissionGate.cs:7`](../src/ClawdDotNet.Core/Security/PermissionGate.cs#L7)
```csharp
public bool IsAllowed(string agentId, string toolName, AgentConfig agentConfig)
=> agentConfig.Tools.ContainsKey(toolName);
```
Der einzige Check ist "Tool ist dem Agenten zugewiesen" — was `ToolRegistry.GetForAgent` ohnehin
schon filtert. Der `agentId`-Parameter wird nicht verwendet. Es gibt keine Aktions-Granularität
(lesen vs. schreiben vs. senden), kein Rate-Limiting, kein Audit. Die gesamte Sicherheit liegt
ad-hoc in den einzelnen Tools — jedes mit eigener, inkonsistenter Logik.
**Fix:** Das Gate zum zentralen Policy-Punkt ausbauen: Aktions-Ebene (`Tool.Action`),
Rate-Limits pro Agent/Tool/Zeitfenster, Audit-Log jedes Aufrufs mit Argumenten, und ein
Approval-Hook für irreversible Aktionen (siehe F-A1).
### S5 — WebFetch: kein SSRF-Schutz bei Redirects
[`WebFetchTool.cs:58`](../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L58)
Die Domain-Whitelist wird nur auf die *ursprüngliche* URL angewendet. `HttpClient` folgt
Redirects standardmäßig — eine erlaubte Domain kann auf `http://169.254.169.254/`,
`http://localhost:8418/` (dein Gitea!) oder beliebige interne Hosts weiterleiten.
Nebenbei: `uri.Host.Replace("www.", "")` ([Z.59](../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L59))
ersetzt das Fragment überall im Hostnamen, nicht nur am Anfang.
**Fix:** `HttpClientHandler { AllowAutoRedirect = false }` und Redirects manuell auflösen,
dabei jede Zwischen-URL erneut gegen die Whitelist prüfen. Zusätzlich private IP-Bereiche
(RFC1918, Loopback, Link-Local) hart blockieren.
### S6 — FileRW: Path-Traversal-Prüfung per Präfix
[`FileRWTool.cs:211`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L211)
```csharp
if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
```
Ohne abschließenden Verzeichnis-Separator: Root `C:\Instances\Dev\Workspace` erlaubt Zugriff auf
`C:\Instances\Dev\Workspace-Backup\…`. In der aktuellen Verzeichnisstruktur nicht direkt
ausnutzbar, aber eine Zeitbombe.
**Fix:** `rootPath` mit `Path.DirectorySeparatorChar` terminieren, oder besser
`Path.GetRelativePath` + Prüfung auf `..`-Präfix.
### S7 — Secrets im Klartext
`openRouterApiKey`, DB-`connectionString` (mit Passwort), Mail-`password`, Telegram-`password2FA`
liegen unverschlüsselt in `AgentSettings.json` / `InstanceConfig.json`
([`InstanceConfig.cs:46`](../src/ClawdDotNet.Core/Config/InstanceConfig.cs#L46)).
**Fix:** DPAPI (`ProtectedData.Protect` mit `CurrentUser`-Scope) für alle Secret-Felder,
oder Windows Credential Manager. Zumindest sollten die Felder beim Speichern verschlüsselt
und erst zur Laufzeit entschlüsselt werden.
---
## 2. Bugs (P1)
### B1 — ContextCompactor zerstört die tool_call-Paarung ⚠️
[`ContextCompactor.cs:140`](../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
```csharp
var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
messages.Clear();
messages.AddRange(tail);
```
Die letzten 6 Nachrichten werden blind übernommen. Wenn die Grenze mitten in einer
Tool-Sequenz liegt, beginnt der Tail mit einer `tool`-Nachricht, deren zugehörige
`assistant`-Nachricht mit `tool_calls` gerade gelöscht wurde. Die OpenRouter/Anthropic-API
lehnt das mit HTTP 400 ab ("tool_result without preceding tool_use").
Das passiert **genau dann**, wenn der Kontext lang wird — also im wichtigsten Moment.
Bei einer typischen Sequenz `assistant(2 tool_calls) → tool → tool` ist die Wahrscheinlichkeit
hoch.
**Fix:** Die Tail-Grenze auf einen sicheren Schnittpunkt verschieben — rückwärts suchen bis zu
einer Nachricht mit Rolle `user` oder `assistant` ohne `tool_calls`. Analog muss ein
`assistant` mit `tool_calls` am Ende immer seine vollständigen `tool`-Antworten behalten.
### B2 — Race Condition auf dem geteilten Chat-Kontext ⚠️
[`AgentEngine.cs:250`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L250) und Z.268/316/322
`_chatContexts[agentId]` ist eine geteilte `List<ChatMessage>`. Nur der *Lookup* läuft unter
`_lock` — alle `messages.Add(...)` im Loop passieren ungeschützt.
Für denselben Agenten kann `ChatAsync` mehrfach parallel laufen:
- Telegram-/Mail-ToolJob weckt den Agenten (`ToolJobScheduler.cs:187`),
- der Benutzer schickt gleichzeitig eine WebView-Nachricht,
- ein anderer Agent ruft `send_message` auf (`AgentEngine.cs:528`).
Folgen: `List<T>`-Korruption (nicht threadsicher), verschränkte Tool-Call-Sequenzen →
API-400-Fehler, und `_runningChats[agentId] = linkedCts` überschreibt den vorherigen Eintrag,
sodass `AbortChat` nur den zuletzt gestarteten Lauf abbricht.
**Fix:** Pro Agent ein `SemaphoreSlim(1,1)`, das den gesamten `ChatAsync`-Durchlauf serialisiert.
Wartende Nachrichten in eine Queue statt parallel starten.
Beim Umsetzen kamen zwei Folgeprobleme dazu, die denselben Ursprung haben:
- `ExecuteToolCallAsync` fing `OperationCanceledException` mit ab und gab sie als
Tool-Fehlerergebnis zurück. Der Abbruch griff dadurch erst einen Schritt später.
- `send_message` an den eigenen Agenten wäre mit dem neuen Gate in einen Deadlock gelaufen
(der laufende Chat hält es bereits) — wird jetzt abgelehnt.
### B3 — `maxTokens` vermischt Abrechnungs-Budget und Kontextgröße ⚠️
[`LoopGuard.cs:22`](../src/ClawdDotNet.Core/Engine/LoopGuard.cs#L22), Defaults in
[`AgentConfig.cs:105`](../src/ClawdDotNet.Core/Config/AgentConfig.cs#L105)
`RecordTokens` summiert `response.Usage.TotalTokens` über alle Schritte. Da jeder Schritt den
**kompletten** Kontext erneut sendet, wächst diese Summe quadratisch:
| Schritt | Kontext | kumuliert |
|---|---|---|
| 1 | 20.000 | 20.000 |
| 2 | 21.000 | 41.000 |
| 3 | 22.000 | 63.000 |
| 4 | 23.000 | **86.000** → Abbruch |
Mit `maxTokens: 80_000` bricht ein völlig normaler Chat nach 4 Schritten mit
"Max tokens exceeded" ab, obwohl der Kontext nie über 23k lag. Das ist vermutlich die
häufigste Frustquelle im laufenden Betrieb.
**Fix:** Zwei getrennte Begriffe:
- `maxCumulativeTokens` — Kostenbudget pro Run (bewusst hoch, z.B. 500k)
- `maxContextTokens` — Kontextgröße, steuert die Compaction (existiert bereits)
und beide getrennt im UI ausweisen.
### B4 — Kostenanzeige ist doppelt falsch
[`frm_main.cs:292`](../frm_main.cs#L292)
```csharp
_statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2);
```
1. **50/50-Aufteilung**: Reales Verhältnis in Agenten-Loops ist eher 95:5 (Prompt:Completion).
Da Output ~5x teurer ist, überschätzt die Anzeige die Kosten um Faktor ~2,5.
2. **Modell fehlt in der Preistabelle**: `AgentConfig.Model` hat den Default
`anthropic/claude-sonnet-4-5` — dieser Eintrag existiert in `ModelPricing`
([`OpenRouterStatusService.cs:15`](../Services/OpenRouterStatusService.cs#L15)) nicht.
`CalculateCost` gibt dann stillschweigend `0` zurück.
Die Tabelle ist zudem veraltet (`claude-sonnet-4`, `claude-opus-4`, `claude-haiku-4.5`).
**Fix:** `Usage` um `PromptTokens`/`CompletionTokens` erweitern und bis in `AgentRunResult`
durchreichen. Preise live vom `/models`-Endpoint holen (liefert `pricing.prompt`/`pricing.completion`)
statt hartzucodieren. Bei unbekanntem Modell sichtbar warnen statt 0 anzuzeigen.
### B5 — Tool-Ergebnisse landen ungekappt im Kontext
Ein einziger `WebFetch` mit dem Default `maxResponseKb: 512`
([`WebFetchTool.cs:90`](../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L90)) erzeugt bis zu
512 KB Text ≈ **130.000 Tokens** in einer einzigen Tool-Antwort. `FileRW.read`
([`FileRWTool.cs:245`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L245)) hat gar kein Limit,
`Database.query` und `Mail.read_inbox` ebenfalls nicht.
Gekürzt wird erst nachträglich in der Compaction — und dort nur außerhalb der letzten
6 Nachrichten. Der teure Request ist zu dem Zeitpunkt längst bezahlt.
**Fix:** Kappung beim Einfügen in `ExecuteToolCallAsync`
([`AgentEngine.cs:668`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L668)) — zentral, für alle
Tools, mit konfigurierbarem `maxToolResultTokens` und einem Hinweis an das Modell, dass gekürzt
wurde (inkl. Angebot, gezielt nachzulesen).
### B6 — `Task.Delay` wirft bei langen Cron-Intervallen
[`AgentScheduler.cs:89`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L89) und
[`ToolJobScheduler.cs:126`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L126)
`Task.Delay` wirft `ArgumentOutOfRangeException` bei Werten über ~24,8 Tagen. Ein jährlicher
Cron (`0 3 1 1 *`) erzeugt eine Wartezeit von bis zu 365 Tagen. Der `catch` fängt nur
`OperationCanceledException` — die Exception beendet den Scheduler-Task still. Da die Tasks in
`_schedulerTasks` nie beobachtet werden (`await` erst beim Dispose), fällt das nicht auf: der
Job läuft ab dann einfach nie wieder.
**Fix:** Delay auf max. 1 Tag deckeln und in einer Schleife erneut prüfen. Zusätzlich alle
Scheduler-Tasks mit einem `ContinueWith`-Fehler-Logger versehen.
### B7 — Cron rechnet in Lokalzeit
[`CronExpression.cs:51`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L51) mit
`DateTime.Now`. Bei Zeitumstellung: im Oktober läuft ein `0 2 * * *`-Job doppelt, im März gar nicht.
**Fix:** Intern in UTC rechnen und nur für die Anzeige konvertieren, oder `TimeZoneInfo`
explizit berücksichtigen.
### B8 — Keine Rekursionsbremse bei AgentComm / AgentSpawn
[`AgentEngine.cs:510`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L510)
Agent A ruft `send_message(B)``ChatAsync(B)` → B ruft `send_message(A)` → … Es gibt keine
Tiefenbegrenzung und keinen Zyklus-Check. Bei `SpawnAgentAsync` schützt immerhin der
`IsRunning`-Check, bei `send_message` nicht. Begrenzt wird das nur durch die Timeouts —
in der Zwischenzeit brennt jeder Hop einen vollständigen LLM-Run.
**Fix:** Aufruf-Tiefe im `AgentToolContext` mitführen (`maxDelegationDepth`, Default 3) und
den Aufrufpfad zur Zyklenerkennung mitgeben.
### B9 — `index_Count` liest die Datei außerhalb des Locks erneut
[`FileRWTool.cs:569`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L569) ruft
[`index_Count`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L572) auf, nachdem das Lock
freigegeben wurde — Race mit parallelen `stock_add`-Aufrufen, plus ein überflüssiges
vollständiges Parsen der Index-Datei. Der Zähler ist innerhalb des Locks ohnehin bekannt
(`index.Count`).
### B10 — `PersistChatState` schreibt bei jedem Eintrag alles neu
[`AgentEngine.cs:593`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L593)
Vollständige History **und** vollständiger Kontext werden als `WriteIndented`-JSON bei jedem
einzelnen Chat-Eintrag rausgeschrieben → O(n²) Schreiblast über eine Sitzung. Bei einem Agenten
mit 500 Nachrichten sind das mehrere MB pro Nachricht.
**Fix:** History als Append-only JSONL, Kontext nur periodisch/beim Beenden persistieren,
`WriteIndented = false`.
### B11 — Kein `max_tokens` im Request
[`AgentEngine.cs:105`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L105) und Z.279 setzen
`ChatRequest.MaxTokens` nie. Ein Modell kann in einem Schritt sein volles Output-Limit
ausschöpfen.
### B12 — Kein Retry/Backoff
[`OpenRouterClient.cs:42`](../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L42) wirft bei jedem
Nicht-2xx sofort. Ein einzelnes HTTP 429 killt einen kompletten geplanten Run. Bei OpenRouter
sind 429/502/503 im Normalbetrieb zu erwarten.
**Fix:** Polly o.ä. mit exponentiellem Backoff + Jitter für 429/5xx, `Retry-After` respektieren.
### B14 — Compaction dupliziert den System-Prompt bei kurzen Konversationen ⚠️
[`ContextCompactor.cs:140`](../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
*Gefunden durch den Property-Test, nicht beim Lesen des Codes.*
```csharp
var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
messages.Clear();
if (systemMsg is not null) messages.Add(systemMsg); // ← System-Prompt
messages.AddRange(tail); // ← enthält ihn nochmal
```
Bei `messages.Count <= 6` ist `Skip(0)` — der Tail enthält also **alle** Nachrichten
einschließlich der System-Nachricht. Diese wird danach ein zweites Mal angehängt.
Ergebnis: zwei `system`-Nachrichten, die zweite mitten in der Konversation. Zusätzlich wird
die komplette Konversation dupliziert, obwohl sie gerade zusammengefasst wurde — die
Compaction *vergrößert* den Kontext in diesem Fall, statt ihn zu verkleinern.
Erreichbar, sobald wenige Nachrichten die Token-Schwelle reißen — z.B. ein einzelner
`WebFetch` mit 500 KB Ergebnis (siehe B5). Minimales Gegenbeispiel aus FsCheck:
System-Prompt + eine User-Nachricht.
**Fix:** Den System-Prompt aus dem Tail herausfiltern, bevor er wieder angehängt wird —
und den Tail generell erst ab der Position hinter der System-Nachricht bilden.
### B13 — Inkonsistente `instanceId`
`RunAsync`/`ChatAsync` bekommen `instanceId` als Parameter, `SendMessageAsync`/`SpawnAgentAsync`
verwenden dagegen das Feld `_instanceId`, das nur gesetzt wird, wenn
`SetAgentConfigProvider` aufgerufen wurde — sonst `""`.
---
## 3. Konzeptfehler (P2)
### K1 — Geplante Agenten haben kein Gedächtnis
`RunAsync` baut bei jedem Cron-Tick eine frische Nachrichtenliste
([`AgentEngine.cs:89`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L89)). Ein Agent, der alle
30 Minuten läuft, weiß nichts von seinem letzten Lauf: er ruft dieselben Quellen erneut ab,
zieht dieselben Schlüsse und kann keine Entwicklung über Zeit verfolgen.
Das ist gleichzeitig die größte Fähigkeitslücke **und** eine dauerhafte Token-Verschwendung.
Der `IStateStore` (SQLite) existiert bereits — es fehlt das Konzept darüber.
### K2 — Prompt-Injection ist unbehandelt
Agenten lesen untrusted Content (Web, Mail, Telegram, X, YouTube-Transkripte) und besitzen
gleichzeitig ausgehende Fähigkeiten (Mail senden, X posten, Dateien schreiben, DB schreiben).
Tool-Ergebnisse werden ununterscheidbar von echten Anweisungen in den Kontext gelegt.
Das ist die klassische gefährliche Kombination aus Datenzugriff, untrusted Input und
Exfiltrations-Kanal. Ein präparierter Tweet oder eine Mail kann den Agenten steuern.
**Ansatz:** Tool-Ergebnisse konsequent als Daten rahmen (`<untrusted_content source="...">`),
im System-Prompt verankern, dass daraus keine Anweisungen befolgt werden, und irreversible
Aktionen an eine Freigabe koppeln (siehe F-A1).
### K3 — Keine Tests
Kein Testprojekt in `ClawdDotNet.slnx`. Bei einer Engine mit Zustandsmaschine, Compaction,
Scheduling und Nebenläufigkeit ist das riskant — die Bugs B1 und B2 wären mit
Unit-Tests sofort aufgefallen.
**Ansatz:** `ClawdDotNet.Core.Tests` mit einem Fake-`OpenRouterClient` (Interface extrahieren).
Erste Kandidaten: `CronExpression`, `ContextCompactor` (Paarungs-Invarianten!), `LoopGuard`,
`FileRWTool`-Pfadprüfungen.
### K4 — Kein Streaming
`request.Stream = false` fest verdrahtet ([`OpenRouterClient.cs:34`](../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L34)).
Bei langen Antworten wirkt die UI eingefroren; es gibt nur den Typing-Indicator.
### K5 — Kein Kostenlimit
Es gibt kein hartes Budget pro Agent/Instanz/Tag. Ein Agent in einer Schleife (siehe B8) kann
unbeaufsichtigt Guthaben verbrennen. Die Credits-Anzeige ist rein informativ.
### K6 — Chat-Historie wächst unbegrenzt
`_chatHistories` und die zugehörigen JSON-Dateien werden nie rotiert oder archiviert.
---
## 4. Token-Optimierung
Das ist der Bereich mit dem größten unmittelbaren Hebel. Nach Wirkung sortiert:
### T1 — Prompt-Caching einführen (Einsparung: 6090 % der Input-Tokens) ★★★
Der wichtigste Punkt. Aktuell wird bei **jedem** Schritt der gesamte Prompt neu gesendet und
voll berechnet — inklusive System-Prompt (Identity + Soul + SystemPrompt) und aller
Tool-Definitionen. Das ist ein stabiler Präfix, der sich innerhalb eines Runs nie ändert.
Anthropic-Modelle über OpenRouter unterstützen `cache_control`-Breakpoints: gecachte
Input-Tokens kosten nur ~10 % des Normalpreises (Schreiben in den Cache kostet einmalig ~125 %).
**Rechenbeispiel** — 10-Schritt-Run, Präfix 15k Tokens (System + 10 Tool-Definitionen):
| | Input-Tokens (Präfix) | relative Kosten |
|---|---|---|
| heute | 10 × 15.000 = 150.000 | 100 % |
| mit Caching | 15.000 (×1,25) + 9 × 15.000 (×0,1) ≈ 32.250 effektiv | **~21 %** |
Umsetzung:
1. `ChatMessage.Content` muss das Array-Format unterstützen
(`[{ "type": "text", "text": "...", "cache_control": { "type": "ephemeral" } }]`).
Aktuell ist es ein reiner `string` ([`ChatMessage.cs:12`](../src/ClawdDotNet.Core/Api/Models/ChatMessage.cs#L12)).
2. Cache-Breakpoint ans Ende des System-Prompts und ans Ende der Tool-Definitionen setzen.
3. Optional einen dritten Breakpoint nach der letzten stabilen Konversationsgrenze (rollierend).
4. `Usage` um `prompt_tokens_details.cached_tokens` erweitern, damit der Effekt messbar wird.
Wichtig: Die Reihenfolge der Nachrichten vor dem Breakpoint muss **byte-identisch** bleiben —
d.h. keine Zeitstempel oder wechselnden Hinweise im System-Prompt.
### T2 — Tool-Ergebnisse beim Einfügen kappen (Einsparung: stark schwankend, bis 90 %) ★★★
Siehe B5. Ein einziger ungekappter `WebFetch` kann mehr Tokens erzeugen als der gesamte
restliche Run. Zentral in `ExecuteToolCallAsync` lösen, nicht pro Tool.
Sinnvolle Defaults: 4.000 Tokens pro Tool-Ergebnis, mit Kürzungshinweis und der Möglichkeit,
gezielt weiterzulesen (Offset-Parameter bei `FileRW.read`, `LIMIT`/`OFFSET` bei `Database.query`).
### T3 — Günstiges Modell für die Compaction (Einsparung: ~95 % der Compaction-Kosten) ★★
[`ContextCompactor.cs:114`](../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L114) nutzt
`model` — also das teure Modell des Agenten — um bis zu 30k Zeichen zusammenzufassen.
Bei Opus kostet eine einzige Compaction so mehr als der halbe Run.
**Fix:** `summaryModel` konfigurierbar machen, Default z.B. `google/gemini-2.5-flash` oder
`anthropic/claude-haiku-4.5`. Zusammenfassen ist eine anspruchslose Aufgabe.
### T4 — Proaktiv statt reaktiv kompaktieren ★★
`CompactIfNeededAsync` läuft **nach** dem API-Call
([`AgentEngine.cs:123`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L123)) und nutzt die
`promptTokens` der gerade bezahlten Anfrage. Der überfüllte Prompt wurde also bereits berechnet.
**Fix:** Vor dem Senden prüfen (mit `EstimateTokens`, das es schon gibt) und erst dann den
Request bauen. Die gemessenen `promptTokens` dienen zur Kalibrierung der Schätzung.
### T5 — `maxTokens`-Semantik korrigieren ★★
Siehe B3. Aktuell führt der Fehler dazu, dass Läufe abbrechen und **neu gestartet** werden —
was die Tokenkosten verdoppelt statt sie zu begrenzen.
### T6 — Tool-Definitionen verschlanken ★
Die Beschreibungen sind großzügig: `FileRW` allein hat ~500 Zeichen Description plus ein
Schema mit 11 Properties ≈ 900 Tokens
([`FileRWTool.cs:26`](../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L26)). Bei 8 zugewiesenen
Tools sind das schnell 46k Tokens — bei **jedem** Schritt (ohne Caching).
Maßnahmen:
- Beschreibungen auf das Nötige kürzen, Beispiele in den System-Prompt statt ins Schema.
- `stock_add` aus `FileRW` herauslösen — es ist ein fachfremdes Feature, das das Schema aufbläht,
auch für Agenten, die es nie nutzen.
- Für Agenten mit vielen Tools: zweistufiges Laden (`list_toolsets``load_toolset`).
### T7 — `RunAsync` statt `ChatAsync` für Job-Wakeups prüfen ★
[`ToolJobScheduler.cs:186`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L186)
entscheidet über `UseChatContext`. Mit `true` wird die komplette (potenziell riesige)
Chat-Historie in einen "Prüfe neue Mails"-Tick gezogen. Für zustandslose Ticks ist `RunAsync`
um Größenordnungen günstiger — der Default sollte bewusst gesetzt und im UI erklärt sein.
### T8 — `max_tokens` setzen ★
Siehe B11. Verhindert Ausreißer bei der Ausgabe (die teuerste Token-Art).
### T9 — Caching-Wirkung messbar machen ★
Ohne `cached_tokens` im `Usage`-Modell lässt sich der Erfolg von T1 nicht belegen. Sollte
zusammen mit T1 kommen und im Token-Dashboard auftauchen.
### Erwartete Gesamtwirkung
T1 + T2 + T3 + T4 zusammen sollten die Kosten eines typischen Multi-Step-Runs auf
**etwa ein Viertel bis ein Fünftel** senken — ohne Qualitätsverlust, da nur Redundanz und
Ballast entfernt werden.
---
## 5. Vorschläge für Erweiterungen
### Neue Tools
| Tool | Nutzen |
|---|---|
| **Memory** | Strukturiertes Langzeitgedächtnis (Fakten, Entscheidungen, Beobachtungen mit Zeitstempel) auf dem vorhandenen SQLite-Store. Löst K1 und spart massiv Tokens, weil Wissen nicht neu hergeleitet wird. **Höchste Priorität.** |
| **WebSearch** | Aktuell können Agenten nur bekannte Domains abrufen (`WebFetch`), aber nichts finden. Brave/Tavily/SearXNG-Anbindung. |
| **Http** | Generisches REST-Tool mit Allowlist pro Agent. `DirectAPI` ist fest auf Finanz-Provider verdrahtet — jede neue API erfordert heute Code. |
| **Shell** | Sandboxed, mit Kommando-Allowlist. Ersetzt die hartcodierten yt-dlp/ffmpeg-Aufrufe (und behebt S2 gleich mit). |
| **Git** | Für den "Senior Developer"-Agenten — jetzt, wo das Projekt in Git liegt. |
| **Vision** | Charts/Screenshots analysieren — für das Trading-Team naheliegend. |
| **Notify** | Einheitliche Benachrichtigung (Telegram existiert; ntfy/Discord/Push ergänzen). |
### Neue Konzepte
**F-A1 — Freigabe-Workflow (Human-in-the-Loop)**
Irreversible Aktionen (Mail senden, X posten, DB-Schreibzugriff, Datei löschen) landen in einer
Freigabe-Queue im Hauptfenster, statt sofort ausgeführt zu werden. Pro Tool/Aktion
konfigurierbar: `auto` | `approve` | `deny`. Das entschärft K2 und S1/S2 erheblich.
**F-A2 — Audit-Log**
Jeder Tool-Aufruf mit Agent, Zeitstempel, Argumenten, Ergebnis-Status in eine eigene
SQLite-Tabelle. Aktuell existiert nur `LogDebug` — nach einem Zwischenfall ist nicht
rekonstruierbar, was ein Agent getan hat.
**F-A3 — Budget-Guard**
Hartes USD-Limit pro Agent und pro Instanz und Tag. Bei Überschreitung wird der Agent pausiert
und im UI markiert. Setzt B4 (korrekte Kostenerfassung) voraus.
**F-A4 — Run-Historie**
`AgentRunResult` wird aktuell nur im Speicher als "letztes Ergebnis" gehalten
([`AgentScheduler.cs:14`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L14)).
Persistierte Runs (mit Schritten, Tokens, Kosten, Fehlern) wären die Grundlage für Diagnose
und Kostenanalyse.
**F-A5 — Task-Queue statt rekursivem `send_message`**
Agent-zu-Agent-Kommunikation läuft heute als synchrone Rekursion (B8). Eine echte
Aufgaben-Queue mit Zustand (`pending`/`running`/`done`) wäre robuster, nachvollziehbar und
würde Zyklen strukturell verhindern.
**F-A6 — Secret-Store**
DPAPI-verschlüsselte Secrets (S7), mit UI zum Setzen/Rotieren.
**F-A7 — Testprojekt**
Siehe K3.
---
## 6. Vorgeschlagene Reihenfolge
**Sofort — es blockiert oder gefährdet den Betrieb**
1. ~~B1 Compaction-Paarung (bricht produktiv ab)~~ ✅ behoben
2. ~~B3 `maxTokens`-Semantik (bricht produktiv ab)~~ ✅ behoben
2b. ~~B14 System-Prompt-Duplikat~~ ✅ behoben
3. ~~B2 Race Condition im Chat-Kontext~~ ✅ behoben
4. ~~S2 yt-dlp-Injection~~ ✅ behoben
5. ~~S3 API-Key-Leak~~ ✅ behoben
**Kurzfristig — größter Nutzen pro Aufwand**
6. ~~T1 Prompt-Caching~~ ✅ umgesetzt (inkl. T9 `cached_tokens`)
7. ~~T2 Tool-Ergebnisse kappen (= B5)~~ ✅ umgesetzt
8. ~~T3 Günstiges Compaction-Modell~~ ✅ umgesetzt
9. ~~B4 Kostenerfassung korrigieren~~ ✅ teilweise: Prompt/Completion werden jetzt
getrennt erfasst statt 50/50 geschätzt. Offen bleibt die veraltete, hartcodierte
Preistabelle (`ModelPricing`) — Preise sollten vom `/models`-Endpoint kommen.
10. ~~B12 Retry/Backoff~~ ✅ umgesetzt
11. T4 Proaktiv statt reaktiv kompaktieren
**Mittelfristig — Fundament**
11. ~~S1 DatabaseTool absichern~~ ✅ behoben (`SqlGuard`)
12. Memory-Tool (K1)
13. F-A1 Freigabe-Workflow + F-A2 Audit-Log
14. K3 Testprojekt
15. S4 PermissionGate ausbauen
**Danach**
16. Restliche Bugs (B6B10, B13), S4/S7, weitere Tools, Streaming
(~~S5~~ ✅ SSRF, ~~S6~~ ✅ Pfadprüfung sind behoben)