Ablage je Einheit, Stufe 1: benannte Arbeitsbereiche mit Mitgliedern (#684)

- WorkspaceConfig in der Instanzkonfiguration: Id, Name, Zweck,
  Eigentuemer-Einheit, Mitglieder (Einheit oder Agent, Stufe Read/Write/
  Manage, Untereinheiten ein/aus), geschuetzte Pfade. Das Recht steht am
  Bereich, nicht am Agenten.
- OrgChart: AccessTo, WorkspacesFor, UnitWorkspace; Problems nennt
  ungueltige Ids und tote Verweise. WorkspaceLayout: Bereiche liegen unter
  Agents/SharedWorkspaces/<id>/, neben dem unveraenderten SharedWorkspace.
- FileRW: workspace ist ein Name - personal, shared (wie bisher ueber
  sharedAccessLevel), unit (eigene Einheit) oder eine Bereichs-Id. Ein
  unbekannter und ein nicht erlaubter Bereich bekommen dieselbe Antwort.
  Loeschen braucht Manage wie frueher Admin. Der #662-Hinweis sucht in allen
  erlaubten Bereichen.
- Mit Einheiten ist shared:/tasks/ fuer FileRW gesperrt: Aufgaben laufen
  ueber das Brett, sonst umginge eine direkt geschriebene Datei Brett- und
  Adressbuchpruefung. Ohne Einheiten unveraendert.
- Organisationsabschnitt im Prompt nennt die Bereiche mit Stufe und Zweck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 16:52:03 +02:00
co-authored by Claude Opus 5.5
parent ba5fe4cdf7
commit ff20ce2563
8 changed files with 901 additions and 139 deletions
@@ -0,0 +1,56 @@
using ClawdDotNet.Core.Config;
namespace ClawdDotNet.Core.Organisation;
/// <summary>#684: A workspace an agent may use, and how far.</summary>
/// <param name="IsOwnUnit">The workspace of the agent's own unit — reachable as <c>unit</c>.</param>
public sealed record WorkspaceGrant(WorkspaceConfig Workspace, WorkspaceAccess Access, bool IsOwnUnit);
/// <summary>
/// #684: Where the workspaces live on disk, and which names are taken.
///
/// <para>The instance's one shared folder is <c>Agents/SharedWorkspace</c>, and it stays
/// there: FTP, RocketChat, the agent editor, the task files and the backups all find it by
/// that path. The workspaces with member lists live beside it, one folder each under
/// <c>Agents/SharedWorkspaces/</c>. Found from the shared folder's path, which every tool
/// context already carries, so nothing new has to be passed around.</para>
/// </summary>
public static class WorkspaceLayout
{
public const string Personal = "personal";
public const string Shared = "shared";
/// <summary>The own unit's workspace, without having to know the unit's id.</summary>
public const string Unit = "unit";
/// <summary>The folder next to <c>SharedWorkspace</c> that holds the workspaces.</summary>
public const string ContainerFolder = "SharedWorkspaces";
/// <summary>Names a workspace cannot have — they mean something else in FileRW.</summary>
public static readonly IReadOnlyList<string> Reserved = [Personal, Shared, Unit];
/// <summary>Letters, digits, '-' and '_' — the same rule as for unit ids, since a unit's
/// workspace carries the unit's id — and not one of the reserved names.</summary>
public static bool IsValidId(string? id)
=> !string.IsNullOrWhiteSpace(id)
&& id.Length <= 64
&& id.All(c => char.IsLetterOrDigit(c) || c is '-' or '_')
&& !Reserved.Contains(id, StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The folder of a workspace, from the path of the instance's shared folder;
/// <c>null</c> when that is not configured or the id is not valid.
/// </summary>
public static string? PathOf(string? sharedWorkspacePath, string workspaceId)
{
if (string.IsNullOrWhiteSpace(sharedWorkspacePath) || !IsValidId(workspaceId))
return null;
var shared = Path.GetFullPath(sharedWorkspacePath)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var agentsDirectory = Path.GetDirectoryName(shared);
return agentsDirectory is null
? null
: Path.Combine(agentsDirectory, ContainerFolder, workspaceId);
}
}