Werkzeugkasten: Zugangsdaten je Einheit (#692)

A unit holds credentials for its members, by tool and setting - needed once,
not once per agent, and other units never get them.

- UnitConfig.credentials, protected by ConfigSecrets like every secret.
- OrgChart.ToolSettingsFor: an agent's own settings, completed by the nearest
  unit up that holds the value; own non-empty values win; no tool is granted.
  Used for runs, tool jobs and preflight probes.
- OrgAdministration.SetUnitCredential, PUT /organisation/units/{id}/credentials;
  the view lists held settings, never values.
- Templates never carry unit credentials; they are named instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 12:50:27 +02:00
co-authored by Claude Opus 5.5
parent 661807b2d0
commit 9a265888f8
17 changed files with 306 additions and 13 deletions
@@ -24,7 +24,9 @@ public sealed record UnitDto(
// #693: the lead's spot checks; null: none.
UnitInspectionDto? Inspection = null,
// #702: what the unit puts to a blind vote; null: nothing.
UnitVotesDto? Votes = null);
UnitVotesDto? Votes = null,
// #692: the credentials the unit holds, by tool — names only, never values.
IReadOnlyDictionary<string, IReadOnlyList<string>>? CredentialKeys = null);
/// <summary>#698: One version of a unit's doctrine.</summary>
public sealed record DoctrineDto(int Version, string Text, string SetBy, DateTime SetAt, string? RunId, string Reason);
@@ -42,6 +44,9 @@ public sealed record InspectionInputDto(bool Enabled, double? Rate, double? Dens
public sealed record FindingDto(string Kind, string Text);
/// <summary>#692: One credential a unit holds; an empty value removes it.</summary>
public sealed record UnitCredentialInputDto(string Tool, string Key, string? Value, string Reason = "");
/// <summary>#691: A template to preview or import, with what the human changes on the way in.</summary>
public sealed record TemplateImportDto(
System.Text.Json.JsonElement Template,
@@ -30,7 +30,10 @@ internal static class OrganisationEndpoints
chart.PathOf(u.Id), chart.Depth(u.Id),
u.Approvals is { } ap ? new UnitApprovalsDto(ap.Operations, ap.PerDay, ap.Hours) : null,
u.Inspection is { } ins ? new UnitInspectionDto(ins.Rate, ins.DenseRate, ins.NewDays, ins.PerDay) : null,
u.Votes is { } v ? new UnitVotesDto(v.Operations, v.Voters, v.ApproveAt, v.RejectAt, v.EffectiveQuorum, v.Hours, v.PerDay) : null))
u.Votes is { } v ? new UnitVotesDto(v.Operations, v.Voters, v.ApproveAt, v.RejectAt, v.EffectiveQuorum, v.Hours, v.PerDay) : null,
u.Credentials is { Count: > 0 } held
? held.ToDictionary(h => h.Key, h => (IReadOnlyList<string>)h.Value.Keys.Order(StringComparer.OrdinalIgnoreCase).ToList())
: null))
.ToList();
var agents = host.Instance.Agents
@@ -105,6 +108,10 @@ internal static class OrganisationEndpoints
return Results.Ok(new OutcomeDto(true, set.IsEmpty ? $"Doctrine of '{unit.Name}' removed." : $"Doctrine of '{unit.Name}' set (version {set.Version})."));
});
// #692: Credentials a unit holds for its members — written here, never read back.
org.MapPut("/units/{unitId}/credentials", (AppHost host, string unitId, UnitCredentialInputDto input) =>
host.Organisation.SetUnitCredential(unitId, input.Tool, input.Key, input.Value, input.Reason).ToResponse());
// #691: The organisation as a template file — exported without credentials, imported only after a preview and a confirmation.
org.MapGet("/template", (AppHost host, string? name, string? description) =>
Results.Text(System.Text.Json.JsonSerializer.Serialize(