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)
123 lines
4.7 KiB
C#
123 lines
4.7 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Runtime.InteropServices;
|
||
using Gott.Models;
|
||
|
||
namespace Gott.Services;
|
||
|
||
public static class WorkspaceHelper
|
||
{
|
||
/// <summary>True wenn die Plattform Windows ist.</summary>
|
||
internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||
|
||
/// <summary>Plattformunabhängiges Temp-Verzeichnis ohne Trailing-Separator.</summary>
|
||
internal static string TempDir => Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||
|
||
/// <summary>
|
||
/// Prüft ob ein Pfad ein Sub-Pfad eines Basis-Pfads ist (oder gleich).
|
||
/// Auf Windows case-insensitive, auf Linux case-sensitive.
|
||
/// </summary>
|
||
internal static bool IsSubPathOf(string path, string basePath)
|
||
{
|
||
var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||
var normalizedPath = Path.GetFullPath(path);
|
||
var normalizedBase = Path.GetFullPath(basePath);
|
||
return normalizedPath.Equals(normalizedBase, comparison)
|
||
|| normalizedPath.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gibt den relativen Pfad zurück – immer mit Forward-Slashes (für Agent-Output/API).
|
||
/// </summary>
|
||
internal static string GetRelativePath(string relativeTo, string path)
|
||
{
|
||
var rel = Path.GetRelativePath(relativeTo, path);
|
||
return IsWindows ? rel.Replace('\\', '/') : rel;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gibt den Shell-Dateinamen zurück (cmd.exe auf Windows, /bin/bash auf Linux).
|
||
/// </summary>
|
||
internal static string ShellExecutable => IsWindows ? "cmd.exe" : "/bin/bash";
|
||
|
||
/// <summary>
|
||
/// Löst den Workspace-Pfad einer Session auf.
|
||
/// session.Workspace ist relativ zum user.DefaultPath.
|
||
/// Gibt den absoluten Pfad zurück oder null wenn kein Workspace konfiguriert ist.
|
||
/// </summary>
|
||
public static string ResolveWorkspace(SessionRecord session, UserRecord user)
|
||
{
|
||
if (session == null || string.IsNullOrWhiteSpace(session.Workspace))
|
||
return null;
|
||
|
||
var workspace = session.Workspace.Trim();
|
||
|
||
// Wenn der Workspace bereits ein absoluter Pfad ist (Legacy/Fallback)
|
||
if (Path.IsPathRooted(workspace))
|
||
return Path.GetFullPath(workspace);
|
||
|
||
// Relativer Pfad → mit DefaultPath des Users kombinieren
|
||
var basePath = user?.DefaultPath?.Trim();
|
||
if (string.IsNullOrWhiteSpace(basePath))
|
||
return null; // Kein DefaultPath gesetzt → relativer Workspace kann nicht aufgelöst werden
|
||
|
||
var resolved = Path.GetFullPath(Path.Combine(basePath, workspace));
|
||
|
||
// Sicherheit: darf nicht aus dem DefaultPath ausbrechen
|
||
if (!IsSubPathOf(resolved, Path.GetFullPath(basePath)))
|
||
return null;
|
||
|
||
return resolved;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Validiert einen relativen Workspace-Pfad.
|
||
/// Prüft auf Path-Traversal (.. etc.).
|
||
/// Gibt den bereinigten Pfad zurück oder null bei ungültigem Pfad.
|
||
/// </summary>
|
||
public static string ValidateRelativeWorkspace(string workspace)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(workspace))
|
||
return null;
|
||
|
||
var trimmed = workspace.Trim().TrimStart('/', '\\').TrimEnd('/', '\\');
|
||
if (string.IsNullOrEmpty(trimmed))
|
||
return null;
|
||
|
||
// Path-Traversal verhindern
|
||
if (trimmed.Contains("..") || trimmed.Contains("~"))
|
||
return null;
|
||
|
||
// Keine absoluten Pfade erlauben
|
||
if (Path.IsPathRooted(trimmed))
|
||
return null;
|
||
|
||
return trimmed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Konvertiert einen absoluten Workspace-Pfad in einen relativen Pfad zum DefaultPath.
|
||
/// Wird beim Speichern verwendet, damit alte absolute Pfade in relative umgewandelt werden.
|
||
/// </summary>
|
||
public static string ToRelativeWorkspace(string absoluteWorkspace, string defaultPath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(absoluteWorkspace))
|
||
return null;
|
||
|
||
if (string.IsNullOrWhiteSpace(defaultPath))
|
||
return absoluteWorkspace; // Kein DefaultPath → absoluten Pfad beibehalten (Legacy)
|
||
|
||
var normalizedBase = Path.GetFullPath(defaultPath.Trim());
|
||
var normalizedWorkspace = Path.GetFullPath(absoluteWorkspace.Trim());
|
||
var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||
|
||
if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison))
|
||
return normalizedWorkspace.Substring(normalizedBase.Length + 1);
|
||
|
||
if (normalizedWorkspace.Equals(normalizedBase, comparison))
|
||
return "";
|
||
|
||
return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy)
|
||
}
|
||
}
|