Files
gott/Services/ToolService.cs
T
Meleki 10da2bd964 feat: Remote-Verbindungen – Windows-Workstations per WebSocket steuern
- RemoteConnectionManager: Singleton-Service verwaltet WebSocket-Verbindungen
  mit Command-Execution (Send/Receive mit Timeout + SemaphoreSlim-Locking)
- RemoteEndpoint: WebSocket-Endpoint /api/remote (auth-frei), REST-APIs für
  Connections/Assign/Unassign, PowerShell-Script-Auslieferung mit dynamischer URL
- remote.ps1: PowerShell-Client für Windows – verbindet per WebSocket,
  sendet Hostname/User/OS, empfängt und führt Befehle aus, Heartbeat
- remote_execute Tool: Neues Agent-Tool, nur verfügbar wenn Session eine
  zugeordnete Remote-Verbindung hat, führt PowerShell-Befehle remote aus
- Frontend: Menüpunkt 'Remote-Verbindungen' im Chat-Dropdown, Modal zeigt
  alle aktiven WebSocket-Verbindungen (Hostname, User, OS, Verbindungszeit),
  Verbinden/Trennen-Buttons pro Verbindung und Session
- Auth: /api/remote und /remote.ps1 von Authentifizierung ausgenommen

Architektur: Windows-Client → WebSocket → GOTT-Server → Agent-Tool
Session-gebunden: Eine Verbindung wird einer Session zugeordnet
2026-03-11 07:04:24 +01:00

657 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
namespace Gott.Services;
public class ToolService
{
private readonly IConfiguration _config;
public ToolService(IConfiguration config)
{
_config = config;
}
private int ExecuteTimeoutSeconds => _config.GetValue<int>("ExecuteTimeout", 180);
public const string AskUserChoiceToolName = "ask_user_choice";
/// <summary>
/// Read-only Tools die im Plan-Modus verfügbar bleiben.
/// Write-Tools (write_file, edit_file, delete_file, execute) werden entfernt.
/// </summary>
private static readonly HashSet<string> ReadOnlyToolNames = new HashSet<string>
{
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
"memory_read", "memory_write", "memory_list",
"remote_execute", AskUserChoiceToolName
};
public object[] FilterReadOnlyTools(object[] tools)
{
var filtered = new List<object>();
foreach (var tool in tools)
{
var json = JsonSerializer.Serialize(tool);
var el = JsonSerializer.Deserialize<JsonElement>(json);
if (el.TryGetProperty("name", out var nameProp))
{
var name = nameProp.GetString() ?? "";
if (ReadOnlyToolNames.Contains(name))
filtered.Add(tool);
}
}
return filtered.ToArray();
}
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false)
{
var allTools = new List<object>();
if (includeUserChoice)
allTools.Add(GetAskUserChoiceTool());
if (includeFileTools)
allTools.AddRange(GetFileTools());
if (includeMemory)
allTools.AddRange(GetMemoryToolDefinitions());
if (includeRemote)
allTools.Add(GetRemoteExecuteTool());
return allTools.ToArray();
}
private object GetAskUserChoiceTool()
{
return new
{
name = AskUserChoiceToolName,
description = "Stellt dem User eine Frage mit Auswahlmöglichkeiten, wenn du unsicher bist oder es mehrere sinnvolle Wege gibt. Der User wählt eine Option und du arbeitest mit seiner Antwort weiter.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["question"] = new { type = "string", description = "Die Frage an den User" },
["choices"] = new
{
type = "array",
description = "Liste der Auswahlmöglichkeiten (2-5 Optionen). MUSS ein JSON-Array mit Strings sein, z.B. [\"Option A\", \"Option B\", \"Option C\"]. Niemals einen einzelnen String verwenden.",
items = new { type = "string", description = "Eine einzelne Auswahlmöglichkeit als kurzer, prägnanter Text" },
minItems = 2,
maxItems = 5
}
},
required = new[] { "question", "choices" }
}
};
}
private object[] GetFileTools()
{
return new object[]
{
new
{
name = "read_file",
description = "Liest den Inhalt einer Datei. Gibt den Textinhalt zurück.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Dateipfad" }
},
required = new[] { "path" }
}
},
new
{
name = "write_file",
description = "Erstellt oder überschreibt eine Datei mit dem angegebenen Inhalt.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Dateipfad" },
["content"] = new { type = "string", description = "Dateiinhalt" }
},
required = new[] { "path", "content" }
}
},
new
{
name = "edit_file",
description = "Ersetzt einen exakten Textabschnitt in einer Datei. oldText muss exakt matchen.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Dateipfad" },
["old_text"] = new { type = "string", description = "Exakter Text der ersetzt werden soll" },
["new_text"] = new { type = "string", description = "Neuer Text" }
},
required = new[] { "path", "old_text", "new_text" }
}
},
new
{
name = "list_files",
description = "Listet Dateien und Ordner in einem Verzeichnis auf.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Verzeichnispfad (leer = Root)" }
},
required = Array.Empty<string>()
}
},
new
{
name = "delete_file",
description = "Löscht eine Datei oder ein leeres Verzeichnis.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Pfad zur Datei oder zum Verzeichnis" }
},
required = new[] { "path" }
}
},
new
{
name = "execute",
description = $"Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: {ExecuteTimeoutSeconds} Sekunden.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
},
required = new[] { "command" }
}
},
new
{
name = "background_execute",
description = "Startet einen Shell-Befehl als Hintergrund-Prozess im Workspace-Verzeichnis. Der Prozess läuft unabhängig weiter – es wird NICHT auf dessen Ende gewartet. Gibt die Prozess-ID (PID) zurück. Nutze dies für lang laufende Prozesse wie Server, Watcher oder Build-Prozesse. stdout/stderr werden NICHT erfasst.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen im Hintergrund" }
},
required = new[] { "command" }
}
},
new
{
name = "glob",
description = "Findet Dateien im Workspace anhand eines Glob-Patterns (z.B. '**/*.cs', 'src/*.js'). Gibt eine Liste passender Dateipfade zurück.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["pattern"] = new { type = "string", description = "Glob-Pattern, z.B. '**/*.cs' oder 'src/**/*.js'" },
["path"] = new { type = "string", description = "Startverzeichnis (relativ zum Workspace, leer = Root)" }
},
required = new[] { "pattern" }
}
},
new
{
name = "grep",
description = "Durchsucht Dateien im Workspace nach einem Regex- oder Text-Pattern. Gibt Treffer mit Dateiname und Zeilennummer zurück.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["pattern"] = new { type = "string", description = "Suchbegriff oder Regex-Pattern" },
["path"] = new { type = "string", description = "Zu durchsuchendes Verzeichnis oder Datei (relativ, leer = Root)" },
["glob"] = new { type = "string", description = "Dateifilter als Glob-Pattern, z.B. '*.cs' (leer = alle Dateien)" },
["case_sensitive"] = new { type = "boolean", description = "Groß-/Kleinschreibung beachten (Default: false)" }
},
required = new[] { "pattern" }
}
},
new
{
name = "todo_write",
description = "Schreibt die komplette Todo-Liste. Ersetzt immer die gesamte Liste. Nutze dies um Aufgaben zu planen und den Fortschritt zu verfolgen.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["todos"] = new
{
type = "array",
description = "Liste aller Todo-Einträge",
items = new
{
type = "object",
properties = new Dictionary<string, object>
{
["id"] = new { type = "string", description = "Eindeutige ID (z.B. '1', '2', 'task-1')" },
["content"] = new { type = "string", description = "Beschreibung der Aufgabe" },
["status"] = new { type = "string", @enum = new[] { "pending", "in_progress", "completed" }, description = "Status der Aufgabe" },
["priority"] = new { type = "string", @enum = new[] { "high", "medium", "low" }, description = "Priorität (optional)" }
},
required = new[] { "id", "content", "status" }
}
}
},
required = new[] { "todos" }
}
}
};
}
private object GetRemoteExecuteTool()
{
return new
{
name = "remote_execute",
description = "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["command"] = new { type = "string", description = "PowerShell-Befehl zum Ausführen auf dem Remote-PC" }
},
required = new[] { "command" }
}
};
}
private object[] GetMemoryToolDefinitions()
{
return new object[]
{
new
{
name = "memory_read",
description = "Liest eine Memory-Datei des Agents. Nutze dies um gespeicherte Erinnerungen, Notizen oder Kontextinformationen abzurufen.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Pfad zur Memory-Datei (z.B. 'MEMORY.md' oder 'memory/YYYY-MM-DD.md')" }
},
required = new[] { "path" }
}
},
new
{
name = "memory_write",
description = "Schreibt oder überschreibt eine Memory-Datei. Nutze dies um wichtige Informationen, Entscheidungen oder Präferenzen dauerhaft zu speichern.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Relativer Pfad zur Memory-Datei (z.B. 'MEMORY.md' oder 'memory/YYYY-MM-DD.md')" },
["content"] = new { type = "string", description = "Inhalt der Datei" },
["append"] = new { type = "boolean", description = "Wenn true, wird der Inhalt angehängt statt überschrieben (Default: false)" }
},
required = new[] { "path", "content" }
}
},
new
{
name = "memory_list",
description = "Listet alle Memory-Dateien des Agents auf.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["path"] = new { type = "string", description = "Optionaler Unterpfad zum Auflisten (leer = Root)" }
},
required = Array.Empty<string>()
}
}
};
}
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null)
{
switch (name)
{
case "read_file":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
if (!File.Exists(path))
return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
return File.ReadAllText(path);
}
case "write_file":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
var dir = Path.GetDirectoryName(path);
if (dir != null) Directory.CreateDirectory(dir);
var content = input.GetProperty("content").GetString() ?? "";
File.WriteAllText(path, content);
return $"Datei geschrieben: {WorkspaceHelper.GetRelativePath(workspace, path)} ({content.Length} Zeichen)";
}
case "edit_file":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
if (!File.Exists(path))
return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
var fileContent = File.ReadAllText(path);
var oldText = input.GetProperty("old_text").GetString() ?? "";
var newText = input.GetProperty("new_text").GetString() ?? "";
if (!fileContent.Contains(oldText))
return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig.";
fileContent = fileContent.Replace(oldText, newText);
File.WriteAllText(path, fileContent);
return $"Datei bearbeitet: {WorkspaceHelper.GetRelativePath(workspace, path)}";
}
case "list_files":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", workspace);
if (!Directory.Exists(path))
return $"Fehler: Verzeichnis nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
var entries = new List<string>();
foreach (var dir in Directory.GetDirectories(path))
entries.Add(WorkspaceHelper.GetRelativePath(workspace, dir) + "/");
foreach (var file in Directory.GetFiles(path))
entries.Add(WorkspaceHelper.GetRelativePath(workspace, file));
return entries.Count == 0 ? "(leer)" : string.Join("\n", entries);
}
case "delete_file":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
if (File.Exists(path))
{
File.Delete(path);
return $"Datei gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}";
}
if (Directory.Exists(path))
{
Directory.Delete(path, recursive: false);
return $"Verzeichnis gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}";
}
return $"Fehler: Nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
}
case "execute":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var command = input.GetProperty("command").GetString() ?? "";
// Unter Windows: Befehl in temp .cmd-Datei schreiben und mit /c ausführen.
// stdin-Ansatz funktioniert nicht zuverlässig (interaktiver Modus, &-Parsing).
string scriptPath = null;
var psi = new ProcessStartInfo
{
FileName = WorkspaceHelper.ShellExecutable,
WorkingDirectory = workspace,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
if (WorkspaceHelper.IsWindows)
{
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_exec_{Guid.NewGuid():N}.cmd");
File.WriteAllText(scriptPath, $"@echo off\r\n{command}\r\n");
psi.Arguments = $"/c \"{scriptPath}\"";
}
else
{
psi.RedirectStandardInput = true;
}
using var proc = Process.Start(psi);
if (proc == null)
{
if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
return "Fehler: Prozess konnte nicht gestartet werden";
}
if (!WorkspaceHelper.IsWindows)
{
await proc.StandardInput.WriteAsync(command);
proc.StandardInput.Close();
}
// Timeout UND externen CancellationToken verknüpfen
var timeoutSeconds = ExecuteTimeoutSeconds;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
// stdout/stderr VOR WaitForExit lesen – verhindert Deadlock
// wenn Pipe-Buffer volllaufen (Windows: nur 4KB!)
var stdoutTask = proc.StandardOutput.ReadToEndAsync();
var stderrTask = proc.StandardError.ReadToEndAsync();
try
{
await proc.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
proc.Kill(entireProcessTree: true);
if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
// Wenn der externe Token abgebrochen wurde, weiterwerfen
cancellationToken.ThrowIfCancellationRequested();
return $"Fehler: Timeout nach {timeoutSeconds} Sekunden - Prozess abgebrochen";
}
if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
var stdout = await stdoutTask;
var stderr = await stderrTask;
var result = new StringBuilder();
if (!string.IsNullOrEmpty(stdout)) result.Append(stdout);
if (!string.IsNullOrEmpty(stderr))
{
if (result.Length > 0) result.Append("\n--- stderr ---\n");
result.Append(stderr);
}
if (result.Length == 0) result.Append($"(exit code {proc.ExitCode})");
else if (proc.ExitCode != 0) result.Append($"\n(exit code {proc.ExitCode})");
return result.ToString();
}
case "background_execute":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var command = input.GetProperty("command").GetString() ?? "";
// Befehl in temporäre Script-Datei schreiben und losgelöst starten.
// So funktionieren auch mehrzeilige Befehle zuverlässig.
var scriptId = Guid.NewGuid().ToString("N")[..8];
string scriptPath;
if (WorkspaceHelper.IsWindows)
{
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_bg_{scriptId}.cmd");
// Script löscht sich nach Ausführung selbst
File.WriteAllText(scriptPath, $"@echo off\r\n{command}\r\ndel \"%~f0\"\r\n");
}
else
{
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_bg_{scriptId}.sh");
File.WriteAllText(scriptPath, $"#!/bin/sh\n{command}\nrm -f \"{scriptPath}\"\n");
// Ausführbar machen
var chmod = Process.Start(new ProcessStartInfo { FileName = "chmod", Arguments = $"+x \"{scriptPath}\"", UseShellExecute = false, CreateNoWindow = true });
chmod?.WaitForExit(5000);
}
var psi = new ProcessStartInfo
{
FileName = WorkspaceHelper.IsWindows ? "cmd.exe" : scriptPath,
Arguments = WorkspaceHelper.IsWindows ? $"/c \"{scriptPath}\"" : "",
WorkingDirectory = workspace,
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(psi);
if (proc == null) return "Fehler: Prozess konnte nicht gestartet werden";
return $"Hintergrund-Prozess gestartet (PID: {proc.Id})";
}
case "glob":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var pattern = input.GetProperty("pattern").GetString() ?? "*";
var basePath = input.TryGetProperty("path", out var gp) ? gp.GetString() ?? "" : "";
var searchRoot = string.IsNullOrEmpty(basePath)
? workspace
: ResolvePath(basePath, workspace);
if (!Directory.Exists(searchRoot))
return $"Fehler: Verzeichnis nicht gefunden: {basePath}";
var matcher = new Matcher();
matcher.AddInclude(pattern);
var dirInfo = new DirectoryInfoWrapper(new DirectoryInfo(searchRoot));
var result = matcher.Execute(dirInfo);
var matches = result.Files
.Select(f => WorkspaceHelper.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path)))
.OrderBy(f => f)
.ToList();
return matches.Count == 0 ? "(keine Treffer)" : string.Join("\n", matches);
}
case "grep":
{
if (string.IsNullOrEmpty(workspace))
return "Fehler: Kein Workspace konfiguriert";
var pattern = input.GetProperty("pattern").GetString() ?? "";
var searchPath = input.TryGetProperty("path", out var sp) ? sp.GetString() ?? "" : "";
var fileGlob = input.TryGetProperty("glob", out var fg) ? fg.GetString() ?? "*" : "*";
var caseSensitive = input.TryGetProperty("case_sensitive", out var cs) && cs.GetBoolean();
var regexOptions = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase;
Regex regex;
try { regex = new Regex(pattern, regexOptions); }
catch { regex = new Regex(Regex.Escape(pattern), regexOptions); }
var searchRoot = string.IsNullOrEmpty(searchPath)
? workspace
: ResolvePath(searchPath, workspace);
IEnumerable<string> files;
if (File.Exists(searchRoot))
files = new[] { searchRoot };
else if (Directory.Exists(searchRoot))
files = Directory.GetFiles(searchRoot, fileGlob, SearchOption.AllDirectories);
else
return $"Fehler: Pfad nicht gefunden: {searchPath}";
var results = new List<string>();
foreach (var file in files.OrderBy(f => f))
{
string[] lines;
try { lines = File.ReadAllLines(file); }
catch { continue; }
for (var i = 0; i < lines.Length; i++)
{
if (regex.IsMatch(lines[i]))
results.Add($"{WorkspaceHelper.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}");
}
}
if (results.Count == 0) return "(keine Treffer)";
if (results.Count > 500) results = results.Take(500).Append("... (auf 500 Treffer begrenzt)").ToList();
return string.Join("\n", results);
}
case "todo_write":
{
var todos = input.GetProperty("todos");
var sb = new StringBuilder();
foreach (var todo in todos.EnumerateArray())
{
var id = todo.GetProperty("id").GetString();
var content = todo.GetProperty("content").GetString();
var status = todo.GetProperty("status").GetString();
var priority = todo.TryGetProperty("priority", out var prio) ? prio.GetString() : null;
var icon = status switch { "completed" => "✅", "in_progress" => "🔄", _ => "⬜" };
var prioTag = priority != null ? $" [{priority}]" : "";
sb.Append($"{icon} [{id}]{prioTag} {content}\n");
}
return sb.Length == 0 ? "(leere Todo-Liste)" : sb.ToString().TrimEnd();
}
case "memory_read":
{
if (memoryService == null || string.IsNullOrEmpty(agentId))
return "Fehler: Memory-Service nicht verfügbar";
var path = input.GetProperty("path").GetString() ?? "";
var content = memoryService.ReadMemoryFile(agentId, path);
if (content == null)
return $"Datei nicht gefunden: {path}";
return content;
}
case "memory_write":
{
if (memoryService == null || string.IsNullOrEmpty(agentId))
return "Fehler: Memory-Service nicht verfügbar";
var path = input.GetProperty("path").GetString() ?? "";
var content = input.GetProperty("content").GetString() ?? "";
var append = input.TryGetProperty("append", out var a) && a.GetBoolean();
if (append)
memoryService.AppendMemoryFile(agentId, path, content);
else
memoryService.WriteMemoryFile(agentId, path, content);
return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)";
}
case "memory_list":
{
if (memoryService == null || string.IsNullOrEmpty(agentId))
return "Fehler: Memory-Service nicht verfügbar";
var subPath = input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "";
var files = memoryService.ListMemoryFiles(agentId, subPath);
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
}
case "remote_execute":
{
if (remoteMgr == null || string.IsNullOrEmpty(sessionId))
return "Fehler: Keine Remote-Verbindung verfügbar";
var conn = remoteMgr.GetBySessionId(sessionId);
if (conn == null)
return "Fehler: Keine Remote-Verbindung für diese Session zugeordnet";
var command = input.GetProperty("command").GetString() ?? "";
var result = await remoteMgr.ExecuteCommand(conn.Id, command, ExecuteTimeoutSeconds, cancellationToken);
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(result.Output))
sb.Append(result.Output);
if (!string.IsNullOrEmpty(result.Error))
{
if (sb.Length > 0) sb.AppendLine();
sb.Append("[stderr] ").Append(result.Error);
}
if (result.ExitCode != 0)
{
if (sb.Length > 0) sb.AppendLine();
sb.Append($"[Exit Code: {result.ExitCode}]");
}
return sb.Length > 0 ? sb.ToString() : "(keine Ausgabe)";
}
default:
return $"Fehler: Unbekanntes Tool: {name}";
}
}
private string ResolvePath(string relativePath, string workspace)
{
var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? ""));
if (!WorkspaceHelper.IsSubPathOf(full, workspace))
throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt");
return full;
}
}