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:
Richard
2026-07-26 18:21:46 +02:00
co-authored by Claude Opus 4.8
commit 2fed388c99
154 changed files with 29736 additions and 0 deletions
@@ -0,0 +1,230 @@
using System.Data;
using System.Data.Common;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using MySqlConnector;
using Npgsql;
namespace ClawdDotNet.Tools.Database;
public enum DatabaseAccessLevel
{
ReadOnly,
ReadWrite,
Admin
}
public sealed class DatabaseTool : IAgentTool
{
public string Name => "Database";
public string Description => "Erlaubt den Zugriff auf SQL (MySQL, Postgres, MSSQL) und NoSQL (MongoDB) Datenbanken mit verschiedenen Zugriffsebenen.";
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["query", "insert", "upsert"],
"description": "Die auszuführende Aktion"
},
"sql": {
"type": "string",
"description": "Das auszuführende SQL Statement (nur für SQL-Typen)"
},
"collection": {
"type": "string",
"description": "Der Name der Collection (nur für MongoDB)"
},
"filter": {
"type": "string",
"description": "JSON-Filter (nur für MongoDB)"
},
"document": {
"type": "string",
"description": "JSON-Dokument zum Einfügen/Update (nur für MongoDB)"
}
},
"required": ["action"]
}
""").RootElement.Clone();
public async Task<ToolResult> ExecuteAsync(
JsonElement input,
AgentToolContext context,
CancellationToken ct)
{
var action = input.GetProperty("action").GetString()
?? throw new ArgumentException("'action' is required");
var type = context.ToolConfig.TryGetValue("type", out var t) ? t?.ToString()?.ToLowerInvariant() : null;
var connectionString = context.ToolConfig.TryGetValue("connectionString", out var cs) ? cs?.ToString() : null;
if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(connectionString))
{
return ToolResult.Fail("Konfigurationsfehler: 'type' und 'connectionString' sind erforderlich.");
}
try
{
if (type == "mongodb")
{
return await HandleMongoAsync(action, input, connectionString, context, ct);
}
else
{
return await HandleSqlAsync(type, action, input, connectionString, context, ct);
}
}
catch (Exception ex)
{
context.Logger.LogError(ex, "Fehler bei Database Aktion {Action} ({Type})", action, type);
return ToolResult.Fail($"Fehler: {ex.Message}");
}
}
private async Task<ToolResult> HandleSqlAsync(string type, string action, JsonElement input, string connectionString, AgentToolContext context, CancellationToken ct)
{
var sql = input.TryGetProperty("sql", out var s) ? s.GetString() : null;
if (string.IsNullOrWhiteSpace(sql)) return ToolResult.Fail("'sql' ist erforderlich.");
var accessLevel = GetAccessLevel(context);
// Sicherheitsprüfungen
if (IsAdminAttempt(sql))
{
if (accessLevel != DatabaseAccessLevel.Admin)
return ToolResult.Fail("Sicherheitsfehler: Strukturänderungen (DDL) sind für diesen Agenten nicht erlaubt.");
}
else if (IsWriteAttempt(sql))
{
if (accessLevel == DatabaseAccessLevel.ReadOnly)
return ToolResult.Fail("Sicherheitsfehler: Schreibzugriff ist für diesen Agenten deaktiviert (ReadOnly).");
}
// Tabellen-Whitelist-Prüfung
if (!IsTableAllowed(sql, context))
{
return ToolResult.Fail("Sicherheitsfehler: Zugriff auf eine oder mehrere Tabellen im Statement ist nicht erlaubt.");
}
using DbConnection conn = type switch
{
"mysql" => new MySqlConnection(connectionString),
"postgres" => new NpgsqlConnection(connectionString),
"mssql" => new SqlConnection(connectionString),
_ => throw new NotSupportedException($"SQL Typ '{type}' wird nicht unterstützt.")
};
await conn.OpenAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
if (action == "query")
{
using var reader = await cmd.ExecuteReaderAsync(ct);
var results = new List<Dictionary<string, object>>();
while (await reader.ReadAsync(ct))
{
var row = new Dictionary<string, object>();
for (int i = 0; i < reader.FieldCount; i++)
{
row[reader.GetName(i)] = reader.GetValue(i);
}
results.Add(row);
}
return ToolResult.Ok(JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
var affected = await cmd.ExecuteNonQueryAsync(ct);
return ToolResult.Ok($"{affected} Zeilen betroffen.");
}
}
private async Task<ToolResult> HandleMongoAsync(string action, JsonElement input, string connectionString, AgentToolContext context, CancellationToken ct)
{
var collectionName = input.TryGetProperty("collection", out var c) ? c.GetString() : null;
if (string.IsNullOrWhiteSpace(collectionName)) return ToolResult.Fail("'collection' ist erforderlich.");
if (!IsTableAllowed(collectionName, context))
{
return ToolResult.Fail($"Sicherheitsfehler: Zugriff auf Collection '{collectionName}' ist nicht erlaubt.");
}
var accessLevel = GetAccessLevel(context);
var client = new MongoClient(connectionString);
var dbName = new MongoUrl(connectionString).DatabaseName;
var db = client.GetDatabase(dbName);
var collection = db.GetCollection<BsonDocument>(collectionName);
if (action == "query")
{
var filterJson = input.TryGetProperty("filter", out var f) ? f.GetString() : "{}";
var filter = BsonDocument.Parse(filterJson);
var docs = await collection.Find(filter).Limit(100).ToListAsync(ct);
var results = docs.Select(d => d.ToJson()).ToList();
return ToolResult.Ok("[" + string.Join(",", results) + "]");
}
else if (action == "insert")
{
if (accessLevel == DatabaseAccessLevel.ReadOnly) return ToolResult.Fail("Sicherheitsfehler: Schreibzugriff deaktiviert.");
var docJson = input.TryGetProperty("document", out var d) ? d.GetString() : throw new ArgumentException("'document' erforderlich.");
var doc = BsonDocument.Parse(docJson);
await collection.InsertOneAsync(doc, cancellationToken: ct);
return ToolResult.Ok("Dokument erfolgreich eingefügt.");
}
else // upsert
{
if (accessLevel == DatabaseAccessLevel.ReadOnly) return ToolResult.Fail("Sicherheitsfehler: Schreibzugriff deaktiviert.");
return ToolResult.Fail("Upsert für MongoDB noch nicht voll implementiert.");
}
}
private bool IsWriteAttempt(string sql)
{
var lower = sql.ToLowerInvariant();
return lower.Contains("insert") || lower.Contains("update") || lower.Contains("delete");
}
private bool IsAdminAttempt(string sql)
{
var lower = sql.ToLowerInvariant();
return lower.Contains("drop") || lower.Contains("alter") || lower.Contains("create") || lower.Contains("truncate");
}
private DatabaseAccessLevel GetAccessLevel(AgentToolContext context)
{
if (context.ToolConfig.TryGetValue("accessLevel", out var val) && val != null)
{
if (Enum.TryParse<DatabaseAccessLevel>(val.ToString(), true, out var level))
return level;
}
// Rückfall auf altes 'allowWrite' für Abwärtskompatibilität
if (context.ToolConfig.TryGetValue("allowWrite", out var aw) && aw is JsonElement je && je.GetBoolean())
{
return DatabaseAccessLevel.ReadWrite;
}
return DatabaseAccessLevel.ReadOnly;
}
private bool IsTableAllowed(string input, AgentToolContext context)
{
if (!context.ToolConfig.TryGetValue("allowedTables", out var val) || val is not JsonElement je)
{
return false;
}
var allowed = je.EnumerateArray().Select(x => x.GetString()?.ToLowerInvariant()).ToList();
var inputLower = input.ToLowerInvariant();
return allowed.Any(t => t != null && inputLower.Contains(t));
}
}