Files
git.stella-ops.org/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Infrastructure/Execution/PackRunApprovalDecisionService.cs
StellaOps Bot 18d87c64c5 feat: add PolicyPackSelectorComponent with tests and integration
- 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.
2025-12-05 21:24:34 +02:00

147 lines
5.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}