Files
gott/Services/WorkspaceHelper.cs
T
Meleki d6d7b6c04f 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)
2026-03-09 18:45:26 +01:00

123 lines
4.7 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.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)
}
}