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:
1 parent
8e7bda50f9
commit
4e1ed81f37
12 files changed
+240
-106
No files matched your search
@@ -45,9 +45,8 @@ public static class ProfileEndpoint
|
||||
|
||||
var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null;
|
||||
var persona = body.TryGetProperty("persona", out var p) ? p.GetString() : null;
|
||||
var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : null;
|
||||
|
||||
await db.UpdateUserProfile(userId, displayName, persona, defaultPath);
|
||||
await db.UpdateUserProfile(userId, displayName, persona);
|
||||
|
||||
var user = await db.GetUser(userId);
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
|
||||
@@ -63,7 +63,17 @@ public static class SessionEndpoint
|
||||
var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null;
|
||||
var image = imageService.ResizeDataUrl(imageRaw);
|
||||
var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null;
|
||||
var workspace = body.TryGetProperty("workspace", out var w) ? w.GetString() : null;
|
||||
var workspaceRaw = body.TryGetProperty("workspace", out var w) ? w.GetString() : null;
|
||||
// Relativen Workspace validieren (Path-Traversal verhindern)
|
||||
var workspace = !string.IsNullOrWhiteSpace(workspaceRaw) && !System.IO.Path.IsPathRooted(workspaceRaw)
|
||||
? WorkspaceHelper.ValidateRelativeWorkspace(workspaceRaw)
|
||||
: workspaceRaw;
|
||||
if (!string.IsNullOrWhiteSpace(workspaceRaw) && workspace == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ungültiger Workspace-Pfad (.. nicht erlaubt)" });
|
||||
return;
|
||||
}
|
||||
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
|
||||
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
|
||||
|
||||
@@ -89,6 +99,23 @@ public static class SessionEndpoint
|
||||
}
|
||||
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
|
||||
// Workspace-Pfad validieren (Path-Traversal verhindern)
|
||||
if (body.TryGetProperty("workspace", out var wsEl))
|
||||
{
|
||||
var wsRaw = wsEl.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(wsRaw) && !System.IO.Path.IsPathRooted(wsRaw))
|
||||
{
|
||||
var validated = WorkspaceHelper.ValidateRelativeWorkspace(wsRaw);
|
||||
if (validated == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ungültiger Workspace-Pfad (.. nicht erlaubt)" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.UpdateSession(sessionId, body, imageService);
|
||||
|
||||
var session = await db.GetSession(sessionId);
|
||||
@@ -141,7 +168,10 @@ public static class SessionEndpoint
|
||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
||||
var user = await db.GetUser(userId);
|
||||
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona());
|
||||
var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any())
|
||||
resolvedWorkspace = "/tmp";
|
||||
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace);
|
||||
|
||||
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
||||
await ctx.Response.WriteAsync(systemPrompt);
|
||||
@@ -175,7 +205,10 @@ public static class SessionEndpoint
|
||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
||||
var user = await db.GetUser(userId);
|
||||
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona());
|
||||
var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any())
|
||||
resolvedWorkspace = "/tmp";
|
||||
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
||||
}
|
||||
@@ -230,7 +263,9 @@ public static class SessionEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : "/tmp";
|
||||
var user = await db.GetUser(userId);
|
||||
var resolvedWs = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : "/tmp";
|
||||
|
||||
if (mode == "foreground")
|
||||
{
|
||||
@@ -343,7 +378,8 @@ public static class SessionEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : null;
|
||||
var user = await db.GetUser(userId);
|
||||
var workingDir = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(workingDir))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Extensions;
|
||||
using Gott.Models;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -52,22 +53,42 @@ public static class WorkspaceEndpoint
|
||||
app.MapDelete("/api/workspace/agent/dir", HandleAgentRmdir);
|
||||
}
|
||||
|
||||
private static async Task HandleTree(HttpContext ctx)
|
||||
/// <summary>
|
||||
/// Lädt Session + User und löst den Workspace-Pfad auf (relativ → absolut via DefaultPath).
|
||||
/// Gibt null zurück und setzt StatusCode 400, wenn kein gültiger Workspace vorhanden ist.
|
||||
/// </summary>
|
||||
private static async Task<(SessionRecord session, string basePath)?> ResolveSessionWorkspace(HttpContext ctx, string sessionId)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
||||
|
||||
// Workspace aus Session holen
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var userId = ctx.GetUserId();
|
||||
var user = await db.GetUser(userId);
|
||||
var resolved = Gott.Services.WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(resolved))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Workspace kann nicht aufgelöst werden. Default-Pfad im Profil setzen." });
|
||||
return null;
|
||||
}
|
||||
|
||||
return (session, Path.GetFullPath(resolved));
|
||||
}
|
||||
|
||||
private static async Task HandleTree(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
|
||||
|
||||
// Sicherheit: nicht aus dem Workspace ausbrechen
|
||||
@@ -122,16 +143,10 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||
return;
|
||||
}
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
@@ -180,16 +195,10 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||
return;
|
||||
}
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
@@ -235,13 +244,6 @@ public static class WorkspaceEndpoint
|
||||
|
||||
var entries = new List<object>();
|
||||
|
||||
// Parent
|
||||
var parent = Directory.GetParent(targetPath);
|
||||
if (parent != null)
|
||||
{
|
||||
entries.Add(new { name = "..", path = parent.FullName.Replace('\\', '/'), type = "dir" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase))
|
||||
@@ -473,16 +475,10 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||
return;
|
||||
}
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
@@ -514,16 +510,10 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
|
||||
return;
|
||||
}
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
||||
@@ -650,15 +640,10 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var basePath = result.Value.basePath;
|
||||
|
||||
var basePath = Path.GetFullPath(session.Workspace);
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
|
||||
Reference in new issue
Block a user