#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:
@@ -167,6 +167,9 @@ public sealed record ForecastTaskDto(
|
||||
public sealed record TaskWhenDto(string Kind, string Value, string TimeZone);
|
||||
|
||||
/// <summary>Ein eingefrorener Aufruf, der auf Freigabe wartet.</summary>
|
||||
/// <param name="PreviousApprovals">#641: Wie oft dieselbe Operation dieses Agenten zuletzt schon freigegeben wurde.</param>
|
||||
/// <param name="CanAllowSimilar">#641: Ob "Gleichartiges kuenftig erlauben" angeboten werden darf.</param>
|
||||
/// <param name="SuggestAllowSimilar">#641: Ob die Oberflaeche die Dauerfreigabe vorschlagen soll.</param>
|
||||
public sealed record StagedCallDto(
|
||||
long Id,
|
||||
string RunId,
|
||||
@@ -176,7 +179,44 @@ public sealed record StagedCallDto(
|
||||
string Proposal,
|
||||
string Status,
|
||||
string ArgumentsJson,
|
||||
DateTime CreatedAt);
|
||||
DateTime CreatedAt,
|
||||
int PreviousApprovals = 0,
|
||||
bool CanAllowSimilar = false,
|
||||
bool SuggestAllowSimilar = false);
|
||||
|
||||
/// <summary>#641: Eine Dauerfreigabe - "Gleichartiges kuenftig erlauben".</summary>
|
||||
public sealed record StandingApprovalDto(
|
||||
long Id,
|
||||
string AgentId,
|
||||
string Tool,
|
||||
string Operation,
|
||||
string GrantedBy,
|
||||
DateTime GrantedAt,
|
||||
long? SourceId,
|
||||
DateTime? RevokedAt,
|
||||
string? RevokedBy);
|
||||
|
||||
/// <summary>
|
||||
/// #641: Die Rechte eines Agenten an einem Werkzeug, je Operation.
|
||||
/// <c>Setting</c>: "erlaubt", "verboten" oder "freigabe" (einzeln vorlegen) - so wie es in der
|
||||
/// Konfiguration steht. <c>Effective</c>: was tatsaechlich geschieht ("auto", "approve",
|
||||
/// "deny") - einschliesslich Dauerfreigaben und Werkzeugvorgabe.
|
||||
/// </summary>
|
||||
public sealed record ToolRightsDto(
|
||||
string Tool,
|
||||
bool Configured,
|
||||
IReadOnlyList<ToolOperationDto> Operations);
|
||||
|
||||
public sealed record ToolOperationDto(
|
||||
string Operation,
|
||||
string Setting,
|
||||
string Effective,
|
||||
string Reason,
|
||||
bool AlwaysApproval,
|
||||
long? StandingApprovalId);
|
||||
|
||||
/// <summary>#641: Neue Rechte. <c>Allowed</c> <c>null</c> = Liste entfernen, Vorgabe des Werkzeugs gilt.</summary>
|
||||
public sealed record ToolRightsInputDto(IReadOnlyList<string>? Allowed, IReadOnlyList<string>? Denied);
|
||||
|
||||
/// <summary>Warum ein Vorschlag abgelehnt wurde.</summary>
|
||||
public sealed record RejectDto(string Reason);
|
||||
|
||||
@@ -139,8 +139,10 @@ internal static class Mapping
|
||||
/// Zusammenfassung davon. Sie sind zugleich Fremdtext aus einem Modell; die Anzeige
|
||||
/// setzt sie als Text ein, nie als HTML.</para>
|
||||
/// </summary>
|
||||
public static StagedCallDto ToDto(this StagedCall call)
|
||||
=> new(
|
||||
public static StagedCallDto ToDto(this StagedCall call, int bisherFreigegeben = 0, bool dauerfreigabenAktiv = false)
|
||||
{
|
||||
var erlaubbar = dauerfreigabenAktiv && StagingService.IstDauerhaftErlaubbar(call);
|
||||
return new(
|
||||
call.Id,
|
||||
call.RunId,
|
||||
call.AgentId,
|
||||
@@ -149,7 +151,14 @@ internal static class Mapping
|
||||
call.Proposal,
|
||||
call.Status.ToString(),
|
||||
call.ArgumentsJson,
|
||||
call.CreatedAt);
|
||||
call.CreatedAt,
|
||||
bisherFreigegeben,
|
||||
erlaubbar,
|
||||
erlaubbar && bisherFreigegeben >= StagingService.VorschlagAb);
|
||||
}
|
||||
|
||||
public static StandingApprovalDto ToDto(this Dauerfreigabe d)
|
||||
=> new(d.Id, d.AgentId, d.Tool, d.Operation, d.ErteiltVon, d.ErteiltAm, d.AnlassId, d.WiderrufenAm, d.WiderrufenVon);
|
||||
|
||||
public static UsageDto ToDto(this RunUsage usage)
|
||||
=> new(
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using ClawdDotNet.Core.Staging;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
|
||||
namespace ClawdDotNet.Web.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// #641: Was ein Agent mit einem Werkzeug darf - je Operation, so wie es in der
|
||||
/// Konfiguration steht, und so, wie es tatsaechlich eingestuft wird. Beides zu zeigen ist
|
||||
/// Absicht: Eine Dauerfreigabe oder die Vorgabe des Werkzeugs kann eine Operation laufen
|
||||
/// lassen, die in der Konfiguration nicht als erlaubt steht.
|
||||
/// </summary>
|
||||
public static class WerkzeugrechteAnsicht
|
||||
{
|
||||
public static ToolRightsDto Erstelle(
|
||||
IAgentTool tool, IReadOnlyDictionary<string, object?> settings, IReadOnlyList<Dauerfreigabe> dauerfreigaben)
|
||||
{
|
||||
var rechte = Werkzeugrechte.Aus(settings);
|
||||
var policy = new StagingPolicy();
|
||||
|
||||
// Auch Operationen zeigen, die in der Konfiguration stehen, das Werkzeug aber nicht
|
||||
// (mehr) aufzaehlt - sonst waere ein Tippfehler unsichtbar.
|
||||
var operationen = tool.Operationen
|
||||
.Select(Werkzeugrechte.Normalisiere)
|
||||
.Concat(rechte.Erlaubt?.Where(o => o != Werkzeugrechte.Alles) ?? [])
|
||||
.Concat(rechte.Verboten)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var zeilen = operationen.Select(op =>
|
||||
{
|
||||
var dauer = dauerfreigaben.FirstOrDefault(d => d.Operation == op);
|
||||
var einstufung = FreigabeRegeln.Einstufen(
|
||||
tool.Name, op, rechte, tool.Vorgabe(op, settings), policy, dauerfreigabe: dauer is not null);
|
||||
|
||||
var einstellung = rechte.IstVerboten(op) ? "verboten"
|
||||
: rechte.IstErlaubt(op) ? "erlaubt"
|
||||
: "freigabe";
|
||||
|
||||
return new ToolOperationDto(
|
||||
op,
|
||||
einstellung,
|
||||
einstufung.Entscheidung switch
|
||||
{
|
||||
StagingDecision.Auto => "auto",
|
||||
StagingDecision.Deny => "deny",
|
||||
_ => "approve"
|
||||
},
|
||||
einstufung.Grund,
|
||||
FreigabeRegeln.IstImmerFreigabe(tool.Name, op),
|
||||
dauer?.Id);
|
||||
}).ToList();
|
||||
|
||||
return new ToolRightsDto(tool.Name, rechte.IstKonfiguriert, zeilen);
|
||||
}
|
||||
}
|
||||
@@ -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) =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user