Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using StellaOps.AdvisoryAI.PolicyStudio;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// API request for parsing natural language to policy intent.
|
||||
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
|
||||
/// Task: POLICY-16
|
||||
/// </summary>
|
||||
public sealed record PolicyParseApiRequest
|
||||
{
|
||||
[Required]
|
||||
[MinLength(10)]
|
||||
public required string Input { get; init; }
|
||||
|
||||
public string? DefaultScope { get; init; }
|
||||
public string? OrganizationId { get; init; }
|
||||
public string? PreferredFormat { get; init; }
|
||||
|
||||
public PolicyParseContext ToContext() => new()
|
||||
{
|
||||
DefaultScope = DefaultScope,
|
||||
OrganizationId = OrganizationId,
|
||||
PreferredFormat = PreferredFormat
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API response for policy parse result.
|
||||
/// </summary>
|
||||
public sealed record PolicyParseApiResponse
|
||||
{
|
||||
public required PolicyIntentApiResponse Intent { get; init; }
|
||||
public required bool Success { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
public required string ModelId { get; init; }
|
||||
public required string ParsedAt { get; init; }
|
||||
|
||||
public static PolicyParseApiResponse FromDomain(PolicyParseResult result) => new()
|
||||
{
|
||||
Intent = PolicyIntentApiResponse.FromDomain(result.Intent),
|
||||
Success = result.Success,
|
||||
ErrorMessage = result.ErrorMessage,
|
||||
ModelId = result.ModelId,
|
||||
ParsedAt = result.ParsedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API representation of policy intent.
|
||||
/// </summary>
|
||||
public sealed record PolicyIntentApiResponse
|
||||
{
|
||||
public required string IntentId { get; init; }
|
||||
public required string IntentType { get; init; }
|
||||
public required string OriginalInput { get; init; }
|
||||
public required IReadOnlyList<PolicyConditionApiResponse> Conditions { get; init; }
|
||||
public required IReadOnlyList<PolicyActionApiResponse> Actions { get; init; }
|
||||
public required string Scope { get; init; }
|
||||
public string? ScopeId { get; init; }
|
||||
public required int Priority { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public IReadOnlyList<string>? ClarifyingQuestions { get; init; }
|
||||
|
||||
public static PolicyIntentApiResponse FromDomain(PolicyIntent intent) => new()
|
||||
{
|
||||
IntentId = intent.IntentId,
|
||||
IntentType = intent.IntentType.ToString(),
|
||||
OriginalInput = intent.OriginalInput,
|
||||
Conditions = intent.Conditions.Select(c => new PolicyConditionApiResponse
|
||||
{
|
||||
Field = c.Field,
|
||||
Operator = c.Operator,
|
||||
Value = c.Value,
|
||||
Connector = c.Connector
|
||||
}).ToList(),
|
||||
Actions = intent.Actions.Select(a => new PolicyActionApiResponse
|
||||
{
|
||||
ActionType = a.ActionType,
|
||||
Parameters = a.Parameters
|
||||
}).ToList(),
|
||||
Scope = intent.Scope,
|
||||
ScopeId = intent.ScopeId,
|
||||
Priority = intent.Priority,
|
||||
Confidence = intent.Confidence,
|
||||
ClarifyingQuestions = intent.ClarifyingQuestions
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record PolicyConditionApiResponse
|
||||
{
|
||||
public required string Field { get; init; }
|
||||
public required string Operator { get; init; }
|
||||
public required object Value { get; init; }
|
||||
public string? Connector { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PolicyActionApiResponse
|
||||
{
|
||||
public required string ActionType { get; init; }
|
||||
public required IReadOnlyDictionary<string, object> Parameters { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API request for generating rules from intent.
|
||||
/// Task: POLICY-17
|
||||
/// </summary>
|
||||
public sealed record PolicyGenerateApiRequest
|
||||
{
|
||||
[Required]
|
||||
public required string IntentId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API response for rule generation.
|
||||
/// </summary>
|
||||
public sealed record RuleGenerationApiResponse
|
||||
{
|
||||
public required IReadOnlyList<LatticeRuleApiResponse> Rules { get; init; }
|
||||
public required bool Success { get; init; }
|
||||
public required IReadOnlyList<string> Warnings { get; init; }
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
public required string IntentId { get; init; }
|
||||
public required string GeneratedAt { get; init; }
|
||||
|
||||
public static RuleGenerationApiResponse FromDomain(RuleGenerationResult result) => new()
|
||||
{
|
||||
Rules = result.Rules.Select(r => new LatticeRuleApiResponse
|
||||
{
|
||||
RuleId = r.RuleId,
|
||||
Name = r.Name,
|
||||
Description = r.Description,
|
||||
LatticeExpression = r.LatticeExpression,
|
||||
Disposition = r.Disposition,
|
||||
Priority = r.Priority,
|
||||
Scope = r.Scope,
|
||||
Enabled = r.Enabled
|
||||
}).ToList(),
|
||||
Success = result.Success,
|
||||
Warnings = result.Warnings,
|
||||
Errors = result.Errors,
|
||||
IntentId = result.IntentId,
|
||||
GeneratedAt = result.GeneratedAt
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record LatticeRuleApiResponse
|
||||
{
|
||||
public required string RuleId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string LatticeExpression { get; init; }
|
||||
public required string Disposition { get; init; }
|
||||
public required int Priority { get; init; }
|
||||
public required string Scope { get; init; }
|
||||
public bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API request for validating rules.
|
||||
/// Task: POLICY-18
|
||||
/// </summary>
|
||||
public sealed record PolicyValidateApiRequest
|
||||
{
|
||||
[Required]
|
||||
public required IReadOnlyList<string> RuleIds { get; init; }
|
||||
|
||||
public IReadOnlyList<string>? ExistingRuleIds { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API response for validation result.
|
||||
/// </summary>
|
||||
public sealed record ValidationApiResponse
|
||||
{
|
||||
public required bool Valid { get; init; }
|
||||
public required IReadOnlyList<RuleConflictApiResponse> Conflicts { get; init; }
|
||||
public required IReadOnlyList<string> UnreachableConditions { get; init; }
|
||||
public required IReadOnlyList<string> PotentialLoops { get; init; }
|
||||
public required double Coverage { get; init; }
|
||||
public required IReadOnlyList<PolicyTestCaseApiResponse> TestCases { get; init; }
|
||||
public TestRunApiResponse? TestResults { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RuleConflictApiResponse
|
||||
{
|
||||
public required string RuleId1 { get; init; }
|
||||
public required string RuleId2 { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string SuggestedResolution { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PolicyTestCaseApiResponse
|
||||
{
|
||||
public required string TestCaseId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required IReadOnlyDictionary<string, object> Input { get; init; }
|
||||
public required string ExpectedDisposition { get; init; }
|
||||
public required string Description { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestRunApiResponse
|
||||
{
|
||||
public required int Total { get; init; }
|
||||
public required int Passed { get; init; }
|
||||
public required int Failed { get; init; }
|
||||
public required bool Success { get; init; }
|
||||
public required string RunAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API request for compiling policy bundle.
|
||||
/// Task: POLICY-19
|
||||
/// </summary>
|
||||
public sealed record PolicyCompileApiRequest
|
||||
{
|
||||
[Required]
|
||||
public required IReadOnlyList<string> RuleIds { get; init; }
|
||||
|
||||
[Required]
|
||||
public required string BundleName { get; init; }
|
||||
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API response for compiled policy bundle.
|
||||
/// </summary>
|
||||
public sealed record PolicyBundleApiResponse
|
||||
{
|
||||
public required string BundleId { get; init; }
|
||||
public required string BundleName { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required int RuleCount { get; init; }
|
||||
public required string CompiledAt { get; init; }
|
||||
public required string ContentHash { get; init; }
|
||||
public string? SignatureId { get; init; }
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user