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,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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user