sprints work

This commit is contained in:
master
2026-01-10 20:32:13 +02:00
parent 0d5eda86fc
commit 17d0631b8e
189 changed files with 40667 additions and 497 deletions

View File

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