Freigaben buendeln statt Weckflut (#643), Freigabemodell ueber Werkzeugrechte (#641)

#643: Entscheidungen wecken den Agenten nur noch bei Ablehnung oder gescheiterter
Ausfuehrung, gesammelt in einer Aufgabe je Agent (5 min Ruhefenster), frisch per
@new:agent statt im fortgefuehrten Chatverlauf. Eine einmalige Aufgabe, deren Lauf
wegen Budget oder Herunterfahren gar nicht stattfand, bleibt faellig (Wiedervorlage
nach 30 min) statt mit gesetztem Marker liegenzubleiben.

#641: Rechte stehen in der Werkzeugkonfiguration des Agenten (erlaubt/verboten).
Erlaubtes laeuft, Nicht-Erlaubtes wird einzeln vorgelegt statt verweigert, Verbotenes
gar nicht erst. "Gleichartiges kuenftig erlauben" erteilt eine widerrufbare
Dauerfreigabe je Agent/Werkzeug/Operation; nach drei gleichartigen Freigaben schlaegt
die Oberflaeche sie vor. Aenderungen an Agenten bleiben ausnahmslos freigabepflichtig.
Database: Operation ist das SQL-Schluesselwort; die eigene Zugriffspruefung greift nur
noch ohne Freigabestelle (AgentToolContext.FreigabeGeprueft). WebUI: Rechte-Tabelle je
Werkzeug, Dauerfreigaben mit Widerruf.

Version 0.1.11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-19 14:37:44 +02:00
co-authored by Claude Opus 5
parent f74e49ba35
commit 2f0361278b
36 changed files with 1687 additions and 137 deletions
+49 -3
View File
@@ -144,6 +144,27 @@ internal static class ApiEndpoints
return Results.Ok(Mapping.ToDto(toolName, settings));
});
// #641: Rechte je Werkzeug - was laeuft, was vorgelegt wird, was verboten ist.
agents.MapGet("/{agentId}/tools/{toolName}/rights", async (
AppHost host, string agentId, string toolName, CancellationToken ct) =>
{
if (host.AgentAdmin.Find(agentId) is not { } agent)
return Results.NotFound(new OutcomeDto(false, $"Kein Agent mit der ID '{agentId}'."));
if (!agent.Tools.TryGetValue(toolName, out var settings))
return Results.NotFound(new OutcomeDto(false, $"Werkzeug '{toolName}' ist nicht zugewiesen."));
if (host.Tools.Get(toolName) is not { } tool)
return Results.NotFound(new OutcomeDto(false, $"Werkzeug '{toolName}' ist nicht installiert."));
var dauer = host.Staging is { } service
? (await service.ListDauerfreigabenAsync(false, ct)).Where(d => d.AgentId == agentId && d.Tool == toolName).ToList()
: [];
return Results.Ok(WerkzeugrechteAnsicht.Erstelle(tool, settings.AsReadOnly(), dauer));
});
agents.MapPut("/{agentId}/tools/{toolName}/rights", (
AppHost host, string agentId, string toolName, ToolRightsInputDto input) =>
host.AgentAdmin.SetToolRights(agentId, toolName, input.Allowed, input.Denied ?? []).ToResponse());
agents.MapPost("/{agentId}/run", async (
AppHost host, string agentId, CancellationToken ct) =>
(await host.Runs.RunAgentAsync(agentId, message: null, ct)).ToResponse());
@@ -275,19 +296,44 @@ internal static class ApiEndpoints
return Unavailable("Keine Freigabestelle in dieser Instanz.");
var pending = await service.ListPendingAsync(ct);
return Results.Ok(pending.Select(c => c.ToDto()).ToList());
var liste = new List<StagedCallDto>(pending.Count);
foreach (var call in pending)
liste.Add(call.ToDto(await service.BisherFreigegebenAsync(call, ct), service.KenntDauerfreigaben));
return Results.Ok(liste);
});
staging.MapPost("/{id:long}/approve", async (AppHost host, long id, CancellationToken ct) =>
// #641: similar=true - "Gleichartiges kuenftig erlauben".
staging.MapPost("/{id:long}/approve", async (
AppHost host, long id, CancellationToken ct, bool similar = false) =>
{
if (host.Staging is not { } service)
return Unavailable("Keine Freigabestelle in dieser Instanz.");
var result = await service.ApproveAsync(id, decidedBy: "webui", ct);
var result = await service.ApproveAsync(id, decidedBy: "webui", gleichartigErlauben: similar, ct);
return Results.Ok(new OutcomeDto(
result.Kind is Core.Staging.StagingResultKind.Executed, result.Message));
});
staging.MapGet("/standing", async (AppHost host, CancellationToken ct, bool revoked = false) =>
{
if (host.Staging is not { } service)
return Unavailable("Keine Freigabestelle in dieser Instanz.");
var liste = await service.ListDauerfreigabenAsync(revoked, ct);
return Results.Ok(liste.Select(d => d.ToDto()).ToList());
});
staging.MapPost("/standing/{id:long}/revoke", async (AppHost host, long id, CancellationToken ct) =>
{
if (host.Staging is not { } service)
return Unavailable("Keine Freigabestelle in dieser Instanz.");
var ok = await service.WiderrufenAsync(id, "webui", ct);
return Results.Ok(new OutcomeDto(ok, ok
? $"Dauerfreigabe #{id} widerrufen. Die Operation wird wieder einzeln vorgelegt."
: $"Dauerfreigabe #{id} gibt es nicht oder sie ist schon widerrufen."));
});
staging.MapPost("/{id:long}/reject", async (
AppHost host, long id, RejectDto input, CancellationToken ct) =>
{