//
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Determinization;
using System.Security.Claims;
namespace StellaOps.Policy.Engine.Endpoints;
///
/// API endpoints for determinization configuration.
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
///
public static class DeterminizationConfigEndpoints
{
///
/// Maps determinization config endpoints.
///
public static IEndpointRouteBuilder MapDeterminizationConfigEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/v1/policy/config/determinization")
.WithTags("Determinization Configuration");
// Read endpoints (policy viewer access)
group.MapGet("", GetEffectiveConfig)
.WithName("GetEffectiveDeterminizationConfig")
.WithSummary("Get effective determinization configuration for the current tenant")
.Produces(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
group.MapGet("/defaults", GetDefaultConfig)
.WithName("GetDefaultDeterminizationConfig")
.WithSummary("Get default determinization configuration")
.Produces(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
group.MapGet("/audit", GetAuditHistory)
.WithName("GetDeterminizationConfigAuditHistory")
.WithSummary("Get audit history for determinization configuration changes")
.Produces(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
// Write endpoints (policy admin access)
group.MapPut("", UpdateConfig)
.WithName("UpdateDeterminizationConfig")
.WithSummary("Update determinization configuration for the current tenant")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization("PolicyAdmin");
group.MapPost("/validate", ValidateConfig)
.WithName("ValidateDeterminizationConfig")
.WithSummary("Validate determinization configuration without saving")
.Produces(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
return endpoints;
}
private static async Task GetEffectiveConfig(
HttpContext context,
IDeterminizationConfigStore configStore,
ILogger logger,
CancellationToken ct)
{
var tenantId = GetTenantId(context);
logger.LogDebug("Getting effective determinization config for tenant {TenantId}", tenantId);
var config = await configStore.GetEffectiveConfigAsync(tenantId, ct);
return Results.Ok(new EffectiveConfigResponse
{
Config = config.Config,
IsDefault = config.IsDefault,
TenantId = config.TenantId,
LastUpdatedAt = config.LastUpdatedAt,
LastUpdatedBy = config.LastUpdatedBy,
Version = config.Version
});
}
private static IResult GetDefaultConfig(
ILogger logger)
{
logger.LogDebug("Getting default determinization config");
return Results.Ok(new DeterminizationOptions());
}
private static async Task GetAuditHistory(
HttpContext context,
IDeterminizationConfigStore configStore,
ILogger logger,
int limit = 50,
CancellationToken ct = default)
{
var tenantId = GetTenantId(context);
logger.LogDebug("Getting audit history for tenant {TenantId}", tenantId);
var entries = await configStore.GetAuditHistoryAsync(tenantId, limit, ct);
return Results.Ok(new AuditHistoryResponse
{
Entries = entries.Select(e => new AuditEntryDto
{
Id = e.Id,
ChangedAt = e.ChangedAt,
Actor = e.Actor,
Reason = e.Reason,
Source = e.Source,
Summary = e.Summary
}).ToList()
});
}
private static async Task UpdateConfig(
HttpContext context,
IDeterminizationConfigStore configStore,
ILogger logger,
UpdateConfigRequest request,
CancellationToken ct)
{
var tenantId = GetTenantId(context);
var actor = GetActorId(context);
logger.LogInformation(
"Updating determinization config for tenant {TenantId} by {Actor}: {Reason}",
tenantId,
actor,
request.Reason);
// Validate config
var validation = ValidateConfigInternal(request.Config);
if (!validation.IsValid)
{
return Results.BadRequest(new { errors = validation.Errors });
}
// Save with audit
await configStore.SaveConfigAsync(
tenantId,
request.Config,
new ConfigAuditInfo
{
Actor = actor,
Reason = request.Reason,
Source = "API",
CorrelationId = context.TraceIdentifier
},
ct);
// Return updated config
var updated = await configStore.GetEffectiveConfigAsync(tenantId, ct);
return Results.Ok(new EffectiveConfigResponse
{
Config = updated.Config,
IsDefault = updated.IsDefault,
TenantId = updated.TenantId,
LastUpdatedAt = updated.LastUpdatedAt,
LastUpdatedBy = updated.LastUpdatedBy,
Version = updated.Version
});
}
private static IResult ValidateConfig(
ValidateConfigRequest request,
ILogger logger)
{
logger.LogDebug("Validating determinization config");
var validation = ValidateConfigInternal(request.Config);
return Results.Ok(new ValidationResponse
{
IsValid = validation.IsValid,
Errors = validation.Errors,
Warnings = validation.Warnings
});
}
private static (bool IsValid, List Errors, List Warnings) ValidateConfigInternal(
DeterminizationOptions config)
{
var errors = new List();
var warnings = new List();
// Validate trigger config
if (config.Triggers.EpssDeltaThreshold < 0 || config.Triggers.EpssDeltaThreshold > 1)
{
errors.Add("EpssDeltaThreshold must be between 0 and 1");
}
if (config.Triggers.EpssDeltaThreshold < 0.1)
{
warnings.Add("EpssDeltaThreshold below 0.1 may cause excessive reanalysis");
}
// Validate conflict policy
if (config.ConflictPolicy.EscalationSeverityThreshold < 0 || config.ConflictPolicy.EscalationSeverityThreshold > 1)
{
errors.Add("EscalationSeverityThreshold must be between 0 and 1");
}
if (config.ConflictPolicy.ConflictTtlHours < 1)
{
errors.Add("ConflictTtlHours must be at least 1");
}
// Validate environment thresholds
ValidateThresholds(config.EnvironmentThresholds.Development, "Development", errors, warnings);
ValidateThresholds(config.EnvironmentThresholds.Staging, "Staging", errors, warnings);
ValidateThresholds(config.EnvironmentThresholds.Production, "Production", errors, warnings);
return (errors.Count == 0, errors, warnings);
}
private static void ValidateThresholds(
EnvironmentThresholdValues threshold,
string envName,
List errors,
List warnings)
{
if (threshold.MaxPassEntropy < 0 || threshold.MaxPassEntropy > 1)
{
errors.Add($"{envName}.MaxPassEntropy must be between 0 and 1");
}
if (threshold.MinEvidenceCount < 0)
{
errors.Add($"{envName}.MinEvidenceCount must be >= 0");
}
if (threshold.MaxPassEntropy > 0.8)
{
warnings.Add($"{envName}.MaxPassEntropy above 0.8 may reduce confidence controls");
}
}
private static string GetTenantId(HttpContext context)
{
return context.User.FindFirstValue("tenant_id") ?? "default";
}
private static string GetActorId(HttpContext context)
{
return context.User.FindFirstValue(ClaimTypes.NameIdentifier)
?? context.User.FindFirstValue("sub")
?? "system";
}
}
// DTOs
/// Effective config response.
public sealed record EffectiveConfigResponse
{
public required DeterminizationOptions Config { get; init; }
public required bool IsDefault { get; init; }
public string? TenantId { get; init; }
public DateTimeOffset? LastUpdatedAt { get; init; }
public string? LastUpdatedBy { get; init; }
public int Version { get; init; }
}
/// Update config request.
public sealed record UpdateConfigRequest
{
public required DeterminizationOptions Config { get; init; }
public required string Reason { get; init; }
}
/// Validate config request.
public sealed record ValidateConfigRequest
{
public required DeterminizationOptions Config { get; init; }
}
/// Validation response.
public sealed record ValidationResponse
{
public required bool IsValid { get; init; }
public required List Errors { get; init; }
public required List Warnings { get; init; }
}
/// Audit history response.
public sealed record AuditHistoryResponse
{
public required List Entries { get; init; }
}
/// Audit entry DTO.
public sealed record AuditEntryDto
{
public required Guid Id { get; init; }
public required DateTimeOffset ChangedAt { get; init; }
public required string Actor { get; init; }
public required string Reason { get; init; }
public string? Source { get; init; }
public string? Summary { get; init; }
}