Fruehjahrsputz: WinForms-Altlast entfernt, Dokumentation nachgezogen

Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx
angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand
davor liegt unter dem Tag vor-fruehjahrsputz-2026-08.

Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr
Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare,
UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte
Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py).
Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem
Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen.

Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um,
mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die
Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb
Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad.

Toter Code
- PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich
  mehr, seit alle neun Seiten portiert sind.
- Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf
  dieselbe Fassung auf, der Verweis hob nichts an.

Zwei Fehler, die dabei sichtbar wurden
- Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und
  schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der
  BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und
  in den geordneten Abbau aufgenommen.
- SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt.
  Aus der Ansicht waren sie laengst verschwunden, gelesen und beim
  Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der
  Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam
  den alten Wert zurueck.

Pakete: keine bekannten Sicherheitsluecken mehr
- SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite
  bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch).
- SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst
  als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene
  Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine
  Anhebung, kein Ballast; das steht jetzt als Kommentar dabei.

Dokumentation
- Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate,
  Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein
  gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung.
  Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig.
- Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr,
  sondern Beschreibung des Stands.
- Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet;
  der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig.
- Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden
  Dokumenten richtiggestellt.

Build fehlerfrei, 585 Tests gruen (6 uebersprungen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:26:14 +02:00
co-authored by Claude Opus 5
parent 33d95a6c3f
commit 2853541629
79 changed files with 186 additions and 18381 deletions
+4 -2
View File
@@ -222,9 +222,11 @@ zu sehen, mit wem er überhaupt spricht. Zeitraum, Kanalfilter, Freitextsuche.
trägt eine kleine Kennzeichnung des Kanals (`Rückfrage` / `Auftrag` / `Kommentar` /
`Ergebnis`) und, wo vorhanden, die anklickbare Task-Id.
**Technisch**: über den vorhandenen **WebView2**-Unterbau, wie ihn `frm_chat` schon nutzt
**Technisch**: damals über den WebView2-Unterbau von `frm_chat` gedacht. Beides gibt es
seit der Avalonia-Portierung nicht mehr — der Abschnitt ist als Entwurfsstand von damals
zu lesen; die Ansicht wäre heute eine Avalonia-Seite wie `AgentChatsPageView`
(inkl. Virtual-Host-Mapping auf einen lokalen Ordner). Der Verlauf wird als HTML
gerendert. Handgezeichnete Sprechblasen in WinForms wären ein Vielfaches an Aufwand für
gerendert. Handgezeichnete Sprechblasen wären dort ein Vielfaches an Aufwand für
ein schlechteres Ergebnis.
**Paging**: die jüngsten ~200 Nachrichten, „ältere laden" nach oben. Ein Paar mit 50.000
+5
View File
@@ -3,6 +3,11 @@
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
+8
View File
@@ -2,6 +2,14 @@
Stand: 2026-08-06. Reine Bestandsaufnahme und Aufwandsschätzung, **kein** Umbau.
> **Nachtrag 2026-08-23:** Der teure Teil dieser Analyse hat sich erledigt. Die rund
> 8.900 Zeilen Windows-Forms-Oberfläche samt WebView2 und den vier `PropertyGrid`-
> Instanzen gibt es nicht mehr — sie sind durch `src/ClawdDotNet.Desktop` (Avalonia,
> `net10.0`) ersetzt und beim Frühjahrsputz entfernt worden. Damit fällt der zweite
> der beiden unten vorgeschlagenen Schnitte weg: Es gibt keine Windows-gebundene
> Oberfläche mehr, die noch umzuziehen wäre. Offen bleiben die drei Kernstellen
> (DPAPI, Pfadvergleiche, Zeitzonen-IDs) und ein `linux-x64`-Release (Roadmap DC3).
Frage: Was ist nötig, damit ClawdDotNet unter Linux läuft, und was kostet das?
---
@@ -1,27 +1,35 @@
# Avalonia-Portierung — Leitfaden
# Oberfläche — Leitfaden
Für alle, die weitere Ansichten von WinForms nach Avalonia übertragen.
Stand: 2026-08-07.
Für alle, die an `src/ClawdDotNet.Desktop` arbeiten.
Stand: 2026-08-23.
---
## 1. Auftrag
## 1. Stand
Drei Bereiche des Hauptfensters sind noch Platzhalter. In dieser Reihenfolge portieren —
sie steigen im Umfang, und jede baut auf dem Muster der vorigen auf:
**Die Portierung ist abgeschlossen.** Es gibt keine Platzhalter mehr und keine
WinForms-Vorlage, gegen die man vergleichen könnte: Die alte Oberfläche
(`ClawdDotNet.csproj`, `frm_*.cs`, `UI/`, `Models/`, `EmbeddedUI/`) ist am
2026-08-23 aus dem Arbeitsbaum entfernt worden. Wer sie doch einmal braucht,
findet sie im Tag `vor-fruehjahrsputz-2026-08`.
| # | Bereich | WinForms-Vorlage | Daten aus |
|---|---|---|---|
| 1 | **Info** | `frm_main.Designer.cs`, Suchwort `tabPage_Info` | `AppHost.AppVersion`, `AppHost.BuildSummary`, `host.Instance` |
| 2 | **Sicherung** | `UI/BackupPanel.cs` + `UI/BackupPanel.Designer.cs` | `host.InstancePath`, `host.Settings`, `Core.Backup.BackupService` |
| 3 | **Aufgaben** (Jobs/Services/Verlauf) | `frm_main.cs`, Abschnitt `WORKER TAB` ab Zeile 932 | `host.Instance.Agents`, `App.Services.JobHistoryService` |
Neun Bereiche, alle nativ in Avalonia:
**Nicht anfassen:** Chat und Einstellungen. Beide sind Entwurfsarbeit, nicht Übersetzung,
und werden gesondert gemacht.
| Gruppe | Bereich | Ansichtsmodell |
|---|---|---|
| Arbeit | Chat, Agenten, Aufgaben | `ChatPageViewModel`, `AgentsPageViewModel`, `TasksPageViewModel` |
| Analyse | Token-Verbrauch, Agenten-Chats | `TokenUsagePageViewModel`, `AgentChatsPageViewModel` |
| System | Sicherung, Protokoll, Einstellungen, Info | `BackupPageViewModel`, `LogPageViewModel`, `SettingsPageViewModel`, `InfoPageViewModel` |
Die WinForms-Dateien liegen noch im Repository, sind aber **nicht mehr Teil des Builds**
(siehe Kommentar in `ClawdDotNet.slnx`). Sie sind Vorlage zum Lesen — nicht zum Kompilieren,
nicht zum Reparieren.
Dazu die Fenster `InstancePickerWindow`, `LicenseWindow`, `TextEditorWindow` und
die Dialoge zum Anlegen von Agent, Auftrag und Dienst.
Das Erscheinungsbild folgt dem Entwurf in `Mockup/` — wer daran etwas ändert,
liest zuerst `Mockup/extracted/mockup/Implementierungsleitfaden.md`.
**Was in der Oberfläche noch fehlt**, siehe [Roadmap](Roadmap.md):
die Freigabe-Ansicht für gestagte Aufrufe (A2) und die Schaltfläche
„Fehler melden" (DC1).
---
+58 -11
View File
@@ -1,5 +1,22 @@
# Roadmap
**Stand: 2026-08-23** (Frühjahrsputz). Was seither gilt:
| Bereich | Stand |
|---|---|
| A1 Taskboard | **erledigt** — Dateiformat, Scanner mit Claiming, `task_*`-Tool, Migration, Tests |
| A3 Audit-Log | **erledigt** — append-only auf SQLite, Engine stempelt die Herkunft |
| A2 Staging | **halb** — Gate, Policy, Ablage und Dienst stehen und greifen; **die Freigabe-Ansicht fehlt.** Bis sie da ist, legt ein Agent Vorschläge ab, die niemand freigeben kann |
| Oberfläche | **portiert** — neun Bereiche nativ in Avalonia, Entwurf aus `Mockup/` umgesetzt. Die WinForms-Fassung ist entfernt (Tag `vor-fruehjahrsputz-2026-08`) |
| Rocket.Chat | Tool umgesetzt und registriert. **Damit steht A5 zur Entscheidung an** — siehe unten |
| Pakete | keine bekannten Sicherheitslücken mehr; `SharpCompress` und `SQLitePCLRaw.bundle_e_sqlite3` sind als Anhebung direkt verwiesen |
Die vier [Umsetzungspläne](umsetzungsplaene/) vom 2026-08-05 sind **alle noch offen**:
FileRW-Papierkorb, AgentInspector, AgentEditor-Härtung, WebSearch. Ihre Reihenfolge
steht in den Dokumenten selbst; sie sind hier nicht doppelt eingeordnet.
---
Zentrale Liste aller offenen Vorhaben. Sie löst die beiden „Vorgeschlagene
Reihenfolge"-Abschnitte in der [Bestandsaufnahme](Bestandsaufnahme-2026-07.md) und im
[Konzepte-Dokument](Konzepte-Backup-Finanz-Analyse.md) ab — die bleiben als Befund bzw.
@@ -16,7 +33,7 @@ Hintergrund: Konzeptvergleich mit [OpenAlice](https://github.com/TraderAlice/Ope
Staging-Freigabe, Audit-Log und das Skill-Modell. Die Inbox-Idee entfällt zugunsten
der geplanten Matrix-Migration (A5).
### A1 — Taskboard
### A1 — Taskboard — **erledigt**
Aufgaben als Markdown-Dateien mit YAML-Frontmatter im `SharedWorkspace`:
`title`, `status` (`backlog | todo | in_progress | done | canceled`), `priority`,
@@ -73,7 +90,13 @@ Frontmatter, nicht in einen eigenen Mechanismus.
Konzept-Doc: [Taskboard-Konzept](Taskboard-Konzept.md) (Dateiformat,
Wahrheitsaufteilung Datei/DB, Scanner-Verhalten, Invarianten, Migration).
### A2 — Staging-Freigabe für irreversible Aktionen (F-A1 + S4)
### A2 — Staging-Freigabe für irreversible Aktionen (F-A1 + S4) — **Kern erledigt, Ansicht offen**
> **Der offene Rest ist die Freigabe-Ansicht.** `StagingService` bietet
> `ListPendingAsync`, `ApproveAsync` und `RejectAsync`, `AppHost.Staging` reicht ihn
> an die Oberfläche durch — aber kein Ansichtsmodell ruft sie auf. Ein gestagter
> Aufruf liegt damit unbegrenzt in der Ablage. Solange das so ist, ist jede Aktion
> auf `Approve` faktisch eine Aktion auf `Deny`, nur ohne Rückmeldung an den Agenten.
Konzept-Doc: [Staging-Konzept](Staging-Konzept.md).
@@ -98,7 +121,7 @@ Ergänzungen (Juli 2026 beschlossen):
Sicherheitswirkung: Eine Prompt-Injection (K2) kann dann nur noch einen Vorschlag
erzeugen, keine Ausführung.
### A3 — Audit-Log (F-A2)
### A3 — Audit-Log (F-A2) — **erledigt**
Konzept-Doc: [Audit-Konzept](Audit-Konzept.md).
@@ -157,6 +180,13 @@ Agenten) wird auf Element/Matrix umgestellt.
Scope ist noch unbestimmt — braucht ein eigenes Konzept-Doc, bevor es in die
Reihenfolge eingeordnet wird.
**Kollision, seit Rocket.Chat steht (August 2026):** Das
[Rocket.Chat-Tool](../src/ClawdDotNet.Tools.RocketChat/) ist umgesetzt und im
`AppHost` registriert — es bedient genau den Zweck, für den A5 Matrix vorsah.
Entweder A5 entfällt und Rocket.Chat wird der Weg, oder wir betreiben zwei
Chat-Wege nebeneinander. Das ist **die nächste offene Grundsatzentscheidung**;
siehe [RocketChat-Nextcloud-Konzept](RocketChat-Nextcloud-Konzept.md).
### A6 — MySQL-Replikations-Spiegel (optional)
Beschlossen Juli 2026. **SQLite bleibt die einzige Wahrheit** — gearbeitet wird
@@ -203,10 +233,19 @@ Spiegel auch die Historie. Zeitlich passt A6 zu dem Server, der ggf. mit A5
| Memory-Flush vor Compaction | Bevor der ContextCompactor zusammenfasst, bekommt der Agent ein eng begrenztes Fenster, Dauerhaftes per `memory_store` zu sichern — sonst wirft die Compaction Wissen weg | beschlossen (GoClaw-Muster) |
| Memory-Auto-Injection | Relevante Memory-Abstracts werden automatisch eingeblendet (Relevanzschwelle, Deckel ~200 Tokens), **in die Nutzernachricht, nie in den System-Prompt** (Prompt-Cache T1) | beschlossen; löst den offenen Punkt „Automatische Einblendung" im [Memory-Konzept](Memory-Konzept.md) |
| Output-Scrubbing | Bekannte Secret-Werte (Register des `SecretProtector`) werden zentral aus **allen** Tool-Ergebnissen maskiert, bevor sie in Kontext, Historie oder Spiegel (A6) gelangen | beschlossen; schließt die Lücke, die S3 nur für URLs schloss |
| Hygiene-Paket | B9 (`index_Count`-Race), B11/T8 (`max_tokens` setzen), B13 (`instanceId`-Inkonsistenz), F-A6-Rest (UI zum Setzen/Rotieren der Secrets) | Kleinbugs, in einem Aufwasch. B10 geht im Historie-Umzug (A6) auf |
| Hygiene-Paket | B9 (`index_Count`-Race), B13 (`instanceId`-Inkonsistenz), F-A6-Rest (UI zum Setzen/Rotieren der Secrets) | Kleinbugs, in einem Aufwasch. B11/T8 (`max_tokens`) ist erledigt — `AgentEngine` setzt es aus `LoopGuard.MaxResponseTokens`. B10 geht im Historie-Umzug (A6) auf |
Erledigt seit der letzten Fortschreibung: B4 vollständig (Preise kommen live vom
`/models`-Endpunkt, unbekannte Modelle werden sichtbar gemeldet).
`/models`-Endpunkt, unbekannte Modelle werden sichtbar gemeldet). Beim Frühjahrsputz
dazugekommen: die automatische Sicherung lief ins Leere — die Oberfläche bot sie an
und schrieb die Uhrzeit weg, aber der `BackupScheduler` wurde nie gestartet. Jetzt
verdrahtet.
Neu aufgenommen (aus der Ideensammlung, die dabei aufgelöst wurde):
| Punkt | Was |
|---|---|
| Werkzeugwunsch | Agenten bekommen in den Kern-Prompt den Hinweis, sich zu melden, wenn ihnen ein Werkzeug fehlt, das es noch nicht gibt. Der Weg dafür ist da — ein Task an `@human` (A1). Gehört zum Prompt-Kern von A4 |
### B-DC — Deploymentcenter-Anbindung
@@ -221,7 +260,7 @@ Offen:
|---|---|---|
| DC1 | Oberfläche „Fehler melden" | Client vorhanden, Schaltfläche fehlt |
| DC2 | Agenten-Tool für den Bugtracker | macht den Claim/Lease-Workflow des Deploymentcenters nutzbar |
| DC3 | Release-Strecke | **erledigt für win-x64/dev** ([`deploy/publish.py`](../deploy/publish.py), 0.1.1 veröffentlicht). Offen: `linux-x64` nach der Avalonia-Portierung, `prod` |
| DC3 | Release-Strecke | **erledigt für win-x64/dev** ([`deploy/publish.py`](../deploy/publish.py), 0.1.1 veröffentlicht). Offen: `linux-x64` — die Portierung, auf die das wartete, ist durch — und `prod` |
| DC4 | SDK als Git-Submodul unter `external/` statt Cross-Repo-Pfad | betrifft auch die CI |
| DC5 | Betreiber: Evaluator-Cron einrichten, Token ausstellen, `parent_source` pflegen | **ohne den Cron ist die Überwachung wertlos** |
| DC6 | Update anwenden statt nur melden | **erledigt.** Agent liegt im Paket (Prüfsumme geprüft), Rückfrage in der Oberfläche, geordnetes Herunterfahren vor dem Agentenstart, `maintenance` an den Watchdog |
@@ -262,17 +301,20 @@ Notify ist gestrichen — geht in A5 auf.
| # | Vorhaben | Begründung |
|---|---|---|
| 1 | A1 Taskboard | Fundament; löst sechs bestehende Punkte auf einmal |
| 2 | A3 Audit-Log | klein, sofort nützlich; muss vor A2 da sein, damit Freigaben protokolliert werden |
| 3 | A2 Staging-Freigabe | größter Sicherheitsgewinn; Voraussetzung für unbeaufsichtigten Betrieb |
| | A1 Taskboard | Fundament; löste sechs bestehende Punkte auf einmal |
| | A3 Audit-Log | klein, sofort nützlich; musste vor A2 da sein, damit Freigaben protokolliert werden |
| **1** | **A2 — Freigabe-Ansicht** | Der Rest von A2 und das einzige Stück, das jetzt wirklich blockiert: Ohne sie liegen gestagte Aufrufe unbeantwortet. Kleiner Umfang, große Wirkung |
| 2 | FileRW-Papierkorb | [eigener Plan](umsetzungsplaene/UMSETZUNGSPLAN-FileRW-Papierkorb-Cleanup.md); kleinster Eingriff, entschärft ein reales Risiko und erlaubt, `FileRW.delete` von `Approve` auf `Auto` zu senken |
| 3 | A5 entscheiden: Matrix oder Rocket.Chat | Blockiert K4 (Streaming) und färbt auf A6 ab. Entscheidung, keine Umsetzung — deshalb früh |
| 4 | C1 Marktkalender | spart sofort Kosten; nutzt A1-Frontmatter |
| 5 | A4 Skills/Toolsets | Token-Hebel, Cache-stabil |
| 6 | C2 Indicators | Qualität + Kosten |
| 7 | C7 + C8 Ergebnisregister, Aussagen | das eigentliche Leistungsmaß; braucht A3 |
| — | DC1 „Fehler melden"-Schaltfläche | zwischendurch; der Client steht, es fehlt der Knopf |
| — | Hygiene-Paket (B) | zwischendurch, unabhängig |
| — | Historie-Umzug in die Instanz-DB | löst B10 + K6; Voraussetzung für A6 |
| — | A6 MySQL-Spiegel | nach dem Historie-Umzug; natürliches Zuhause auf dem A5-Server |
| — | A5 Matrix | eigenes Konzept-Doc zuerst; Scope klären, dann einordnen |
| — | A5 umsetzen | erst nach der Entscheidung aus Zeile 3; der gewählte Weg bekommt dann sein Konzept-Doc |
Leitlinie der Reihung: erst Nachvollziehbarkeit und Kontrolle (Audit, Staging),
dann Fähigkeiten — ein Agent, der unbeaufsichtigt läuft, braucht zuerst Bremsen,
@@ -283,7 +325,12 @@ dann PS.
## Umsetzung mit Opus 4.6 — Einstufung
Die Entwicklung erfolgt mit Opus 4.6. Die meisten Vorhaben sind damit gut
machbar, sofern die hier notierten Vorgaben mitgegeben werden. Zwei Stellen
machbar, sofern die hier notierten Vorgaben mitgegeben werden.
> Die beiden hier markierten Stellen sind inzwischen gebaut: Der **Scanner-Kern**
> (A1) steht samt Claiming und Reconciliation, die **Fortsetzung nach Freigabe**
> (A2) folgt der Vorgabe unten — der Lauf endet beim Staging regulär, kein Suspend.
> Die Tabelle bleibt als Begründung stehen, warum es so gebaut wurde. Zwei Stellen
berühren Nebenläufigkeits-Invarianten bzw. Engine-Querschnitte — sie sind für
Opus 5 / Fable markiert oder durch eine Architektur-Vorgabe entschärft.
+13 -6
View File
@@ -6,7 +6,14 @@ Der typische Ablauf ist die Kombination aus beidem — „schreib mir die Auswer
sie in die Cloud" im Chat, Datei in Nextcloud, Link zurück in den Chat.
Dieses Dokument prüft die Machbarkeit, legt den Schnitt fest und benennt die Punkte, die
vor der Umsetzung entschieden werden müssen. **Es ist noch keine Umsetzungsfreigabe.**
vor der Umsetzung entschieden werden müssen.
> **Stand 2026-08-23:** Der Rocket.Chat-Teil ist **umgesetzt** —
> `src/ClawdDotNet.Tools.RocketChat`, im `AppHost` registriert, `send_file`
> freigabepflichtig. Der Befund unten bleibt als Begründung stehen; Abschnitt 2.0
> hält fest, was die Messung gegen die echte Instanz an den Annahmen korrigiert hat.
> **Offen:** der Nextcloud-Teil und die Kollision mit Roadmap A5 (Matrix) — die ist
> eine Entscheidung, keine Umsetzung.
Verwandt: [Taskboard-Konzept](Taskboard-Konzept.md) (Scanner/Wake), [Staging-Konzept](Staging-Konzept.md)
(Freigaben), [Audit-Konzept](Audit-Konzept.md), [Roadmap](Roadmap.md) (A5 — siehe Konflikt unten).
@@ -177,7 +184,7 @@ der richtige Ansatz und wird von Rocket.Chat direkt unterstützt.
- Authentifiziert wird jeder Aufruf über zwei Header: `X-Auth-Token` und `X-User-Id`.
**Entscheidung, die ich empfehle:** Das Anlegen der Benutzer ist **kein Agenten-Tool**.
Es ist eine einmalige Einrichtungsfunktion in der WinForms-Oberfläche
Es ist eine einmalige Einrichtungsfunktion in der Oberfläche
(Instanz-Einstellungen → Rocket.Chat → „Agenten-Benutzer anlegen"). Sonst müsste ein
Agent ein Admin-Token halten — und ein Admin-Token in Reichweite einer Prompt-Injection
ist genau das, was A2 verhindern soll. Der Admin-Token liegt in der **Instanz**-Konfiguration,
@@ -444,7 +451,7 @@ Das ist die Anforderung, die die Architektur bestimmt — nicht der Chat selbst.
| Kanal | Unabhängig von Rocket.Chat? | Richtung |
|---|---|---|
| WinForms-Chat (`frm_chat`) | vollständig — läuft in der App selbst | beide |
| Chat-Seite in der App (`ChatPageView`) | vollständig — läuft in der App selbst | beide |
| Telegram-Bot-Tool | ja — fremde Infrastruktur | beide |
| Mail-Tool | ja, sofern der Mailserver anderswo läuft | beide |
| Web-Chat / `ClawdDotNetApi` | ja, aber nur im lokalen Netz | beide |
@@ -483,7 +490,7 @@ Ein Tool-Job `rocketchat_health` (Takt ~5 Minuten, `GET /api/info`):
**Eingehend während des Ausfalls:** Der Telegram-Poll-Job bleibt dauerhaft aktiv, nur mit
langsamem Takt (z. B. alle 5 Minuten). Er kostet nichts, wenn nichts kommt — und ist im
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Der WinForms-Chat ist ohnehin immer
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Die eingebaute Chat-Seite ist ohnehin immer
da, solange die App läuft.
**Entschieden (August 2026): Telegram ist der Notfallkanal.** Die Kanalliste lautet damit
@@ -706,9 +713,9 @@ Damit der Zuschnitt klar ist:
- Keine Rocket.Chat-**App** (Apps-Engine, TypeScript im Server) — wir bleiben Client.
- Keine Verwaltung von Rocket.Chat durch Agenten (Benutzer anlegen, Räume erstellen,
Rechte vergeben). Das ist Admin-Arbeit in der WinForms-Oberfläche.
Rechte vergeben). Das ist Admin-Arbeit in der Oberfläche.
- Keine Sprach-/Videofunktionen, keine Nextcloud Talk-Anbindung.
- Kein Ersatz für den WinForms-Chat — der bleibt und ist die unterste Rückfallebene.
- Kein Ersatz für die eingebaute Chat-Seite — die bleibt und ist die unterste Rückfallebene.
- Keine Ende-zu-Ende-Verschlüsselung. Rocket.Chat kann das, aber verschlüsselte Räume sind
über die REST-API nicht lesbar. Agenten arbeiten in unverschlüsselten Räumen — das ist
eine bewusste Einschränkung, die du kennen solltest.
+2 -1
View File
@@ -102,7 +102,8 @@ Erst nach gewonnenem Übergang wird der eingefrorene Aufruf ausgeführt.
## Offen
- **Review-Oberfläche** im Hauptfenster (Liste der offenen Vorschläge, Freigeben/Ablehnen)
— die Dienst-API steht bereit; die WinForms-Ansicht ist die verbleibende Integration.
— die Dienst-API steht bereit; die Freigabe-Ansicht in `src/ClawdDotNet.Desktop` ist
die verbleibende Integration und der einzige offene Punkt von A2.
- **Output-Scrubbing** greift auch hier auf den gespeicherten Argument-JSON, sobald es
steht (eigener Roadmap-Punkt).
- **Orders** (Handelsaufträge) reihen sich später als weitere `approve`-Aktionen ein.
+1 -1
View File
@@ -116,7 +116,7 @@ tests/
Die drei Projekte in `ClawdDotNet.slnx` unter einem Ordner `/tests/` eintragen.
> Nebenbefund: In `ClawdDotNet.slnx` fehlen vier Tool-Projekte (AgentComm, AgentSpawn,
> AgentEditor, SocialMediaManager), obwohl sie in `ClawdDotNet.csproj` referenziert sind.
> AgentEditor, SocialMediaManager), obwohl sie in `src/ClawdDotNet.App/ClawdDotNet.App.csproj` referenziert sind.
> Sie sollten mit aufgenommen werden, sonst laufen sie in der IDE-Solution nicht mit.
---
+3 -2
View File
@@ -21,7 +21,8 @@ ClawdDotNet.sln
│ ├── ClawdDotNet.Tools.MeinTool/ ← Dein Tool-Plugin
│ │ ├── ClawdDotNet.Tools.MeinTool.csproj
│ │ └── MeinToolTool.cs
── ClawdDotNet.Host/WinForms-App (verweist auf Core + alle Tools)
── ClawdDotNet.App/ Fachschicht ohne Oberflaeche (verweist auf Core + alle Tools)
│ └── ClawdDotNet.Desktop/ ← Oberflaeche in Avalonia (verweist auf App)
```
### Neues Tool-Projekt anlegen
@@ -450,7 +451,7 @@ Das Logging-System trennt automatisch nach Modul. Wenn dein Tool den Logger aus
Logs/
├── 2026-05-12/
│ ├── Core.log ← Engine, Scheduler, Config
│ ├── Host.log ← WinForms UI
│ ├── Host.log ← Oberflaeche
│ ├── Tool_Database.log ← Database-Tool
│ ├── Tool_FileRW.log ← FileRW-Tool
│ ├── Tool_MeinTool.log ← Dein Tool!
@@ -0,0 +1,695 @@
# ClawdDotNet Prompt-Anhang: Internet-Tools
Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert drei
unabhängige Internet-Tools sowie ein spezialisiertes Monitoring-Tool für
strukturierte Webseiten (z.B. Capitol Trades).
Alle Tools folgen den bekannten Prinzipien: IAgentTool implementiert,
Konfiguration ausschließlich aus AgentToolContext, keine Tool-zu-Tool-Abhängigkeiten.
---
## Pflicht-Regel: Timestamp in jedem ToolResult
**Diese Regel gilt für ALLE Internet-Tools ohne Ausnahme.**
Jedes `ToolResult.Content` das Finanzdaten, Nachrichten oder externe Daten enthält,
muss ein JSON-Objekt zurückgeben das mindestens enthält:
```json
{
"fetchedAt": "2026-05-13T07:42:00Z",
"dataAsOf": "2026-05-13T07:40:00Z",
"source": "https://...",
"data": { ... }
}
```
- `fetchedAt` = Zeitpunkt des HTTP-Requests (UTC, immer `DateTime.UtcNow`)
- `dataAsOf` = Zeitpunkt der Daten laut Quelle (aus Response-Header, HTML oder API-Feld)
Falls nicht ermittelbar: `null` — NIEMALS schätzen oder weglassen
- `source` = exakte URL die abgerufen wurde
Der System-Prompt jedes Agenten der Internet-Tools nutzt MUSS enthalten:
> "Verwende niemals Daten ohne `fetchedAt`-Feld. Wenn `dataAsOf` null ist,
> teile dem Nutzer mit dass der Datenzeitpunkt unbekannt ist.
> Erfinde niemals Kurse, Preise oder Daten aus dem Gedächtnis."
---
## Tool 1: DirectAPI — Echtzeit-Finanzdaten
**Datei: `ClawdDotNet.Tools.DirectAPI/DirectApiTool.cs`**
Direkter HTTP-Zugriff auf Finanz-APIs. Kein HTML-Parsing.
Strukturiertes JSON mit verifizierbaren Timestamps.
### AgentConfig-Beispiel
```json
"DirectAPI": {
"providers": {
"twelvedata": {
"apiKey": "your-key-here",
"baseUrl": "https://api.twelvedata.com"
},
"alphavantage": {
"apiKey": "your-key-here",
"baseUrl": "https://www.alphavantage.co"
},
"coingecko": {
"baseUrl": "https://api.coingecko.com/api/v3"
},
"yahoo": {
"baseUrl": "https://query1.finance.yahoo.com"
}
},
"defaultProvider": "twelvedata",
"cacheTtlSeconds": 60
}
```
### Tool-Implementierung
```csharp
namespace ClawdDotNet.Tools.DirectAPI;
public sealed class DirectApiTool : IAgentTool
{
public string Name => "DirectAPI";
public string Description => """
Ruft Echtzeit-Finanzdaten von verifizierten APIs ab.
Alle Antworten enthalten fetchedAt und dataAsOf Timestamps.
Aktionen: quote, history, crypto, forex, search
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "symbol"],
"properties": {
"action": {
"type": "string",
"enum": ["quote", "history", "crypto", "forex", "search"],
"description": "quote=aktueller Kurs, history=Kursverlauf, crypto=Krypto, forex=Wechselkurs, search=Symbol suchen"
},
"symbol": { "type": "string", "description": "z.B. NVDA, BTC, EUR/USD" },
"provider": { "type": "string", "description": "optional: twelvedata|alphavantage|coingecko|yahoo" },
"interval": { "type": "string", "description": "für history: 1min|5min|1h|1day" },
"outputsize":{ "type": "integer","description": "für history: Anzahl Datenpunkte, max 500" }
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
// Config aus Context lesen — nie aus statischen Feldern
var config = ctx.ToolConfig["DirectAPI"] as Dictionary<string, object?>
?? throw new InvalidOperationException("DirectAPI config missing");
var providers = config["providers"] as Dictionary<string, object?> ?? new();
var cacheTtl = Convert.ToInt32(config.GetValueOrDefault("cacheTtlSeconds") ?? 60);
var action = input.GetProperty("action").GetString()!;
var symbol = input.GetProperty("symbol").GetString()!;
var provider = input.TryGetProperty("provider", out var p)
? p.GetString()
: config.GetValueOrDefault("defaultProvider")?.ToString()
?? "twelvedata";
// Cache-Check: agentId + symbol + action als Key
var cacheKey = $"directapi:{ctx.AgentId}:{provider}:{action}:{symbol}";
// (Cache-Implementierung über IMemoryCache oder Redis aus Core)
return action switch
{
"quote" => await FetchQuoteAsync(symbol, provider, providers, ct),
"history" => await FetchHistoryAsync(input, symbol, provider, providers, ct),
"crypto" => await FetchCryptoAsync(symbol, providers, ct),
"forex" => await FetchForexAsync(symbol, provider, providers, ct),
"search" => await SearchSymbolAsync(symbol, provider, providers, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> FetchQuoteAsync(
string symbol, string provider,
Dictionary<string, object?> providers, CancellationToken ct)
{
// Jeder Provider hat eigene URL-Struktur
// Gemeinsam: immer Cache-Control: no-cache Header setzen
// Gemeinsam: dataAsOf aus Response extrahieren (nicht schätzen)
// Twelve Data Quote:
// GET https://api.twelvedata.com/quote?symbol={symbol}&apikey={key}
// Response enthält: "datetime" → das ist dataAsOf
// Response enthält: "timestamp" (Unix) → ebenfalls verwertbar
// Yahoo Finance Quote (kein Key nötig):
// GET https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1m&range=1d
// Response: result[0].meta.regularMarketTime (Unix timestamp) → dataAsOf
// Alpha Vantage Quote:
// GET https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={key}
// Response: "Global Quote"."07. latest trading day" → dataAsOf (nur Datum, keine Zeit)
// IMPLEMENTIERUNGSREGEL: dataAsOf IMMER aus der API-Antwort lesen.
// Wenn das Feld fehlt oder leer ist → dataAsOf = null, NICHT DateTime.UtcNow.
throw new NotImplementedException("Implement per provider");
}
// history, crypto, forex, search analog implementieren
}
```
### NuGet
Keine externen HTTP-Bibliotheken. Nur `System.Net.Http.HttpClient` via `IHttpClientFactory`.
---
## Tool 2: WebFetch — Nachrichten & strukturiertes HTML
**Datei: `ClawdDotNet.Tools.WebFetch/WebFetchTool.cs`**
HTTP-Abruf mit Whitelist, Timestamp-Extraktion und HTML-zu-Text-Konvertierung.
Kein JavaScript-Rendering (statisches HTML only).
### AgentConfig-Beispiel
```json
"WebFetch": {
"allowedDomains": [
"reuters.com",
"bloomberg.com",
"sec.gov",
"feeds.finance.yahoo.com",
"reddit.com",
"capitoltrades.com"
],
"maxResponseKb": 512,
"timeoutSeconds": 15,
"userAgent": "ClawdDotNet-Agent/1.0 (Research Bot)"
}
```
### Tool-Implementierung
```csharp
namespace ClawdDotNet.Tools.WebFetch;
public sealed class WebFetchTool : IAgentTool
{
public string Name => "WebFetch";
public string Description => """
Ruft statische Webseiten oder RSS-Feeds ab und extrahiert Text + Timestamps.
Nur Domains aus der Whitelist erlaubt. Kein JavaScript-Rendering.
Aktionen: fetch, rss
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "url"],
"properties": {
"action": {
"type": "string",
"enum": ["fetch", "rss"],
"description": "fetch=HTML-Seite abrufen und zu Text konvertieren, rss=RSS/Atom-Feed parsen"
},
"url": { "type": "string" },
"selector":{ "type": "string",
"description": "optional: CSS-ähnlicher Hint welcher Teil relevant ist, z.B. 'table', 'article'" }
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
var config = ctx.ToolConfig["WebFetch"] as Dictionary<string, object?> ?? new();
var allowedDomains = (config.GetValueOrDefault("allowedDomains")
as List<string>) ?? new List<string>();
var url = input.GetProperty("url").GetString()!;
var action = input.GetProperty("action").GetString()!;
// Domain-Whitelist prüfen
var host = new Uri(url).Host.Replace("www.", "");
if (!allowedDomains.Any(d => host == d || host.EndsWith("." + d)))
return new ToolResult(false, "",
$"Domain '{host}' nicht in der Whitelist dieses Agenten.");
return action switch
{
"fetch" => await FetchPageAsync(url, input, config, ct),
"rss" => await FetchRssAsync(url, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> FetchPageAsync(
string url, JsonElement input,
Dictionary<string, object?> config, CancellationToken ct)
{
using var http = CreateHttpClient(config);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
// Kein Cache — immer frische Daten anfordern
request.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
{ NoCache = true, NoStore = true };
using var response = await http.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
// dataAsOf aus HTTP-Headern extrahieren (Reihenfolge: Last-Modified > Date)
DateTimeOffset? dataAsOf = response.Content.Headers.LastModified
?? response.Headers.Date;
var html = await response.Content.ReadAsStringAsync(ct);
var maxKb = Convert.ToInt32(config.GetValueOrDefault("maxResponseKb") ?? 512);
if (html.Length > maxKb * 1024)
html = html[..(maxKb * 1024)];
// HTML → lesbarer Text (einfache Implementierung ohne externe Libs)
var text = StripHtml(html);
// Versuche dataAsOf aus HTML-Meta-Tags zu verfeinern falls Header fehlt
if (dataAsOf == null)
dataAsOf = ExtractDateFromHtml(html);
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = dataAsOf?.UtcDateTime,
source = url,
data = new { text }
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<ToolResult> FetchRssAsync(string url, CancellationToken ct)
{
// XML parsen mit System.Xml.Linq
// Einträge: title, link, pubDate (→ dataAsOf), description
// Neueste Einträge zuerst, max 20
throw new NotImplementedException();
}
private static string StripHtml(string html)
{
// Einfaches Regex-basiertes Stripping
// Script- und Style-Tags zuerst entfernen, dann alle anderen Tags
// Anschließend HTML-Entities dekodieren (System.Net.WebUtility.HtmlDecode)
// Mehrfache Leerzeilen auf max. 2 reduzieren
throw new NotImplementedException();
}
private static DateTime? ExtractDateFromHtml(string html)
{
// Suche nach: <time datetime="...">, og:article:published_time,
// datePublished JSON-LD, <meta name="date" content="...">
throw new NotImplementedException();
}
private static HttpClient CreateHttpClient(Dictionary<string, object?> config)
{
var client = new HttpClient();
var timeout = Convert.ToInt32(config.GetValueOrDefault("timeoutSeconds") ?? 15);
client.Timeout = TimeSpan.FromSeconds(timeout);
client.DefaultRequestHeaders.UserAgent.ParseAdd(
config.GetValueOrDefault("userAgent")?.ToString()
?? "ClawdDotNet-Agent/1.0");
return client;
}
}
```
### NuGet
`System.Xml.Linq` (im SDK enthalten) für RSS-Parsing. Kein externes HTML-Parser-Paket nötig.
---
## Tool 3: WebMonitor — Strukturiertes Seiten-Monitoring
**Datei: `ClawdDotNet.Tools.WebMonitor/WebMonitorTool.cs`**
Spezialisiert auf wiederkehrende Überprüfung von Seiten auf **neue Einträge**.
Speichert den zuletzt gesehenen Stand in der Datenbank und liefert nur Deltas.
Primärer Anwendungsfall: Capitol Trades, SEC-Filings, jede tabellarische Seite
mit eindeutigen IDs oder fortlaufenden Einträgen.
### Wie Capitol Trades funktioniert
Die Seite `https://www.capitoltrades.com/trades?pageSize=96` liefert:
- Sauber strukturiertes HTML mit einer Tabelle
- Jeder Trade hat eine eindeutige Trade-ID in der Detail-URL: `/trades/20003797558`
- Trade-IDs sind fortlaufend und numerisch aufsteigend
- Kein JavaScript-Rendering nötig — Daten sind im initialen HTML
Strategie: Höchste bekannte Trade-ID als Anker speichern. Bei jedem Check:
alle IDs auf Seite 1 extrahieren, mit gespeicherter Max-ID vergleichen,
nur neue Einträge melden.
### AgentConfig-Beispiel
```json
"WebMonitor": {
"monitors": {
"capitol_trades": {
"url": "https://www.capitoltrades.com/trades?pageSize=96",
"checkIntervalMinutes": 30,
"idPattern": "/trades/(\\d+)",
"idField": "tradeId",
"parser": "capitol_trades",
"alertOnNew": true,
"storeHistory": true
},
"sec_filings": {
"url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent&type=4&dateb=&owner=include&count=40",
"checkIntervalMinutes": 60,
"idPattern": "CIK=(\\d+)",
"parser": "sec_form4",
"alertOnNew": true,
"storeHistory": false
}
}
}
```
### Tool-Implementierung
```csharp
namespace ClawdDotNet.Tools.WebMonitor;
public sealed class WebMonitorTool : IAgentTool
{
public string Name => "WebMonitor";
public string Description => """
Überwacht Webseiten auf neue Einträge und liefert nur die Deltas seit dem letzten Check.
Speichert den Stand in der Datenbank. Ideal für Capitol Trades, SEC-Filings, etc.
Aktionen: check, history, status
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "monitorId"],
"properties": {
"action": {
"type": "string",
"enum": ["check", "history", "status"],
"description": "check=jetzt prüfen und Deltas liefern, history=bisherige Einträge, status=letzter Check-Zeitpunkt"
},
"monitorId": {
"type": "string",
"description": "z.B. capitol_trades, sec_filings — muss in Config definiert sein"
},
"limit": {
"type": "integer",
"description": "max. Anzahl Einträge für history, default 50"
}
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
var config = ctx.ToolConfig["WebMonitor"] as Dictionary<string, object?> ?? new();
var monitors = config["monitors"] as Dictionary<string, object?> ?? new();
var action = input.GetProperty("action").GetString()!;
var monitorId = input.GetProperty("monitorId").GetString()!;
if (!monitors.ContainsKey(monitorId))
return new ToolResult(false, "",
$"Monitor '{monitorId}' nicht in der Config dieses Agenten definiert.");
var monitorConfig = monitors[monitorId] as Dictionary<string, object?> ?? new();
return action switch
{
"check" => await CheckForNewEntriesAsync(monitorId, monitorConfig, ctx, ct),
"history" => await GetHistoryAsync(monitorId, input, ctx, ct),
"status" => await GetStatusAsync(monitorId, ctx, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> CheckForNewEntriesAsync(
string monitorId, Dictionary<string, object?> monitorConfig,
AgentToolContext ctx, CancellationToken ct)
{
var url = monitorConfig["url"]?.ToString()!;
var parser = monitorConfig["parser"]?.ToString() ?? "generic";
var idPattern = monitorConfig["idPattern"]?.ToString();
// 1. Seite abrufen
using var http = new HttpClient();
http.DefaultRequestHeaders.CacheControl =
new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
var html = await http.GetStringAsync(url, ct);
var fetchedAt = DateTime.UtcNow;
// 2. Einträge parsen (parser-spezifisch)
var entries = parser switch
{
"capitol_trades" => ParseCapitolTrades(html),
"sec_form4" => ParseSecForm4(html),
_ => ParseGeneric(html, idPattern)
};
// 3. Letzte bekannte Max-ID aus State laden
// State-Key: "webmonitor:{agentId}:{monitorId}:maxId"
// State-Speicher: über den StateManager aus dem Core
// (wird als Dependency über AgentToolContext injiziert — siehe Core-Erweiterung unten)
var stateKey = $"webmonitor:{ctx.AgentId}:{monitorId}:maxId";
var lastMaxId = await ctx.StateStore.GetAsync(stateKey, ct); // string? → long?
var lastKnown = long.TryParse(lastMaxId, out var l) ? l : 0L;
// 4. Neue Einträge = alle mit ID > lastKnown
var newEntries = entries
.Where(e => e.NumericId > lastKnown)
.OrderBy(e => e.NumericId)
.ToList();
// 5. Neue Max-ID persistieren
if (newEntries.Count > 0)
{
var newMax = newEntries.Max(e => e.NumericId).ToString();
await ctx.StateStore.SetAsync(stateKey, newMax, ct);
// Optional: Einträge in History-Tabelle speichern
if (monitorConfig.GetValueOrDefault("storeHistory") is true)
await StoreHistoryAsync(monitorId, newEntries, ctx, ct);
}
// 6. Letzten Check-Zeitpunkt aktualisieren
await ctx.StateStore.SetAsync(
$"webmonitor:{ctx.AgentId}:{monitorId}:lastCheck",
fetchedAt.ToString("O"), ct);
// 7. Ergebnis
var result = new
{
fetchedAt = fetchedAt,
dataAsOf = fetchedAt, // Capitol Trades: Seite ist immer aktuell
source = url,
monitorId = monitorId,
newCount = newEntries.Count,
data = new
{
newEntries = newEntries,
message = newEntries.Count == 0
? "Keine neuen Einträge seit dem letzten Check."
: $"{newEntries.Count} neue Einträge gefunden."
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private static List<MonitorEntry> ParseCapitolTrades(string html)
{
// HTML-Tabelle parsen mit System.Text.RegularExpressions + String-Operationen
//
// Zu extrahieren pro Zeile:
// tradeId: aus /trades/(\d+) in der Detail-URL → NumericId
// politician: Linktext des Politiker-Links
// party: "Republican" | "Democrat" aus dem Text
// chamber: "House" | "Senate"
// state: 2-Buchstaben-Code
// issuer: Unternehmensname
// ticker: z.B. "AVGO:US" → nur "AVGO"
// published: Datum "8 May 2026" → DateTime
// traded: Datum "27 Apr 2026" → DateTime
// filedAfterDays: Zahl aus "days N"
// owner: "Undisclosed" | "Spouse" | "Joint" | etc.
// tradeType: "buy" | "sell"
// size: "1K15K" | "15K50K" | etc.
// price: "$418.20" → decimal
// detailUrl: vollständige URL
// WICHTIG: Duplikate durch pageSize=96 möglich (selbe Transaktion, 2 IDs).
// Beide IDs einliefern — der Agent entscheidet ob relevant.
throw new NotImplementedException();
}
private static List<MonitorEntry> ParseSecForm4(string html)
=> throw new NotImplementedException();
private static List<MonitorEntry> ParseGeneric(string html, string? idPattern)
=> throw new NotImplementedException();
private Task StoreHistoryAsync(string monitorId,
List<MonitorEntry> entries, AgentToolContext ctx, CancellationToken ct)
=> throw new NotImplementedException();
private Task<ToolResult> GetHistoryAsync(string monitorId,
JsonElement input, AgentToolContext ctx, CancellationToken ct)
=> throw new NotImplementedException();
private Task<ToolResult> GetStatusAsync(string monitorId,
AgentToolContext ctx, CancellationToken ct)
=> throw new NotImplementedException();
}
public sealed record MonitorEntry(
long NumericId,
string RawId,
string DetailUrl,
DateTime? PublishedAt,
DateTime? TradedAt,
Dictionary<string, string> Fields // flexible Felder je nach Parser
);
```
### Core-Erweiterung: IStateStore
`WebMonitor` braucht persistenten State zwischen Runs (die letzte bekannte Trade-ID).
Dafür muss `AgentToolContext` um ein `IStateStore` erweitert werden:
```csharp
// Core/Tools/AgentToolContext.cs — erweitern:
public sealed record AgentToolContext(
string AgentId,
string InstanceId,
IReadOnlyDictionary<string, object?> ToolConfig,
IStateStore StateStore, // NEU
ILogger Logger,
CancellationToken CancellationToken
);
// Core/State/IStateStore.cs — neues Interface:
namespace ClawdDotNet.Core.State;
public interface IStateStore
{
Task<string?> GetAsync(string key, CancellationToken ct);
Task SetAsync(string key, string value, CancellationToken ct);
Task DeleteAsync(string key, CancellationToken ct);
}
// Implementierungen (im Core oder als separates Projekt):
// - JsonFileStateStore → speichert in ./data/{instanceId}/state.json
// - SqliteStateStore → speichert in ./data/{instanceId}/state.db (empfohlen)
// Beide implementieren IStateStore.
// SqliteStateStore ist bevorzugt: atomic writes, kein Datenverlust bei Absturz.
// NuGet: Microsoft.Data.Sqlite
```
---
## Automatisches Monitoring via Scheduler
Das `WebMonitor`-Tool wird typischerweise nicht interaktiv genutzt, sondern
vom Scheduler getriggert. Konfiguration in der AgentConfig:
```json
{
"agentId": "capitol-watcher",
"displayName": "Capitol Trades Monitor",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du überwachst Politiker-Trades auf Capitol Trades. Bei neuen Trades analysierst du: Welcher Sektor? Auffälliges Timing? Cluster mehrerer Politiker beim selben Wert? Fasse neue Trades prägnant zusammen. Verwende niemals Daten ohne fetchedAt-Feld.",
"tools": {
"WebMonitor": {
"monitors": {
"capitol_trades": {
"url": "https://www.capitoltrades.com/trades?pageSize=96",
"checkIntervalMinutes": 30,
"idPattern": "/trades/(\\d+)",
"parser": "capitol_trades",
"alertOnNew": true,
"storeHistory": true
}
}
},
"Database": {
"connectionString": "...",
"allowedTables": ["capitol_trades_history", "trade_alerts"]
},
"Mail": {
"smtpHost": "...",
"allowedRecipients": ["owner@example.com"]
}
},
"scheduler": {
"cron": "*/30 * * * *",
"runOnStart": true
},
"loopGuard": {
"maxSteps": 10,
"maxTokens": 30000
}
}
```
Ablauf eines automatischen Runs:
1. Scheduler feuert alle 30 Minuten
2. Agent ruft `WebMonitor.check(capitol_trades)` auf
3. Tool liefert neue Trades als Delta
4. Agent analysiert: Cluster? Insider-Timing? Sektor-Häufung?
5. Bei relevanten Funden: `Mail.send` oder `Database.insert` zur Archivierung
6. Run beendet — kein manueller Eingriff nötig
---
## Implementierungsreihenfolge (für Claude Code)
Bearbeite diesen Abschnitt nach Abschluss der Core- und WinForms-Phase:
1. `IStateStore` Interface + `SqliteStateStore` Implementierung in Core anlegen
2. `AgentToolContext` um `IStateStore` erweitern, alle bestehenden Tool-Calls anpassen
3. `DirectApiTool` implementieren:
- Twelve Data quote + history
- Yahoo Finance quote (kein Key nötig, als Fallback)
- CoinGecko crypto
- Timestamp-Extraktion aus jeweiligem Response-Format
4. `WebFetchTool` implementieren:
- Domain-Whitelist-Check
- HttpClient mit no-cache Headers
- Einfaches HTML-Stripping (ohne externe Libs)
- RSS/Atom-Parser mit System.Xml.Linq
- Timestamp-Extraktion aus HTML-Meta-Tags
5. `WebMonitorTool` implementieren:
- `ParseCapitolTrades` als ersten Parser (Regex auf HTML-Tabelle)
- `CheckForNewEntriesAsync` mit IStateStore-Integration
- `GetHistory` und `GetStatus` Aktionen
- `ParseSecForm4` als zweiten Parser
6. xUnit-Tests:
- `ParseCapitolTrades` gegen gespeichertes HTML-Sample testen
- Delta-Logik: lastKnownId=X, neue IDs=[X-1, X, X+1, X+2] → nur X+1 und X+2
- Domain-Whitelist: erlaubte und gesperrte Domain testen
- Timestamp-Extraktion: Last-Modified Header, og:article:published_time, kein Header
7. Beispiel-Config `capitol-team.json` anlegen
8. In `Program.cs`: WebMonitorTool registrieren, SqliteStateStore als IStateStore in DI
**Beginne mit Schritt 1 dieses Abschnitts.**
@@ -0,0 +1,718 @@
# ClawdDotNet Prompt-Anhang: Tool "TelegramClient"
Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert das Tool
`TelegramClient`, das über die Telegram Client API (MTProto) auf den persönlichen
Telegram-Account des Nutzers zugreift. Dieses Tool ist NICHT der bereits vorhandene
Telegram Bot — es nutzt die User-API und kann damit auch Nachrichten aus privaten
Gruppen lesen, in denen der Nutzer Mitglied ist.
**Nur Lese-Zugriff. Kein Senden von Nachrichten.**
---
## Voraussetzungen
### Telegram API Credentials
Der Nutzer muss einmalig auf https://my.telegram.org/apps eine App registrieren.
Ergebnis: `api_id` (Integer) und `api_hash` (String). Diese Werte repräsentieren
die Anwendung (nicht den User) und werden in der InstanceConfig gespeichert.
### Erstmalige Authentifizierung
Beim allerersten Start muss der Nutzer sich interaktiv authentifizieren:
1. Telefonnummer eingeben
2. Verifizierungscode eingeben (kommt per Telegram-App, SMS oder Anruf)
3. Optional: 2FA-Passwort eingeben
Danach wird eine Session-Datei gespeichert. Alle weiteren Starts verwenden
diese Session automatisch — kein erneuter Login nötig.
### NuGet
```xml
<PackageReference Include="WTelegramClient" Version="4.*" />
```
---
## Architektur: Shared Client, Read-Only Access
### Warum ein Shared Client?
Die Telegram Client API erlaubt pro Telefonnummer nur EINE aktive MTProto-Verbindung.
Mehrere Agent-Runs dürfen NICHT jeweils einen eigenen WTelegram.Client instanziieren —
das würde die Session invalidieren und den Login auf dem echten Telegram-Client killen.
Lösung: Ein einziger `WTelegram.Client` wird im Host instanziiert und als Singleton
an alle Agenten weitergegeben. Das Tool selbst ist stateless und greift über den
Shared Client auf Telegram zu.
```
Host (Program.cs)
└─ TelegramClientManager (Singleton)
└─ WTelegram.Client (eine Instanz pro Prozess)
├─ Agent A: TelegramClient-Tool → liest Gruppe "Aktien-Chat"
├─ Agent B: TelegramClient-Tool → liest Gruppe "Krypto-Signals"
└─ Agent C: TelegramClient-Tool → liest DMs
```
### Concurrency
WTelegram.Client ist NICHT thread-safe für gleichzeitige API-Calls.
Der `TelegramClientManager` muss alle Aufrufe über einen `SemaphoreSlim(1,1)`
serialisieren. Da wir nur lesen und die Calls schnell sind (<500ms), ist
die Serialisierung kein Bottleneck.
---
## TelegramClientManager
**Datei: `Host/Services/TelegramClientManager.cs`**
Verwaltet die einzige WTelegram.Client-Instanz. Wird im Host als Singleton registriert.
```csharp
namespace ClawdDotNet.Host.Services;
using WTelegram;
using TL;
public sealed class TelegramClientManager : IAsyncDisposable
{
private Client? _client;
private User? _self;
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly ILogger<TelegramClientManager> _logger;
// Config-Werte aus InstanceConfig
private readonly int _apiId;
private readonly string _apiHash;
private readonly string _phoneNumber;
private readonly string _sessionPath;
private readonly string? _2faPassword;
// Event für interaktive Login-Aufforderung (Code-Eingabe via UI)
public event Func<string, Task<string>>? OnLoginCodeRequired;
public event Func<Task<string>>? On2FAPasswordRequired;
public bool IsConnected => _client?.User != null;
public User? Self => _self;
public TelegramClientManager(
Config.InstanceConfig config,
ILogger<TelegramClientManager> logger)
{
_logger = logger;
var tgConfig = config.TelegramClient
?? throw new InvalidOperationException("TelegramClient config missing in InstanceConfig");
_apiId = tgConfig.ApiId;
_apiHash = tgConfig.ApiHash;
_phoneNumber = tgConfig.PhoneNumber;
_sessionPath = Path.Combine(config.WorkingDirectory, $"telegram_{config.InstanceId}.session");
_2faPassword = tgConfig.Password2FA;
}
public async Task ConnectAsync(CancellationToken ct)
{
// WTelegram.Client mit Config-Callback instanziieren
_client = new Client(ConfigCallback, _sessionPath);
// Logging an ILogger umleiten
Helpers.Log = (lvl, msg) =>
_logger.Log((Microsoft.Extensions.Logging.LogLevel)lvl, "WTelegram: {Message}", msg);
_self = await _client.LoginUserIfNeeded();
_logger.LogInformation(
"Telegram: logged in as {Name} (id {Id})",
_self.first_name, _self.id);
}
private string? ConfigCallback(string what) => what switch
{
"api_id" => _apiId.ToString(),
"api_hash" => _apiHash,
"phone_number" => _phoneNumber,
"session_pathname" => _sessionPath,
// Interaktiver Code — wird über Event an die UI weitergeleitet
"verification_code" => OnLoginCodeRequired != null
? OnLoginCodeRequired("Bitte Telegram-Verifizierungscode eingeben:").Result
: throw new InvalidOperationException(
"Verification code required but no UI handler registered. " +
"Connect OnLoginCodeRequired to prompt the user."),
// 2FA-Passwort — aus Config oder interaktiv
"password" => _2faPassword
?? (On2FAPasswordRequired != null
? On2FAPasswordRequired().Result
: throw new InvalidOperationException(
"2FA password required but not configured.")),
_ => null // Defaults für alles andere
};
/// Alle Dialoge (Chats, Gruppen, Kanäle, DMs) auflisten
public async Task<Messages_Dialogs> GetAllDialogsAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Messages_GetAllDialogs(); }
finally { _gate.Release(); }
}
/// Alle Gruppen/Kanäle auflisten (ohne DMs)
public async Task<Messages_Chats> GetAllChatsAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Messages_GetAllChats(); }
finally { _gate.Release(); }
}
/// Nachrichten aus einem Chat/Kanal/Gruppe lesen
/// peer: Chat-ID oder Username
/// minId: nur Nachrichten neuer als diese ID (für Delta-Abfragen)
/// limit: max. Anzahl Nachrichten
public async Task<Messages_MessagesBase> GetMessagesAsync(
InputPeer peer, int minId = 0, int limit = 50, CancellationToken ct = default)
{
await _gate.WaitAsync(ct);
try
{
return await _client!.Messages_GetHistory(
peer, offset_id: 0, offset_date: default,
add_offset: 0, limit: limit, max_id: 0, min_id: minId, hash: 0);
}
finally { _gate.Release(); }
}
/// Peer über Username oder Chat-ID auflösen
public async Task<IPeerInfo> ResolveUsernameAsync(string username, CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Contacts_ResolveUsername(username.TrimStart('@')); }
finally { _gate.Release(); }
}
/// Peer über bekannte Chat-ID auflösen (benötigt vorherigen GetAllChats/Dialogs Aufruf)
public InputPeer? GetInputPeerFromCache(long chatId)
=> _client!.GetInputPeerID(chatId);
public async ValueTask DisposeAsync()
{
_client?.Dispose();
_gate.Dispose();
}
}
```
---
## TelegramClientTool — das IAgentTool
**Datei: `ClawdDotNet.Tools.TelegramClient/TelegramClientTool.cs`**
```csharp
namespace ClawdDotNet.Tools.TelegramClient;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Host.Services; // TelegramClientManager
using TL;
using System.Text.Json;
public sealed class TelegramClientTool : IAgentTool
{
// Manager wird per DI injiziert (Singleton im Host)
private readonly TelegramClientManager _tg;
public TelegramClientTool(TelegramClientManager tg) => _tg = tg;
public string Name => "TelegramClient";
public string Description => """
Liest Nachrichten aus dem persönlichen Telegram-Account des Nutzers.
Zugriff auf alle Chats, Gruppen und Kanäle in denen der Nutzer Mitglied ist.
NUR LESEN kein Senden, kein Löschen, kein Bearbeiten.
Aktionen: list_chats, read_messages, read_new
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action"],
"properties": {
"action": {
"type": "string",
"enum": ["list_chats", "read_messages", "read_new"],
"description": "list_chats: alle Chats/Gruppen/Kanäle auflisten. read_messages: letzte N Nachrichten aus einem Chat lesen. read_new: nur neue Nachrichten seit letztem Abruf."
},
"chatId": {
"type": "integer",
"description": "Chat-ID aus list_chats Ergebnis. Erforderlich für read_messages und read_new."
},
"username": {
"type": "string",
"description": "Alternativ zu chatId: @username einer Gruppe/Person auflösen."
},
"limit": {
"type": "integer",
"description": "Max. Anzahl Nachrichten (default: 30, max: 100)"
}
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
// ---- Permission-Check: Welche Chats darf dieser Agent lesen? ----
var config = ctx.ToolConfig.GetValueOrDefault("TelegramClient")
as Dictionary<string, object?> ?? new();
var allowedChats = config.GetValueOrDefault("allowedChatIds")
as List<long>; // null = alle erlaubt
var allowedUsernames = config.GetValueOrDefault("allowedUsernames")
as List<string>;
if (!_tg.IsConnected)
return new ToolResult(false, "",
"Telegram-Client ist nicht verbunden. Bitte zuerst authentifizieren.");
var action = input.GetProperty("action").GetString()!;
return action switch
{
"list_chats" => await ListChatsAsync(allowedChats, ct),
"read_messages" => await ReadMessagesAsync(input, allowedChats, config, ctx, ct),
"read_new" => await ReadNewAsync(input, allowedChats, config, ctx, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> ListChatsAsync(
List<long>? allowedChats, CancellationToken ct)
{
var dialogs = await _tg.GetAllDialogsAsync(ct);
var chatList = new List<object>();
foreach (Dialog dialog in dialogs.dialogs)
{
var peer = dialogs.UserOrChat(dialog);
if (peer == null) continue;
var chatId = dialog.Peer.ID;
// Filter: nur erlaubte Chats anzeigen (wenn Whitelist definiert)
if (allowedChats != null && !allowedChats.Contains(chatId))
continue;
var info = peer switch
{
User user when user.IsActive => new
{
chatId = chatId,
type = "user",
name = $"{user.first_name} {user.last_name}".Trim(),
username = user.MainUsername,
unread = dialog.UnreadCount,
lastMsgId = dialog.TopMessage
} as object,
ChatBase chat when chat.IsActive => new
{
chatId = chatId,
type = chat is Channel ch
? (ch.IsGroup ? "supergroup" : "channel")
: "group",
name = chat.Title,
username = (chat as Channel)?.MainUsername,
unread = dialog.UnreadCount,
lastMsgId = dialog.TopMessage
} as object,
_ => null
};
if (info != null) chatList.Add(info);
}
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = DateTime.UtcNow,
source = "telegram_client_api",
data = new
{
totalChats = chatList.Count,
chats = chatList
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<ToolResult> ReadMessagesAsync(
JsonElement input, List<long>? allowedChats,
Dictionary<string, object?> config,
AgentToolContext ctx, CancellationToken ct)
{
var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
if (error != null) return new ToolResult(false, "", error);
var limit = input.TryGetProperty("limit", out var l)
? Math.Clamp(l.GetInt32(), 1, 100)
: 30;
var messages = await _tg.GetMessagesAsync(peer!, minId: 0, limit: limit, ct: ct);
var msgList = FormatMessages(messages);
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = DateTime.UtcNow,
source = $"telegram_chat_{chatId}",
data = new
{
chatId = chatId,
count = msgList.Count,
messages = msgList
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<ToolResult> ReadNewAsync(
JsonElement input, List<long>? allowedChats,
Dictionary<string, object?> config,
AgentToolContext ctx, CancellationToken ct)
{
var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
if (error != null) return new ToolResult(false, "", error);
// Letzte bekannte Message-ID aus StateStore laden
var stateKey = $"tgclient:{ctx.AgentId}:chat_{chatId}:lastMsgId";
var lastIdStr = await ctx.StateStore.GetAsync(stateKey, ct);
var lastId = int.TryParse(lastIdStr, out var id) ? id : 0;
var limit = input.TryGetProperty("limit", out var l)
? Math.Clamp(l.GetInt32(), 1, 100)
: 50;
// min_id = lastId → nur Nachrichten neuer als lastId
var messages = await _tg.GetMessagesAsync(peer!, minId: lastId, limit: limit, ct: ct);
var msgList = FormatMessages(messages);
// Neue Max-ID persistieren
if (msgList.Count > 0)
{
var newMaxId = msgList.Max(m => m.messageId);
await ctx.StateStore.SetAsync(stateKey, newMaxId.ToString(), ct);
}
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = DateTime.UtcNow,
source = $"telegram_chat_{chatId}",
data = new
{
chatId = chatId,
sinceId = lastId,
newCount = msgList.Count,
messages = msgList
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<(InputPeer? peer, long chatId, string? error)> ResolvePeerAsync(
JsonElement input, List<long>? allowedChats, CancellationToken ct)
{
long chatId = 0;
InputPeer? peer = null;
if (input.TryGetProperty("chatId", out var cid))
{
chatId = cid.GetInt64();
if (allowedChats != null && !allowedChats.Contains(chatId))
return (null, chatId,
$"Agent hat keinen Zugriff auf Chat {chatId}.");
peer = _tg.GetInputPeerFromCache(chatId);
if (peer == null)
{
// Cache befüllen durch einmaligen GetAllDialogs-Aufruf
await _tg.GetAllDialogsAsync(ct);
peer = _tg.GetInputPeerFromCache(chatId);
}
}
else if (input.TryGetProperty("username", out var uname))
{
var resolved = await _tg.ResolveUsernameAsync(uname.GetString()!, ct);
peer = resolved?.ToInputPeer();
chatId = peer?.ID ?? 0;
if (allowedChats != null && !allowedChats.Contains(chatId))
return (null, chatId,
$"Agent hat keinen Zugriff auf Chat @{uname.GetString()}.");
}
if (peer == null)
return (null, 0, "chatId oder username muss angegeben werden.");
return (peer, chatId, null);
}
private static List<FormattedMessage> FormatMessages(Messages_MessagesBase messages)
{
var result = new List<FormattedMessage>();
foreach (var msgBase in messages.Messages)
{
var from = messages.UserOrChat(msgBase.From ?? msgBase.Peer);
var fromName = from switch
{
User u => $"{u.first_name} {u.last_name}".Trim(),
ChatBase c => c.Title,
_ => "Unknown"
};
if (msgBase is Message msg)
{
result.Add(new FormattedMessage(
messageId: msg.ID,
date: msg.Date,
from: fromName,
fromId: msgBase.From?.ID ?? 0,
text: msg.message,
hasMedia: msg.media != null,
mediaType: msg.media?.GetType().Name,
replyToId: (msg.reply_to as MessageReplyHeader)?.reply_to_msg_id,
forwardFrom: msg.fwd_from != null
? msg.fwd_from.from_name ?? "forwarded"
: null,
views: msg.views
));
}
}
return result.OrderBy(m => m.messageId).ToList();
}
private sealed record FormattedMessage(
int messageId,
DateTime date,
string from,
long fromId,
string? text,
bool hasMedia,
string? mediaType,
int? replyToId,
string? forwardFrom,
int? views
);
}
```
---
## AgentConfig-Beispiel
```json
{
"agentId": "telegram-scout",
"displayName": "Telegram News-Scout",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du überwachst Telegram-Gruppen auf relevante Finanznachrichten und Trading-Signale. Fasse neue Nachrichten zusammen und bewerte ihre Relevanz. Verwende niemals Daten ohne fetchedAt-Feld.",
"tools": {
"TelegramClient": {
"allowedChatIds": [1001234567890, 1009876543210],
"allowedUsernames": ["aktien_chat", "crypto_signals_de"]
},
"Database": {
"connectionString": "...",
"allowedTables": ["telegram_messages", "signal_archive"]
}
},
"scheduler": {
"cron": "*/15 * * * *",
"runOnStart": true
},
"loopGuard": {
"maxSteps": 10,
"maxTokens": 30000
}
}
```
Ein Agent ohne `TelegramClient`-Eintrag in seiner Config bekommt das Tool
gar nicht erst in seinem LLM-Tool-Set angezeigt (normales Permission-Verhalten).
Ein Agent MIT Config aber ohne `allowedChatIds` (= null) darf alle Chats lesen.
---
## InstanceConfig-Erweiterung
```csharp
// Config/InstanceConfig.cs — neues optionales Feld:
public sealed class InstanceConfig
{
// ... bestehende Felder ...
public TelegramClientConfig? TelegramClient { get; set; }
}
public sealed class TelegramClientConfig
{
public int ApiId { get; set; } // von https://my.telegram.org/apps
public string ApiHash { get; set; } = ""; // von https://my.telegram.org/apps
public string PhoneNumber { get; set; } = ""; // z.B. "+491701234567"
public string? Password2FA { get; set; } // optional, nur bei aktivierter 2FA
}
```
```json
// In stock-team.json:
{
"instanceId": "stock-01",
"instanceName": "Aktien-Team",
"openRouterApiKey": "sk-or-...",
"workingDirectory": "./data/stock/",
"webServerPort": 8081,
"telegramClient": {
"apiId": 12345678,
"apiHash": "abcdef1234567890abcdef1234567890",
"phoneNumber": "+491701234567"
},
"agents": [ ... ]
}
```
---
## Interaktiver Login in WinForms
Der erste Login erfordert einen Verifizierungscode. Dieser wird über das
bestehende Chat-UI in `frm_main` abgefragt — nicht über die Konsole.
**In `frm_main` oder `Program.cs` beim Start:**
```csharp
var tgManager = provider.GetRequiredService<TelegramClientManager>();
// UI-Handler für Code-Eingabe registrieren
tgManager.OnLoginCodeRequired = async (prompt) =>
{
// Auf UI-Thread: InputBox oder Chat-Nachricht anzeigen
string? code = null;
mainForm.Invoke(() =>
{
code = Microsoft.VisualBasic.Interaction.InputBox(
prompt, "Telegram Verifizierung", "");
});
return code ?? "";
};
tgManager.On2FAPasswordRequired = async () =>
{
string? pw = null;
mainForm.Invoke(() =>
{
pw = Microsoft.VisualBasic.Interaction.InputBox(
"Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
});
return pw ?? "";
};
// Verbindung herstellen (nutzt Session-Datei wenn vorhanden)
try
{
await tgManager.ConnectAsync(CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Telegram: Login fehlgeschlagen");
// App startet trotzdem — TelegramClient-Tool meldet "nicht verbunden"
}
```
Nach erfolgreichem Login wird die Session-Datei
`./data/{instanceId}/telegram_{instanceId}.session` gespeichert.
Alle weiteren Starts loggen automatisch ein — kein Code mehr nötig.
---
## Sicherheitsregeln
1. **NUR LESEN** — Das Tool implementiert keine Sende-Funktionen.
Es gibt keine `send_message`-Action. Der `TelegramClientManager`
exponiert bewusst keine `SendMessageAsync`-Methode.
2. **Session-Datei ist sensibel** — Sie enthält die Auth-Keys für den
Telegram-Account. Die Datei liegt im `WorkingDirectory` und darf
NICHT vom FileRW-Tool erreichbar sein. In der AgentConfig für
FileRW darf der `rootPath` NIEMALS auf das WorkingDirectory zeigen
wenn dort die Session-Datei liegt. Empfehlung: Session-Datei in
einem Unterordner `./data/{instanceId}/sessions/` speichern, der
für kein FileRW-Tool als rootPath konfiguriert ist.
3. **Chat-Whitelist pro Agent** — Über `allowedChatIds` kann eingeschränkt
werden welche Chats ein Agent lesen darf. Ein Finanzmarkt-Agent hat
keinen Zugriff auf private DMs. Ein SEO-Agent hat keinen Zugriff auf
Trading-Gruppen.
4. **Rate Limiting** — Die Telegram Client API hat undokumentierte Rate-Limits.
Bei zu vielen Requests kommt ein `FLOOD_WAIT_X` Error. Der
`TelegramClientManager` muss `FloodException` abfangen und
`await Task.Delay(ex.X * 1000)` warten bevor er den Call wiederholt.
Empfehlung: mindestens 1 Sekunde Pause zwischen aufeinanderfolgenden
API-Calls (der SemaphoreSlim allein reicht nicht).
---
## Besonderheiten von WTelegramClient
### Terminology-Mapping
In der Telegram Client API unterscheiden sich die Begriffe von der Benutzeroberfläche:
| Telegram-App | API-Bezeichnung | C#-Typ |
|---|---|---|
| Gruppe (klein) | Chat | `Chat` |
| Gruppe (groß) | Channel mit IsGroup | `Channel` (IsGroup) |
| Kanal | Channel ohne IsGroup | `Channel` (!IsGroup) |
| Privatnachricht | User | `User` |
### access_hash-Problem
Telegram-API-Calls benötigen für die meisten Peers einen `access_hash`.
Dieser wird automatisch gecacht wenn vorher `Messages_GetAllDialogs()`
oder `Messages_GetAllChats()` aufgerufen wurde. Deshalb MUSS bei jedem
Start (nach Login) einmalig `GetAllDialogsAsync()` aufgerufen werden,
bevor `GetMessagesAsync()` funktioniert.
### Session-Datei
- Pfad konfigurierbar über `session_pathname` in der Config-Callback
- Verschlüsselt (Standard-Verschlüsselung von WTelegramClient)
- NICHT zwischen Rechnern portierbar (an Hardware gebunden)
- Bei Session-Problemen: Datei löschen → neuer Login erforderlich
---
## Implementierungsreihenfolge (für Claude Code)
1. `TelegramClientConfig` zu `InstanceConfig` hinzufügen
2. `TelegramClientManager` implementieren (Singleton, SemaphoreSlim, Rate-Limit-Schutz)
3. `TelegramClientTool` implementieren (list_chats, read_messages, read_new)
4. Host: Login-Flow in `Program.cs` / `frm_main` integrieren (InputBox für Code)
5. Sicherheits-Check: Session-Pfad darf nicht in FileRW-rootPath liegen
6. xUnit-Tests: FormatMessages-Serialisierung, Chat-Whitelist-Filter, Rate-Limit-Handling
7. Beispiel-Config ergänzen: `stock-team.json` mit TelegramClient-Eintrag
**Beginne mit Schritt 1 dieses Abschnitts.**
@@ -0,0 +1,792 @@
# ClawdDotNet Prompt-Anhang: WinForms & WebView2 Integration
Dieser Abschnitt ergänzt den Haupt-Entwicklungsprompt und behandelt ausschließlich
die WinForms-UI-Schicht mit WebView2. Er baut auf den bereits definierten Core-Typen
(AgentConfig, InstanceConfig, AgentEngine, IAgentTool etc.) auf.
---
## Übersicht: Zwei WebView2-Kontexte
Es gibt genau zwei WebView2-Kontexte im Host. Sie sind vollständig getrennt
und haben unterschiedliche Sicherheits-Scopes:
| Kontext | Control | Form | Zweck |
|---|---|---|---|
| `webView_chat` | `WebView2` in `frm_main` | Hauptfenster | Agentenübersicht + Auswahl + Chat mit einem Agenten |
| `webView_chat2` | `WebView2` in `frm_chat` | Einzelchat-Fenster | Chat mit genau einem Agenten, mehrfach öffenbar |
`frm_chat` ist bewusst ein eigenständiges, nicht-modales Fenster — es kann mehrfach
instanziiert werden, sodass der Nutzer mehrere Agenten-Chats nebeneinander
auf dem Bildschirm überwachen kann. Jede `frm_chat`-Instanz kennt genau einen `AgentId`.
---
## Sicherheitsarchitektur: Physische Trennung der WebRoots
### Zwei Hostnamen, zwei Quellen — niemals überlappend
```
Assembly (Embedded Resources) Disk (vom FileRW-Tool beschreibbar)
────────────────────────────── ──────────────────────────────────
Host/EmbeddedUI/ data/{instanceId}/
overview.html ← frm_main wwwroot/ ← Kestrel-Root
overview.css webView_chat index.html
overview.js styles/
chat.html ← frm_chat data/
chat.css webView_chat2 assets/
bridge.js
```
**Kernregel:** `EmbeddedUI/` existiert nur als Assembly-Resource.
Sie hat keinen Dateisystempfad, auf den ein Tool zeigen könnte.
Kein `FileRW`-Tool bekommt jemals einen `rootPath`, der auf `EmbeddedUI/` zeigt.
### WebView2 Virtual Host Mapping
```csharp
// Beide Mappings werden in InitWebViewAsync() jeder Form gesetzt:
// Intern aus Assembly-Stream (temporär extrahiert beim Start)
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(), // einmalig beim App-Start nach temp/
CoreWebView2HostResourceAccessKind.DenyCors);
// Extern Agent-generierte Inhalte auf Disk
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
_instanceConfig.WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
```
`ui.clwd.internal` → nur lesbar, kein Cross-Origin-Zugriff von außen
`dash.clwd.local` → lesbar für den WebView, schreibbar nur durch FileRW-Tool
### Startvalidierung (Pflicht, einmalig in Program.cs)
```csharp
// Sicherheitscheck beim App-Start Exception wenn verletzt:
var wwwAbs = Path.GetFullPath(instanceConfig.WwwRootPath);
var uiAbs = Path.GetFullPath(EmbeddedUiManager.GetExtractedPath());
if (wwwAbs.StartsWith(uiAbs) || uiAbs.StartsWith(wwwAbs))
throw new InvalidOperationException(
"SECURITY: WwwRootPath and EmbeddedUI path must never overlap.");
```
---
## EmbeddedUiManager
**Datei: `Host/UI/EmbeddedUiManager.cs`**
Aufgabe: HTML/CSS/JS-Dateien aus den Assembly Embedded Resources einmalig beim
Programmstart in einen temporären Ordner extrahieren. WebView2 kann nur auf
Dateisystempfade mappen, nicht direkt auf Streams.
```csharp
namespace ClawdDotNet.Host.UI;
public static class EmbeddedUiManager
{
private static string? _extractedPath;
// Einmalig beim App-Start aufrufen (vor Application.Run)
public static string ExtractToTemp()
{
if (_extractedPath != null) return _extractedPath;
var tempDir = Path.Combine(Path.GetTempPath(), "ClawdDotNet_UI",
Assembly.GetExecutingAssembly()
.GetName().Version?.ToString() ?? "dev");
Directory.CreateDirectory(tempDir);
var asm = Assembly.GetExecutingAssembly();
// Alle Embedded Resources im Namespace "ClawdDotNet.Host.EmbeddedUI"
foreach (var name in asm.GetManifestResourceNames()
.Where(n => n.Contains(".EmbeddedUI.")))
{
// "ClawdDotNet.Host.EmbeddedUI.chat.css" → "chat.css"
var fileName = name.Split(".EmbeddedUI.").Last();
var dest = Path.Combine(tempDir, fileName);
using var stream = asm.GetManifestResourceStream(name)!;
using var file = File.Create(dest);
stream.CopyTo(file);
}
_extractedPath = tempDir;
return tempDir;
}
public static string GetExtractedPath()
=> _extractedPath ?? throw new InvalidOperationException(
"EmbeddedUiManager.ExtractToTemp() must be called first.");
}
```
Embedded Resources werden in der `.csproj` so eingebunden:
```xml
<ItemGroup>
<EmbeddedResource Include="EmbeddedUI\**\*" />
</ItemGroup>
```
---
## C#JavaScript Bridge
**Datei: `Host/UI/WebViewBridge.cs`**
Eine Bridge-Instanz pro WebView2-Control. Kapselt die gesamte
bidirektionale Kommunikation. Keine rohen `ExecuteScriptAsync`-Aufrufe
außerhalb dieser Klasse.
```csharp
namespace ClawdDotNet.Host.UI;
public sealed class WebViewBridge : IDisposable
{
private readonly Microsoft.Web.WebView2.WinForms.WebView2 _wv;
private readonly ILogger<WebViewBridge> _logger;
// Eingehende Nachrichten vom Browser → C#
public event Action<BridgeMessage>? MessageReceived;
public WebViewBridge(
Microsoft.Web.WebView2.WinForms.WebView2 webView,
ILogger<WebViewBridge> logger)
{
_wv = webView;
_logger = logger;
_wv.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
}
// C# → Browser: typisiert, immer als JSON
public async Task SendAsync(BridgeMessage message, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(message, BridgeJsonOptions.Default);
// Muss auf dem UI-Thread ausgeführt werden
await _wv.InvokeAsync(async () =>
await _wv.CoreWebView2.ExecuteScriptAsync(
$"window.__bridge?.receive({json})"));
}
private void OnWebMessageReceived(object? sender,
CoreWebView2WebMessageReceivedEventArgs e)
{
try
{
var msg = JsonSerializer.Deserialize<BridgeMessage>(
e.WebMessageAsJson, BridgeJsonOptions.Default);
if (msg != null) MessageReceived?.Invoke(msg);
}
catch (Exception ex)
{
_logger.LogError(ex, "Bridge: failed to deserialize incoming message");
}
}
public void Dispose()
=> _wv.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
}
```
### BridgeMessage Nachrichtenformat
**Datei: `Host/UI/BridgeMessage.cs`**
Alle Nachrichten in beide Richtungen verwenden diesen Typ.
Das `Type`-Feld bestimmt, was in `Payload` steckt.
```csharp
namespace ClawdDotNet.Host.UI;
public sealed record BridgeMessage(
string Type, // siehe Konstanten unten
string? AgentId = null,
string? Content = null, // Chat-Text, HTML-Snippet
string? Status = null, // "running" | "idle" | "error"
int? StepCount = null,
int? TokenCount = null,
string? Error = null,
object? Extra = null // type-spezifische Zusatzdaten
);
// Typ-Konstanten (C# → Browser)
public static class BridgeTypes
{
// frm_main: overview.html
public const string AgentListUpdate = "agent_list_update"; // Alle Agenten initial laden
public const string AgentStatusUpdate = "agent_status"; // Statusänderung eines Agenten
public const string SelectAgent = "select_agent"; // Agenten im Chat auswählen
// frm_main + frm_chat: chat.html
public const string ChatMessage = "chat_message"; // Neue Nachricht anzeigen
public const string ChatTyping = "chat_typing"; // Tipp-Indikator an/aus
public const string ChatHistory = "chat_history"; // Verlauf beim Öffnen laden
public const string RunStarted = "run_started"; // Agent-Run begann
public const string RunFinished = "run_finished"; // Agent-Run beendet
// Browser → C# (eingehend)
public const string UserMessage = "user_message"; // Nutzer hat Enter gedrückt
public const string OpenAgentChat = "open_agent_chat"; // "Eigenes Fenster öffnen"
public const string RunNow = "run_now"; // Manueller Run-Trigger
public const string AbortRun = "abort_run"; // Run abbrechen
}
```
---
## frm_main Hauptfenster
**Datei: `Host/Forms/frm_main.cs`**
`frm_main` enthält `webView_chat` (bereits angelegt). Dieses WebView zeigt
`overview.html`: eine Seitenleiste mit allen Agenten und einen Chat-Bereich
für den aktuell ausgewählten Agenten.
```csharp
namespace ClawdDotNet.Host.Forms;
public partial class frm_main : Form
{
private readonly InstanceConfig _instance;
private readonly AgentEngine _engine;
private readonly AgentScheduler _scheduler;
private readonly ILogger<frm_main> _logger;
private WebViewBridge? _bridge;
private string? _selectedAgentId;
// Offene Einzelchat-Fenster: AgentId → frm_chat
private readonly Dictionary<string, frm_chat> _chatWindows = new();
public frm_main(InstanceConfig instance, AgentEngine engine,
AgentScheduler scheduler, ILogger<frm_main> logger)
{
InitializeComponent();
_instance = instance;
_engine = engine;
_scheduler = scheduler;
_logger = logger;
}
private async void frm_main_Load(object sender, EventArgs e)
{
await InitWebViewAsync();
_scheduler.RunStatusChanged += OnRunStatusChanged; // Event aus Core
}
private async Task InitWebViewAsync()
{
await webView_chat.EnsureCoreWebView2Async();
// Virtual Host Mappings
webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(),
CoreWebView2HostResourceAccessKind.DenyCors);
webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
_instance.WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
_bridge = new WebViewBridge(webView_chat, /* logger */);
_bridge.MessageReceived += OnBridgeMessage;
webView_chat.CoreWebView2.Navigate(
"https://ui.clwd.internal/overview.html");
// Kurz warten bis DOM bereit, dann Agentenliste senden
await Task.Delay(300);
await PushAgentListAsync();
}
private async Task PushAgentListAsync()
{
// Alle AgentConfigs als Liste → overview.html baut die Sidebar auf
var agents = _instance.Agents.Select(a => new
{
agentId = a.AgentId,
displayName = a.DisplayName,
model = a.Model,
status = _engine.GetStatus(a.AgentId) // "idle"|"running"|"error"
});
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
Extra: agents));
}
private async void OnBridgeMessage(BridgeMessage msg)
{
// Immer auf UI-Thread
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
// Nutzer hat im Chat Enter gedrückt
if (_selectedAgentId is null || msg.Content is null) break;
await HandleUserMessageAsync(_selectedAgentId, msg.Content);
break;
case BridgeTypes.SelectAgent:
// Agenten in der Sidebar angeklickt → Chat-Verlauf laden
_selectedAgentId = msg.AgentId;
await LoadChatHistoryAsync(msg.AgentId!);
break;
case BridgeTypes.OpenAgentChat:
// "Eigenes Fenster" Button → frm_chat öffnen oder fokussieren
OpenChatWindow(msg.AgentId!);
break;
case BridgeTypes.RunNow:
_ = _engine.RunAsync(msg.AgentId!, CancellationToken.None);
break;
case BridgeTypes.AbortRun:
_engine.Abort(msg.AgentId!);
break;
}
}
private void OpenChatWindow(string agentId)
{
if (_chatWindows.TryGetValue(agentId, out var existing)
&& !existing.IsDisposed)
{
existing.BringToFront();
return;
}
var agentConfig = _instance.Agents.First(a => a.AgentId == agentId);
var frm = new frm_chat(agentConfig, _engine, /* logger */);
frm.FormClosed += (_, _) => _chatWindows.Remove(agentId);
_chatWindows[agentId] = frm;
frm.Show(this); // nicht-modal, Elternfenster = frm_main
}
private void OnRunStatusChanged(string agentId, AgentRunStatus status)
{
// Vom Scheduler/Engine gefeuert auf UI-Thread pushen
this.InvokeAsync(async () =>
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentStatusUpdate,
AgentId: agentId,
Status: status.ToString().ToLower(),
StepCount: status.StepCount,
TokenCount: status.TokensUsed)));
}
private async Task HandleUserMessageAsync(string agentId, string text)
{
// Eigene Nachricht sofort anzeigen
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: agentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
// Tipp-Indikator an
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping, AgentId: agentId));
// Chat-Run starten (non-blocking)
_ = Task.Run(async () =>
{
var result = await _engine.ChatAsync(agentId, text, CancellationToken.None);
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: agentId,
Content: result.FinalMessage,
Extra: new { role = "agent", timestamp = DateTime.Now }));
});
}
private async Task LoadChatHistoryAsync(string agentId)
{
var history = await _engine.GetChatHistoryAsync(agentId);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: agentId,
Extra: history));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
_scheduler.RunStatusChanged -= OnRunStatusChanged;
base.OnFormClosed(e);
}
}
```
---
## frm_chat Einzelchat-Fenster
**Datei: `Host/Forms/frm_chat.cs`**
`frm_chat` enthält `webView_chat2` (bereits angelegt). Dieses Fenster zeigt
den Chat mit genau einem Agenten. Es kann beliebig oft gleichzeitig geöffnet
sein — jede Instanz ist vollständig unabhängig.
```csharp
namespace ClawdDotNet.Host.Forms;
public partial class frm_chat : Form
{
private readonly AgentConfig _agentConfig;
private readonly AgentEngine _engine;
private readonly ILogger<frm_chat> _logger;
private WebViewBridge? _bridge;
public frm_chat(AgentConfig agentConfig, AgentEngine engine,
ILogger<frm_chat> logger)
{
InitializeComponent();
_agentConfig = agentConfig;
_engine = engine;
_logger = logger;
// Fenstertitel = Agent-Name
Text = $"Chat {agentConfig.DisplayName}";
}
private async void frm_chat_Load(object sender, EventArgs e)
=> await InitWebViewAsync();
private async Task InitWebViewAsync()
{
await webView_chat2.EnsureCoreWebView2Async();
// Identische Virtual Host Mappings wie frm_main
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(),
CoreWebView2HostResourceAccessKind.DenyCors);
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
// WwwRootPath kommt vom InstanceConfig über DI/Singleton
ServiceLocator.Get<InstanceConfig>().WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
_bridge = new WebViewBridge(webView_chat2, /* logger */);
_bridge.MessageReceived += OnBridgeMessage;
// chat.html lädt für einen bestimmten Agenten
webView_chat2.CoreWebView2.Navigate(
$"https://ui.clwd.internal/chat.html?agent={_agentConfig.AgentId}");
await Task.Delay(300);
// AgentInfo und Verlauf initial senden
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
AgentId: _agentConfig.AgentId,
Extra: new { agents = new[] { new {
agentId = _agentConfig.AgentId,
displayName = _agentConfig.DisplayName,
model = _agentConfig.Model
}}}));
await LoadChatHistoryAsync();
}
private async void OnBridgeMessage(BridgeMessage msg)
{
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
await HandleUserMessageAsync(msg.Content ?? "");
break;
case BridgeTypes.RunNow:
_ = _engine.RunAsync(_agentConfig.AgentId, CancellationToken.None);
break;
case BridgeTypes.AbortRun:
_engine.Abort(_agentConfig.AgentId);
break;
}
}
private async Task HandleUserMessageAsync(string text)
{
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping, AgentId: _agentConfig.AgentId));
_ = Task.Run(async () =>
{
var result = await _engine.ChatAsync(
_agentConfig.AgentId, text, CancellationToken.None);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: result.FinalMessage,
Extra: new { role = "agent", timestamp = DateTime.Now }));
});
}
private async Task LoadChatHistoryAsync()
{
var history = await _engine.GetChatHistoryAsync(_agentConfig.AgentId);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: _agentConfig.AgentId,
Extra: history));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
base.OnFormClosed(e);
}
}
```
---
## Embedded HTML/JS/CSS Dateistruktur
Alle Dateien liegen in `Host/EmbeddedUI/`. Build Action: `Embedded Resource`.
### overview.html (für webView_chat in frm_main)
Dieses HTML baut die komplette Ansicht aus dem Mockup auf:
- Linke Sidebar: Agentenliste (wird via Bridge befüllt)
- Rechter Bereich: Chat mit dem aktuell ausgewählten Agenten
- "Eigenes Fenster"-Button pro Agent → sendet `open_agent_chat`-Nachricht
Kommunikationsprotokoll (JavaScript-Seite):
```javascript
// bridge.js wird von beiden HTML-Seiten eingebunden
window.__bridge = {
// Eingehend von C#
receive(msg) {
document.dispatchEvent(
new CustomEvent('bridge:' + msg.type, { detail: msg }));
},
// Ausgehend zu C#
send(msg) {
window.chrome.webview.postMessage(JSON.stringify(msg));
}
};
// Beispiel: auf Agentenliste reagieren
document.addEventListener('bridge:agent_list_update', e => {
renderSidebar(e.detail.extra.agents);
});
// Beispiel: Nachricht senden
function sendUserMessage(agentId, text) {
window.__bridge.send({
type: 'user_message',
agentId: agentId,
content: text
});
}
```
### chat.html (für webView_chat2 in frm_chat)
Vereinfachte Version ohne Sidebar — nur der Chat-Bereich.
Liest den `?agent=`-URL-Parameter beim Laden und stellt sich
damit auf den entsprechenden Agenten ein.
```javascript
// chat.html Init
const agentId = new URLSearchParams(location.search).get('agent');
document.addEventListener('bridge:chat_history', e => {
if (e.detail.agentId !== agentId) return;
renderHistory(e.detail.extra);
});
document.addEventListener('bridge:chat_message', e => {
if (e.detail.agentId !== agentId) return;
appendBubble(e.detail.extra.role, e.detail.content,
e.detail.extra.timestamp);
});
document.addEventListener('bridge:chat_typing', e => {
if (e.detail.agentId !== agentId) return;
showTypingIndicator();
});
```
---
## Program.cs Startup-Reihenfolge
```csharp
// Host/Program.cs
[STAThread]
static async Task Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// 1. Config laden (--config Argument oder default)
var configPath = GetConfigPath(args);
var instance = InstanceConfig.LoadFromFile(configPath);
// 2. Sicherheitscheck: Pfade dürfen sich nicht überlappen
var uiPath = EmbeddedUiManager.ExtractToTemp(); // extrahiert EmbeddedUI
var wwwPath = Path.GetFullPath(instance.WwwRootPath);
if (wwwPath.StartsWith(uiPath) || uiPath.StartsWith(wwwPath))
throw new InvalidOperationException(
"SECURITY: WwwRootPath and EmbeddedUI path must never overlap.");
// 3. DI-Container aufbauen
var services = new ServiceCollection();
services.AddSingleton(instance);
services.AddSingleton<ToolRegistry>();
services.AddSingleton<PermissionGate>();
services.AddSingleton<AgentEngine>();
services.AddSingleton<AgentScheduler>();
services.AddSingleton<OpenRouterClient>();
services.AddLogging(b => b.AddConsole());
// 4. Tools registrieren (Host ist der einzige Ort, der Tool-Typen kennt)
var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ToolRegistry>();
registry.Register(new DatabaseTool());
registry.Register(new FileRwTool());
registry.Register(new MailTool());
// 5. Scheduler starten
var scheduler = provider.GetRequiredService<AgentScheduler>();
await scheduler.StartAsync(CancellationToken.None);
// 6. WinForms starten
var mainForm = provider.GetRequiredService<frm_main>();
Application.Run(mainForm);
// 7. Cleanup
await scheduler.StopAsync(CancellationToken.None);
}
```
---
## Core-Erweiterungen für Chat-Support
Der `AgentEngine` im Core benötigt zwei zusätzliche Methoden für
den interaktiven Chat-Modus (ergänze Phase 1.7):
```csharp
// AgentEngine zusätzliche Methoden
// Interaktiver Chat: eine Nutzer-Nachricht → Agent-Antwort
// Unterschied zum autonomen Run: kein Scheduler-Trigger,
// Verlauf wird an bestehende Konversation angehängt
Task<AgentRunResult> ChatAsync(string agentId, string userMessage, CancellationToken ct);
// Chat-Verlauf aus dem StateManager laden
// Rückgabe: Liste von { role, content, timestamp }
Task<IReadOnlyList<ChatEntry>> GetChatHistoryAsync(string agentId);
// Aktuellen Run-Status abrufen (für Statusanzeige in der Sidebar)
AgentRunStatus GetStatus(string agentId);
// Laufenden Run abbrechen
void Abort(string agentId);
```
```csharp
// Neue Typen in Core/Engine/
public sealed record ChatEntry(
string Role, // "user" | "agent" | "tool"
string Content,
DateTime Timestamp
);
public sealed record AgentRunStatus(
string State, // "idle" | "running" | "error"
int StepCount,
int TokensUsed,
string? LastError
);
```
Der StateManager (Phase 1.2) speichert den Chat-Verlauf pro AgentId
persistent in der konfigurierten Datenbank oder als JSON-Datei,
sodass Verläufe auch nach Programm-Neustart verfügbar sind.
---
## Entwicklungsregeln: WinForms-spezifisch
- **Kein UI-Thread-Blocking:** Alle `await`-Aufrufe in Forms immer mit
`ConfigureAwait(false)` oder explizitem `InvokeAsync`. Bridge-Callbacks
kommen auf beliebigen Threads — immer per `InvokeRequired` prüfen.
- **WebView2 ist async:** `EnsureCoreWebView2Async()` muss abgewartet sein
bevor `CoreWebView2`-Eigenschaften gesetzt werden.
- **Keine direkte Form-zu-Form-Kommunikation:** `frm_chat` kommuniziert
ausschließlich über `AgentEngine` und `WebViewBridge`. Kein direkter
Methodenaufruf zwischen Form-Instanzen.
- **frm_chat ist nicht-modal:** Immer `frm.Show(owner)` statt
`frm.ShowDialog()`. Mehrere Instanzen mit demselben AgentId: nur
eine öffnen, fokussieren wenn vorhanden (Dictionary-Check in frm_main).
- **Bridge-Nachrichten immer typisiert:** Kein rohes JSON-String-Bauen
außerhalb von `WebViewBridge` und `BridgeMessage`.
- **EmbeddedUI ist readonly:** Keine dynamischen Schreibzugriffe auf
die extrahierten UI-Dateien zur Laufzeit. UI-Änderungen erfordern
Neu-Kompilierung.
---
## NuGet-Pakete (Host-Projekt)
```xml
<PackageReference Include="Microsoft.Web.WebView2" Version="*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="*" />
```
---
## Implementierungsreihenfolge (für Claude Code)
Bearbeite diesen Abschnitt nach Abschluss der Core-Phase (Phasen 12):
1. `EmbeddedUiManager` implementieren und Sicherheitscheck in `Program.cs` einbauen
2. `BridgeMessage` + `BridgeTypes` + `WebViewBridge` implementieren
3. Core: `ChatAsync`, `GetChatHistoryAsync`, `GetStatus`, `Abort` zu `AgentEngine` hinzufügen
4. Core: `ChatEntry` und `AgentRunStatus` Records anlegen
5. `frm_main`: `InitWebViewAsync`, `PushAgentListAsync`, Bridge-Handler implementieren
6. `frm_chat`: vollständig implementieren
7. `EmbeddedUI/bridge.js` erstellen (gemeinsame Bridge-Logik für beide HTML-Seiten)
8. `EmbeddedUI/overview.html` + `overview.css` erstellen (Sidebar + Chat-Bereich)
9. `EmbeddedUI/chat.html` + `chat.css` erstellen (Einzelchat, agentId aus URL-Parameter)
10. `Program.cs` Startup-Reihenfolge implementieren
11. xUnit-Tests: Bridge-Serialisierung, Pfad-Overlap-Check, frm_chat Isolation
**Beginne mit Schritt 1 dieses Abschnitts.**
+470
View File
@@ -0,0 +1,470 @@
# ClawdDotNet Claude Code Entwicklungs-Prompt
## Projektkontext
Wir entwickeln **ClawdDotNet** einen modularen "Coworking Space" für AI-Agenten in **C# .NET 10 / WinForms**.
Das Projekt ist bereits angelegt und hat erste UI-Steuerelemente.
Das System ermöglicht es, mehrere spezialisierte AI-Agenten parallel laufen zu lassen, die gemeinsam
strukturierte Aufgaben erledigen (z.B. Finanzmarktanalyse, Trading-Empfehlungen, SEO, Programmierung).
Als LLM-Backend wird **OpenRouter** verwendet, damit jeder Agent flexibel ein anderes Modell nutzen kann.
---
## Kernprinzipien diese gelten für JEDE Zeile Code
1. **Agenten blockieren sich niemals gegenseitig.**
Alle Agent-Runs laufen vollständig async/await mit eigenem CancellationToken.
Kein shared mutable state ohne explizites Locking. Kein Agent wartet synchron auf einen anderen.
2. **Mehrinstanzfähigkeit von Anfang an.**
Die Anwendung kann mehrfach gleichzeitig gestartet werden (z.B. ein Prozess für Aktien-Team,
ein Prozess für Krypto-Team). Jede Instanz ist vollständig isoliert:
- Eigene Konfigurationsdatei (per Instanz wählbar beim Start, z.B. `--config stock-team.json`)
- Eigene Datenbankverbindungen (keine Shared-DB-Locks ohne explizites Design dafür)
- Eigener Arbeitsordner und wwwroot-Ordner
- Eigener Netzwerk-Port für den integrierten Webserver (konfigurierbar)
- Keine globalen Singletons, keine statischen Felder mit Zustand
3. **Core ist niemals von einem Tool abhängig.**
Der Core kompiliert und läuft vollständig ohne jedes Tool. Fehlt ein Tool, bleibt der Core
funktionsfähig. Tools werden zur Laufzeit registriert.
4. **Tools sind niemals voneinander abhängig.**
Tool A darf Tool B weder referenzieren noch aufrufen. Jedes Tool ist ein eigenständiges Projekt/Assembly.
5. **Jedes Tool ist pro Agent konfiguriert.**
Agent A kann auf eine andere Datenbank zugreifen als Agent B. Agent A darf in `./wwwroot/` schreiben,
Agent B nicht. Die Konfiguration liegt in der AgentConfig, nicht im Tool-Code.
---
## Architektur-Übersicht
```
ClawdDotNet.sln
├── src/
│ ├── ClawdDotNet.Core/ ← .NET 10 Klassenbibliothek, KEIN Tool-Verweis
│ ├── ClawdDotNet.Tools.Database/ ← Tool-Plugin, nur Core-Verweis
│ ├── ClawdDotNet.Tools.FileRW/ ← Tool-Plugin, nur Core-Verweis
│ ├── ClawdDotNet.Tools.Mail/ ← Tool-Plugin, nur Core-Verweis
│ └── ClawdDotNet.Host/ ← WinForms .NET 10, verweist auf Core + alle Tools
└── configs/
├── stock-team.json
└── crypto-team.json
```
---
## Phase 1: Core implementieren
### 1.1 Kern-Interfaces und Datentypen
**Datei: `Core/Tools/IAgentTool.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
public interface IAgentTool
{
/// Eindeutiger Name, den das LLM in tool_calls verwendet
string Name { get; }
/// Natürlichsprachige Beschreibung für das LLM (geht in den System-Prompt)
string Description { get; }
/// JSON Schema des Input-Objekts (OpenAI Function Calling Format)
System.Text.Json.JsonElement InputSchema { get; }
/// Ausführung bekommt NUR seinen eigenen Kontext, nie andere Tools
Task<ToolResult> ExecuteAsync(
System.Text.Json.JsonElement input,
AgentToolContext context,
CancellationToken ct);
}
```
**Datei: `Core/Tools/AgentToolContext.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
/// Wird vom Core befüllt und an das Tool übergeben.
/// Das Tool liest seine Konfiguration NUR aus ToolConfig[tool.Name].
public sealed record AgentToolContext(
string AgentId,
string InstanceId, // Mehrinstanz-Isolation
IReadOnlyDictionary<string, object?> ToolConfig, // tool-spezifische Config aus AgentConfig
Microsoft.Extensions.Logging.ILogger Logger,
CancellationToken CancellationToken
);
```
**Datei: `Core/Tools/ToolResult.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
public sealed record ToolResult(
bool Success,
string Content, // JSON oder Plaintext, geht zurück ans LLM
string? ErrorMessage = null
);
```
### 1.2 AgentConfig (Konfigurationsmodell)
**Datei: `Core/Config/AgentConfig.cs`**
```csharp
namespace ClawdDotNet.Core.Config;
public sealed class AgentConfig
{
public string AgentId { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Model { get; set; } = "anthropic/claude-sonnet-4-5";
public string SystemPrompt { get; set; } = "";
/// Welche Tools darf dieser Agent nutzen?
/// Key = Tool.Name, Value = tool-spezifische Konfiguration (frei definierbar je Tool)
public Dictionary<string, Dictionary<string, object?>> Tools { get; set; } = new();
public SchedulerConfig? Scheduler { get; set; }
public LoopGuardConfig LoopGuard { get; set; } = new();
}
public sealed class SchedulerConfig
{
public string Cron { get; set; } = ""; // z.B. "0 7 * * 1-5"
public bool RunOnStart { get; set; } = false;
}
public sealed class LoopGuardConfig
{
public int MaxSteps { get; set; } = 20;
public int MaxTokens { get; set; } = 80_000;
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(10);
}
```
**Datei: `Core/Config/InstanceConfig.cs`**
```csharp
namespace ClawdDotNet.Core.Config;
/// Instanz-weite Konfiguration (eine pro laufendem Prozess)
public sealed class InstanceConfig
{
public string InstanceId { get; set; } = Guid.NewGuid().ToString("N")[..8];
public string InstanceName { get; set; } = "Default";
public string OpenRouterApiKey { get; set; } = "";
public string WorkingDirectory { get; set; } = "./data/";
public int WebServerPort { get; set; } = 8080;
public List<AgentConfig> Agents { get; set; } = new();
}
```
### 1.3 Tool Registry
**Datei: `Core/Tools/ToolRegistry.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
/// Thread-safe Registry. Wird beim Start im Host befüllt.
/// Der Core kennt keine konkreten Tool-Typen.
public sealed class ToolRegistry
{
private readonly Dictionary<string, IAgentTool> _tools = new();
private readonly Lock _lock = new();
public void Register(IAgentTool tool)
{
lock (_lock)
_tools[tool.Name] = tool;
}
public IAgentTool? Get(string name)
{
lock (_lock)
return _tools.GetValueOrDefault(name);
}
/// Gibt nur die Tools zurück, für die der Agent eine Config hat
public IReadOnlyList<IAgentTool> GetForAgent(AgentConfig agent)
{
lock (_lock)
return _tools.Values
.Where(t => agent.Tools.ContainsKey(t.Name))
.ToList();
}
}
```
### 1.4 Permission Gate
**Datei: `Core/Security/PermissionGate.cs`**
```csharp
namespace ClawdDotNet.Core.Security;
public sealed class PermissionGate
{
public bool IsAllowed(string agentId, string toolName,
Config.AgentConfig agentConfig)
=> agentConfig.Tools.ContainsKey(toolName);
public void Enforce(string agentId, string toolName,
Config.AgentConfig agentConfig)
{
if (!IsAllowed(agentId, toolName, agentConfig))
throw new ToolAccessDeniedException(agentId, toolName);
}
}
public sealed class ToolAccessDeniedException(string agentId, string toolName)
: Exception($"Agent '{agentId}' has no access to tool '{toolName}'.");
```
### 1.5 Loop Guard
**Datei: `Core/Engine/LoopGuard.cs`**
```csharp
namespace ClawdDotNet.Core.Engine;
/// Pro Agent-Run instanziieren, nicht wiederverwenden.
public sealed class LoopGuard
{
private readonly Config.LoopGuardConfig _cfg;
private int _steps;
private int _tokens;
public LoopGuard(Config.LoopGuardConfig cfg) => _cfg = cfg;
public void RecordStep()
{
if (Interlocked.Increment(ref _steps) > _cfg.MaxSteps)
throw new LoopLimitExceededException($"Max steps ({_cfg.MaxSteps}) exceeded.");
}
public void RecordTokens(int count)
{
if (Interlocked.Add(ref _tokens, count) > _cfg.MaxTokens)
throw new LoopLimitExceededException($"Max tokens ({_cfg.MaxTokens}) exceeded.");
}
}
public sealed class LoopLimitExceededException(string message) : Exception(message);
```
### 1.6 OpenRouter Client
**Datei: `Core/Api/OpenRouterClient.cs`**
Implementiere einen schlanken HTTP-Client gegen `https://openrouter.ai/api/v1/chat/completions`.
Format ist OpenAI-kompatibel (JSON).
```csharp
namespace ClawdDotNet.Core.Api;
public sealed class OpenRouterClient : IDisposable
{
// Basis-URL: https://openrouter.ai/api/v1/
// Header: Authorization: Bearer {ApiKey}
// Header: HTTP-Referer: ClawdDotNet
// Format: OpenAI Chat Completions JSON
// Methoden:
// Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct)
// IAsyncEnumerable<ChatChunk> StreamAsync(ChatRequest request, CancellationToken ct) [optional]
// ChatRequest enthält: model, messages[], tools[] (optional), tool_choice
// ChatResponse enthält: choices[0].message (content + tool_calls), usage (prompt_tokens, completion_tokens)
}
```
Nutze `System.Net.Http.HttpClient` mit `IHttpClientFactory`-Muster.
Keine externen HTTP-Bibliotheken. Serialisierung mit `System.Text.Json`.
### 1.7 Agent Engine
**Datei: `Core/Engine/AgentEngine.cs`**
Der Kern des Agentenablaufs. Pro Agent-Run wird eine neue Instanz erzeugt.
```
Ablauf eines Agent-Runs:
1. AgentConfig laden → erlaubte Tools aus Registry holen → Tool-Beschreibungen für LLM bauen
2. System-Prompt + User-Message an OpenRouter senden (mit Tool-Definitionen)
3. Antwort prüfen:
a. Enthält tool_calls → PermissionGate.Enforce → Tool.ExecuteAsync → Ergebnis zurück ans LLM
b. Kein tool_call → Antwort ist final → Run beendet
4. Nach jedem Schritt: LoopGuard.RecordStep() + LoopGuard.RecordTokens(usage)
5. Bei Exception: sauber abbrechen, Status = Failed, Exception loggen
```
Wichtig:
- Jeder Run bekommt seinen eigenen `CancellationToken` (kombiniert aus Timeout + externer Abbruch)
- Kein `await` ohne CancellationToken
- Rückgabe: `AgentRunResult` mit Status, FinalMessage, StepCount, TokensUsed, Duration
### 1.8 Scheduler
**Datei: `Core/Scheduling/AgentScheduler.cs`**
- Nutze `System.Threading.PeriodicTimer` oder Cron-Parsing (einfaches eigenes Parsing oder NCrontab NuGet)
- Pro AgentConfig mit `Scheduler != null` wird ein eigener Timer gestartet
- Scheduled Runs werden als `Task` gestartet (fire-and-forget mit Exception-Handling)
- `RunOnStart = true` → erster Run sofort beim Registrieren
- Scheduler ist instanzweit (ein Scheduler pro Prozess, verwaltet alle Agenten)
---
## Phase 2: Tools implementieren
### Tool: Database (`ClawdDotNet.Tools.Database`)
AgentConfig-Beispiel:
```json
"Database": {
"connectionString": "Server=localhost;Database=markets;User=agent_a;Password=...;",
"allowedTables": ["quotes", "indicators", "news"]
}
```
Implementiere `IAgentTool` mit diesen Operationen (via `action`-Feld im Input):
- `query` SELECT, nur auf `allowedTables`, SQL-Injection-Schutz via Parameterized Queries
- `insert` INSERT, nur auf `allowedTables`
- `upsert` INSERT ... ON DUPLICATE KEY UPDATE
Verbindungsstring kommt IMMER aus `context.ToolConfig`, nie aus statischen Feldern.
NuGet: `MySqlConnector` (für MySQL) und/oder `MongoDB.Driver` (für MongoDB), je nach Config-Eintrag `"type": "mysql"` oder `"type": "mongodb"`.
### Tool: FileRW (`ClawdDotNet.Tools.FileRW`)
AgentConfig-Beispiel:
```json
"FileRW": {
"rootPath": "./data/analyst/",
"allowWrite": true,
"allowedExtensions": [".txt", ".json", ".html", ".md"]
}
```
Operationen: `read`, `write`, `append`, `list`, `delete`
**Sicherheit (Pflicht):**
- Alle Pfade werden mit `Path.GetFullPath` aufgelöst
- Prüfe: `resolvedPath.StartsWith(Path.GetFullPath(rootPath))` — sonst `PathTraversalException`
- Nur Dateien mit erlaubter Extension (aus `allowedExtensions`) dürfen gelesen/geschrieben werden
### Tool: Mail (`ClawdDotNet.Tools.Mail`)
AgentConfig-Beispiel:
```json
"Mail": {
"imapHost": "imap.example.com",
"imapPort": 993,
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"username": "agent@example.com",
"password": "...",
"allowedRecipients": ["owner@example.com"]
}
```
Operationen: `send`, `read_inbox`, `read_message`, `mark_read`
NuGet: `MailKit`
Empfänger müssen in `allowedRecipients` stehen, sonst Exception.
---
## Phase 3: Host (WinForms)
**Datei: `Host/Program.cs`**
Startparameter: `--config <pfad>` (optional, default: `./config.json`)
```csharp
// Startup-Ablauf:
// 1. InstanceConfig aus JSON laden (Pfad aus --config Argument)
// 2. ToolRegistry befüllen (Database, FileRW, Mail registrieren)
// 3. PermissionGate, AgentScheduler, OpenRouterClient instanziieren
// 4. AgentScheduler starten (alle Agenten aus InstanceConfig)
// 5. WinForms Application.Run(new MainForm(...))
```
**MainForm:** Zeigt pro Agent eine Statuszeile (AgentId, letzter Run, Status, Token-Verbrauch).
Manueller "Run now"-Button pro Agent. Log-Output in einer ListBox oder RichTextBox.
---
## Konfigurationsbeispiel: `stock-team.json`
```json
{
"instanceId": "stock-01",
"instanceName": "Aktien-Team",
"openRouterApiKey": "sk-or-...",
"workingDirectory": "./data/stock/",
"webServerPort": 8081,
"agents": [
{
"agentId": "market-analyst",
"displayName": "Marktanalyse",
"model": "anthropic/claude-sonnet-4-5",
"systemPrompt": "Du bist ein erfahrener Marktanalyst...",
"tools": {
"Database": {
"connectionString": "Server=db1;Database=stocks;...",
"allowedTables": ["quotes", "indicators"]
},
"FileRW": {
"rootPath": "./data/stock/analyst/",
"allowWrite": true,
"allowedExtensions": [".json", ".txt"]
}
},
"scheduler": { "cron": "0 7 * * 1-5", "runOnStart": false },
"loopGuard": { "maxSteps": 25, "maxTokens": 100000 }
},
{
"agentId": "webdev",
"displayName": "Web-Entwickler",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du erstellst HTML-Dashboards...",
"tools": {
"FileRW": {
"rootPath": "./data/stock/wwwroot/",
"allowWrite": true,
"allowedExtensions": [".html", ".css", ".js", ".json"]
}
},
"loopGuard": { "maxSteps": 10, "maxTokens": 40000 }
}
]
}
```
---
## Entwicklungsregeln (für Claude Code)
- **Keine externen Frameworks** außer: `MailKit`, `MySqlConnector`, `MongoDB.Driver`, optional `NCrontab`
- **Keine statischen Zustände** in Tools oder Engine-Komponenten
- **Jeder await-Aufruf** bekommt einen CancellationToken
- **Exceptions** in Agent-Runs niemals schlucken — loggen und als `AgentRunResult` mit Status=Failed zurückgeben
- **Alle Pfadoperationen** in FileRW mit Path-Traversal-Check
- **Connection Strings** kommen immer aus `AgentToolContext.ToolConfig`, nie hardcoded
- **Tests**: Für Core-Komponenten (PermissionGate, LoopGuard, FileRW-Pfadprüfung) xUnit-Unit-Tests anlegen
- **Logging**: `Microsoft.Extensions.Logging.ILogger` überall, kein Console.WriteLine in Produktionscode
---
## Startreihenfolge für Claude Code
1. Solution-Struktur und .csproj-Dateien anlegen (Projekt-Verweise korrekt setzen)
2. Core vollständig implementieren (Interfaces, Config, Registry, Gate, Guard, Client, Engine, Scheduler)
3. Tool: FileRW (einfachstes Tool, gut testbar)
4. Tool: Database
5. Tool: Mail
6. Host: Program.cs Startup, MainForm UI
7. xUnit Tests für Core + FileRW
8. Beispiel-Configs erstellen
**Beginne mit Schritt 1.**
+27
View File
@@ -0,0 +1,27 @@
# Archiv
Die vier Dokumente hier sind die **Entwicklungs-Prompts aus der Anfangszeit** des
Projekts (MaiJuli 2026). Sie haben ClawdDotNet aufgebaut und beschreiben deshalb
den Stand von damals — unter anderem eine WinForms-Oberfläche mit WebView2, die es
seit dem Frühjahrsputz vom 2026-08-23 nicht mehr gibt.
**Sie werden nicht mehr fortgeschrieben.** Wer den heutigen Stand sucht, findet ihn
in [Roadmap](../Roadmap.md), [Bestandsaufnahme](../Bestandsaufnahme-2026-07.md) und
den Konzept-Dokumenten daneben.
Aufgehoben werden sie aus zwei Gründen:
1. **Herkunft.** Sie erklären, warum das System so geschnitten ist, wie es
geschnitten ist — der Schichtschnitt Core/Tools/App stammt von hier.
2. **Eine Regel gilt weiter.** `ClawdDotNet_Prompt_InternetTools.md` enthält die
Pflichtfelder `fetchedAt` / `dataAsOf` / `source`, an die sich alle Internet-Tools
halten. Der [WebSearch-Umsetzungsplan](../umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md)
verweist darauf. Zieht diese Regel eines Tages in ein eigenes Dokument um, kann
die Datei ganz weg.
| Datei | Was darin steht | Was davon noch gilt |
|---|---|---|
| `ClawdDotNet_StartPrompt.md` | Gesamtentwurf, Kernklassen, Beispielkonfiguration | Schichtschnitt und Tool-Vertrag; Oberfläche und `configs/*.json` überholt |
| `ClawdDotNet_Prompt_WebviewChatWinForms.md` | WinForms-Oberfläche mit WebView2-Chat | nichts — ersetzt durch `src/ClawdDotNet.Desktop` |
| `ClawdDotNet_Prompt_TelegramClient.md` | Entwurf des TelegramClient-Tools | umgesetzt in `src/ClawdDotNet.Tools.TelegramClient` |
| `ClawdDotNet_Prompt_InternetTools.md` | WebFetch, DirectAPI, WebMonitor | die Pflichtfelder-Regel (siehe oben) |
@@ -20,7 +20,7 @@ Internetzugang ist bereits vorhanden, aber nur in eine Richtung:
| `WebMonitor` | strukturierte Seiten überwachen | feste Zielseiten |
Alle drei liefern die Pflichtfelder `fetchedAt` / `dataAsOf` / `source` aus
`ClawdDotNet_Prompt_InternetTools.md` und sind gegen SSRF abgesichert
`docs/archiv/ClawdDotNet_Prompt_InternetTools.md` und sind gegen SSRF abgesichert
(`UrlGuard` prüft Schema, private Netze und Whitelist, auch über Redirects
hinweg — `UrlSanitizer` analog für DirectAPI).
@@ -153,7 +153,7 @@ Ergänzend im Tool:
### Slice 3 — Betrieb
- [ ] Rechercheagent-Vorlage in `docs/InstanceSetupGuide.md` gemäß Abschnitt 4
- [ ] `ClawdDotNet_Prompt_InternetTools.md` um das Tool ergänzen
- [ ] `docs/archiv/ClawdDotNet_Prompt_InternetTools.md` um das Tool ergänzen
- [ ] `docs/ToolDevelopmentGuide.md`: Steckbrief
---