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 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 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); // Statement analysieren: Kommentare, Mehrfach-Statements und String-Literale // werden dabei behandelt, bevor Operation und Tabellen bestimmt werden. var inspection = SqlGuard.Inspect(sql, GetAllowedTables(context)); if (!inspection.IsValid) return ToolResult.Fail($"Sicherheitsfehler: {inspection.Error}"); switch (inspection.Operation) { case SqlOperation.Schema when accessLevel != DatabaseAccessLevel.Admin: return ToolResult.Fail( "Sicherheitsfehler: Strukturänderungen (DDL) sind für diesen Agenten nicht erlaubt."); case SqlOperation.Write when accessLevel == DatabaseAccessLevel.ReadOnly: return ToolResult.Fail( "Sicherheitsfehler: Schreibzugriff ist für diesen Agenten deaktiviert (ReadOnly)."); } 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>(); while (await reader.ReadAsync(ct)) { var row = new Dictionary(); 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 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 (!IsCollectionAllowed(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(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 DatabaseAccessLevel GetAccessLevel(AgentToolContext context) { if (context.ToolConfig.TryGetValue("accessLevel", out var val) && val != null) { if (Enum.TryParse(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; } internal static List GetAllowedTables(AgentToolContext context) { if (!context.ToolConfig.TryGetValue("allowedTables", out var val) || val is not JsonElement je || je.ValueKind != JsonValueKind.Array) { return []; } return je.EnumerateArray() .Select(x => x.GetString()) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToList()!; } /// /// MongoDB kennt kein SQL — hier genügt der exakte Vergleich des Collection-Namens. /// Bisher wurde auch hier per Teilzeichenkette geprüft. /// private static bool IsCollectionAllowed(string collection, AgentToolContext context) => GetAllowedTables(context).Contains(collection, StringComparer.OrdinalIgnoreCase); }