Kontrollpflicht der Leitung: Stichproben gegen das Audit (#693)

A lead looks at the runs of the agents it leads - held against the task and
against what the engine stamped, never by asking the agent.

- RunInspector (Core/Inspection): the mechanical check and first profile of
  the inspector foundation (#204). Flags failed runs, tool errors, refusals,
  unfinished calls, tasks answered with bookkeeping alone, and operations the
  agent names but never called in the run (the 2026-09-14 pattern).
- SpotChecks: picks runs per unit (rate, denser for new agents, new models and
  after a failure, daily cap), deterministic by run id; one task per lead.
- Escalation tool: spot_checks, inspect_run, check_done, propose_measure.
  Measures go to a human; nothing is carried out automatically. A security
  suspicion is its own case, not a personnel matter.
- Organisation view: settings per unit, recent checks, measures to decide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 11:40:20 +02:00
co-authored by Claude Opus 5.5
parent ab1bd67464
commit 68e723a9c0
19 changed files with 1621 additions and 6 deletions
@@ -20,7 +20,9 @@ public sealed record UnitDto(
string Path,
int Depth,
// #687: what the lead may approve instead of the human; null: nothing.
UnitApprovalsDto? Approvals = null);
UnitApprovalsDto? Approvals = null,
// #693: the lead's spot checks; null: none.
UnitInspectionDto? Inspection = 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);
@@ -31,6 +33,27 @@ public sealed record UnitApprovalsDto(IReadOnlyList<string> Operations, int PerD
public sealed record ApprovalsInputDto(IReadOnlyList<string>? Operations, int? PerDay, double? Hours, string Reason = "");
/// <summary>#693: How often the lead looks at the runs of the agents it leads.</summary>
public sealed record UnitInspectionDto(double Rate, double DenseRate, int NewDays, int PerDay);
public sealed record InspectionInputDto(bool Enabled, double? Rate, double? DenseRate, int? NewDays, int? PerDay, string Reason = "");
public sealed record FindingDto(string Kind, string Text);
/// <summary>#693: A run picked for the lead, with what the audit log showed and the lead's verdict.</summary>
public sealed record SpotCheckDto(
long Id, string Lead, string AgentId, string RunId, string Why, IReadOnlyList<FindingDto> Findings,
DateTime CreatedAt, string? Verdict, string? Note, DateTime? ReviewedAt);
/// <summary>#693: A measure a lead proposed; a human decides it.</summary>
public sealed record MeasureDto(
long Id, long? CheckId, string AgentId, string ProposedBy, string Kind, string Reasoning,
DateTime ProposedAt, string Status, string? DecidedBy, DateTime? DecidedAt, string? Note);
public sealed record SpotCheckOverviewDto(IReadOnlyList<SpotCheckDto> Checks, IReadOnlyList<MeasureDto> Measures);
public sealed record MeasureDecisionDto(bool Accept, string Note = "");
public sealed record UnitDefaultsDto(string? Model, decimal? DailyCostUsd, long? DailyTokens, string? WhenExhausted);
/// <summary>An agent as the organisation page shows it: where it is, whom it answers to.</summary>
@@ -28,7 +28,8 @@ internal static class OrganisationEndpoints
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),
u.Approvals is { } ap ? new UnitApprovalsDto(ap.Operations, ap.PerDay, ap.Hours) : null))
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))
.ToList();
var agents = host.Instance.Agents
@@ -103,6 +104,32 @@ 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})."));
});
// #693: The lead's spot checks — how often, what came of them, and the measures it proposed.
org.MapPut("/units/{unitId}/inspection", (AppHost host, string unitId, InspectionInputDto input) =>
host.Organisation.SetInspection(unitId, input.Enabled, input.Rate, input.DenseRate, input.NewDays, input.PerDay, input.Reason).ToResponse());
org.MapGet("/units/{unitId}/spot-checks", async (AppHost host, string unitId, CancellationToken ct) =>
{
if (host.SpotChecks is not { } checks)
return Results.Ok(new SpotCheckOverviewDto([], []));
var list = await checks.ForUnitAsync(unitId, 30, ct);
var measures = await checks.MeasuresForUnitAsync(unitId, 30, ct);
return Results.Ok(new SpotCheckOverviewDto(
list.Select(c => new SpotCheckDto(c.Id, c.Lead, c.AgentId, c.RunId, c.Why,
c.Findings.Select(f => new FindingDto(f.Kind.ToString(), f.Text)).ToList(),
c.CreatedAt, c.Verdict, c.Note, c.ReviewedAt)).ToList(),
measures.Select(m => new MeasureDto(m.Id, m.CheckId, m.AgentId, m.ProposedBy,
Core.Inspection.SpotChecks.Label(m.Kind), m.Reasoning, m.ProposedAt, m.Status, m.DecidedBy, m.DecidedAt, m.Note)).ToList()));
});
org.MapPost("/measures/{measureId:long}/decide", async (AppHost host, long measureId, MeasureDecisionDto input, CancellationToken ct) =>
{
if (host.SpotChecks is not { } checks)
return Results.BadRequest(new OutcomeDto(false, "Spot checks are not available in this instance."));
var decided = await checks.DecideMeasureAsync(measureId, input.Accept, input.Note, "human", ct);
return decided.Ok ? Results.Ok(new OutcomeDto(true, decided.Message)) : Results.BadRequest(new OutcomeDto(false, decided.Message));
});
// #687: What the lead may approve instead of the human.
org.MapPut("/units/{unitId}/approvals", (AppHost host, string unitId, ApprovalsInputDto input) =>
host.Organisation.SetApprovals(unitId, input.Operations, input.PerDay, input.Hours, input.Reason).ToResponse());