# 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 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 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> 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 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 _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 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 CompleteAsync(ChatRequest request, CancellationToken ct) // IAsyncEnumerable 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 ` (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.**