using ClawdDotNet.Core.Config; namespace ClawdDotNet.Core.Organisation; /// #718: A workspace with a member list, as one call may use it. public sealed record NamedWorkspace(WorkspaceConfig Workspace, string Root, WorkspaceAccess Access); /// /// #718: The workspaces with member lists (#684) for every tool that moves files — FileRW, /// FTP, RocketChat — resolved in one place instead of once per tool. /// /// Unknown and not allowed answer the same. A name that is no workspace and a /// workspace the agent is not a member of both give null, so an answer never reveals /// that a workspace exists. /// /// personal and shared are not resolved here: they belong to each tool /// as they always did (FileRW with its sharedAccessLevel), and running installations /// keep what they have. /// public static class NamedWorkspaces { /// /// Splits name:path. A prefix counts only if it is personal, shared, /// unit or could be a workspace id — so a colon elsewhere in a path (an NTFS stream, /// a drive letter) is left for the path check to refuse. Without a prefix: personal. /// public static (string Workspace, string Relative) Split(string path) { var colon = path.IndexOf(':'); if (colon > 0) { var prefix = path[..colon]; var rest = path[(colon + 1)..]; var reserved = prefix.ToLowerInvariant(); if (WorkspaceLayout.Reserved.Contains(reserved)) return (reserved, rest); if (WorkspaceLayout.IsValidId(prefix)) return (prefix, rest); } return (WorkspaceLayout.Personal, path); } /// /// The workspace behind unit or an id, if the agent may use it; null /// otherwise — also for personal and shared, which the caller handles. /// public static NamedWorkspace? Resolve(string name, string agentId, OrgChart? organisation, string? sharedWorkspacePath) { if (organisation is null || name.Equals(WorkspaceLayout.Personal, StringComparison.OrdinalIgnoreCase) || name.Equals(WorkspaceLayout.Shared, StringComparison.OrdinalIgnoreCase)) return null; var workspace = name.Equals(WorkspaceLayout.Unit, StringComparison.OrdinalIgnoreCase) ? organisation.UnitWorkspace(organisation.UnitOf(agentId)?.Id) : organisation.Workspace(name); if (workspace is null) return null; var access = organisation.AccessTo(agentId, workspace.Id); if (access == WorkspaceAccess.None || WorkspaceLayout.PathOf(sharedWorkspacePath, workspace.Id) is not { } root) return null; return new NamedWorkspace(workspace, root, access); } /// The names an agent can use — for the answer to a name that is not one of them. public static IReadOnlyList Available(string agentId, OrgChart? organisation, bool withShared) { var names = new List { WorkspaceLayout.Personal }; if (withShared) names.Add(WorkspaceLayout.Shared); if (organisation is not null) names.AddRange(organisation.WorkspacesFor(agentId).Select(g => g.IsOwnUnit ? $"{WorkspaceLayout.Unit} (= {g.Workspace.Id})" : g.Workspace.Id)); return names; } public static string Unknown(string requested, string agentId, OrgChart? organisation, bool withShared) => $"Unknown workspace '{requested}'. You can use: {string.Join(", ", Available(agentId, organisation, withShared))}."; }