feat: DefaultPath als Base-Path für User-Workspaces

- WorkspaceHelper.cs: Zentrale Workspace-Auflösung (relativ → absolut)
- Session-Workspace wird relativ zum User-DefaultPath gespeichert
- System-Prompt bekommt aufgelösten absoluten Pfad
- Path-Traversal-Schutz: '..' in Workspace-Pfaden blockiert
- Browse-API gibt '..' nicht mehr als Eintrag zurück
- Folder-Browser: basePath-Limit, Back-Button navigiert zum Parent
- DefaultPath nur durch Admin bearbeitbar (Profil: readonly)
- Legacy: Absolute Pfade in session.Workspace weiterhin kompatibel
This commit is contained in:
Meleki committed 2026-03-05 22:49:09 +01:00
1 parent 8e7bda50f9
commit 4e1ed81f37
12 files changed
+240 -106

No files matched your search

+6 -4
View File
@@ -58,14 +58,16 @@ public class ChatService
var agentMemoryDir = _memoryService.GetAgentMemoryDir(agent.AgentId);
var agentSkills = await _db.GetAgentSkills(agent.AgentId);
var user = userId != null ? await _db.GetUser(userId) : null;
var systemPromptText = SystemPromptBuilder.Build(
agent, session, markedMessages, _memoryService, agentMemoryDir,
agentSkills, _skillService, userPersona: user?.GetEffectivePersona());
var workspace = session.Workspace;
var workspace = WorkspaceHelper.ResolveWorkspace(session, user);
if (string.IsNullOrEmpty(workspace) && agentSkills.Any())
workspace = "/tmp";
var systemPromptText = SystemPromptBuilder.Build(
agent, session, markedMessages, _memoryService, agentMemoryDir,
agentSkills, _skillService, userPersona: user?.GetEffectivePersona(),
resolvedWorkspace: workspace);
var toolChoice = agent.ToolChoice ?? "any";
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
+3 -3
View File
@@ -889,12 +889,12 @@ public class DatabaseService
new { userId, passwordHash, now = DateTime.UtcNow.ToString("o") });
}
public async Task UpdateUserProfile(string userId, string displayName, string persona, string defaultPath = null)
public async Task UpdateUserProfile(string userId, string displayName, string persona)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE users SET display_name = @displayName, persona = @persona, default_path = @defaultPath, updated_at = @now WHERE user_id = @userId",
new { userId, displayName, persona, defaultPath, now = DateTime.UtcNow.ToString("o") });
"UPDATE users SET display_name = @displayName, persona = @persona, updated_at = @now WHERE user_id = @userId",
new { userId, displayName, persona, now = DateTime.UtcNow.ToString("o") });
}
public async Task<string> GetUserSettings(string userId)
+8 -7
View File
@@ -13,7 +13,7 @@ public class PromptBlock
public static class SystemPromptBuilder
{
public static List<PromptBlock> BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null, string userPersona = null)
public static List<PromptBlock> BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null, string userPersona = null, string resolvedWorkspace = null)
{
var blocks = new List<PromptBlock>();
@@ -47,7 +47,8 @@ public static class SystemPromptBuilder
bool toolAny = string.Equals(agent.ToolChoice, "any", System.StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(session.Workspace))
var effectiveWorkspace = resolvedWorkspace ?? session.Workspace;
if (!string.IsNullOrWhiteSpace(effectiveWorkspace))
{
if (toolAny)
{
@@ -67,7 +68,7 @@ JEDE Datei-Operation und JEDER Shell-Befehl MUSS über einen Tool-Aufruf erfolge
Content = @$"## Tools
Du hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten.
Der Workspace-Pfad ist: `{session.Workspace}`
Der Workspace-Pfad ist: `{effectiveWorkspace}`
Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen."
});
}
@@ -137,7 +138,7 @@ Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder a
}
// Git-Commit-Verhalten: relevant sobald Workspace vorhanden (unabhängig von ToolChoice)
if (!string.IsNullOrWhiteSpace(session.Workspace))
if (!string.IsNullOrWhiteSpace(effectiveWorkspace))
{
var autoCommit = (agent.AutoCommit ?? "").ToLowerInvariant();
if (autoCommit == "mention")
@@ -170,7 +171,7 @@ Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder a
// autoCommit == "" → kein Block, Thema wird nicht erwähnt
}
if (toolAny && !string.IsNullOrWhiteSpace(session.Workspace))
if (toolAny && !string.IsNullOrWhiteSpace(effectiveWorkspace))
{
blocks.Add(new PromptBlock
{
@@ -211,9 +212,9 @@ Diese Regel hat höchste Priorität und gilt für JEDE Antwort: Wenn eine Datei
return blocks;
}
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null, string userPersona = null)
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null, string userPersona = null, string resolvedWorkspace = null)
{
var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, skillSlugs, skillService, userPersona);
var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, skillSlugs, skillService, userPersona, resolvedWorkspace);
return string.Join("\n\n", blocks.Select(b => b.Content));
}
}
+88
View File
@@ -0,0 +1,88 @@
using System;
using System.IO;
using Gott.Models;
namespace Gott.Services;
public static class WorkspaceHelper
{
/// <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
var normalizedBase = Path.GetFullPath(basePath);
if (!resolved.StartsWith(normalizedBase + Path.DirectorySeparatorChar) && resolved != normalizedBase)
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());
if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar))
return normalizedWorkspace.Substring(normalizedBase.Length + 1);
if (normalizedWorkspace == normalizedBase)
return "";
return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy)
}
}