sprints work
This commit is contained in:
@@ -0,0 +1,904 @@
|
||||
// <copyright file="RunEndpoints.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.AdvisoryAI.Runs;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for AI investigation runs.
|
||||
/// Sprint: SPRINT_20260109_011_003_BE Task: RUN-006
|
||||
/// </summary>
|
||||
public static class RunEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps run endpoints to the route builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The endpoint route builder.</param>
|
||||
/// <returns>The route group builder.</returns>
|
||||
public static RouteGroupBuilder MapRunEndpoints(this IEndpointRouteBuilder builder)
|
||||
{
|
||||
var group = builder.MapGroup("/api/v1/runs")
|
||||
.WithTags("Runs");
|
||||
|
||||
group.MapPost("/", CreateRunAsync)
|
||||
.WithName("CreateRun")
|
||||
.WithSummary("Creates a new AI investigation run")
|
||||
.Produces<RunDto>(StatusCodes.Status201Created)
|
||||
.ProducesValidationProblem();
|
||||
|
||||
group.MapGet("/{runId}", GetRunAsync)
|
||||
.WithName("GetRun")
|
||||
.WithSummary("Gets a run by ID")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapGet("/", QueryRunsAsync)
|
||||
.WithName("QueryRuns")
|
||||
.WithSummary("Queries runs with filters")
|
||||
.Produces<RunQueryResultDto>();
|
||||
|
||||
group.MapGet("/{runId}/timeline", GetTimelineAsync)
|
||||
.WithName("GetRunTimeline")
|
||||
.WithSummary("Gets the event timeline for a run")
|
||||
.Produces<ImmutableArray<RunEventDto>>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/events", AddEventAsync)
|
||||
.WithName("AddRunEvent")
|
||||
.WithSummary("Adds an event to a run")
|
||||
.Produces<RunEventDto>(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/turns/user", AddUserTurnAsync)
|
||||
.WithName("AddUserTurn")
|
||||
.WithSummary("Adds a user turn to the run")
|
||||
.Produces<RunEventDto>(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/turns/assistant", AddAssistantTurnAsync)
|
||||
.WithName("AddAssistantTurn")
|
||||
.WithSummary("Adds an assistant turn to the run")
|
||||
.Produces<RunEventDto>(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/actions", ProposeActionAsync)
|
||||
.WithName("ProposeAction")
|
||||
.WithSummary("Proposes an action in the run")
|
||||
.Produces<RunEventDto>(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/approval/request", RequestApprovalAsync)
|
||||
.WithName("RequestApproval")
|
||||
.WithSummary("Requests approval for pending actions")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/approval/decide", ApproveAsync)
|
||||
.WithName("ApproveRun")
|
||||
.WithSummary("Approves or rejects a run")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/{runId}/actions/{actionEventId}/execute", ExecuteActionAsync)
|
||||
.WithName("ExecuteAction")
|
||||
.WithSummary("Executes an approved action")
|
||||
.Produces<RunEventDto>()
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/{runId}/artifacts", AddArtifactAsync)
|
||||
.WithName("AddArtifact")
|
||||
.WithSummary("Adds an artifact to the run")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/complete", CompleteRunAsync)
|
||||
.WithName("CompleteRun")
|
||||
.WithSummary("Completes a run")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/{runId}/cancel", CancelRunAsync)
|
||||
.WithName("CancelRun")
|
||||
.WithSummary("Cancels a run")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/{runId}/handoff", HandOffRunAsync)
|
||||
.WithName("HandOffRun")
|
||||
.WithSummary("Hands off a run to another user")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{runId}/attest", AttestRunAsync)
|
||||
.WithName("AttestRun")
|
||||
.WithSummary("Creates an attestation for a completed run")
|
||||
.Produces<RunDto>()
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapGet("/active", GetActiveRunsAsync)
|
||||
.WithName("GetActiveRuns")
|
||||
.WithSummary("Gets active runs for the current user")
|
||||
.Produces<ImmutableArray<RunDto>>();
|
||||
|
||||
group.MapGet("/pending-approval", GetPendingApprovalAsync)
|
||||
.WithName("GetPendingApproval")
|
||||
.WithSummary("Gets runs pending approval")
|
||||
.Produces<ImmutableArray<RunDto>>();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateRunAsync(
|
||||
[FromBody] CreateRunRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
userId ??= "anonymous";
|
||||
|
||||
var run = await runService.CreateAsync(new CreateRunRequest
|
||||
{
|
||||
TenantId = tenantId,
|
||||
InitiatedBy = userId,
|
||||
Title = request.Title,
|
||||
Objective = request.Objective,
|
||||
Context = request.Context is not null ? MapToContext(request.Context) : null,
|
||||
Metadata = request.Metadata?.ToImmutableDictionary()
|
||||
}, ct);
|
||||
|
||||
return Results.Created($"/api/v1/runs/{run.RunId}", MapToDto(run));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetRunAsync(
|
||||
string runId,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
var run = await runService.GetAsync(tenantId, runId, ct);
|
||||
if (run is null)
|
||||
{
|
||||
return Results.NotFound(new { message = $"Run {runId} not found" });
|
||||
}
|
||||
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
|
||||
private static async Task<IResult> QueryRunsAsync(
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromQuery] string? initiatedBy,
|
||||
[FromQuery] string? cveId,
|
||||
[FromQuery] string? component,
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 20,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
ImmutableArray<RunStatus>? statuses = null;
|
||||
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<RunStatus>(status, true, out var parsedStatus))
|
||||
{
|
||||
statuses = [parsedStatus];
|
||||
}
|
||||
|
||||
var result = await runService.QueryAsync(new RunQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
InitiatedBy = initiatedBy,
|
||||
CveId = cveId,
|
||||
Component = component,
|
||||
Statuses = statuses,
|
||||
Skip = skip,
|
||||
Take = take
|
||||
}, ct);
|
||||
|
||||
return Results.Ok(new RunQueryResultDto
|
||||
{
|
||||
Runs = result.Runs.Select(MapToDto).ToImmutableArray(),
|
||||
TotalCount = result.TotalCount,
|
||||
HasMore = result.HasMore
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetTimelineAsync(
|
||||
string runId,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
var events = await runService.GetTimelineAsync(tenantId, runId, skip, take, ct);
|
||||
return Results.Ok(events.Select(MapEventToDto).ToImmutableArray());
|
||||
}
|
||||
|
||||
private static async Task<IResult> AddEventAsync(
|
||||
string runId,
|
||||
[FromBody] AddEventRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var evt = await runService.AddEventAsync(tenantId, runId, new AddRunEventRequest
|
||||
{
|
||||
Type = request.Type,
|
||||
ActorId = userId,
|
||||
Content = request.Content,
|
||||
EvidenceLinks = request.EvidenceLinks,
|
||||
ParentEventId = request.ParentEventId,
|
||||
Metadata = request.Metadata?.ToImmutableDictionary()
|
||||
}, ct);
|
||||
|
||||
return Results.Created($"/api/v1/runs/{runId}/events/{evt.EventId}", MapEventToDto(evt));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> AddUserTurnAsync(
|
||||
string runId,
|
||||
[FromBody] AddTurnRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
userId ??= "anonymous";
|
||||
|
||||
try
|
||||
{
|
||||
var evt = await runService.AddUserTurnAsync(
|
||||
tenantId, runId, request.Message, userId, request.EvidenceLinks, ct);
|
||||
|
||||
return Results.Created($"/api/v1/runs/{runId}/events/{evt.EventId}", MapEventToDto(evt));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> AddAssistantTurnAsync(
|
||||
string runId,
|
||||
[FromBody] AddTurnRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var evt = await runService.AddAssistantTurnAsync(
|
||||
tenantId, runId, request.Message, request.EvidenceLinks, ct);
|
||||
|
||||
return Results.Created($"/api/v1/runs/{runId}/events/{evt.EventId}", MapEventToDto(evt));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> ProposeActionAsync(
|
||||
string runId,
|
||||
[FromBody] ProposeActionRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var evt = await runService.ProposeActionAsync(tenantId, runId, new ProposeActionRequest
|
||||
{
|
||||
ActionType = request.ActionType,
|
||||
Subject = request.Subject,
|
||||
Rationale = request.Rationale,
|
||||
RequiresApproval = request.RequiresApproval,
|
||||
Parameters = request.Parameters?.ToImmutableDictionary(),
|
||||
EvidenceLinks = request.EvidenceLinks
|
||||
}, ct);
|
||||
|
||||
return Results.Created($"/api/v1/runs/{runId}/events/{evt.EventId}", MapEventToDto(evt));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> RequestApprovalAsync(
|
||||
string runId,
|
||||
[FromBody] RequestApprovalDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.RequestApprovalAsync(
|
||||
tenantId, runId, [.. request.Approvers], request.Reason, ct);
|
||||
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> ApproveAsync(
|
||||
string runId,
|
||||
[FromBody] ApprovalDecisionDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
userId ??= "anonymous";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.ApproveAsync(
|
||||
tenantId, runId, request.Approved, userId, request.Reason, ct);
|
||||
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> ExecuteActionAsync(
|
||||
string runId,
|
||||
string actionEventId,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var evt = await runService.ExecuteActionAsync(tenantId, runId, actionEventId, ct);
|
||||
return Results.Ok(MapEventToDto(evt));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> AddArtifactAsync(
|
||||
string runId,
|
||||
[FromBody] AddArtifactRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.AddArtifactAsync(tenantId, runId, new RunArtifact
|
||||
{
|
||||
ArtifactId = request.ArtifactId ?? Guid.NewGuid().ToString("N"),
|
||||
Type = request.Type,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ContentDigest = request.ContentDigest,
|
||||
ContentSize = request.ContentSize,
|
||||
MediaType = request.MediaType,
|
||||
StorageUri = request.StorageUri,
|
||||
IsInline = request.IsInline,
|
||||
InlineContent = request.InlineContent,
|
||||
Metadata = request.Metadata?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
|
||||
}, ct);
|
||||
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> CompleteRunAsync(
|
||||
string runId,
|
||||
[FromBody] CompleteRunRequestDto? request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.CompleteAsync(tenantId, runId, request?.Summary, ct);
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> CancelRunAsync(
|
||||
string runId,
|
||||
[FromBody] CancelRunRequestDto? request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.CancelAsync(tenantId, runId, request?.Reason, ct);
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandOffRunAsync(
|
||||
string runId,
|
||||
[FromBody] HandOffRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.HandOffAsync(tenantId, runId, request.ToUserId, request.Message, ct);
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> AttestRunAsync(
|
||||
string runId,
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
try
|
||||
{
|
||||
var run = await runService.AttestAsync(tenantId, runId, ct);
|
||||
return Results.Ok(MapToDto(run));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetActiveRunsAsync(
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
userId ??= "anonymous";
|
||||
|
||||
var result = await runService.QueryAsync(new RunQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
InitiatedBy = userId,
|
||||
Statuses = [RunStatus.Created, RunStatus.Active, RunStatus.PendingApproval],
|
||||
Take = 50
|
||||
}, ct);
|
||||
|
||||
return Results.Ok(result.Runs.Select(MapToDto).ToImmutableArray());
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetPendingApprovalAsync(
|
||||
[FromServices] IRunService runService,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
tenantId ??= "default";
|
||||
|
||||
var result = await runService.QueryAsync(new RunQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Statuses = [RunStatus.PendingApproval],
|
||||
Take = 50
|
||||
}, ct);
|
||||
|
||||
return Results.Ok(result.Runs.Select(MapToDto).ToImmutableArray());
|
||||
}
|
||||
|
||||
private static RunDto MapToDto(Run run) => new()
|
||||
{
|
||||
RunId = run.RunId,
|
||||
TenantId = run.TenantId,
|
||||
InitiatedBy = run.InitiatedBy,
|
||||
Title = run.Title,
|
||||
Objective = run.Objective,
|
||||
Status = run.Status.ToString(),
|
||||
CreatedAt = run.CreatedAt,
|
||||
UpdatedAt = run.UpdatedAt,
|
||||
CompletedAt = run.CompletedAt,
|
||||
EventCount = run.Events.Length,
|
||||
ArtifactCount = run.Artifacts.Length,
|
||||
ContentDigest = run.ContentDigest,
|
||||
IsAttested = run.Attestation is not null,
|
||||
Context = MapContextToDto(run.Context),
|
||||
Approval = run.Approval is not null ? MapApprovalToDto(run.Approval) : null,
|
||||
Metadata = run.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
};
|
||||
|
||||
private static RunEventDto MapEventToDto(RunEvent evt) => new()
|
||||
{
|
||||
EventId = evt.EventId,
|
||||
Type = evt.Type.ToString(),
|
||||
Timestamp = evt.Timestamp,
|
||||
ActorId = evt.ActorId,
|
||||
SequenceNumber = evt.SequenceNumber,
|
||||
ParentEventId = evt.ParentEventId,
|
||||
EvidenceLinkCount = evt.EvidenceLinks.Length,
|
||||
Metadata = evt.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
};
|
||||
|
||||
private static RunContextDto MapContextToDto(RunContext context) => new()
|
||||
{
|
||||
FocusedCveId = context.FocusedCveId,
|
||||
FocusedComponent = context.FocusedComponent,
|
||||
SbomDigest = context.SbomDigest,
|
||||
ImageReference = context.ImageReference,
|
||||
Tags = [.. context.Tags],
|
||||
IsOpsMemoryEnriched = context.OpsMemory?.IsEnriched ?? false
|
||||
};
|
||||
|
||||
private static ApprovalInfoDto MapApprovalToDto(ApprovalInfo approval) => new()
|
||||
{
|
||||
Required = approval.Required,
|
||||
Approvers = [.. approval.Approvers],
|
||||
Approved = approval.Approved,
|
||||
ApprovedBy = approval.ApprovedBy,
|
||||
ApprovedAt = approval.ApprovedAt,
|
||||
Reason = approval.Reason
|
||||
};
|
||||
|
||||
private static RunContext MapToContext(RunContextDto dto) => new()
|
||||
{
|
||||
FocusedCveId = dto.FocusedCveId,
|
||||
FocusedComponent = dto.FocusedComponent,
|
||||
SbomDigest = dto.SbomDigest,
|
||||
ImageReference = dto.ImageReference,
|
||||
Tags = [.. dto.Tags ?? []]
|
||||
};
|
||||
}
|
||||
|
||||
// DTOs
|
||||
|
||||
/// <summary>DTO for creating a run.</summary>
|
||||
public sealed record CreateRunRequestDto
|
||||
{
|
||||
/// <summary>Gets the run title.</summary>
|
||||
public required string Title { get; init; }
|
||||
|
||||
/// <summary>Gets the run objective.</summary>
|
||||
public string? Objective { get; init; }
|
||||
|
||||
/// <summary>Gets the context.</summary>
|
||||
public RunContextDto? Context { get; init; }
|
||||
|
||||
/// <summary>Gets metadata.</summary>
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for run context.</summary>
|
||||
public sealed record RunContextDto
|
||||
{
|
||||
/// <summary>Gets the focused CVE ID.</summary>
|
||||
public string? FocusedCveId { get; init; }
|
||||
|
||||
/// <summary>Gets the focused component.</summary>
|
||||
public string? FocusedComponent { get; init; }
|
||||
|
||||
/// <summary>Gets the SBOM digest.</summary>
|
||||
public string? SbomDigest { get; init; }
|
||||
|
||||
/// <summary>Gets the image reference.</summary>
|
||||
public string? ImageReference { get; init; }
|
||||
|
||||
/// <summary>Gets the tags.</summary>
|
||||
public List<string>? Tags { get; init; }
|
||||
|
||||
/// <summary>Gets whether OpsMemory enrichment was applied.</summary>
|
||||
public bool IsOpsMemoryEnriched { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for a run.</summary>
|
||||
public sealed record RunDto
|
||||
{
|
||||
/// <summary>Gets the run ID.</summary>
|
||||
public required string RunId { get; init; }
|
||||
|
||||
/// <summary>Gets the tenant ID.</summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>Gets the initiator.</summary>
|
||||
public required string InitiatedBy { get; init; }
|
||||
|
||||
/// <summary>Gets the title.</summary>
|
||||
public required string Title { get; init; }
|
||||
|
||||
/// <summary>Gets the objective.</summary>
|
||||
public string? Objective { get; init; }
|
||||
|
||||
/// <summary>Gets the status.</summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>Gets the created timestamp.</summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>Gets the updated timestamp.</summary>
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
|
||||
/// <summary>Gets the completed timestamp.</summary>
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
|
||||
/// <summary>Gets the event count.</summary>
|
||||
public int EventCount { get; init; }
|
||||
|
||||
/// <summary>Gets the artifact count.</summary>
|
||||
public int ArtifactCount { get; init; }
|
||||
|
||||
/// <summary>Gets the content digest.</summary>
|
||||
public string? ContentDigest { get; init; }
|
||||
|
||||
/// <summary>Gets whether the run is attested.</summary>
|
||||
public bool IsAttested { get; init; }
|
||||
|
||||
/// <summary>Gets the context.</summary>
|
||||
public RunContextDto? Context { get; init; }
|
||||
|
||||
/// <summary>Gets the approval info.</summary>
|
||||
public ApprovalInfoDto? Approval { get; init; }
|
||||
|
||||
/// <summary>Gets metadata.</summary>
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for run event.</summary>
|
||||
public sealed record RunEventDto
|
||||
{
|
||||
/// <summary>Gets the event ID.</summary>
|
||||
public required string EventId { get; init; }
|
||||
|
||||
/// <summary>Gets the event type.</summary>
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>Gets the timestamp.</summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>Gets the actor ID.</summary>
|
||||
public string? ActorId { get; init; }
|
||||
|
||||
/// <summary>Gets the sequence number.</summary>
|
||||
public int SequenceNumber { get; init; }
|
||||
|
||||
/// <summary>Gets the parent event ID.</summary>
|
||||
public string? ParentEventId { get; init; }
|
||||
|
||||
/// <summary>Gets the evidence link count.</summary>
|
||||
public int EvidenceLinkCount { get; init; }
|
||||
|
||||
/// <summary>Gets metadata.</summary>
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for approval info.</summary>
|
||||
public sealed record ApprovalInfoDto
|
||||
{
|
||||
/// <summary>Gets whether approval is required.</summary>
|
||||
public bool Required { get; init; }
|
||||
|
||||
/// <summary>Gets the approvers.</summary>
|
||||
public List<string> Approvers { get; init; } = [];
|
||||
|
||||
/// <summary>Gets whether approved.</summary>
|
||||
public bool? Approved { get; init; }
|
||||
|
||||
/// <summary>Gets who approved.</summary>
|
||||
public string? ApprovedBy { get; init; }
|
||||
|
||||
/// <summary>Gets when approved.</summary>
|
||||
public DateTimeOffset? ApprovedAt { get; init; }
|
||||
|
||||
/// <summary>Gets the reason.</summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for query results.</summary>
|
||||
public sealed record RunQueryResultDto
|
||||
{
|
||||
/// <summary>Gets the runs.</summary>
|
||||
public required ImmutableArray<RunDto> Runs { get; init; }
|
||||
|
||||
/// <summary>Gets the total count.</summary>
|
||||
public required int TotalCount { get; init; }
|
||||
|
||||
/// <summary>Gets whether there are more results.</summary>
|
||||
public bool HasMore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for adding an event.</summary>
|
||||
public sealed record AddEventRequestDto
|
||||
{
|
||||
/// <summary>Gets the event type.</summary>
|
||||
public required RunEventType Type { get; init; }
|
||||
|
||||
/// <summary>Gets the content.</summary>
|
||||
public RunEventContent? Content { get; init; }
|
||||
|
||||
/// <summary>Gets evidence links.</summary>
|
||||
public ImmutableArray<EvidenceLink>? EvidenceLinks { get; init; }
|
||||
|
||||
/// <summary>Gets the parent event ID.</summary>
|
||||
public string? ParentEventId { get; init; }
|
||||
|
||||
/// <summary>Gets metadata.</summary>
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for adding a turn.</summary>
|
||||
public sealed record AddTurnRequestDto
|
||||
{
|
||||
/// <summary>Gets the message.</summary>
|
||||
public required string Message { get; init; }
|
||||
|
||||
/// <summary>Gets evidence links.</summary>
|
||||
public ImmutableArray<EvidenceLink>? EvidenceLinks { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for proposing an action.</summary>
|
||||
public sealed record ProposeActionRequestDto
|
||||
{
|
||||
/// <summary>Gets the action type.</summary>
|
||||
public required string ActionType { get; init; }
|
||||
|
||||
/// <summary>Gets the subject.</summary>
|
||||
public string? Subject { get; init; }
|
||||
|
||||
/// <summary>Gets the rationale.</summary>
|
||||
public string? Rationale { get; init; }
|
||||
|
||||
/// <summary>Gets whether approval is required.</summary>
|
||||
public bool RequiresApproval { get; init; } = true;
|
||||
|
||||
/// <summary>Gets the parameters.</summary>
|
||||
public Dictionary<string, string>? Parameters { get; init; }
|
||||
|
||||
/// <summary>Gets evidence links.</summary>
|
||||
public ImmutableArray<EvidenceLink>? EvidenceLinks { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for requesting approval.</summary>
|
||||
public sealed record RequestApprovalDto
|
||||
{
|
||||
/// <summary>Gets the approvers.</summary>
|
||||
public required List<string> Approvers { get; init; }
|
||||
|
||||
/// <summary>Gets the reason.</summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for approval decision.</summary>
|
||||
public sealed record ApprovalDecisionDto
|
||||
{
|
||||
/// <summary>Gets whether approved.</summary>
|
||||
public required bool Approved { get; init; }
|
||||
|
||||
/// <summary>Gets the reason.</summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for adding an artifact.</summary>
|
||||
public sealed record AddArtifactRequestDto
|
||||
{
|
||||
/// <summary>Gets the artifact ID.</summary>
|
||||
public string? ArtifactId { get; init; }
|
||||
|
||||
/// <summary>Gets the artifact type.</summary>
|
||||
public required ArtifactType Type { get; init; }
|
||||
|
||||
/// <summary>Gets the name.</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>Gets the description.</summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>Gets the content digest.</summary>
|
||||
public required string ContentDigest { get; init; }
|
||||
|
||||
/// <summary>Gets the content size.</summary>
|
||||
public long ContentSize { get; init; }
|
||||
|
||||
/// <summary>Gets the media type.</summary>
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
/// <summary>Gets the storage URI.</summary>
|
||||
public string? StorageUri { get; init; }
|
||||
|
||||
/// <summary>Gets whether inline.</summary>
|
||||
public bool IsInline { get; init; }
|
||||
|
||||
/// <summary>Gets inline content.</summary>
|
||||
public string? InlineContent { get; init; }
|
||||
|
||||
/// <summary>Gets metadata.</summary>
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for completing a run.</summary>
|
||||
public sealed record CompleteRunRequestDto
|
||||
{
|
||||
/// <summary>Gets the summary.</summary>
|
||||
public string? Summary { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for canceling a run.</summary>
|
||||
public sealed record CancelRunRequestDto
|
||||
{
|
||||
/// <summary>Gets the reason.</summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for hand off.</summary>
|
||||
public sealed record HandOffRequestDto
|
||||
{
|
||||
/// <summary>Gets the target user ID.</summary>
|
||||
public required string ToUserId { get; init; }
|
||||
|
||||
/// <summary>Gets the message.</summary>
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user