wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10

This commit is contained in:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
using StellaOps.AdvisoryAI.Attestation;
using StellaOps.AdvisoryAI.Attestation.Models;
using StellaOps.AdvisoryAI.Attestation.Storage;
using StellaOps.AdvisoryAI.WebService.Security;
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
@@ -26,36 +27,48 @@ public static class AttestationEndpoints
// GET /v1/advisory-ai/runs/{runId}/attestation
app.MapGet("/v1/advisory-ai/runs/{runId}/attestation", HandleGetRunAttestation)
.WithName("advisory-ai.runs.attestation.get")
.WithSummary("Get the attestation record for a completed AI run")
.WithDescription("Returns the AI attestation for a completed investigation run, including the DSSE envelope if the run was cryptographically signed. Tenant isolation is enforced; requests for runs belonging to a different tenant return 404. Returns 404 if the run has not been attested.")
.WithTags("Attestations")
.Produces<RunAttestationResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// GET /v1/advisory-ai/runs/{runId}/claims
app.MapGet("/v1/advisory-ai/runs/{runId}/claims", HandleGetRunClaims)
.WithName("advisory-ai.runs.claims.list")
.WithSummary("List AI-generated claims for a run")
.WithDescription("Returns all claim-level attestations recorded during an AI investigation run, each describing an individual assertion made by the AI (e.g. reachability verdict, remediation recommendation, risk rating). Claims are linked to the parent run attestation and can be independently verified.")
.WithTags("Attestations")
.Produces<ClaimsListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// GET /v1/advisory-ai/attestations/recent
app.MapGet("/v1/advisory-ai/attestations/recent", HandleListRecentAttestations)
.WithName("advisory-ai.attestations.recent")
.WithSummary("List recent AI attestations for the current tenant")
.WithDescription("Returns the most recent AI run attestations for the authenticated tenant, ordered by creation time descending. Limit defaults to 20 and is capped at 100. Use this endpoint to monitor recent AI activity and surface attestations for downstream signing or audit workflows.")
.WithTags("Attestations")
.Produces<RecentAttestationsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// POST /v1/advisory-ai/attestations/verify
app.MapPost("/v1/advisory-ai/attestations/verify", HandleVerifyAttestation)
.WithName("advisory-ai.attestations.verify")
.WithSummary("Verify the cryptographic integrity of an AI run attestation")
.WithDescription("Verifies the content digest and DSSE envelope signature of a previously recorded AI run attestation. Returns a structured result including per-component validity flags (digest, signature) and the signing key ID. Returns 400 if the attestation is not found, is tampered, or belongs to a different tenant.")
.WithTags("Attestations")
.Produces<AttestationVerificationResponse>(StatusCodes.Status200OK)
.Produces<AttestationVerificationResponse>(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
}

View File

@@ -17,6 +17,7 @@ using StellaOps.AdvisoryAI.Chat.Routing;
using StellaOps.AdvisoryAI.Chat.Services;
using StellaOps.AdvisoryAI.Chat.Settings;
using StellaOps.AdvisoryAI.WebService.Contracts;
using StellaOps.AdvisoryAI.WebService.Security;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text.Json;
@@ -43,7 +44,8 @@ public static class ChatEndpoints
public static RouteGroupBuilder MapChatEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/api/v1/chat")
.WithTags("Advisory Chat");
.WithTags("Advisory Chat")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy);
// Single query endpoint (non-streaming)
group.MapPost("/query", ProcessQueryAsync)
@@ -68,6 +70,7 @@ public static class ChatEndpoints
group.MapPost("/intent", DetectIntentAsync)
.WithName("DetectChatIntent")
.WithSummary("Detects intent from a user query without generating a full response")
.WithDescription("Classifies the user query into one of the advisory chat intents (explain, remediate, assess-risk, compare, etc.) and extracts structured parameters such as finding ID, package PURL, image reference, and environment. Useful for pre-routing or UI intent indicators without consuming LLM quota.")
.Produces<IntentDetectionResponse>(StatusCodes.Status200OK)
.ProducesValidationProblem();
@@ -75,6 +78,7 @@ public static class ChatEndpoints
group.MapPost("/evidence-preview", PreviewEvidenceBundleAsync)
.WithName("PreviewEvidenceBundle")
.WithSummary("Previews the evidence bundle that would be assembled for a query")
.WithDescription("Assembles and returns a preview of the evidence bundle that would be passed to the LLM for the specified finding, without generating an AI response. Indicates which evidence types are available (VEX, reachability, binary patch, provenance, policy, ops memory, fix options) and their status.")
.Produces<EvidenceBundlePreviewResponse>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest);
@@ -82,29 +86,34 @@ public static class ChatEndpoints
group.MapGet("/settings", GetChatSettingsAsync)
.WithName("GetChatSettings")
.WithSummary("Gets effective chat settings for the caller")
.WithDescription("Returns the effective advisory chat settings for the current tenant and user, merging global defaults, tenant overrides, and user overrides. Includes quota limits and tool access configuration.")
.Produces<ChatSettingsResponse>(StatusCodes.Status200OK);
group.MapPut("/settings", UpdateChatSettingsAsync)
.WithName("UpdateChatSettings")
.WithSummary("Updates chat settings overrides (tenant or user)")
.WithDescription("Applies quota and tool access overrides for the current tenant (default) or a specific user (scope=user). Overrides are layered on top of global defaults; only fields present in the request body are changed.")
.Produces<ChatSettingsResponse>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest);
group.MapDelete("/settings", ClearChatSettingsAsync)
.WithName("ClearChatSettings")
.WithSummary("Clears chat settings overrides (tenant or user)")
.WithDescription("Removes all tenant-level or user-level chat settings overrides, reverting the affected scope to global defaults. Use scope=user to clear only the user-level override for the current user.")
.Produces(StatusCodes.Status204NoContent);
// Doctor endpoint
group.MapGet("/doctor", GetChatDoctorAsync)
.WithName("GetChatDoctor")
.WithSummary("Returns chat limit status and tool access diagnostics")
.WithDescription("Returns a diagnostics report for the current tenant and user, including remaining quota across all dimensions (requests/min, requests/day, tokens/day, tool calls/day), tool provider availability, and the last quota denial if any. Referenced by error responses via the doctor action hint.")
.Produces<ChatDoctorResponse>(StatusCodes.Status200OK);
// Health/status endpoint for chat service
group.MapGet("/status", GetChatStatusAsync)
.WithName("GetChatStatus")
.WithSummary("Gets the status of the advisory chat service")
.WithDescription("Returns the current operational status of the advisory chat service, including whether chat is enabled, the configured inference provider and model, maximum token limit, and whether guardrails and audit logging are active.")
.Produces<ChatServiceStatusResponse>(StatusCodes.Status200OK);
return group;

View File

@@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.AdvisoryAI.WebService.Security;
using StellaOps.Determinism;
using StellaOps.Evidence.Pack;
using StellaOps.Evidence.Pack.Models;
@@ -27,62 +28,83 @@ public static class EvidencePackEndpoints
// POST /v1/evidence-packs - Create Evidence Pack
app.MapPost("/v1/evidence-packs", HandleCreateEvidencePack)
.WithName("evidence-packs.create")
.WithSummary("Create an evidence pack")
.WithDescription("Creates a new evidence pack containing AI-generated claims and supporting evidence items for a vulnerability subject. Claims are linked to evidence items by ID. The pack is assigned a content digest for tamper detection and can subsequently be signed via the sign endpoint.")
.WithTags("EvidencePacks")
.Produces<EvidencePackResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.RequireRateLimiting("advisory-ai");
// GET /v1/evidence-packs/{packId} - Get Evidence Pack
app.MapGet("/v1/evidence-packs/{packId}", HandleGetEvidencePack)
.WithName("evidence-packs.get")
.WithSummary("Get an evidence pack by ID")
.WithDescription("Returns the full evidence pack record including all claims, evidence items, subject, context, and related links (sign, verify, export). Access is tenant-scoped; packs from other tenants return 404.")
.WithTags("EvidencePacks")
.Produces<EvidencePackResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// POST /v1/evidence-packs/{packId}/sign - Sign Evidence Pack
app.MapPost("/v1/evidence-packs/{packId}/sign", HandleSignEvidencePack)
.WithName("evidence-packs.sign")
.WithSummary("Sign an evidence pack")
.WithDescription("Signs the specified evidence pack using DSSE (Dead Simple Signing Envelope), producing a cryptographic attestation over the pack's content digest. The resulting signed pack and DSSE envelope are returned and stored for subsequent verification.")
.WithTags("EvidencePacks")
.Produces<SignedEvidencePackResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.RequireRateLimiting("advisory-ai");
// POST /v1/evidence-packs/{packId}/verify - Verify Evidence Pack
app.MapPost("/v1/evidence-packs/{packId}/verify", HandleVerifyEvidencePack)
.WithName("evidence-packs.verify")
.WithSummary("Verify an evidence pack's signature and integrity")
.WithDescription("Verifies the cryptographic signature and content digest of a signed evidence pack. Returns per-evidence URI resolution results, digest match status, and signing key ID. Returns 400 if the pack has not been signed or verification fails.")
.WithTags("EvidencePacks")
.Produces<EvidencePackVerificationResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// GET /v1/evidence-packs/{packId}/export - Export Evidence Pack
app.MapGet("/v1/evidence-packs/{packId}/export", HandleExportEvidencePack)
.WithName("evidence-packs.export")
.WithSummary("Export an evidence pack in a specified format")
.WithDescription("Exports an evidence pack in the requested format. Supported formats: json (default), markdown, html, pdf, signedjson, evidencecard, and evidencecardcompact. The format query parameter controls the output; the appropriate Content-Type and filename are set in the response.")
.WithTags("EvidencePacks")
.Produces<byte[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// GET /v1/runs/{runId}/evidence-packs - List Evidence Packs for Run
app.MapGet("/v1/runs/{runId}/evidence-packs", HandleListRunEvidencePacks)
.WithName("evidence-packs.list-by-run")
.WithSummary("List evidence packs for a run")
.WithDescription("Returns all evidence packs associated with a specific AI investigation run, filtered to the current tenant. Includes pack summaries with claim count, evidence count, subject type, and CVE ID.")
.WithTags("EvidencePacks")
.Produces<EvidencePackListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
// GET /v1/evidence-packs - List Evidence Packs
app.MapGet("/v1/evidence-packs", HandleListEvidencePacks)
.WithName("evidence-packs.list")
.WithSummary("List evidence packs")
.WithDescription("Returns a paginated list of evidence packs for the current tenant, optionally filtered by CVE ID or run ID. Supports limit up to 100. Results include pack summaries with subject type, claim count, and evidence count.")
.WithTags("EvidencePacks")
.Produces<EvidencePackListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
.RequireRateLimiting("advisory-ai");
}

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.AdvisoryAI.KnowledgeSearch;
using StellaOps.AdvisoryAI.WebService.Security;
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
@@ -17,11 +18,14 @@ public static class KnowledgeSearchEndpoints
public static RouteGroupBuilder MapKnowledgeSearchEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/v1/advisory-ai")
.WithTags("Advisory AI - Knowledge Search");
.WithTags("Advisory AI - Knowledge Search")
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy);
group.MapPost("/search", SearchAsync)
.WithName("AdvisoryAiKnowledgeSearch")
.WithSummary("Searches AdvisoryAI deterministic knowledge index (docs/api/doctor).")
.WithDescription("Performs a hybrid full-text and vector similarity search over the AdvisoryAI deterministic knowledge index, which is composed of product documentation, OpenAPI specs, and Doctor health check projections. Supports filtering by content type (docs, api, doctor), product, version, service, and tags. Returns ranked result snippets with actionable open-actions for UI navigation.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<AdvisoryKnowledgeSearchResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status403Forbidden);
@@ -29,6 +33,8 @@ public static class KnowledgeSearchEndpoints
group.MapPost("/index/rebuild", RebuildIndexAsync)
.WithName("AdvisoryAiKnowledgeIndexRebuild")
.WithSummary("Rebuilds AdvisoryAI knowledge search index from deterministic local sources.")
.WithDescription("Triggers a full rebuild of the knowledge search index from local deterministic sources: product documentation files, embedded OpenAPI specs, and Doctor health check metadata. The rebuild is synchronous and returns document, chunk, and operation counts with duration. Requires admin-level scope; does not fetch external content.")
.RequireAuthorization(AdvisoryAIPolicies.AdminPolicy)
.Produces<AdvisoryKnowledgeRebuildResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden);
@@ -187,7 +193,10 @@ public static class KnowledgeSearchEndpoints
CheckCode = result.Open.Doctor.CheckCode,
Severity = result.Open.Doctor.Severity,
CanRun = result.Open.Doctor.CanRun,
RunCommand = result.Open.Doctor.RunCommand
RunCommand = result.Open.Doctor.RunCommand,
Control = result.Open.Doctor.Control,
RequiresConfirmation = result.Open.Doctor.RequiresConfirmation,
IsDestructive = result.Open.Doctor.IsDestructive
}
};
@@ -350,6 +359,12 @@ public sealed record AdvisoryKnowledgeOpenDoctorAction
public bool CanRun { get; init; } = true;
public string RunCommand { get; init; } = string.Empty;
public string Control { get; init; } = "safe";
public bool RequiresConfirmation { get; init; }
public bool IsDestructive { get; init; }
}
public sealed record AdvisoryKnowledgeSearchDiagnostics

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.AdvisoryAI.Inference.LlmProviders;
using StellaOps.AdvisoryAI.Plugin.Unified;
using StellaOps.AdvisoryAI.WebService.Security;
using StellaOps.Plugin.Abstractions.Capabilities;
using System.Security.Cryptography;
using System.Text;
@@ -27,17 +28,21 @@ public static class LlmAdapterEndpoints
public static RouteGroupBuilder MapLlmAdapterEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/v1/advisory-ai/adapters")
.WithTags("Advisory AI - LLM Adapters");
.WithTags("Advisory AI - LLM Adapters")
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy);
group.MapGet("/llm/providers", ListProvidersAsync)
.WithName("ListLlmProviders")
.WithSummary("Lists LLM providers exposed via the unified adapter layer.")
.WithDescription("Returns all LLM providers registered in the unified plugin catalog, including their configuration status, validation result, availability, and the completion path to use for each provider. Configured-but-invalid providers are included with error details. Use this endpoint to discover which providers are ready to serve completions before invoking them.")
.Produces<IReadOnlyList<LlmProviderExposureResponse>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden);
group.MapPost("/llm/{providerId}/chat/completions", CompleteWithProviderAsync)
.WithName("LlmProviderChatCompletions")
.WithSummary("OpenAI-compatible chat completion for a specific unified provider.")
.WithDescription("Submits a chat completion request to the specified LLM provider via the unified adapter layer using an OpenAI-compatible message format. Streaming is not supported; use non-streaming mode only. Returns 404 if the provider is not configured for adapter exposure, 503 if the provider is temporarily unavailable. Caller scopes are validated against gateway-managed headers.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<OpenAiChatCompletionResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status403Forbidden)
@@ -47,6 +52,8 @@ public static class LlmAdapterEndpoints
group.MapPost("/openai/v1/chat/completions", CompleteOpenAiCompatAsync)
.WithName("OpenAiAdapterChatCompletions")
.WithSummary("OpenAI-compatible chat completion alias backed by providerId=openai.")
.WithDescription("Convenience alias that routes chat completion requests to the provider with id 'openai', using the same OpenAI-compatible request/response format as the generic provider endpoint. Intended for drop-in compatibility with clients expecting the standard OpenAI path. Streaming is not supported.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<OpenAiChatCompletionResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status403Forbidden)

View File

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.AdvisoryAI.Runs;
using StellaOps.AdvisoryAI.WebService.Security;
using StellaOps.Determinism;
using System.Collections.Immutable;
@@ -27,64 +28,82 @@ public static class RunEndpoints
public static RouteGroupBuilder MapRunEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/api/v1/runs")
.WithTags("Runs");
.WithTags("Runs")
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy);
group.MapPost("/", CreateRunAsync)
.WithName("CreateRun")
.WithSummary("Creates a new AI investigation run")
.WithDescription("Creates a new AI investigation run scoped to the authenticated tenant, capturing the title, objective, and optional CVE/component/SBOM context. The run begins in the Created state and accumulates events as the investigation progresses. Returns 201 with the initial run state and a Location header.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>(StatusCodes.Status201Created)
.ProducesValidationProblem();
group.MapGet("/{runId}", GetRunAsync)
.WithName("GetRun")
.WithSummary("Gets a run by ID")
.WithDescription("Returns the current state of an AI investigation run, including status, event count, artifact count, content digest, attestation flag, context, and approval info. Returns 404 if the run does not exist or belongs to a different tenant.")
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/", QueryRunsAsync)
.WithName("QueryRuns")
.WithSummary("Queries runs with filters")
.WithDescription("Returns a paginated list of AI investigation runs for the current tenant, optionally filtered by initiator, CVE ID, component, and status. Supports skip/take pagination. Results are ordered by creation time descending.")
.Produces<RunQueryResultDto>();
group.MapGet("/{runId}/timeline", GetTimelineAsync)
.WithName("GetRunTimeline")
.WithSummary("Gets the event timeline for a run")
.WithDescription("Returns the ordered event timeline for an AI investigation run, including user turns, assistant turns, proposed actions, approvals, and artifact additions. Supports skip/take pagination over the event sequence. Returns 404 if the run does not exist.")
.Produces<ImmutableArray<RunEventDto>>()
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/events", AddEventAsync)
.WithName("AddRunEvent")
.WithSummary("Adds an event to a run")
.WithDescription("Appends a typed event to an active AI investigation run, supporting arbitrary event types with optional content payload, evidence links, and parent event reference for threading. Returns 201 with the created event. Returns 404 if the run does not exist or is in a terminal state.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunEventDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/turns/user", AddUserTurnAsync)
.WithName("AddUserTurn")
.WithSummary("Adds a user turn to the run")
.WithDescription("Appends a user conversational turn to an active AI investigation run, recording the message text, actor ID, and optional evidence links. User turns drive the investigation dialogue and are included in the run content digest for attestation purposes.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunEventDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/turns/assistant", AddAssistantTurnAsync)
.WithName("AddAssistantTurn")
.WithSummary("Adds an assistant turn to the run")
.WithDescription("Appends an AI assistant conversational turn to an active run, recording the generated message and optional evidence links. Assistant turns are included in the run content digest and contribute to the attestable evidence chain for the investigation.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunEventDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/actions", ProposeActionAsync)
.WithName("ProposeAction")
.WithSummary("Proposes an action in the run")
.WithDescription("Records an AI-proposed action in a run, including the action type, subject, rationale, parameters, and whether human approval is required before execution. Actions flagged as requiring approval transition the run to PendingApproval once approval is requested. Returns 404 if the run is not active.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunEventDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/approval/request", RequestApprovalAsync)
.WithName("RequestApproval")
.WithSummary("Requests approval for pending actions")
.WithDescription("Transitions a run to the PendingApproval state and notifies the designated approvers. The request body specifies the approver IDs and an optional reason. Returns the updated run state. Returns 404 if the run does not exist or is not in a state that allows approval requests.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/approval/decide", ApproveAsync)
.WithName("ApproveRun")
.WithSummary("Approves or rejects a run")
.WithDescription("Records an approval or rejection decision for a run in PendingApproval state. On approval, the run transitions back to Active so approved actions can be executed. On rejection, the run is cancelled. Returns 400 if the run is not in an approvable state.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest);
@@ -92,6 +111,8 @@ public static class RunEndpoints
group.MapPost("/{runId}/actions/{actionEventId}/execute", ExecuteActionAsync)
.WithName("ExecuteAction")
.WithSummary("Executes an approved action")
.WithDescription("Marks a previously proposed and approved action as executed, recording the execution result in the run timeline. Only actions that have been approved may be executed; attempting to execute a pending or rejected action returns 400. Returns 404 if the run or action event does not exist.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunEventDto>()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest);
@@ -99,12 +120,16 @@ public static class RunEndpoints
group.MapPost("/{runId}/artifacts", AddArtifactAsync)
.WithName("AddArtifact")
.WithSummary("Adds an artifact to the run")
.WithDescription("Attaches an artifact (evidence pack, report, SBOM snippet, or other typed asset) to an active run. The artifact is recorded with its content digest, media type, size, and optional inline content. Adding an artifact updates the run's content digest, contributing to its attestation chain.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/complete", CompleteRunAsync)
.WithName("CompleteRun")
.WithSummary("Completes a run")
.WithDescription("Transitions an active AI investigation run to the Completed terminal state, optionally recording a summary of findings. Once completed, the run is immutable and ready for attestation. Returns 400 if the run is already in a terminal state.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest);
@@ -112,6 +137,8 @@ public static class RunEndpoints
group.MapPost("/{runId}/cancel", CancelRunAsync)
.WithName("CancelRun")
.WithSummary("Cancels a run")
.WithDescription("Transitions an active or pending-approval AI investigation run to the Cancelled terminal state, optionally recording a cancellation reason. Cancelled runs are immutable and excluded from active and pending-approval queries. Returns 400 if the run is already in a terminal state.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest);
@@ -119,12 +146,16 @@ public static class RunEndpoints
group.MapPost("/{runId}/handoff", HandOffRunAsync)
.WithName("HandOffRun")
.WithSummary("Hands off a run to another user")
.WithDescription("Transfers ownership of an active AI investigation run to another user within the same tenant. A hand-off event is recorded in the run timeline with the target user ID and an optional message. Returns 404 if the run does not exist or the target user is not valid.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{runId}/attest", AttestRunAsync)
.WithName("AttestRun")
.WithSummary("Creates an attestation for a completed run")
.WithDescription("Generates and persists a cryptographic attestation for a completed AI investigation run, recording the content digest, model metadata, and claim hashes. The attestation can optionally be signed via the attestation sign endpoint. Returns 400 if the run is not in a terminal state or has already been attested.")
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
.Produces<RunDto>()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest);
@@ -132,11 +163,13 @@ public static class RunEndpoints
group.MapGet("/active", GetActiveRunsAsync)
.WithName("GetActiveRuns")
.WithSummary("Gets active runs for the current user")
.WithDescription("Returns up to 50 AI investigation runs in Created, Active, or PendingApproval state that were initiated by the current user within the authenticated tenant. Use this endpoint to resume in-progress investigations or surface runs awaiting user input.")
.Produces<ImmutableArray<RunDto>>();
group.MapGet("/pending-approval", GetPendingApprovalAsync)
.WithName("GetPendingApproval")
.WithSummary("Gets runs pending approval")
.WithDescription("Returns up to 50 AI investigation runs in the PendingApproval state for the authenticated tenant. Use this endpoint to surface runs that are blocked on a human approval decision before their proposed actions can be executed.")
.Produces<ImmutableArray<RunDto>>();
return group;