Windows Shell-Ausführung: Temp-Script statt stdin/ShellArguments, background_execute Tool, konfigurierbarer Timeout

Execute-Tools (execute, background_execute, SessionEndpoint.HandleExec):
- Windows: Befehl in temp .cmd-Datei statt cmd.exe stdin (interaktiver Modus,
  &-Parsing, Mehr?-Prompt) oder ShellArguments (falsches \"-Escaping für cmd.exe)
- Linux: weiterhin stdin an /bin/bash (funktioniert zuverlässig)
- Script-Cleanup in allen Codepfaden (Success, Timeout, Error)

Neues Tool background_execute:
- Startet Prozesse losgelöst ohne Warten, gibt PID zurück
- Temp-Script-Ansatz: funktioniert auch mit mehrzeiligen Befehlen
- Windows: .cmd mit del "%~f0" (Selbstlöschung)
- Linux: .sh mit rm -f (Selbstlöschung)

Konfigurierbarer Execute-Timeout:
- Neues Server-Setting ExecuteTimeout (Default: 180s)
- IConfiguration in ToolService injected
- Tool-Description zeigt aktuellen Timeout dynamisch an

SessionEndpoint.HandleExec (foreground):
- Pipe-Deadlock-Fix: ReadToEndAsync vor WaitForExitAsync
- Gleicher Temp-Script-Ansatz wie ToolService

WorkspaceHelper.ShellArguments() entfernt (keine Aufrufer mehr)
This commit is contained in:
Meleki committed 2026-03-09 18:45:26 +01:00
1 parent 3e590a6d03
commit d6d7b6c04f
5 files changed
+102 -20

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ public static class ServerSettingsEndpoint
new SettingDefinition("MaxIterations", "int", 100, "Max Tool-Loop Iterationen pro Chat", "number"),
new SettingDefinition("MaxHistory", "int", 120, "Max History-Messages pro Request", "number"),
new SettingDefinition("ReminderInterval", "int", 30, "System-Prompt Reminder alle N Messages (0 = deaktiviert)", "number"),
new SettingDefinition("ExecuteTimeout", "int", 180, "Timeout für Shell-Befehle in Sekunden", "number"),
new SettingDefinition("PayloadLogging", "bool", false, "API-Payloads loggen (Debug)", "toggle"),
new SettingDefinition("PayloadLoggingGzip", "bool", false, "Payload-Logs als .gz komprimieren", "toggle"),
new SettingDefinition("KreaToken", "string", "", "Krea API Token", "password"),
+18 -4
View File
@@ -325,22 +325,31 @@ public static class SessionEndpoint
// Synchron ausführen, auf Ergebnis warten
try
{
string scriptPath = null;
var psi = new ProcessStartInfo
{
FileName = WorkspaceHelper.ShellExecutable,
WorkingDirectory = workingDir,
RedirectStandardInput = !WorkspaceHelper.IsWindows,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
if (WorkspaceHelper.IsWindows)
psi.Arguments = WorkspaceHelper.ShellArguments(command);
{
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_exec_{Guid.NewGuid():N}.cmd");
await System.IO.File.WriteAllTextAsync(scriptPath, $"@echo off\r\n{command}\r\n");
psi.Arguments = $"/c \"{scriptPath}\"";
}
else
{
psi.RedirectStandardInput = true;
}
using var proc = System.Diagnostics.Process.Start(psi);
if (proc == null)
{
if (scriptPath != null) try { System.IO.File.Delete(scriptPath); } catch { }
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "Prozess konnte nicht gestartet werden" });
return;
@@ -352,6 +361,9 @@ public static class SessionEndpoint
proc.StandardInput.Close();
}
var stdoutTask = proc.StandardOutput.ReadToEndAsync();
var stderrTask = proc.StandardError.ReadToEndAsync();
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
@@ -360,12 +372,14 @@ public static class SessionEndpoint
catch (OperationCanceledException)
{
proc.Kill(entireProcessTree: true);
if (scriptPath != null) try { System.IO.File.Delete(scriptPath); } catch { }
await ctx.Response.WriteAsJsonAsync(new { status = "timeout", output = "Timeout nach 30 Sekunden", exitCode = -1 });
return;
}
if (scriptPath != null) try { System.IO.File.Delete(scriptPath); } catch { }
var stdout = await proc.StandardOutput.ReadToEndAsync();
var stderr = await proc.StandardError.ReadToEndAsync();
var stdout = await stdoutTask;
var stderr = await stderrTask;
var output = new System.Text.StringBuilder();
if (!string.IsNullOrEmpty(stdout)) output.Append(stdout);