Eine Roadmap statt neun verteilter Listen
Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind in docs/Roadmap.md zusammengefuehrt. Aufbau der neuen Roadmap - Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig / zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in einem Konzeptpapier zu verschwinden. - Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1), damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen. - Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung, 7 Herkunftskarte. Was dabei sichtbar wurde - Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten - allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte. - B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A, warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute. - Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren in keiner Roadmap verzeichnet. Archiv Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2 (gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt, dass ihre Quelltext-Verweise ins Leere gehen koennen. Bauplan bleibt Bauplan Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile, die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt. Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der Roadmap, nicht im Plan. Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
# ClawdDotNet — Bestandsaufnahme (Juli 2026)
|
||||
|
||||
Vollständiges Review von Core-Engine, Security-Layer, Tools, Scheduling und UI.
|
||||
Stand: Commit `92e50d3`.
|
||||
|
||||
> **Dies ist ein datierter Befund, keine Todoliste.** Er wird nicht fortgeschrieben.
|
||||
> Was davon offen ist, steht in der [Roadmap](../Roadmap.md) — dort auch, was seither
|
||||
> erledigt wurde. Die Abschnitte zur Oberfläche beziehen sich auf die Windows-Forms-
|
||||
> Fassung, die es seit dem 2026-08-23 nicht mehr gibt.
|
||||
|
||||
**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: 60–90 % 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 4–6k 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
|
||||
|
||||
> **Abgelöst durch die [Roadmap](../Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> werden dort weitergeführt; dieser Abschnitt bleibt als Stand der Bestandsaufnahme
|
||||
> eingefroren. F-A1/S4, F-A2, F-A5, T6, T7 sowie B6–B8 sind in den Roadmap-Vorhaben
|
||||
> A1–A4 aufgegangen.
|
||||
|
||||
**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~~ ✅ vollständig: Prompt/Completion getrennt
|
||||
erfasst, Preise kommen live vom `/models`-Endpunkt (`ModelPricingCatalog`),
|
||||
Modelle ohne Preisdaten werden sichtbar gemeldet statt still mit 0 gerechnet.
|
||||
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 (B6–B10, B13), S4 PermissionGate, weitere Tools, Streaming
|
||||
(~~S5~~ ✅ SSRF, ~~S6~~ ✅ Pfadprüfung, ~~S7~~ ✅ Secrets sind behoben)
|
||||
|
||||
---
|
||||
|
||||
## 7. Umgesetzt
|
||||
|
||||
| Punkt | Was |
|
||||
|---|---|
|
||||
| B1, B14 | Compaction: sicherer Schnittpunkt, kein doppelter System-Prompt |
|
||||
| B2 | Chat-Läufe pro Agent serialisiert, `AbortChat` erreicht alle |
|
||||
| B3 | `maxCumulativeTokens` von `maxContextTokens` getrennt |
|
||||
| B4 | Prompt/Completion getrennt erfasst, Preise live vom Anbieter |
|
||||
| B12 | Retry mit Backoff für 429/5xx |
|
||||
| S1 | `SqlGuard` statt Teilzeichenketten-Prüfung |
|
||||
| S2 | `YouTubeUrl` + `ArgumentList` gegen Options-Injection |
|
||||
| S3 | `UrlSanitizer` gegen API-Key-Leak ins Modell |
|
||||
| S5 | `UrlGuard`, Redirects einzeln geprüft |
|
||||
| S6 | `WorkspacePath` auf Verzeichnisgrenzen |
|
||||
| S7 | `SecretProtector` (DPAPI) für Zugangsdaten |
|
||||
| T1, T9 | Prompt-Caching mit Breakpoints, `cached_tokens` gemessen |
|
||||
| T2 | Tool-Ergebnisse zentral gekappt |
|
||||
| T3 | Günstiges Modell für die Zusammenfassung |
|
||||
| K3 | Testfundament: 309 Tests, davon ~90 gezielte Angriffsfälle |
|
||||
Reference in New Issue
Block a user