#631: Derselbe Vorschlag (gleicher Agent, gleiches Werkzeug, gleiche Argumente unabhaengig von Schluesselreihenfolge) wird nicht erneut angelegt - der Agent erfaehrt die bestehende Nummer. Offene Vorschlaege verfallen nach 24 h (Status Expired). #609: Werkzeuge koennen den Freigabeschluessel aus dem Inhalt bestimmen (IAgentTool.StagingAktion). Database: SELECT/SHOW/DESCRIBE -> query, INSERT/UPDATE/DELETE -> write, DDL -> schema; write und schema sind freigabepflichtig. Ausgefuehrt wird nach Statement-Art, schreibende Statements melden die Zeilenzahl. #579: Database-Vorpruefung (Zugriffsebene, Tabellenfreigabe, SqlGuard) - was das Werkzeug ablehnen wuerde, kommt nicht mehr in die Freigabeliste. #610: SqlGuard ueberspringt IF [NOT] EXISTS und erkennt DESCRIBE/DESC <tabelle>. #632: WebFetch fetch mit filter liefert aus JSON/JSONP nur passende Eintraege; beim Blaettern durch grosses JSON empfiehlt der Hinweis den filter. Version 0.1.10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,19 @@ 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 string Description => """
|
||||
Zugriff auf eine SQL-Datenbank (MySQL/MariaDB, Postgres, MSSQL) oder MongoDB.
|
||||
|
||||
SQL: Schicke jedes Statement mit action "query" und "sql" - lesend wie schreibend. Die Art
|
||||
(lesen, schreiben, Struktur aendern) ergibt sich aus dem Statement selbst. Ein Statement je
|
||||
Aufruf, keine SQL-Kommentare. Nur die freigegebenen Tabellen sind erreichbar.
|
||||
Tabellenstruktur: "DESCRIBE <tabelle>" oder "SHOW COLUMNS FROM <tabelle>" - pruefe die
|
||||
Spaltennamen, bevor du schreibst. Schreibende Statements liefern die Zahl betroffener Zeilen.
|
||||
Je nach Einstellung werden schreibende oder strukturaendernde Statements zur Freigabe
|
||||
vorgelegt statt sofort ausgefuehrt; liegt derselbe Vorschlag schon vor, lege ihn nicht erneut vor.
|
||||
|
||||
MongoDB: action query (filter), insert (document) in einer freigegebenen collection.
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
{
|
||||
@@ -54,6 +66,73 @@ public sealed class DatabaseTool : IAgentTool
|
||||
}
|
||||
""").RootElement.Clone();
|
||||
|
||||
/// <summary>
|
||||
/// #579: Was das Werkzeug ohnehin ablehnen wuerde, kommt nicht in die Freigabeliste. Im
|
||||
/// Testbetrieb legte ein Agent mit ReadOnly seine Test-INSERTs zur Freigabe vor - nach der
|
||||
/// Freigabe scheiterten sie an genau dieser Pruefung.
|
||||
/// </summary>
|
||||
public string? Preflight(JsonElement input, AgentToolContext context)
|
||||
{
|
||||
var type = GetType(context);
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
return null; // Konfigurationsfehler meldet die Ausfuehrung selbst
|
||||
|
||||
var accessLevel = GetAccessLevel(context);
|
||||
|
||||
if (type == "mongodb")
|
||||
{
|
||||
var action = GetString(input, "action");
|
||||
var collection = GetString(input, "collection");
|
||||
if (!string.IsNullOrWhiteSpace(collection) && !IsCollectionAllowed(collection, context))
|
||||
return $"Sicherheitsfehler: Zugriff auf Collection '{collection}' ist nicht erlaubt.";
|
||||
if (action is "insert" or "upsert" && accessLevel == DatabaseAccessLevel.ReadOnly)
|
||||
return "Sicherheitsfehler: Schreibzugriff deaktiviert.";
|
||||
return null;
|
||||
}
|
||||
|
||||
var sql = GetString(input, "sql");
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
return "'sql' ist erforderlich.";
|
||||
|
||||
var inspection = SqlGuard.Inspect(sql, GetAllowedTables(context));
|
||||
return Verweigerung(inspection, accessLevel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #609: Der Freigabeschluessel folgt dem Statement, nicht dem action-Argument. Vorher lief
|
||||
/// ein INSERT, das als action "insert" zur Freigabe lag, eine Sekunde spaeter als action
|
||||
/// "query" ohne Freigabe durch.
|
||||
/// </summary>
|
||||
public string? StagingAktion(JsonElement input)
|
||||
{
|
||||
var sql = GetString(input, "sql");
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
return null; // MongoDB: dort gilt das action-Argument
|
||||
|
||||
return SqlGuard.Inspect(sql, ["*"]).Operation switch
|
||||
{
|
||||
SqlOperation.Read => "query",
|
||||
SqlOperation.Write => "write",
|
||||
SqlOperation.Schema => "schema",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string? Verweigerung(SqlInspection inspection, DatabaseAccessLevel accessLevel)
|
||||
{
|
||||
if (!inspection.IsValid)
|
||||
return $"Sicherheitsfehler: {inspection.Error}";
|
||||
|
||||
return inspection.Operation switch
|
||||
{
|
||||
SqlOperation.Schema when accessLevel != DatabaseAccessLevel.Admin =>
|
||||
"Sicherheitsfehler: Strukturänderungen (DDL) sind für diesen Agenten nicht erlaubt.",
|
||||
SqlOperation.Write when accessLevel == DatabaseAccessLevel.ReadOnly =>
|
||||
"Sicherheitsfehler: Schreibzugriff ist für diesen Agenten deaktiviert (ReadOnly).",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(
|
||||
JsonElement input,
|
||||
AgentToolContext context,
|
||||
@@ -62,7 +141,7 @@ public sealed class DatabaseTool : IAgentTool
|
||||
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 type = GetType(context);
|
||||
var connectionString = context.ToolConfig.TryGetValue("connectionString", out var cs) ? cs?.ToString() : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(connectionString))
|
||||
@@ -99,19 +178,8 @@ public sealed class DatabaseTool : IAgentTool
|
||||
// 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).");
|
||||
}
|
||||
if (Verweigerung(inspection, accessLevel) is { } verweigert)
|
||||
return ToolResult.Fail(verweigert);
|
||||
|
||||
using DbConnection conn = type switch
|
||||
{
|
||||
@@ -125,7 +193,9 @@ public sealed class DatabaseTool : IAgentTool
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
|
||||
if (action == "query")
|
||||
// #609: Ob gelesen oder geschrieben wird, entscheidet das Statement. Vorher fuehrte
|
||||
// action "query" ein INSERT per ExecuteReader aus und meldete "[]" statt der Zeilenzahl.
|
||||
if (inspection.Operation == SqlOperation.Read)
|
||||
{
|
||||
using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
var results = new List<Dictionary<string, object>>();
|
||||
@@ -143,7 +213,9 @@ public sealed class DatabaseTool : IAgentTool
|
||||
else
|
||||
{
|
||||
var affected = await cmd.ExecuteNonQueryAsync(ct);
|
||||
return ToolResult.Ok($"{affected} Zeilen betroffen.");
|
||||
return ToolResult.Ok(inspection.Operation == SqlOperation.Schema
|
||||
? $"Strukturänderung ausgeführt ({affected} Zeilen betroffen)."
|
||||
: $"{affected} Zeilen betroffen.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +258,15 @@ public sealed class DatabaseTool : IAgentTool
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseAccessLevel GetAccessLevel(AgentToolContext context)
|
||||
private static string? GetType(AgentToolContext context)
|
||||
=> context.ToolConfig.TryGetValue("type", out var t) ? t?.ToString()?.ToLowerInvariant() : null;
|
||||
|
||||
private static string? GetString(JsonElement input, string name)
|
||||
=> input.ValueKind == JsonValueKind.Object && input.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
||||
? v.GetString()
|
||||
: null;
|
||||
|
||||
private static DatabaseAccessLevel GetAccessLevel(AgentToolContext context)
|
||||
{
|
||||
if (context.ToolConfig.TryGetValue("accessLevel", out var val) && val != null)
|
||||
{
|
||||
|
||||
@@ -64,6 +64,19 @@ public static class SqlGuard
|
||||
@"ADD|DROP|RENAME|MODIFY|ALTER)\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// #610: "CREATE TABLE IF NOT EXISTS t" / "DROP TABLE IF EXISTS t" - ohne das galt "IF" als
|
||||
/// Tabellenname und das uebliche, wiederholbare Anlegen wurde abgelehnt.
|
||||
/// </summary>
|
||||
private static readonly Regex IfExists = new(
|
||||
@"^\s*IF\s+(?:NOT\s+)?EXISTS\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>#610: "DESCRIBE t" / "DESC t" nennen die Tabelle ohne Schluesselwort davor.</summary>
|
||||
private static readonly Regex Describe = new(
|
||||
@"^\s*(?:DESCRIBE|DESC)\s+(?<rest>.*)$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);
|
||||
|
||||
/// <summary>Erster Bezeichner eines Listeneintrags — ein evtl. folgender Alias wird ignoriert.</summary>
|
||||
private static readonly Regex LeadingIdentifier = new(
|
||||
@"^[\s`\[""]*(?<table>[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?)",
|
||||
@@ -205,9 +218,14 @@ public static class SqlGuard
|
||||
{
|
||||
var tables = new List<string>();
|
||||
|
||||
if (Describe.Match(normalized) is { Success: true } describe)
|
||||
tables.AddRange(ParseTableList(describe.Groups["rest"].Value).Take(1));
|
||||
|
||||
foreach (Match keyword in TableKeyword.Matches(normalized))
|
||||
{
|
||||
var rest = normalized[(keyword.Index + keyword.Length)..];
|
||||
if (IfExists.Match(rest) is { Success: true } ifExists)
|
||||
rest = rest[ifExists.Length..];
|
||||
tables.AddRange(ParseTableList(rest));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user