- Implemented PolicyPackSelectorComponent for selecting policy packs. - Added unit tests for component behavior, including API success and error handling. - Introduced monaco-workers type declarations for editor workers. - Created acceptance tests for guardrails with stubs for AT1–AT10. - Established SCA Failure Catalogue Fixtures for regression testing. - Developed plugin determinism harness with stubs for PL1–PL10. - Added scripts for evidence upload and verification processes.
147 lines
5.8 KiB
C#
147 lines
5.8 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using StellaOps.TaskRunner.Core.Execution;
|
||
using StellaOps.TaskRunner.Core.Planning;
|
||
using System.Text.RegularExpressions;
|
||
|
||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||
|
||
public sealed class PackRunApprovalDecisionService
|
||
{
|
||
private readonly IPackRunApprovalStore _approvalStore;
|
||
private readonly IPackRunStateStore _stateStore;
|
||
private readonly IPackRunJobScheduler _scheduler;
|
||
private readonly ILogger<PackRunApprovalDecisionService> _logger;
|
||
|
||
public PackRunApprovalDecisionService(
|
||
IPackRunApprovalStore approvalStore,
|
||
IPackRunStateStore stateStore,
|
||
IPackRunJobScheduler scheduler,
|
||
ILogger<PackRunApprovalDecisionService> logger)
|
||
{
|
||
_approvalStore = approvalStore ?? throw new ArgumentNullException(nameof(approvalStore));
|
||
_stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
|
||
_scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
}
|
||
|
||
public async Task<PackRunApprovalDecisionResult> ApplyAsync(
|
||
PackRunApprovalDecisionRequest request,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(request);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(request.RunId);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(request.ApprovalId);
|
||
|
||
var runId = request.RunId.Trim();
|
||
var approvalId = request.ApprovalId.Trim();
|
||
|
||
if (!IsSha256Digest(request.PlanHash))
|
||
{
|
||
_logger.LogWarning(
|
||
"Approval decision for run {RunId} rejected – plan hash format invalid (expected sha256:<64-hex>).",
|
||
runId);
|
||
return PackRunApprovalDecisionResult.PlanHashMismatch;
|
||
}
|
||
|
||
var state = await _stateStore.GetAsync(runId, cancellationToken).ConfigureAwait(false);
|
||
if (state is null)
|
||
{
|
||
_logger.LogWarning("Approval decision for run {RunId} rejected – run state not found.", runId);
|
||
return PackRunApprovalDecisionResult.NotFound;
|
||
}
|
||
|
||
var approvals = await _approvalStore.GetAsync(runId, cancellationToken).ConfigureAwait(false);
|
||
if (approvals.Count == 0)
|
||
{
|
||
_logger.LogWarning("Approval decision for run {RunId} rejected – approval state not found.", runId);
|
||
return PackRunApprovalDecisionResult.NotFound;
|
||
}
|
||
|
||
if (!string.Equals(state.PlanHash, request.PlanHash, StringComparison.Ordinal))
|
||
{
|
||
_logger.LogWarning(
|
||
"Approval decision for run {RunId} rejected – plan hash mismatch (expected {Expected}, got {Actual}).",
|
||
runId,
|
||
state.PlanHash,
|
||
request.PlanHash);
|
||
return PackRunApprovalDecisionResult.PlanHashMismatch;
|
||
}
|
||
|
||
var requestedAt = state.RequestedAt != default ? state.RequestedAt : state.CreatedAt;
|
||
var coordinator = PackRunApprovalCoordinator.Restore(state.Plan, approvals, requestedAt);
|
||
|
||
ApprovalActionResult actionResult;
|
||
var now = DateTimeOffset.UtcNow;
|
||
|
||
switch (request.Decision)
|
||
{
|
||
case PackRunApprovalDecisionType.Approved:
|
||
actionResult = coordinator.Approve(approvalId, request.ActorId ?? "system", now, request.Summary);
|
||
break;
|
||
|
||
case PackRunApprovalDecisionType.Rejected:
|
||
actionResult = coordinator.Reject(approvalId, request.ActorId ?? "system", now, request.Summary);
|
||
break;
|
||
|
||
case PackRunApprovalDecisionType.Expired:
|
||
actionResult = coordinator.Expire(approvalId, now, request.Summary);
|
||
break;
|
||
|
||
default:
|
||
throw new ArgumentOutOfRangeException(nameof(request.Decision), request.Decision, "Unsupported approval decision.");
|
||
}
|
||
|
||
await _approvalStore.UpdateAsync(runId, actionResult.State, cancellationToken).ConfigureAwait(false);
|
||
|
||
_logger.LogInformation(
|
||
"Applied approval decision {Decision} for run {RunId} (approval {ApprovalId}, actor={ActorId}).",
|
||
request.Decision,
|
||
runId,
|
||
approvalId,
|
||
request.ActorId ?? "(system)");
|
||
|
||
if (actionResult.ShouldResumeRun && request.Decision == PackRunApprovalDecisionType.Approved)
|
||
{
|
||
var context = new PackRunExecutionContext(runId, state.Plan, requestedAt);
|
||
await _scheduler.ScheduleAsync(context, cancellationToken).ConfigureAwait(false);
|
||
_logger.LogInformation("Scheduled run {RunId} for resume after approvals completed.", runId);
|
||
return PackRunApprovalDecisionResult.Resumed;
|
||
}
|
||
|
||
return PackRunApprovalDecisionResult.Applied;
|
||
}
|
||
|
||
private static bool IsSha256Digest(string value)
|
||
=> !string.IsNullOrWhiteSpace(value)
|
||
&& Sha256Pattern.IsMatch(value);
|
||
|
||
private static readonly Regex Sha256Pattern = new(
|
||
"^sha256:[0-9a-f]{64}$",
|
||
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||
}
|
||
|
||
public sealed record PackRunApprovalDecisionRequest(
|
||
string RunId,
|
||
string ApprovalId,
|
||
string PlanHash,
|
||
PackRunApprovalDecisionType Decision,
|
||
string? ActorId,
|
||
string? Summary);
|
||
|
||
public enum PackRunApprovalDecisionType
|
||
{
|
||
Approved,
|
||
Rejected,
|
||
Expired
|
||
}
|
||
|
||
public sealed record PackRunApprovalDecisionResult(string Status)
|
||
{
|
||
public static PackRunApprovalDecisionResult NotFound { get; } = new("not_found");
|
||
public static PackRunApprovalDecisionResult PlanHashMismatch { get; } = new("plan_hash_mismatch");
|
||
public static PackRunApprovalDecisionResult Applied { get; } = new("applied");
|
||
public static PackRunApprovalDecisionResult Resumed { get; } = new("resumed");
|
||
|
||
public bool ShouldResume => ReferenceEquals(this, Resumed);
|
||
}
|