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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--badge-spacing);
|
||||
}
|
||||
|
||||
.fe-dir-label {
|
||||
|
||||
+1
-5
@@ -398,11 +398,7 @@
|
||||
<label>Anzeigename</label>
|
||||
<input type="text" id="profileDisplayName" placeholder="Anzeigename">
|
||||
<label>Standard-Pfad</label>
|
||||
<span class="setting-hint">Wird als Basis-Pfad beim Auswählen eines Workspace in Sessions verwendet.</span>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="profileDefaultPath" placeholder="z.B. /home/user/projects" autocomplete="off">
|
||||
<button type="button" id="profileDefaultPathBrowse" class="icon-btn"><i class="fa fa-folder"></i></button>
|
||||
</div>
|
||||
<input type="text" id="profileDefaultPath" disabled placeholder="Wird vom Administrator festgelegt">
|
||||
<label>Persona</label>
|
||||
<span class="setting-hint">Wird im System-Prompt verwendet, wenn der Agent "Kennt sein Gegenüber" aktiviert hat.</span>
|
||||
<textarea id="profilePersona" rows="8" placeholder="Beschreibe dich selbst – Name, Rolle, Präferenzen, Kommunikationsstil..."></textarea>
|
||||
|
||||
@@ -504,6 +504,7 @@ function openFolderBrowser(opts = {}) {
|
||||
mode: 'folder',
|
||||
onSelect: opts.onSelect || null,
|
||||
currentAbsPath: opts.startPath || '/',
|
||||
basePath: (opts.basePath || '').replace(/\/+$/, '') || null,
|
||||
history: []
|
||||
};
|
||||
|
||||
@@ -592,16 +593,37 @@ async function fbLoadDir(absPath) {
|
||||
sidebar.innerHTML = '<div class="fe-empty">Leer</div>';
|
||||
}
|
||||
|
||||
document.getElementById('feBack').disabled = _feState.history.length === 0;
|
||||
const currentNorm = data.current.replace(/\/+$/, '');
|
||||
const atBase = _feState.basePath && currentNorm === _feState.basePath;
|
||||
const atRoot = currentNorm === '' || currentNorm === '/';
|
||||
document.getElementById('feBack').disabled = atBase || (atRoot && _feState.history.length === 0);
|
||||
} catch (e) {
|
||||
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function fbGoBack() {
|
||||
if (!_feState || _feState.history.length === 0) return;
|
||||
const prev = _feState.history.pop();
|
||||
fbLoadDir(prev);
|
||||
if (!_feState) return;
|
||||
// Nicht über den basePath hinaus navigieren
|
||||
const currentNorm = _feState.currentAbsPath.replace(/\/+$/, '');
|
||||
if (_feState.basePath && currentNorm === _feState.basePath) return;
|
||||
|
||||
if (_feState.history.length > 0) {
|
||||
const prev = _feState.history.pop();
|
||||
// Prüfe ob der vorherige Pfad über dem basePath liegt
|
||||
if (_feState.basePath && !prev.replace(/\/+$/, '').startsWith(_feState.basePath)) {
|
||||
_feState.history.length = 0;
|
||||
fbLoadDir(_feState.basePath);
|
||||
return;
|
||||
}
|
||||
fbLoadDir(prev);
|
||||
} else {
|
||||
// Kein History-Eintrag → zum Parent navigieren
|
||||
const parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || '/' : '/';
|
||||
if (parent !== currentNorm) {
|
||||
fbLoadDir(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Folder Browser: Create Directory ──
|
||||
|
||||
@@ -10,7 +10,6 @@ registerPage('profile', {
|
||||
var usernameInput = document.getElementById('profileUsername');
|
||||
var displayNameInput = document.getElementById('profileDisplayName');
|
||||
var defaultPathInput = document.getElementById('profileDefaultPath');
|
||||
var defaultPathBrowse = document.getElementById('profileDefaultPathBrowse');
|
||||
var personaInput = document.getElementById('profilePersona');
|
||||
var saveBtn = document.getElementById('profileSaveBtn');
|
||||
var messageDiv = document.getElementById('profileMessage');
|
||||
@@ -43,18 +42,6 @@ registerPage('profile', {
|
||||
displayNameInput.value = profile.displayName || '';
|
||||
defaultPathInput.value = profile.defaultPath || '';
|
||||
|
||||
// Default Path Browse
|
||||
if (defaultPathBrowse) {
|
||||
defaultPathBrowse.addEventListener('click', function() {
|
||||
openFolderBrowser({
|
||||
startPath: defaultPathInput.value.trim() || '/',
|
||||
onSelect: function(path) {
|
||||
defaultPathInput.value = path;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Profil speichern
|
||||
saveBtn.addEventListener('click', async function() {
|
||||
saveBtn.disabled = true;
|
||||
@@ -64,7 +51,6 @@ registerPage('profile', {
|
||||
try {
|
||||
var updated = await apiUpdateProfile({
|
||||
displayName: displayNameInput.value.trim(),
|
||||
defaultPath: defaultPathInput.value.trim() || null,
|
||||
persona: _profileMde.value().trim()
|
||||
});
|
||||
|
||||
|
||||
@@ -129,10 +129,29 @@ registerPage('session', {
|
||||
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
|
||||
if (workspaceBrowse) {
|
||||
workspaceBrowse.addEventListener('click', () => {
|
||||
const defaultPath = (currentUser && currentUser.defaultPath) || '';
|
||||
const currentVal = workspaceEl.value.trim();
|
||||
// Für den Folder-Browser den absoluten Startpfad zusammenbauen
|
||||
let startPath = '/';
|
||||
if (defaultPath && currentVal) {
|
||||
startPath = defaultPath.replace(/\/+$/, '') + '/' + currentVal;
|
||||
} else if (defaultPath) {
|
||||
startPath = defaultPath;
|
||||
} else if (currentVal) {
|
||||
startPath = currentVal;
|
||||
}
|
||||
openFolderBrowser({
|
||||
startPath: workspaceEl.value.trim() || (currentUser && currentUser.defaultPath) || '/',
|
||||
onSelect(path) {
|
||||
workspaceEl.value = path;
|
||||
startPath,
|
||||
basePath: defaultPath || null,
|
||||
onSelect(absolutePath) {
|
||||
// Relativen Pfad zum DefaultPath berechnen
|
||||
if (defaultPath && absolutePath.startsWith(defaultPath)) {
|
||||
let rel = absolutePath.slice(defaultPath.length).replace(/^\/+/, '');
|
||||
workspaceEl.value = rel || '';
|
||||
} else {
|
||||
// Kein DefaultPath oder Pfad außerhalb → absoluten Pfad beibehalten (Legacy)
|
||||
workspaceEl.value = absolutePath;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user