synergy moats product advisory implementations
This commit is contained in:
339
src/Api/StellaOps.Api/Controllers/BlockExplanationController.cs
Normal file
339
src/Api/StellaOps.Api/Controllers/BlockExplanationController.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BlockExplanationController.cs
|
||||
// Sprint: SPRINT_20260117_026_CLI_why_blocked_command
|
||||
// Task: WHY-001 - Backend API for Block Explanation
|
||||
// Description: API endpoint to retrieve block explanation for an artifact
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace StellaOps.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for artifact block explanation endpoints.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("v1/artifacts")]
|
||||
[Authorize]
|
||||
public class BlockExplanationController : ControllerBase
|
||||
{
|
||||
private readonly IBlockExplanationService _explanationService;
|
||||
private readonly ILogger<BlockExplanationController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockExplanationController"/> class.
|
||||
/// </summary>
|
||||
public BlockExplanationController(
|
||||
IBlockExplanationService explanationService,
|
||||
ILogger<BlockExplanationController> logger)
|
||||
{
|
||||
_explanationService = explanationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the block explanation for an artifact.
|
||||
/// </summary>
|
||||
/// <param name="digest">The artifact digest (e.g., sha256:abc123...).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The block explanation or NotFound if artifact is not blocked.</returns>
|
||||
/// <response code="200">Returns the block explanation.</response>
|
||||
/// <response code="404">Artifact not found or not blocked.</response>
|
||||
[HttpGet("{digest}/block-explanation")]
|
||||
[ProducesResponseType(typeof(BlockExplanationResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetBlockExplanation(
|
||||
[FromRoute] string digest,
|
||||
CancellationToken ct)
|
||||
{
|
||||
_logger.LogDebug("Getting block explanation for artifact {Digest}", digest);
|
||||
|
||||
var explanation = await _explanationService.GetBlockExplanationAsync(digest, ct);
|
||||
|
||||
if (explanation == null)
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Artifact not blocked",
|
||||
Detail = $"Artifact {digest} is not blocked or does not exist",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(explanation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the block explanation with full evidence details.
|
||||
/// </summary>
|
||||
/// <param name="digest">The artifact digest.</param>
|
||||
/// <param name="includeTrace">Whether to include policy evaluation trace.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The detailed block explanation.</returns>
|
||||
[HttpGet("{digest}/block-explanation/detailed")]
|
||||
[ProducesResponseType(typeof(DetailedBlockExplanationResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetDetailedBlockExplanation(
|
||||
[FromRoute] string digest,
|
||||
[FromQuery] bool includeTrace = false,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Getting detailed block explanation for artifact {Digest}", digest);
|
||||
|
||||
var explanation = await _explanationService.GetDetailedBlockExplanationAsync(
|
||||
digest, includeTrace, ct);
|
||||
|
||||
if (explanation == null)
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Artifact not blocked",
|
||||
Detail = $"Artifact {digest} is not blocked or does not exist",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(explanation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for block explanation.
|
||||
/// </summary>
|
||||
public sealed record BlockExplanationResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The artifact digest.
|
||||
/// </summary>
|
||||
public required string ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the artifact is blocked.
|
||||
/// </summary>
|
||||
public bool IsBlocked { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The gate that blocked the artifact.
|
||||
/// </summary>
|
||||
public required GateDecision GateDecision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence artifact references.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<EvidenceReference> EvidenceReferences { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Replay token for deterministic verification.
|
||||
/// </summary>
|
||||
public required string ReplayToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the block decision was made.
|
||||
/// </summary>
|
||||
public DateTimeOffset BlockedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verdict ID for reference.
|
||||
/// </summary>
|
||||
public string? VerdictId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed block explanation with full evidence.
|
||||
/// </summary>
|
||||
public sealed record DetailedBlockExplanationResponse : BlockExplanationResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Full policy evaluation trace.
|
||||
/// </summary>
|
||||
public PolicyEvaluationTrace? EvaluationTrace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Full evidence details.
|
||||
/// </summary>
|
||||
public IReadOnlyList<EvidenceDetail>? EvidenceDetails { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate decision details.
|
||||
/// </summary>
|
||||
public sealed record GateDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate identifier.
|
||||
/// </summary>
|
||||
public required string GateId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate display name.
|
||||
/// </summary>
|
||||
public required string GateName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Decision status.
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable reason for the decision.
|
||||
/// </summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Suggested remediation action.
|
||||
/// </summary>
|
||||
public string? Suggestion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version used.
|
||||
/// </summary>
|
||||
public string? PolicyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Threshold that was not met (if applicable).
|
||||
/// </summary>
|
||||
public ThresholdInfo? Threshold { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Threshold information for gate decisions.
|
||||
/// </summary>
|
||||
public sealed record ThresholdInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Threshold name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Required threshold value.
|
||||
/// </summary>
|
||||
public required double Required { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual value observed.
|
||||
/// </summary>
|
||||
public required double Actual { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Comparison operator.
|
||||
/// </summary>
|
||||
public required string Operator { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to an evidence artifact.
|
||||
/// </summary>
|
||||
public sealed record EvidenceReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Evidence type.
|
||||
/// </summary>
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed ID.
|
||||
/// </summary>
|
||||
public required string ContentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence source.
|
||||
/// </summary>
|
||||
public required string Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when evidence was collected.
|
||||
/// </summary>
|
||||
public DateTimeOffset CollectedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CLI command to retrieve this evidence.
|
||||
/// </summary>
|
||||
public string? RetrievalCommand { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full evidence details.
|
||||
/// </summary>
|
||||
public sealed record EvidenceDetail : EvidenceReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Evidence content (JSON).
|
||||
/// </summary>
|
||||
public object? Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content size in bytes.
|
||||
/// </summary>
|
||||
public long? SizeBytes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy evaluation trace.
|
||||
/// </summary>
|
||||
public sealed record PolicyEvaluationTrace
|
||||
{
|
||||
/// <summary>
|
||||
/// Trace ID.
|
||||
/// </summary>
|
||||
public required string TraceId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation steps.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<EvaluationStep> Steps { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total evaluation duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single evaluation step.
|
||||
/// </summary>
|
||||
public sealed record EvaluationStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Step index.
|
||||
/// </summary>
|
||||
public int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate ID evaluated.
|
||||
/// </summary>
|
||||
public required string GateId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Input values.
|
||||
/// </summary>
|
||||
public object? Inputs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Output decision.
|
||||
/// </summary>
|
||||
public required string Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Step duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for block explanations.
|
||||
/// </summary>
|
||||
public interface IBlockExplanationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the block explanation for an artifact.
|
||||
/// </summary>
|
||||
Task<BlockExplanationResponse?> GetBlockExplanationAsync(string digest, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed block explanation with full evidence.
|
||||
/// </summary>
|
||||
Task<DetailedBlockExplanationResponse?> GetDetailedBlockExplanationAsync(
|
||||
string digest, bool includeTrace, CancellationToken ct);
|
||||
}
|
||||
Reference in New Issue
Block a user