FUll implementation plan (first draft)

This commit is contained in:
master
2025-10-19 00:28:48 +03:00
parent 052da7a7d0
commit 8dc7273e27
125 changed files with 5438 additions and 166 deletions

View File

@@ -4,6 +4,8 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text;
@@ -340,6 +342,310 @@ internal static class CommandHandlers
}
}
public static Task HandleExcititorInitAsync(
IServiceProvider services,
IReadOnlyList<string> providers,
bool resume,
bool verbose,
CancellationToken cancellationToken)
{
var normalizedProviders = NormalizeProviders(providers);
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
if (normalizedProviders.Count > 0)
{
payload["providers"] = normalizedProviders;
}
if (resume)
{
payload["resume"] = true;
}
return ExecuteExcititorCommandAsync(
services,
commandName: "excititor init",
verbose,
new Dictionary<string, object?>
{
["providers"] = normalizedProviders.Count,
["resume"] = resume
},
client => client.ExecuteExcititorOperationAsync("init", HttpMethod.Post, RemoveNullValues(payload), cancellationToken),
cancellationToken);
}
public static Task HandleExcititorPullAsync(
IServiceProvider services,
IReadOnlyList<string> providers,
DateTimeOffset? since,
TimeSpan? window,
bool force,
bool verbose,
CancellationToken cancellationToken)
{
var normalizedProviders = NormalizeProviders(providers);
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
if (normalizedProviders.Count > 0)
{
payload["providers"] = normalizedProviders;
}
if (since.HasValue)
{
payload["since"] = since.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture);
}
if (window.HasValue)
{
payload["window"] = window.Value.ToString("c", CultureInfo.InvariantCulture);
}
if (force)
{
payload["force"] = true;
}
return ExecuteExcititorCommandAsync(
services,
commandName: "excititor pull",
verbose,
new Dictionary<string, object?>
{
["providers"] = normalizedProviders.Count,
["force"] = force,
["since"] = since?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
["window"] = window?.ToString("c", CultureInfo.InvariantCulture)
},
client => client.ExecuteExcititorOperationAsync("ingest/run", HttpMethod.Post, RemoveNullValues(payload), cancellationToken),
cancellationToken);
}
public static Task HandleExcititorResumeAsync(
IServiceProvider services,
IReadOnlyList<string> providers,
string? checkpoint,
bool verbose,
CancellationToken cancellationToken)
{
var normalizedProviders = NormalizeProviders(providers);
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
if (normalizedProviders.Count > 0)
{
payload["providers"] = normalizedProviders;
}
if (!string.IsNullOrWhiteSpace(checkpoint))
{
payload["checkpoint"] = checkpoint.Trim();
}
return ExecuteExcititorCommandAsync(
services,
commandName: "excititor resume",
verbose,
new Dictionary<string, object?>
{
["providers"] = normalizedProviders.Count,
["checkpoint"] = checkpoint
},
client => client.ExecuteExcititorOperationAsync("ingest/resume", HttpMethod.Post, RemoveNullValues(payload), cancellationToken),
cancellationToken);
}
public static async Task HandleExcititorListProvidersAsync(
IServiceProvider services,
bool includeDisabled,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("excititor-list-providers");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
var previousLevel = verbosity.MinimumLevel;
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
using var activity = CliActivitySource.Instance.StartActivity("cli.excititor.list-providers", ActivityKind.Client);
activity?.SetTag("stellaops.cli.command", "excititor list-providers");
activity?.SetTag("stellaops.cli.include_disabled", includeDisabled);
using var duration = CliMetrics.MeasureCommandDuration("excititor list-providers");
try
{
var providers = await client.GetExcititorProvidersAsync(includeDisabled, cancellationToken).ConfigureAwait(false);
Environment.ExitCode = 0;
logger.LogInformation("Providers returned: {Count}", providers.Count);
if (providers.Count > 0)
{
if (AnsiConsole.Profile.Capabilities.Interactive)
{
var table = new Table().Border(TableBorder.Rounded).AddColumns("Provider", "Kind", "Trust", "Enabled", "Last Ingested");
foreach (var provider in providers)
{
table.AddRow(
provider.Id,
provider.Kind,
string.IsNullOrWhiteSpace(provider.TrustTier) ? "-" : provider.TrustTier,
provider.Enabled ? "yes" : "no",
provider.LastIngestedAt?.ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture) ?? "unknown");
}
AnsiConsole.Write(table);
}
else
{
foreach (var provider in providers)
{
logger.LogInformation("{ProviderId} [{Kind}] Enabled={Enabled} Trust={Trust} LastIngested={LastIngested}",
provider.Id,
provider.Kind,
provider.Enabled ? "yes" : "no",
string.IsNullOrWhiteSpace(provider.TrustTier) ? "-" : provider.TrustTier,
provider.LastIngestedAt?.ToString("O", CultureInfo.InvariantCulture) ?? "unknown");
}
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to list Excititor providers.");
Environment.ExitCode = 1;
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
public static Task HandleExcititorExportAsync(
IServiceProvider services,
string format,
bool delta,
string? scope,
DateTimeOffset? since,
string? provider,
bool verbose,
CancellationToken cancellationToken)
{
var payload = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["format"] = string.IsNullOrWhiteSpace(format) ? "openvex" : format.Trim(),
["delta"] = delta
};
if (!string.IsNullOrWhiteSpace(scope))
{
payload["scope"] = scope.Trim();
}
if (since.HasValue)
{
payload["since"] = since.Value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture);
}
if (!string.IsNullOrWhiteSpace(provider))
{
payload["provider"] = provider.Trim();
}
return ExecuteExcititorCommandAsync(
services,
commandName: "excititor export",
verbose,
new Dictionary<string, object?>
{
["format"] = payload["format"],
["delta"] = delta,
["scope"] = scope,
["since"] = since?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
["provider"] = provider
},
client => client.ExecuteExcititorOperationAsync("export", HttpMethod.Post, RemoveNullValues(payload), cancellationToken),
cancellationToken);
}
public static Task HandleExcititorVerifyAsync(
IServiceProvider services,
string? exportId,
string? digest,
string? attestationPath,
bool verbose,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(exportId) && string.IsNullOrWhiteSpace(digest) && string.IsNullOrWhiteSpace(attestationPath))
{
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("excititor-verify");
logger.LogError("At least one of --export-id, --digest, or --attestation must be provided.");
Environment.ExitCode = 1;
return Task.CompletedTask;
}
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
if (!string.IsNullOrWhiteSpace(exportId))
{
payload["exportId"] = exportId.Trim();
}
if (!string.IsNullOrWhiteSpace(digest))
{
payload["digest"] = digest.Trim();
}
if (!string.IsNullOrWhiteSpace(attestationPath))
{
var fullPath = Path.GetFullPath(attestationPath);
if (!File.Exists(fullPath))
{
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("excititor-verify");
logger.LogError("Attestation file not found at {Path}.", fullPath);
Environment.ExitCode = 1;
return Task.CompletedTask;
}
var bytes = File.ReadAllBytes(fullPath);
payload["attestation"] = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["fileName"] = Path.GetFileName(fullPath),
["base64"] = Convert.ToBase64String(bytes)
};
}
return ExecuteExcititorCommandAsync(
services,
commandName: "excititor verify",
verbose,
new Dictionary<string, object?>
{
["export_id"] = exportId,
["digest"] = digest,
["attestation_path"] = attestationPath
},
client => client.ExecuteExcititorOperationAsync("verify", HttpMethod.Post, RemoveNullValues(payload), cancellationToken),
cancellationToken);
}
public static Task HandleExcititorReconcileAsync(
IServiceProvider services,
IReadOnlyList<string> providers,
TimeSpan? maxAge,
bool verbose,
CancellationToken cancellationToken)
{
var normalizedProviders = NormalizeProviders(providers);
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
if (normalizedProviders.Count > 0)
{
payload["providers"] = normalizedProviders;
}
if (maxAge.HasValue)
{
payload["maxAge"] = maxAge.Value.ToString("c", CultureInfo.InvariantCulture);
}
return ExecuteExcititorCommandAsync(
services,
commandName: "excititor reconcile",
verbose,
new Dictionary<string, object?>
{
["providers"] = normalizedProviders.Count,
["max_age"] = maxAge?.ToString("c", CultureInfo.InvariantCulture)
},
client => client.ExecuteExcititorOperationAsync("reconcile", HttpMethod.Post, RemoveNullValues(payload), cancellationToken),
cancellationToken);
}
public static async Task HandleAuthLoginAsync(
IServiceProvider services,
StellaOpsCliOptions options,
@@ -1111,12 +1417,109 @@ internal static class CommandHandlers
"jti"
};
private static async Task ExecuteExcititorCommandAsync(
IServiceProvider services,
string commandName,
bool verbose,
IDictionary<string, object?>? activityTags,
Func<IBackendOperationsClient, Task<ExcititorOperationResult>> operation,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(commandName.Replace(' ', '-'));
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
var previousLevel = verbosity.MinimumLevel;
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
using var activity = CliActivitySource.Instance.StartActivity($"cli.{commandName.Replace(' ', '.')}" , ActivityKind.Client);
activity?.SetTag("stellaops.cli.command", commandName);
if (activityTags is not null)
{
foreach (var tag in activityTags)
{
activity?.SetTag(tag.Key, tag.Value);
}
}
using var duration = CliMetrics.MeasureCommandDuration(commandName);
try
{
var result = await operation(client).ConfigureAwait(false);
if (result.Success)
{
if (!string.IsNullOrWhiteSpace(result.Message))
{
logger.LogInformation(result.Message);
}
else
{
logger.LogInformation("Operation completed successfully.");
}
if (!string.IsNullOrWhiteSpace(result.Location))
{
logger.LogInformation("Location: {Location}", result.Location);
}
if (result.Payload is JsonElement payload && payload.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null)
{
logger.LogDebug("Response payload: {Payload}", payload.ToString());
}
Environment.ExitCode = 0;
}
else
{
logger.LogError(string.IsNullOrWhiteSpace(result.Message) ? "Operation failed." : result.Message);
Environment.ExitCode = 1;
}
}
catch (Exception ex)
{
logger.LogError(ex, "Excititor operation failed.");
Environment.ExitCode = 1;
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
private static IReadOnlyList<string> NormalizeProviders(IReadOnlyList<string> providers)
{
if (providers is null || providers.Count == 0)
{
return Array.Empty<string>();
}
var list = new List<string>();
foreach (var provider in providers)
{
if (!string.IsNullOrWhiteSpace(provider))
{
list.Add(provider.Trim());
}
}
return list.Count == 0 ? Array.Empty<string>() : list;
}
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())
{
source.Remove(key);
}
return source;
}
private static async Task TriggerJobAsync(
IBackendOperationsClient client,
ILogger logger,
string jobKind,
IDictionary<string, object?> parameters,
CancellationToken cancellationToken)
IDictionary<string, object?> parameters,
CancellationToken cancellationToken)
{
JobTriggerResult result = await client.TriggerJobAsync(jobKind, parameters, cancellationToken).ConfigureAwait(false);
if (result.Success)