This commit is contained in:
StellaOps Bot
2025-12-26 15:19:07 +02:00
25 changed files with 3377 additions and 132 deletions

View File

@@ -17,6 +17,7 @@ using StellaOps.AdvisoryAI.Metrics;
using StellaOps.AdvisoryAI.Outputs;
using StellaOps.AdvisoryAI.Orchestration;
using StellaOps.AdvisoryAI.Queue;
using StellaOps.AdvisoryAI.PolicyStudio;
using StellaOps.AdvisoryAI.Remediation;
using StellaOps.AdvisoryAI.WebService.Contracts;
using StellaOps.Router.AspNet;
@@ -107,6 +108,19 @@ app.MapPost("/v1/advisory-ai/remediation/apply", HandleApplyRemediation)
app.MapGet("/v1/advisory-ai/remediation/status/{prId}", HandleRemediationStatus)
.RequireRateLimiting("advisory-ai");
// Policy Studio endpoints (SPRINT_20251226_017_AI_policy_copilot)
app.MapPost("/v1/advisory-ai/policy/studio/parse", HandlePolicyParse)
.RequireRateLimiting("advisory-ai");
app.MapPost("/v1/advisory-ai/policy/studio/generate", HandlePolicyGenerate)
.RequireRateLimiting("advisory-ai");
app.MapPost("/v1/advisory-ai/policy/studio/validate", HandlePolicyValidate)
.RequireRateLimiting("advisory-ai");
app.MapPost("/v1/advisory-ai/policy/studio/compile", HandlePolicyCompile)
.RequireRateLimiting("advisory-ai");
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerOptions);
@@ -476,6 +490,165 @@ static async Task<IResult> HandleRemediationStatus(
}
}
static bool EnsurePolicyAuthorized(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))
{
return false;
}
var allowed = scopes
.SelectMany(value => value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return allowed.Contains("advisory:run") || allowed.Contains("policy:write");
}
// POLICY-16: POST /v1/advisory-ai/policy/studio/parse
static async Task<IResult> HandlePolicyParse(
HttpContext httpContext,
PolicyParseApiRequest request,
IPolicyIntentParser intentParser,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.policy_parse", ActivityKind.Server);
activity?.SetTag("advisory.input_length", request.Input.Length);
if (!EnsurePolicyAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
try
{
var result = await intentParser.ParseAsync(request.Input, request.ToContext(), cancellationToken).ConfigureAwait(false);
activity?.SetTag("advisory.intent_id", result.Intent.IntentId);
activity?.SetTag("advisory.confidence", result.Intent.Confidence);
return Results.Ok(PolicyParseApiResponse.FromDomain(result));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
// POLICY-17: POST /v1/advisory-ai/policy/studio/generate
static async Task<IResult> HandlePolicyGenerate(
HttpContext httpContext,
PolicyGenerateApiRequest request,
IPolicyIntentStore intentStore,
IPolicyRuleGenerator ruleGenerator,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.policy_generate", ActivityKind.Server);
activity?.SetTag("advisory.intent_id", request.IntentId);
if (!EnsurePolicyAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
var intent = await intentStore.GetAsync(request.IntentId, cancellationToken).ConfigureAwait(false);
if (intent is null)
{
return Results.NotFound(new { error = $"Intent {request.IntentId} not found" });
}
try
{
var result = await ruleGenerator.GenerateAsync(intent, cancellationToken).ConfigureAwait(false);
activity?.SetTag("advisory.rule_count", result.Rules.Count);
return Results.Ok(RuleGenerationApiResponse.FromDomain(result));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
// POLICY-18: POST /v1/advisory-ai/policy/studio/validate
static async Task<IResult> HandlePolicyValidate(
HttpContext httpContext,
PolicyValidateApiRequest request,
IPolicyRuleGenerator ruleGenerator,
ITestCaseSynthesizer testSynthesizer,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.policy_validate", ActivityKind.Server);
activity?.SetTag("advisory.rule_count", request.RuleIds.Count);
if (!EnsurePolicyAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
// In a real implementation, we would fetch rules from storage
// For now, return a mock validation result
var validation = new RuleValidationResult
{
Valid = true,
Conflicts = Array.Empty<RuleConflict>(),
UnreachableConditions = Array.Empty<string>(),
PotentialLoops = Array.Empty<string>(),
Coverage = 0.85
};
return Results.Ok(new ValidationApiResponse
{
Valid = validation.Valid,
Conflicts = validation.Conflicts.Select(c => new RuleConflictApiResponse
{
RuleId1 = c.RuleId1,
RuleId2 = c.RuleId2,
Description = c.Description,
SuggestedResolution = c.SuggestedResolution,
Severity = c.Severity
}).ToList(),
UnreachableConditions = validation.UnreachableConditions.ToList(),
PotentialLoops = validation.PotentialLoops.ToList(),
Coverage = validation.Coverage,
TestCases = Array.Empty<PolicyTestCaseApiResponse>(),
TestResults = null
});
}
// POLICY-19: POST /v1/advisory-ai/policy/studio/compile
static Task<IResult> HandlePolicyCompile(
HttpContext httpContext,
PolicyCompileApiRequest request,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.policy_compile", ActivityKind.Server);
activity?.SetTag("advisory.bundle_name", request.BundleName);
activity?.SetTag("advisory.rule_count", request.RuleIds.Count);
if (!EnsurePolicyAuthorized(httpContext))
{
return Task.FromResult(Results.StatusCode(StatusCodes.Status403Forbidden));
}
// In a real implementation, this would compile rules into a PolicyBundle
var bundleId = $"bundle:{Guid.NewGuid():N}";
var now = DateTime.UtcNow;
var response = new PolicyBundleApiResponse
{
BundleId = bundleId,
BundleName = request.BundleName,
Version = "1.0.0",
RuleCount = request.RuleIds.Count,
CompiledAt = now.ToString("O"),
ContentHash = $"sha256:{Guid.NewGuid():N}",
SignatureId = null // Would be signed in production
};
return Task.FromResult(Results.Ok(response));
}
internal sealed record PipelinePlanRequest(
AdvisoryTaskType? TaskType,
string AdvisoryKey,