Complete batch 012 (golden set diff) and 013 (advisory chat), fix build errors

Sprints completed:
- SPRINT_20260110_012_* (golden set diff layer - 10 sprints)
- SPRINT_20260110_013_* (advisory chat - 4 sprints)

Build fixes applied:
- Fix namespace conflicts with Microsoft.Extensions.Options.Options.Create
- Fix VexDecisionReachabilityIntegrationTests API drift (major rewrite)
- Fix VexSchemaValidationTests FluentAssertions method name
- Fix FixChainGateIntegrationTests ambiguous type references
- Fix AdvisoryAI test files required properties and namespace aliases
- Add stub types for CveMappingController (ICveSymbolMappingService)
- Fix VerdictBuilderService static context issue

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
master
2026-01-11 10:09:07 +02:00
parent a3b2f30a11
commit 7f7eb8b228
232 changed files with 58979 additions and 91 deletions

View File

@@ -0,0 +1,752 @@
// <copyright file="ChatEndpoints.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Chat.Assembly;
using StellaOps.AdvisoryAI.Chat.Inference;
using StellaOps.AdvisoryAI.Chat.Models;
using StellaOps.AdvisoryAI.Chat.Options;
using StellaOps.AdvisoryAI.Chat.Routing;
using StellaOps.AdvisoryAI.Chat.Services;
using StellaOps.AdvisoryAI.WebService.Contracts;
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
/// <summary>
/// API endpoints for Advisory AI Chat with streaming support.
/// Sprint: SPRINT_20260107_013_003 Task: SVC-003
/// </summary>
public static class ChatEndpoints
{
private static readonly JsonSerializerOptions StreamJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
/// <summary>
/// Maps chat endpoints to the route builder.
/// </summary>
/// <param name="builder">The endpoint route builder.</param>
/// <returns>The route group builder.</returns>
public static RouteGroupBuilder MapChatEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/api/v1/chat")
.WithTags("Advisory Chat");
// Single query endpoint (non-streaming)
group.MapPost("/query", ProcessQueryAsync)
.WithName("ProcessChatQuery")
.WithSummary("Processes a chat query and returns an evidence-grounded response")
.WithDescription("Analyzes the user query, assembles evidence bundle, and generates a response with citations.")
.Produces<AdvisoryChatQueryResponse>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status503ServiceUnavailable)
.ProducesValidationProblem();
// Streaming query endpoint
group.MapPost("/query/stream", StreamQueryAsync)
.WithName("StreamChatQuery")
.WithSummary("Streams a chat response as Server-Sent Events")
.WithDescription("Processes the query and streams the response tokens as SSE events.")
.Produces(StatusCodes.Status200OK, contentType: "text/event-stream")
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status503ServiceUnavailable);
// Intent detection endpoint (lightweight)
group.MapPost("/intent", DetectIntentAsync)
.WithName("DetectChatIntent")
.WithSummary("Detects intent from a user query without generating a full response")
.Produces<IntentDetectionResponse>(StatusCodes.Status200OK)
.ProducesValidationProblem();
// Evidence bundle preview endpoint
group.MapPost("/evidence-preview", PreviewEvidenceBundleAsync)
.WithName("PreviewEvidenceBundle")
.WithSummary("Previews the evidence bundle that would be assembled for a query")
.Produces<EvidenceBundlePreviewResponse>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest);
// Health/status endpoint for chat service
group.MapGet("/status", GetChatStatusAsync)
.WithName("GetChatStatus")
.WithSummary("Gets the status of the advisory chat service")
.Produces<ChatServiceStatusResponse>(StatusCodes.Status200OK);
return group;
}
private static async Task<IResult> ProcessQueryAsync(
[FromBody] AdvisoryChatQueryRequest request,
[FromServices] IAdvisoryChatService chatService,
[FromServices] IOptions<AdvisoryChatOptions> options,
[FromServices] ILogger<AdvisoryChatQueryRequest> logger,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
[FromHeader(Name = "X-User-Id")] string? userId,
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
CancellationToken ct)
{
if (!options.Value.Enabled)
{
return Results.Json(
new ErrorResponse { Error = "Advisory chat is disabled", Code = "CHAT_DISABLED" },
statusCode: StatusCodes.Status503ServiceUnavailable);
}
if (string.IsNullOrWhiteSpace(request.Query))
{
return Results.BadRequest(new ErrorResponse { Error = "Query cannot be empty", Code = "INVALID_QUERY" });
}
tenantId ??= "default";
userId ??= "anonymous";
logger.LogDebug("Processing chat query for tenant {TenantId}, user {UserId}", tenantId, userId);
var serviceRequest = new AdvisoryChatRequest
{
TenantId = tenantId,
UserId = userId,
Query = request.Query,
ArtifactDigest = request.ArtifactDigest,
ImageReference = request.ImageReference,
Environment = request.Environment,
CorrelationId = correlationId,
ConversationId = request.ConversationId,
UserRoles = request.UserRoles?.ToImmutableArray() ?? ImmutableArray<string>.Empty
};
var result = await chatService.ProcessQueryAsync(serviceRequest, ct);
if (!result.Success)
{
var statusCode = result.GuardrailBlocked
? StatusCodes.Status400BadRequest
: StatusCodes.Status500InternalServerError;
return Results.Json(
new ErrorResponse
{
Error = result.Error ?? "Query processing failed",
Code = result.GuardrailBlocked ? "GUARDRAIL_BLOCKED" : "PROCESSING_FAILED",
Details = result.GuardrailBlocked
? result.GuardrailViolations.ToDictionary(v => v, _ => (object)"violation")
: null
},
statusCode: statusCode);
}
return Results.Ok(MapToQueryResponse(result));
}
private static async Task StreamQueryAsync(
[FromBody] AdvisoryChatQueryRequest request,
[FromServices] IAdvisoryChatIntentRouter intentRouter,
[FromServices] IEvidenceBundleAssembler evidenceAssembler,
[FromServices] IAdvisoryChatInferenceClient inferenceClient,
[FromServices] IOptions<AdvisoryChatOptions> options,
[FromServices] ILogger<AdvisoryChatQueryRequest> logger,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
[FromHeader(Name = "X-User-Id")] string? userId,
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
HttpContext httpContext,
CancellationToken ct)
{
if (!options.Value.Enabled)
{
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await httpContext.Response.WriteAsJsonAsync(
new ErrorResponse { Error = "Advisory chat is disabled", Code = "CHAT_DISABLED" },
ct);
return;
}
if (string.IsNullOrWhiteSpace(request.Query))
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(
new ErrorResponse { Error = "Query cannot be empty", Code = "INVALID_QUERY" },
ct);
return;
}
tenantId ??= "default";
httpContext.Response.ContentType = "text/event-stream";
httpContext.Response.Headers.CacheControl = "no-cache";
httpContext.Response.Headers.Connection = "keep-alive";
await httpContext.Response.StartAsync(ct);
try
{
// Step 1: Route intent
var routingResult = await intentRouter.RouteAsync(request.Query, ct);
await WriteStreamEventAsync(httpContext, "intent", new
{
intent = routingResult.Intent.ToString(),
confidence = routingResult.Confidence,
parameters = routingResult.Parameters
}, ct);
// Step 2: Resolve context
var artifactDigest = request.ArtifactDigest ?? ExtractDigestFromImageRef(routingResult.Parameters.ImageReference);
var findingId = routingResult.Parameters.FindingId;
if (string.IsNullOrEmpty(artifactDigest) || string.IsNullOrEmpty(findingId))
{
await WriteStreamEventAsync(httpContext, "error", new
{
code = "MISSING_CONTEXT",
message = "Missing artifact digest or finding ID"
}, ct);
await WriteStreamEventAsync(httpContext, "done", new { success = false }, ct);
return;
}
// Step 3: Assemble evidence bundle
await WriteStreamEventAsync(httpContext, "status", new { phase = "assembling_evidence" }, ct);
var assemblyResult = await evidenceAssembler.AssembleAsync(
new EvidenceBundleAssemblyRequest
{
TenantId = tenantId,
ArtifactDigest = artifactDigest,
ImageReference = request.ImageReference ?? routingResult.Parameters.ImageReference,
Environment = request.Environment ?? "unknown",
FindingId = findingId,
PackagePurl = routingResult.Parameters.Package,
CorrelationId = correlationId
},
ct);
if (!assemblyResult.Success || assemblyResult.Bundle is null)
{
await WriteStreamEventAsync(httpContext, "error", new
{
code = "EVIDENCE_ASSEMBLY_FAILED",
message = assemblyResult.Error ?? "Failed to assemble evidence"
}, ct);
await WriteStreamEventAsync(httpContext, "done", new { success = false }, ct);
return;
}
await WriteStreamEventAsync(httpContext, "evidence", new
{
bundleId = assemblyResult.Bundle.BundleId,
evidenceCount = CountEvidence(assemblyResult.Bundle)
}, ct);
// Step 4: Stream inference response
await WriteStreamEventAsync(httpContext, "status", new { phase = "generating_response" }, ct);
await foreach (var chunk in inferenceClient.StreamResponseAsync(
assemblyResult.Bundle,
routingResult,
ct))
{
if (chunk.IsComplete && chunk.FinalResponse is not null)
{
await WriteStreamEventAsync(httpContext, "complete", MapToQueryResponse(
new AdvisoryChatServiceResult
{
Success = true,
Response = chunk.FinalResponse,
Intent = routingResult.Intent,
EvidenceAssembled = true
}), ct);
}
else if (!string.IsNullOrEmpty(chunk.Content))
{
await WriteStreamEventAsync(httpContext, "token", new { content = chunk.Content }, ct);
}
}
await WriteStreamEventAsync(httpContext, "done", new { success = true }, ct);
}
catch (OperationCanceledException)
{
// Client disconnected, nothing to do
logger.LogDebug("Stream cancelled by client");
}
catch (Exception ex)
{
logger.LogError(ex, "Error during streaming response");
await WriteStreamEventAsync(httpContext, "error", new
{
code = "STREAM_ERROR",
message = "An error occurred during streaming"
}, ct);
await WriteStreamEventAsync(httpContext, "done", new { success = false }, ct);
}
}
private static async Task<IResult> DetectIntentAsync(
[FromBody] IntentDetectionRequest request,
[FromServices] IAdvisoryChatIntentRouter intentRouter,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Query))
{
return Results.BadRequest(new ErrorResponse { Error = "Query cannot be empty", Code = "INVALID_QUERY" });
}
var result = await intentRouter.RouteAsync(request.Query, ct);
return Results.Ok(new IntentDetectionResponse
{
Intent = result.Intent.ToString(),
Confidence = result.Confidence,
NormalizedInput = result.NormalizedInput,
ExplicitSlashCommand = result.ExplicitSlashCommand,
Parameters = new IntentParametersResponse
{
FindingId = result.Parameters.FindingId,
Package = result.Parameters.Package,
ImageReference = result.Parameters.ImageReference,
Environment = result.Parameters.Environment,
Duration = result.Parameters.Duration,
Reason = result.Parameters.Reason
}
});
}
private static async Task<IResult> PreviewEvidenceBundleAsync(
[FromBody] EvidencePreviewRequest request,
[FromServices] IEvidenceBundleAssembler evidenceAssembler,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.FindingId))
{
return Results.BadRequest(new ErrorResponse { Error = "FindingId is required", Code = "MISSING_FINDING_ID" });
}
tenantId ??= "default";
var assemblyResult = await evidenceAssembler.AssembleAsync(
new EvidenceBundleAssemblyRequest
{
TenantId = tenantId,
ArtifactDigest = request.ArtifactDigest ?? "unknown",
ImageReference = request.ImageReference,
Environment = request.Environment ?? "unknown",
FindingId = request.FindingId,
PackagePurl = request.PackagePurl,
CorrelationId = correlationId
},
ct);
if (!assemblyResult.Success || assemblyResult.Bundle is null)
{
return Results.BadRequest(new ErrorResponse
{
Error = assemblyResult.Error ?? "Failed to assemble evidence",
Code = "EVIDENCE_ASSEMBLY_FAILED"
});
}
return Results.Ok(new EvidenceBundlePreviewResponse
{
BundleId = assemblyResult.Bundle.BundleId,
FindingId = assemblyResult.Bundle.Finding?.Id,
HasVexData = assemblyResult.Bundle.Verdicts?.Vex is not null,
HasReachabilityData = assemblyResult.Bundle.Reachability is not null,
HasBinaryPatchData = assemblyResult.Bundle.Reachability?.BinaryPatch is not null,
HasProvenanceData = assemblyResult.Bundle.Provenance is not null,
HasPolicyData = assemblyResult.Bundle.Verdicts?.Policy.Length > 0,
HasOpsMemoryData = assemblyResult.Bundle.OpsMemory is not null,
HasFixData = assemblyResult.Bundle.Fixes is not null,
EvidenceSummary = new EvidenceSummary
{
VexStatus = assemblyResult.Bundle.Verdicts?.Vex?.Status.ToString(),
ReachabilityStatus = assemblyResult.Bundle.Reachability?.Status.ToString(),
BinaryPatchDetected = assemblyResult.Bundle.Reachability?.BinaryPatch?.Detected,
PolicyDecision = assemblyResult.Bundle.Verdicts?.Policy.FirstOrDefault()?.Decision.ToString(),
FixOptionsCount = assemblyResult.Bundle.Fixes?.Upgrade.Length ?? 0
}
});
}
private static Task<IResult> GetChatStatusAsync(
[FromServices] IOptions<AdvisoryChatOptions> options)
{
var opts = options.Value;
return Task.FromResult(Results.Ok(new ChatServiceStatusResponse
{
Enabled = opts.Enabled,
InferenceProvider = opts.Inference.Provider.ToString(),
InferenceModel = opts.Inference.Model,
MaxTokens = opts.Inference.MaxTokens,
GuardrailsEnabled = opts.Guardrails.Enabled,
AuditEnabled = opts.Audit.Enabled
}));
}
private static async Task WriteStreamEventAsync<T>(
HttpContext context,
string eventType,
T data,
CancellationToken ct)
{
var json = JsonSerializer.Serialize(data, StreamJsonOptions);
await context.Response.WriteAsync($"event: {eventType}\n", ct);
await context.Response.WriteAsync($"data: {json}\n\n", ct);
await context.Response.Body.FlushAsync(ct);
}
private static string? ExtractDigestFromImageRef(string? imageRef)
{
if (string.IsNullOrEmpty(imageRef))
{
return null;
}
var atIndex = imageRef.IndexOf('@');
if (atIndex > 0 && imageRef.Length > atIndex + 1)
{
return imageRef[(atIndex + 1)..];
}
return null;
}
private static int CountEvidence(AdvisoryChatEvidenceBundle bundle)
{
var count = 0;
if (bundle.Verdicts?.Vex is not null)
{
count++;
}
if (bundle.Reachability is not null)
{
count++;
}
if (bundle.Reachability?.BinaryPatch is not null)
{
count++;
}
if (bundle.Provenance is not null)
{
count++;
}
if (bundle.Verdicts?.Policy.Length > 0)
{
count++;
}
if (bundle.OpsMemory is not null)
{
count++;
}
if (bundle.Fixes is not null)
{
count++;
}
return count;
}
private static AdvisoryChatQueryResponse MapToQueryResponse(AdvisoryChatServiceResult result)
{
var response = result.Response!;
return new AdvisoryChatQueryResponse
{
ResponseId = response.ResponseId,
BundleId = response.BundleId,
Intent = response.Intent.ToString(),
GeneratedAt = response.GeneratedAt,
Summary = response.Summary,
Impact = response.Impact is not null ? new ImpactAssessmentResponse
{
Artifact = response.Impact.Artifact,
Environment = response.Impact.Environment,
AffectedComponent = response.Impact.AffectedComponent,
AffectedVersion = response.Impact.AffectedVersion,
Description = response.Impact.Description
} : null,
Reachability = response.ReachabilityAssessment is not null ? new ReachabilityAssessmentResponse
{
Status = response.ReachabilityAssessment.Status.ToString(),
CallgraphPaths = response.ReachabilityAssessment.CallgraphPaths,
PathDescription = response.ReachabilityAssessment.PathDescription,
BinaryBackportDetected = response.ReachabilityAssessment.BinaryBackport?.Detected
} : null,
Mitigations = response.Mitigations.Select(m => new MitigationOptionResponse
{
Rank = m.Rank,
Type = m.Type.ToString(),
Label = m.Label,
Description = m.Description,
Risk = m.Risk.ToString(),
RequiresApproval = m.RequiresApproval
}).ToList(),
EvidenceLinks = response.EvidenceLinks.Select(e => new EvidenceLinkResponse
{
Type = e.Type.ToString(),
Uri = e.Link,
Label = e.Description,
Confidence = e.Confidence is not null
? e.Confidence == ConfidenceLevel.High ? 0.9
: e.Confidence == ConfidenceLevel.Medium ? 0.7
: e.Confidence == ConfidenceLevel.Low ? 0.4
: 0.2
: null
}).ToList(),
Confidence = new ConfidenceResponse
{
Level = response.Confidence.Level.ToString(),
Score = response.Confidence.Score
},
ProposedActions = response.ProposedActions.Select(a => new ProposedActionResponse
{
ActionType = a.ActionType.ToString(),
Label = a.Label,
PolicyGate = a.RiskLevel?.ToString(),
RequiresConfirmation = a.RequiresApproval ?? false
}).ToList(),
FollowUp = response.FollowUp is not null ? new FollowUpResponse
{
SuggestedQueries = [.. response.FollowUp.SuggestedQueries],
NextSteps = [.. response.FollowUp.NextSteps]
} : null,
Diagnostics = result.Diagnostics is not null ? new DiagnosticsResponse
{
IntentRoutingMs = result.Diagnostics.IntentRoutingMs,
EvidenceAssemblyMs = result.Diagnostics.EvidenceAssemblyMs,
InferenceMs = result.Diagnostics.InferenceMs,
TotalMs = result.Diagnostics.TotalMs,
PromptTokens = result.Diagnostics.PromptTokens,
CompletionTokens = result.Diagnostics.CompletionTokens
} : null
};
}
}
#region Request/Response DTOs
/// <summary>
/// Request to process a chat query.
/// </summary>
public sealed record AdvisoryChatQueryRequest
{
/// <summary>Gets the user query.</summary>
public required string Query { get; init; }
/// <summary>Gets the artifact digest.</summary>
public string? ArtifactDigest { get; init; }
/// <summary>Gets the image reference.</summary>
public string? ImageReference { get; init; }
/// <summary>Gets the environment.</summary>
public string? Environment { get; init; }
/// <summary>Gets the conversation ID for multi-turn.</summary>
public string? ConversationId { get; init; }
/// <summary>Gets the user roles for policy evaluation.</summary>
public List<string>? UserRoles { get; init; }
}
/// <summary>
/// Response from a chat query.
/// </summary>
public sealed record AdvisoryChatQueryResponse
{
/// <summary>Gets the response ID.</summary>
public required string ResponseId { get; init; }
/// <summary>Gets the bundle ID.</summary>
public string? BundleId { get; init; }
/// <summary>Gets the detected intent.</summary>
public required string Intent { get; init; }
/// <summary>Gets the generation timestamp.</summary>
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>Gets the summary.</summary>
public required string Summary { get; init; }
/// <summary>Gets the impact assessment.</summary>
public ImpactAssessmentResponse? Impact { get; init; }
/// <summary>Gets the reachability assessment.</summary>
public ReachabilityAssessmentResponse? Reachability { get; init; }
/// <summary>Gets the mitigation options.</summary>
public List<MitigationOptionResponse> Mitigations { get; init; } = [];
/// <summary>Gets the evidence links.</summary>
public List<EvidenceLinkResponse> EvidenceLinks { get; init; } = [];
/// <summary>Gets the confidence assessment.</summary>
public required ConfidenceResponse Confidence { get; init; }
/// <summary>Gets the proposed actions.</summary>
public List<ProposedActionResponse> ProposedActions { get; init; } = [];
/// <summary>Gets the follow-up suggestions.</summary>
public FollowUpResponse? FollowUp { get; init; }
/// <summary>Gets the diagnostics.</summary>
public DiagnosticsResponse? Diagnostics { get; init; }
}
/// <summary>Impact assessment response.</summary>
public sealed record ImpactAssessmentResponse
{
public string? Artifact { get; init; }
public string? Environment { get; init; }
public string? AffectedComponent { get; init; }
public string? AffectedVersion { get; init; }
public string? Description { get; init; }
}
/// <summary>Reachability assessment response.</summary>
public sealed record ReachabilityAssessmentResponse
{
public required string Status { get; init; }
public int? CallgraphPaths { get; init; }
public string? PathDescription { get; init; }
public bool? BinaryBackportDetected { get; init; }
}
/// <summary>Mitigation option response.</summary>
public sealed record MitigationOptionResponse
{
public required int Rank { get; init; }
public required string Type { get; init; }
public required string Label { get; init; }
public string? Description { get; init; }
public required string Risk { get; init; }
public bool? RequiresApproval { get; init; }
}
/// <summary>Confidence response.</summary>
public sealed record ConfidenceResponse
{
public required string Level { get; init; }
public required double Score { get; init; }
}
/// <summary>Follow-up suggestions response.</summary>
public sealed record FollowUpResponse
{
public List<string> SuggestedQueries { get; init; } = [];
public List<string> NextSteps { get; init; } = [];
}
/// <summary>Diagnostics response.</summary>
public sealed record DiagnosticsResponse
{
public long IntentRoutingMs { get; init; }
public long EvidenceAssemblyMs { get; init; }
public long InferenceMs { get; init; }
public long TotalMs { get; init; }
public int PromptTokens { get; init; }
public int CompletionTokens { get; init; }
}
/// <summary>Request for intent detection.</summary>
public sealed record IntentDetectionRequest
{
/// <summary>Gets the query to analyze.</summary>
public required string Query { get; init; }
}
/// <summary>Response for intent detection.</summary>
public sealed record IntentDetectionResponse
{
public required string Intent { get; init; }
public required double Confidence { get; init; }
public required string NormalizedInput { get; init; }
public bool ExplicitSlashCommand { get; init; }
public IntentParametersResponse? Parameters { get; init; }
}
/// <summary>Intent parameters response.</summary>
public sealed record IntentParametersResponse
{
public string? FindingId { get; init; }
public string? Package { get; init; }
public string? ImageReference { get; init; }
public string? Environment { get; init; }
public string? Duration { get; init; }
public string? Reason { get; init; }
}
/// <summary>Request for evidence preview.</summary>
public sealed record EvidencePreviewRequest
{
public required string FindingId { get; init; }
public string? ArtifactDigest { get; init; }
public string? ImageReference { get; init; }
public string? Environment { get; init; }
public string? PackagePurl { get; init; }
}
/// <summary>Response for evidence bundle preview.</summary>
public sealed record EvidenceBundlePreviewResponse
{
public required string BundleId { get; init; }
public string? FindingId { get; init; }
public bool HasVexData { get; init; }
public bool HasReachabilityData { get; init; }
public bool HasBinaryPatchData { get; init; }
public bool HasProvenanceData { get; init; }
public bool HasPolicyData { get; init; }
public bool HasOpsMemoryData { get; init; }
public bool HasFixData { get; init; }
public EvidenceSummary? EvidenceSummary { get; init; }
}
/// <summary>Evidence summary.</summary>
public sealed record EvidenceSummary
{
public string? VexStatus { get; init; }
public string? ReachabilityStatus { get; init; }
public bool? BinaryPatchDetected { get; init; }
public string? PolicyDecision { get; init; }
public int FixOptionsCount { get; init; }
}
/// <summary>Chat service status response.</summary>
public sealed record ChatServiceStatusResponse
{
public required bool Enabled { get; init; }
public required string InferenceProvider { get; init; }
public required string InferenceModel { get; init; }
public required int MaxTokens { get; init; }
public required bool GuardrailsEnabled { get; init; }
public required bool AuditEnabled { get; init; }
}
/// <summary>Error response.</summary>
public sealed record ErrorResponse
{
public required string Error { get; init; }
public string? Code { get; init; }
public Dictionary<string, object>? Details { get; init; }
}
#endregion