old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
// <copyright file="DeterminizationConfigEndpoints.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Determinization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for determinization configuration.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
|
||||
/// </summary>
|
||||
public static class DeterminizationConfigEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps determinization config endpoints.
|
||||
/// </summary>
|
||||
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<EffectiveConfigResponse>(StatusCodes.Status200OK)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
group.MapGet("/defaults", GetDefaultConfig)
|
||||
.WithName("GetDefaultDeterminizationConfig")
|
||||
.WithSummary("Get default determinization configuration")
|
||||
.Produces<DeterminizationOptions>(StatusCodes.Status200OK)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
group.MapGet("/audit", GetAuditHistory)
|
||||
.WithName("GetDeterminizationConfigAuditHistory")
|
||||
.WithSummary("Get audit history for determinization configuration changes")
|
||||
.Produces<AuditHistoryResponse>(StatusCodes.Status200OK)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
// Write endpoints (policy admin access)
|
||||
group.MapPut("", UpdateConfig)
|
||||
.WithName("UpdateDeterminizationConfig")
|
||||
.WithSummary("Update determinization configuration for the current tenant")
|
||||
.Produces<EffectiveConfigResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization("PolicyAdmin");
|
||||
|
||||
group.MapPost("/validate", ValidateConfig)
|
||||
.WithName("ValidateDeterminizationConfig")
|
||||
.WithSummary("Validate determinization configuration without saving")
|
||||
.Produces<ValidationResponse>(StatusCodes.Status200OK)
|
||||
.RequireAuthorization("PolicyViewer");
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetEffectiveConfig(
|
||||
HttpContext context,
|
||||
IDeterminizationConfigStore configStore,
|
||||
ILogger<DeterminizationConfigEndpoints> 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<DeterminizationConfigEndpoints> logger)
|
||||
{
|
||||
logger.LogDebug("Getting default determinization config");
|
||||
return Results.Ok(new DeterminizationOptions());
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetAuditHistory(
|
||||
HttpContext context,
|
||||
IDeterminizationConfigStore configStore,
|
||||
ILogger<DeterminizationConfigEndpoints> 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<IResult> UpdateConfig(
|
||||
HttpContext context,
|
||||
IDeterminizationConfigStore configStore,
|
||||
ILogger<DeterminizationConfigEndpoints> 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<DeterminizationConfigEndpoints> 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<string> Errors, List<string> Warnings) ValidateConfigInternal(
|
||||
DeterminizationOptions config)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
// 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.Conflicts.EscalationSeverityThreshold < 0 || config.Conflicts.EscalationSeverityThreshold > 1)
|
||||
{
|
||||
errors.Add("EscalationSeverityThreshold must be between 0 and 1");
|
||||
}
|
||||
|
||||
if (config.Conflicts.ConflictTtlHours < 1)
|
||||
{
|
||||
errors.Add("ConflictTtlHours must be at least 1");
|
||||
}
|
||||
|
||||
// Validate environment thresholds
|
||||
ValidateThresholds(config.Thresholds.Development, "Development", errors, warnings);
|
||||
ValidateThresholds(config.Thresholds.Staging, "Staging", errors, warnings);
|
||||
ValidateThresholds(config.Thresholds.Production, "Production", errors, warnings);
|
||||
|
||||
return (errors.Count == 0, errors, warnings);
|
||||
}
|
||||
|
||||
private static void ValidateThresholds(
|
||||
EnvironmentThreshold threshold,
|
||||
string envName,
|
||||
List<string> errors,
|
||||
List<string> warnings)
|
||||
{
|
||||
if (threshold.EpssThreshold < 0 || threshold.EpssThreshold > 1)
|
||||
{
|
||||
errors.Add($"{envName}.EpssThreshold must be between 0 and 1");
|
||||
}
|
||||
|
||||
if (threshold.UncertaintyFactor < 0 || threshold.UncertaintyFactor > 1)
|
||||
{
|
||||
errors.Add($"{envName}.UncertaintyFactor must be between 0 and 1");
|
||||
}
|
||||
|
||||
if (threshold.MinScore < 0 || threshold.MinScore > 100)
|
||||
{
|
||||
errors.Add($"{envName}.MinScore must be between 0 and 100");
|
||||
}
|
||||
|
||||
if (threshold.MaxScore < threshold.MinScore)
|
||||
{
|
||||
errors.Add($"{envName}.MaxScore must be >= MinScore");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/// <summary>Effective config response.</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Update config request.</summary>
|
||||
public sealed record UpdateConfigRequest
|
||||
{
|
||||
public required DeterminizationOptions Config { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Validate config request.</summary>
|
||||
public sealed record ValidateConfigRequest
|
||||
{
|
||||
public required DeterminizationOptions Config { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Validation response.</summary>
|
||||
public sealed record ValidationResponse
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
public required List<string> Errors { get; init; }
|
||||
public required List<string> Warnings { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Audit history response.</summary>
|
||||
public sealed record AuditHistoryResponse
|
||||
{
|
||||
public required List<AuditEntryDto> Entries { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Audit entry DTO.</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Logger wrapper for DI.</summary>
|
||||
file class DeterminizationConfigEndpoints { }
|
||||
Reference in New Issue
Block a user