- OrgAdministration: eine neue Einheit bekommt ihren Bereich (Mitglieder: die Einheit samt Untereinheiten, schreiben); reservierte Namen und belegte Bereichs-Ids werden als Einheiten-Id abgelehnt. Uebergreifende Bereiche anlegen (mit Eigentuemer), Name/Zweck/geschuetzte Ordner aendern, Mitgliederliste ersetzen, nur leere Bereiche loeschen, nie den eigenen einer Einheit. Eine geloeschte Einheit verlaesst alle Mitgliederlisten; ihr Bereich geht nur, wenn er leer ist, sonst bleibt er ohne Eigentuemer. Nachruesten des eigenen Bereichs fuer Einheiten von vor #684. Alles im AuditLog. - API unter /api/organisation/workspaces; die Uebersicht liefert je Bereich Mitglieder, Dateizahl und - von der Instanz berechnet - wer hineinkommt. - Organisationsseite: Bereiche unter der Struktur, Bearbeitung mit Mitglieder-Editor, "Wer hineinkommt", Bereichskarte an der Einheit, Bereiche am Agenten. Sprachschluessel en/de. - Die Nachschau nach einem Absturz bekommt die Organisation mit, sonst fände sie einen Aufruf in einem benannten Bereich nicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
8.0 KiB
C#
147 lines
8.0 KiB
C#
using ClawdDotNet.App;
|
|
using ClawdDotNet.App.Services;
|
|
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Web.Contracts;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
|
|
namespace ClawdDotNet.Web.Endpoints;
|
|
|
|
/// <summary>
|
|
/// #683: The organisation — units, leads, moves, tool ceilings, unit-wide values. Every
|
|
/// decision is made by <see cref="OrgAdministration"/>; this only translates.
|
|
/// </summary>
|
|
internal static class OrganisationEndpoints
|
|
{
|
|
public static void MapOrganisation(this RouteGroupBuilder api)
|
|
{
|
|
var org = api.MapGroup("/organisation");
|
|
|
|
org.MapGet("/", (AppHost host) =>
|
|
{
|
|
var chart = host.Organisation.Current();
|
|
var units = chart.Units
|
|
.OrderBy(u => chart.PathOf(u.Id), StringComparer.OrdinalIgnoreCase)
|
|
.Select(u => new UnitDto(
|
|
u.Id, u.Name, u.ParentId, u.LeaderAgentId, u.Goal, u.ToolCeiling,
|
|
chart.ToolCeiling(u.Id)?.Order(StringComparer.OrdinalIgnoreCase).ToList(),
|
|
new UnitDefaultsDto(u.Defaults.Model, u.Defaults.DailyCostUsd, u.Defaults.DailyTokens, u.Defaults.WhenExhausted),
|
|
chart.PathOf(u.Id), chart.Depth(u.Id)))
|
|
.ToList();
|
|
|
|
var agents = host.Instance.Agents
|
|
.OrderBy(a => a.AgentId, StringComparer.OrdinalIgnoreCase)
|
|
.Select(a => new OrgMemberDto(
|
|
a.AgentId, a.DisplayName, chart.UnitOf(a.AgentId)?.Id, chart.LedBy(a.AgentId) is not null,
|
|
chart.EffectiveLeaderOf(a.AgentId)?.AgentId, a.UnitSinceUtc,
|
|
a.Tools.Keys.Order(StringComparer.OrdinalIgnoreCase).ToList(),
|
|
a.Budget.DailyCostUsd, a.Budget.DailyTokens, a.Model))
|
|
.ToList();
|
|
|
|
var workspaces = chart.Workspaces
|
|
.OrderBy(w => w.Id, StringComparer.OrdinalIgnoreCase)
|
|
.Select(w => new WorkspaceDto(
|
|
w.Id, w.Name, w.Purpose, w.OwnerUnitId,
|
|
ReferenceEquals(chart.UnitWorkspace(w.OwnerUnitId), w),
|
|
w.Members.Select(m => new WorkspaceMemberDto(m.UnitId, m.AgentId, m.Access.ToString(), m.IncludeSubUnits)).ToList(),
|
|
w.ProtectedPaths,
|
|
host.Organisation.FileCount(w.Id),
|
|
chart.Agents
|
|
.Select(a => (a.AgentId, Access: chart.AccessTo(a.AgentId, w.Id)))
|
|
.Where(x => x.Access > WorkspaceAccess.None)
|
|
.OrderBy(x => x.AgentId, StringComparer.OrdinalIgnoreCase)
|
|
.Select(x => new WorkspaceAccessDto(x.AgentId, x.Access.ToString()))
|
|
.ToList()))
|
|
.ToList();
|
|
|
|
return new OrganisationDto(units, agents, chart.Problems(), AgentAdministration.KnownTools, workspaces);
|
|
});
|
|
|
|
org.MapPost("/units", (AppHost host, UnitInputDto input) =>
|
|
host.Organisation.CreateUnit(input.Id, input.Name, input.ParentId, input.Goal, input.Reason).ToResponse());
|
|
|
|
org.MapPatch("/units/{unitId}", (AppHost host, string unitId, UnitUpdateDto input) =>
|
|
host.Organisation.UpdateUnit(unitId, input.Name, input.Goal, input.Reason).ToResponse());
|
|
|
|
org.MapDelete("/units/{unitId}", (AppHost host, string unitId, string? reason) =>
|
|
host.Organisation.DeleteUnit(unitId, reason ?? "").ToResponse());
|
|
|
|
org.MapPut("/units/{unitId}/leader", (AppHost host, string unitId, LeaderInputDto input) =>
|
|
host.Organisation.SetLeader(unitId, input.AgentId, input.Reason).ToResponse());
|
|
|
|
org.MapPost("/units/{unitId}/move", (AppHost host, string unitId, MoveInputDto input) =>
|
|
input.Preview
|
|
? Results.Ok(ToDto(host.Organisation.PreviewMoveUnit(unitId, input.Target)))
|
|
: host.Organisation.MoveUnit(unitId, input.Target, input.Reason, input.DropExcessTools).ToResponse());
|
|
|
|
org.MapPost("/agents/{agentId}/move", (AppHost host, string agentId, MoveInputDto input) =>
|
|
input.Preview
|
|
? Results.Ok(ToDto(host.Organisation.PreviewMoveAgent(agentId, input.Target)))
|
|
: host.Organisation.MoveAgent(agentId, input.Target, input.Reason, input.DropExcessTools).ToResponse());
|
|
|
|
org.MapPut("/units/{unitId}/ceiling", (AppHost host, string unitId, CeilingInputDto input) =>
|
|
input.Preview
|
|
? Results.Ok(ToDto(host.Organisation.PreviewToolCeiling(unitId, input.Tools)))
|
|
: host.Organisation.SetToolCeiling(unitId, input.Tools, input.Reason, input.DropExcessTools).ToResponse());
|
|
|
|
org.MapPost("/units/{unitId}/settings/preview", (AppHost host, string unitId, SettingPreviewInputDto input) =>
|
|
{
|
|
if (!Enum.TryParse<UnitSetting>(input.Setting, ignoreCase: true, out var setting)
|
|
|| !Enum.TryParse<UnitSettingMode>(input.Mode, ignoreCase: true, out var mode))
|
|
return Results.BadRequest(new OutcomeDto(false, "Unknown setting or mode."));
|
|
|
|
return Results.Ok(host.Organisation.PreviewSetting(unitId, setting, input.Value, mode)
|
|
.Select(c => new SettingChangeDto(c.AgentId, c.DisplayName, c.Current, c.Proposed, c.Outlier, c.SelectedByDefault))
|
|
.ToList());
|
|
});
|
|
|
|
MapWorkspaces(org);
|
|
|
|
org.MapPost("/units/{unitId}/settings", (AppHost host, string unitId, SettingApplyDto input) =>
|
|
Enum.TryParse<UnitSetting>(input.Setting, ignoreCase: true, out var setting)
|
|
? host.Organisation.ApplySetting(unitId, setting, input.Values, input.Reason, input.DefaultValue).ToResponse()
|
|
: Results.BadRequest(new OutcomeDto(false, "Unknown setting.")));
|
|
}
|
|
|
|
// ─── workspaces (#684) ───
|
|
|
|
private static void MapWorkspaces(RouteGroupBuilder org)
|
|
{
|
|
org.MapPost("/workspaces", (AppHost host, WorkspaceInputDto input) =>
|
|
ToMembers(input.Members ?? []) is not { } members
|
|
? Results.BadRequest(new OutcomeDto(false, "Unknown access level — Read, Write or Manage."))
|
|
: host.Organisation.CreateWorkspace(input.Id, input.Name, input.Purpose, input.OwnerUnitId, members, input.Reason).ToResponse());
|
|
|
|
org.MapPatch("/workspaces/{workspaceId}", (AppHost host, string workspaceId, WorkspaceUpdateDto input) =>
|
|
host.Organisation.UpdateWorkspace(workspaceId, input.Name, input.Purpose, input.ProtectedPaths, input.Reason).ToResponse());
|
|
|
|
org.MapPut("/workspaces/{workspaceId}/members", (AppHost host, string workspaceId, WorkspaceMembersInputDto input) =>
|
|
ToMembers(input.Members ?? []) is not { } members
|
|
? Results.BadRequest(new OutcomeDto(false, "Unknown access level — Read, Write or Manage."))
|
|
: host.Organisation.SetWorkspaceMembers(workspaceId, members, input.Reason).ToResponse());
|
|
|
|
org.MapDelete("/workspaces/{workspaceId}", (AppHost host, string workspaceId, string? reason) =>
|
|
host.Organisation.DeleteWorkspace(workspaceId, reason ?? "").ToResponse());
|
|
|
|
org.MapPost("/units/{unitId}/workspace", (AppHost host, string unitId, ReasonInputDto? input) =>
|
|
host.Organisation.CreateUnitWorkspace(unitId, input?.Reason ?? "").ToResponse());
|
|
}
|
|
|
|
/// <summary><c>null</c> when a level is not one of Read, Write, Manage.</summary>
|
|
private static List<WorkspaceMemberInput>? ToMembers(IReadOnlyList<WorkspaceMemberDto> members)
|
|
{
|
|
var result = new List<WorkspaceMemberInput>();
|
|
foreach (var member in members)
|
|
{
|
|
if (!Enum.TryParse<WorkspaceAccess>(member.Access, ignoreCase: true, out var access) || !Enum.IsDefined(access))
|
|
return null;
|
|
result.Add(new WorkspaceMemberInput(member.UnitId, member.AgentId, access, member.IncludeSubUnits));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static StructurePreviewDto ToDto(StructurePreview preview)
|
|
=> new(preview.AffectedAgents, preview.ToolsLost, preview.Running, preview.Problems, preview.CanApply);
|
|
}
|