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,7 +49,7 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
_clock = clock ?? TimeProvider.System;
}
public async Task<bool> DispatchAsync(TaskItem task, CancellationToken ct)
public async Task<TaskLauf> DispatchAsync(TaskItem task, CancellationToken ct)
{
// Poll-Tasks (tool_job) ticken ein Tool statt einen Agenten direkt zu starten.
if (task.Type == TaskItemType.ToolJob)
@@ -70,7 +70,7 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
_logger.LogWarning(
"Aufgabe {TaskId}: Assignee '{Assignee}' nicht auflösbar (frischer Lauf braucht einen eindeutigen Agenten)",
task.Id, task.Assignee);
return false;
return TaskLauf.Gescheitert;
}
result = await _engine.RunAsync(
config, message, _instanceId, ct, source: ChatSource.Task, taskId: task.Id);
@@ -84,7 +84,7 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
{
_logger.LogWarning(
"Aufgabe {TaskId}: Agent '{AgentId}' nicht gefunden", task.Id, agentId);
return false;
return TaskLauf.Gescheitert;
}
result = await _engine.ChatAsync(
config, message, _instanceId, ct, source: ChatSource.Task, taskId: task.Id);
@@ -92,10 +92,15 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
}
default:
// @human wird vom Scanner gar nicht erst angestoßen.
return false;
return TaskLauf.Gescheitert;
}
return result.Status == AgentRunStatus.Completed;
return result.Status switch
{
AgentRunStatus.Completed => TaskLauf.Erledigt,
AgentRunStatus.BudgetExceeded or AgentRunStatus.ShuttingDown => TaskLauf.NichtGelaufen,
_ => TaskLauf.Gescheitert
};
}
/// <summary>
@@ -103,14 +108,14 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
/// wenn der Tick etwas meldet. Der Tick selbst gilt als erfolgreich, auch wenn nichts
/// zu tun war — ein Poll ohne Fund ist kein Fehler.
/// </summary>
private async Task<bool> DispatchToolJobAsync(TaskItem task, CancellationToken ct)
private async Task<TaskLauf> DispatchToolJobAsync(TaskItem task, CancellationToken ct)
{
if (_toolRegistry.Get(task.ToolName) is not IToolJobProvider provider)
{
_logger.LogWarning(
"Tool-Job {TaskId}: Tool '{Tool}' ist kein IToolJobProvider oder nicht registriert",
task.Id, task.ToolName);
return false;
return TaskLauf.Gescheitert;
}
// Zielagent (zum Wecken) und dessen Tool-Konfiguration/Workspace.
@@ -124,14 +129,14 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
task.JobTypeId, toolConfig, _stateStore, _logger, ct, agentId, config?.WorkspacePath);
if (!result.ShouldWakeAgent || string.IsNullOrWhiteSpace(result.WakeMessage))
return true; // Poll lief, nichts zu wecken
return TaskLauf.Erledigt; // Poll lief, nichts zu wecken
if (config is null)
{
_logger.LogWarning(
"Tool-Job {TaskId} wollte Agent '{AgentId}' wecken, der aber nicht gefunden wurde",
task.Id, agentId);
return true;
return TaskLauf.Erledigt;
}
// Der Provider entscheidet je Tick, ob mit Kontext (ChatAsync) oder zustandslos.
@@ -140,7 +145,7 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
else
await _engine.RunAsync(config, result.WakeMessage, _instanceId, ct, source: ChatSource.Job, taskId: task.Id);
return true;
return TaskLauf.Erledigt;
}
private static AgentConfig? ResolveFreshAgent(IReadOnlyList<AgentConfig> agents, string agentId)