Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added MongoPackRunApprovalStore for managing approval states with MongoDB.
- Introduced MongoPackRunArtifactUploader for uploading and storing artifacts.
- Created MongoPackRunLogStore to handle logging of pack run events.
- Developed MongoPackRunStateStore for persisting and retrieving pack run states.
- Implemented unit tests for MongoDB stores to ensure correct functionality.
- Added MongoTaskRunnerTestContext for setting up MongoDB test environment.
- Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -24,9 +24,10 @@ using Spectre.Console.Rendering;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Prompts;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Telemetry;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
@@ -426,14 +427,154 @@ internal static class CommandHandlers
{
verbosity.MinimumLevel = previousLevel;
}
}
public static async Task HandleSourcesIngestAsync(
IServiceProvider services,
bool dryRun,
string source,
string input,
}
public static async Task HandleAdviseRunAsync(
IServiceProvider services,
AdvisoryAiTaskType taskType,
string advisoryKey,
string? artifactId,
string? artifactPurl,
string? policyVersion,
string profile,
IReadOnlyList<string> preferredSections,
bool forceRefresh,
int timeoutSeconds,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("advise-run");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
var previousLevel = verbosity.MinimumLevel;
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
using var activity = CliActivitySource.Instance.StartActivity("cli.advisory.run", ActivityKind.Client);
activity?.SetTag("stellaops.cli.command", "advise run");
activity?.SetTag("stellaops.cli.task", taskType.ToString());
using var duration = CliMetrics.MeasureCommandDuration("advisory run");
activity?.SetTag("stellaops.cli.force_refresh", forceRefresh);
var outcome = "error";
try
{
var normalizedKey = advisoryKey?.Trim();
if (string.IsNullOrWhiteSpace(normalizedKey))
{
throw new ArgumentException("Advisory key is required.", nameof(advisoryKey));
}
activity?.SetTag("stellaops.cli.advisory.key", normalizedKey);
var normalizedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile.Trim();
activity?.SetTag("stellaops.cli.profile", normalizedProfile);
var normalizedSections = NormalizeSections(preferredSections);
var request = new AdvisoryPipelinePlanRequestModel
{
TaskType = taskType,
AdvisoryKey = normalizedKey,
ArtifactId = string.IsNullOrWhiteSpace(artifactId) ? null : artifactId!.Trim(),
ArtifactPurl = string.IsNullOrWhiteSpace(artifactPurl) ? null : artifactPurl!.Trim(),
PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion!.Trim(),
Profile = normalizedProfile,
PreferredSections = normalizedSections.Length > 0 ? normalizedSections : null,
ForceRefresh = forceRefresh
};
logger.LogInformation("Requesting advisory plan for {TaskType} (advisory={AdvisoryKey}).", taskType, normalizedKey);
var plan = await client.CreateAdvisoryPipelinePlanAsync(taskType, request, cancellationToken).ConfigureAwait(false);
activity?.SetTag("stellaops.cli.advisory.cache_key", plan.CacheKey);
RenderAdvisoryPlan(plan);
logger.LogInformation("Plan {CacheKey} queued with {Chunks} chunks and {Vectors} vectors.",
plan.CacheKey,
plan.Chunks.Count,
plan.Vectors.Count);
var pollDelay = TimeSpan.FromSeconds(1);
var shouldWait = timeoutSeconds > 0;
var deadline = shouldWait ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(timeoutSeconds) : DateTimeOffset.UtcNow;
AdvisoryPipelineOutputModel? output = null;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
output = await client
.TryGetAdvisoryPipelineOutputAsync(plan.CacheKey, taskType, normalizedProfile, cancellationToken)
.ConfigureAwait(false);
if (output is not null)
{
break;
}
if (!shouldWait || DateTimeOffset.UtcNow >= deadline)
{
break;
}
logger.LogDebug("Advisory output pending for {CacheKey}; retrying in {DelaySeconds}s.", plan.CacheKey, pollDelay.TotalSeconds);
await Task.Delay(pollDelay, cancellationToken).ConfigureAwait(false);
}
if (output is null)
{
logger.LogError("Timed out after {Timeout}s waiting for advisory output (cache key {CacheKey}).",
Math.Max(timeoutSeconds, 0),
plan.CacheKey);
activity?.SetStatus(ActivityStatusCode.Error, "timeout");
outcome = "timeout";
Environment.ExitCode = Environment.ExitCode == 0 ? 70 : Environment.ExitCode;
return;
}
activity?.SetTag("stellaops.cli.advisory.generated_at", output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture));
activity?.SetTag("stellaops.cli.advisory.cache_hit", output.PlanFromCache);
logger.LogInformation("Advisory output ready (cache key {CacheKey}).", output.CacheKey);
RenderAdvisoryOutput(output);
if (output.Guardrail.Blocked)
{
logger.LogError("Guardrail blocked advisory output (cache key {CacheKey}).", output.CacheKey);
activity?.SetStatus(ActivityStatusCode.Error, "guardrail_blocked");
outcome = "blocked";
Environment.ExitCode = Environment.ExitCode == 0 ? 65 : Environment.ExitCode;
return;
}
activity?.SetStatus(ActivityStatusCode.Ok);
outcome = output.PlanFromCache ? "cache-hit" : "ok";
Environment.ExitCode = 0;
}
catch (OperationCanceledException)
{
outcome = "cancelled";
activity?.SetStatus(ActivityStatusCode.Error, "cancelled");
Environment.ExitCode = Environment.ExitCode == 0 ? 130 : Environment.ExitCode;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
logger.LogError(ex, "Failed to run advisory task.");
outcome = "error";
Environment.ExitCode = Environment.ExitCode == 0 ? 1 : Environment.ExitCode;
}
finally
{
activity?.SetTag("stellaops.cli.advisory.outcome", outcome);
CliMetrics.RecordAdvisoryRun(taskType.ToString(), outcome);
verbosity.MinimumLevel = previousLevel;
}
}
public static async Task HandleSourcesIngestAsync(
IServiceProvider services,
bool dryRun,
string source,
string input,
string? tenantOverride,
string format,
bool disableColor,
@@ -6137,7 +6278,156 @@ internal static class CommandHandlers
["ERR_AOC_007"] = 17
};
private static IDictionary<string, object?> RemoveNullValues(Dictionary<string, object?> source)
private static string[] NormalizeSections(IReadOnlyList<string> sections)
{
if (sections is null || sections.Count == 0)
{
return Array.Empty<string>();
}
return sections
.Where(section => !string.IsNullOrWhiteSpace(section))
.Select(section => section.Trim())
.Where(section => section.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static void RenderAdvisoryPlan(AdvisoryPipelinePlanResponseModel plan)
{
var console = AnsiConsole.Console;
var summary = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Advisory Plan[/]");
summary.AddColumn("Field");
summary.AddColumn("Value");
summary.AddRow("Task", Markup.Escape(plan.TaskType));
summary.AddRow("Cache Key", Markup.Escape(plan.CacheKey));
summary.AddRow("Prompt Template", Markup.Escape(plan.PromptTemplate));
summary.AddRow("Chunks", plan.Chunks.Count.ToString(CultureInfo.InvariantCulture));
summary.AddRow("Vectors", plan.Vectors.Count.ToString(CultureInfo.InvariantCulture));
summary.AddRow("Prompt Tokens", plan.Budget.PromptTokens.ToString(CultureInfo.InvariantCulture));
summary.AddRow("Completion Tokens", plan.Budget.CompletionTokens.ToString(CultureInfo.InvariantCulture));
console.Write(summary);
if (plan.Metadata.Count > 0)
{
console.Write(CreateKeyValueTable("Plan Metadata", plan.Metadata));
}
}
private static void RenderAdvisoryOutput(AdvisoryPipelineOutputModel output)
{
var console = AnsiConsole.Console;
var summary = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Advisory Output[/]");
summary.AddColumn("Field");
summary.AddColumn("Value");
summary.AddRow("Cache Key", Markup.Escape(output.CacheKey));
summary.AddRow("Task", Markup.Escape(output.TaskType));
summary.AddRow("Profile", Markup.Escape(output.Profile));
summary.AddRow("Generated", output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture));
summary.AddRow("Plan From Cache", output.PlanFromCache ? "yes" : "no");
summary.AddRow("Citations", output.Citations.Count.ToString(CultureInfo.InvariantCulture));
summary.AddRow("Guardrail Blocked", output.Guardrail.Blocked ? "[red]yes[/]" : "no");
console.Write(summary);
if (!string.IsNullOrWhiteSpace(output.Prompt))
{
var panel = new Panel(new Markup(Markup.Escape(output.Prompt)))
{
Header = new PanelHeader("Prompt"),
Border = BoxBorder.Rounded,
Expand = true
};
console.Write(panel);
}
if (output.Citations.Count > 0)
{
var citations = new Table()
.Border(TableBorder.Minimal)
.Title("[grey]Citations[/]");
citations.AddColumn("Index");
citations.AddColumn("Document");
citations.AddColumn("Chunk");
foreach (var citation in output.Citations.OrderBy(c => c.Index))
{
citations.AddRow(
citation.Index.ToString(CultureInfo.InvariantCulture),
Markup.Escape(citation.DocumentId),
Markup.Escape(citation.ChunkId));
}
console.Write(citations);
}
if (output.Metadata.Count > 0)
{
console.Write(CreateKeyValueTable("Output Metadata", output.Metadata));
}
if (output.Guardrail.Metadata.Count > 0)
{
console.Write(CreateKeyValueTable("Guardrail Metadata", output.Guardrail.Metadata));
}
if (output.Guardrail.Violations.Count > 0)
{
var violations = new Table()
.Border(TableBorder.Minimal)
.Title("[red]Guardrail Violations[/]");
violations.AddColumn("Code");
violations.AddColumn("Message");
foreach (var violation in output.Guardrail.Violations)
{
violations.AddRow(Markup.Escape(violation.Code), Markup.Escape(violation.Message));
}
console.Write(violations);
}
var provenance = new Table()
.Border(TableBorder.Minimal)
.Title("[grey]Provenance[/]");
provenance.AddColumn("Field");
provenance.AddColumn("Value");
provenance.AddRow("Input Digest", Markup.Escape(output.Provenance.InputDigest));
provenance.AddRow("Output Hash", Markup.Escape(output.Provenance.OutputHash));
var signatures = output.Provenance.Signatures.Count == 0
? "none"
: string.Join(Environment.NewLine, output.Provenance.Signatures.Select(Markup.Escape));
provenance.AddRow("Signatures", signatures);
console.Write(provenance);
}
private static Table CreateKeyValueTable(string title, IReadOnlyDictionary<string, string> entries)
{
var table = new Table()
.Border(TableBorder.Minimal)
.Title($"[grey]{Markup.Escape(title)}[/]");
table.AddColumn("Key");
table.AddColumn("Value");
foreach (var kvp in entries.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
table.AddRow(Markup.Escape(kvp.Key), Markup.Escape(kvp.Value));
}
return table;
}
private static IDictionary<string, object?> RemoveNullValues(Dictionary<string, object?> source)
{
foreach (var key in source.Where(kvp => kvp.Value is null).Select(kvp => kvp.Key).ToList())
{