FTP und RocketChat kennen die benannten Arbeitsbereiche (#718)
Gemeinsamer Helfer NamedWorkspaces in Core statt dreier Kopien: unit: und Bereichs-Id als Praefix, Lesen braucht Read, Herunterladen braucht Write, Unbekanntes und Verbotenes antworten gleich. Ein Dateiname aus einer RocketChat-Nachricht waehlt keinen Bereich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1bf26e9ff3
commit
88c42b244d
@@ -11,4 +11,8 @@
|
||||
<ProjectReference Include="..\ClawdDotNet.Core\ClawdDotNet.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ClawdDotNet.Tools.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -57,7 +58,7 @@ public sealed class RocketChatTool : IAgentTool, IToolJobProvider
|
||||
},
|
||||
"localPath": {
|
||||
"type": "string",
|
||||
"description": "Path of the file to send in the workspace (only for send_file): \"report.pdf\" or \"personal:report.pdf\" in personal workspace, \"shared:analysis.csv\" in shared workspace."
|
||||
"description": "Path of the file to send in the workspace (only for send_file): \"report.pdf\" or \"personal:report.pdf\" in personal workspace, \"shared:analysis.csv\" in shared workspace, \"unit:plan.md\" in your unit's workspace, \"<workspace id>:file\" in a workspace you are a member of."
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
@@ -165,8 +166,7 @@ public sealed class RocketChatTool : IAgentTool, IToolJobProvider
|
||||
"'localPath' is required — e.g. \"report.pdf\" (personal workspace) "
|
||||
+ "or \"shared:analysis.csv\" (shared workspace).");
|
||||
|
||||
var (file, pathError) = WorkspaceFile.Resolve(
|
||||
localPath, context.WorkspacePath, context.SharedWorkspacePath);
|
||||
var (file, pathError) = WorkspaceFile.Resolve(localPath, context, write: false);
|
||||
if (file is null)
|
||||
return ToolResult.Fail(pathError!);
|
||||
|
||||
@@ -226,23 +226,18 @@ public sealed class RocketChatTool : IAgentTool, IToolJobProvider
|
||||
// Target name: the model's choice, otherwise the name from the message. Both are
|
||||
// controlled by someone else, so only the bare file name is taken and any path in
|
||||
// it is discarded before anything is resolved.
|
||||
var wanted = GetString(input, "localPath") ?? GetString(input, "filename") ?? fileId;
|
||||
var chosen = GetString(input, "localPath");
|
||||
var wanted = chosen ?? GetString(input, "filename") ?? fileId;
|
||||
|
||||
// Strip the workspace prefix first: on Windows ':' is not a path separator but the
|
||||
// marker for NTFS alternate data streams. If "shared:" stayed in the name,
|
||||
// Path.GetFileName would pass it through unchanged — and the file would end up as
|
||||
// a data stream on the directory instead of a file of its own.
|
||||
var prefix = "";
|
||||
var rest = wanted;
|
||||
if (rest.StartsWith("shared:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
prefix = "shared:";
|
||||
rest = rest[7..];
|
||||
}
|
||||
else if (rest.StartsWith("personal:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
rest = rest[9..];
|
||||
}
|
||||
// #718: only the model's own localPath chooses the workspace. A name from the message
|
||||
// is someone else's — "unit:x.pdf" there is a file name, not a destination.
|
||||
var (workspace, rest) = chosen is not null
|
||||
? NamedWorkspaces.Split(wanted)
|
||||
: (WorkspaceLayout.Personal, wanted.Replace(':', '_'));
|
||||
|
||||
var targetName = Path.GetFileName(rest.Replace('\\', '/'));
|
||||
if (string.IsNullOrWhiteSpace(targetName))
|
||||
@@ -254,10 +249,9 @@ public sealed class RocketChatTool : IAgentTool, IToolJobProvider
|
||||
$"Files of type '{extension}' are not downloaded. "
|
||||
+ "If needed, set 'allowDangerousDownloads' in the tool configuration.");
|
||||
|
||||
// The model may choose the directory (personal:/shared:), but not compose the file
|
||||
// name freely — hence prefix and sanitized name are kept separate.
|
||||
var (file, pathError) = WorkspaceFile.Resolve(
|
||||
prefix + targetName, context.WorkspacePath, context.SharedWorkspacePath);
|
||||
// The model may choose the directory (personal:/shared:/unit:/a workspace id), but not
|
||||
// compose the file name freely — hence prefix and sanitized name are kept separate.
|
||||
var (file, pathError) = WorkspaceFile.Resolve($"{workspace}:{targetName}", context, write: true);
|
||||
if (file is null)
|
||||
return ToolResult.Fail(pathError!);
|
||||
|
||||
@@ -269,7 +263,7 @@ public sealed class RocketChatTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
downloaded = true,
|
||||
fileId,
|
||||
savedAs = $"{(file.Label == "shared" ? "shared:" : "")}{targetName}",
|
||||
savedAs = file.Label == WorkspaceLayout.Personal ? targetName : $"{file.Label}:{targetName}",
|
||||
workspace = file.Label,
|
||||
bytes
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
|
||||
namespace ClawdDotNet.Tools.RocketChat;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,30 +17,31 @@ internal static class WorkspaceFile
|
||||
{
|
||||
public sealed record Resolved(string FullPath, string Label);
|
||||
|
||||
public static (Resolved? File, string? Error) Resolve(
|
||||
string path, string? personalRoot, string? sharedRoot)
|
||||
/// <param name="write">
|
||||
/// #718: The file is written into the workspace (download). A workspace with a member list
|
||||
/// then needs Write; sending a file from it needs Read.
|
||||
/// </param>
|
||||
public static (Resolved? File, string? Error) Resolve(string path, AgentToolContext context, bool write)
|
||||
{
|
||||
string relative;
|
||||
string? root;
|
||||
string label;
|
||||
var (label, relative) = NamedWorkspaces.Split(path);
|
||||
|
||||
if (path.StartsWith("shared:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relative = path[7..];
|
||||
root = sharedRoot;
|
||||
label = "shared";
|
||||
}
|
||||
else if (path.StartsWith("personal:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relative = path[9..];
|
||||
root = personalRoot;
|
||||
label = "personal";
|
||||
}
|
||||
if (label == WorkspaceLayout.Shared)
|
||||
root = context.SharedWorkspacePath;
|
||||
else if (label == WorkspaceLayout.Personal)
|
||||
root = context.WorkspacePath;
|
||||
else
|
||||
{
|
||||
relative = path;
|
||||
root = personalRoot;
|
||||
label = "personal";
|
||||
// #718: "unit:" or a workspace id. Unknown and not allowed answer the same.
|
||||
var named = NamedWorkspaces.Resolve(label, context.AgentId, context.Organisation, context.SharedWorkspacePath);
|
||||
if (named is null)
|
||||
return (null, NamedWorkspaces.Unknown(label, context.AgentId, context.Organisation, withShared: true));
|
||||
if (named.Access < (write ? WorkspaceAccess.Write : WorkspaceAccess.Read))
|
||||
return (null, $"Access denied: this agent may not {(write ? "write to" : "read from")} workspace '{named.Workspace.Id}'.");
|
||||
root = named.Root;
|
||||
label = named.Workspace.Id;
|
||||
if (write)
|
||||
Directory.CreateDirectory(root);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
|
||||
Reference in New Issue
Block a user