Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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.**
|
||||
Reference in New Issue
Block a user