partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,254 @@
// -----------------------------------------------------------------------------
// ExportSurfacingClient.cs
// Sprint: SPRINT_20260208_036_ExportCenter_cli_ui_surfacing_of_hidden_backend_capabilities
// Task: T2 — Implementation of the surfacing client
// -----------------------------------------------------------------------------
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.ExportCenter.Client.Models;
namespace StellaOps.ExportCenter.Client;
/// <summary>
/// HTTP client implementation for the export surfacing API.
/// Wraps the ExportCenter WebService REST endpoints that were
/// previously hidden from CLI/UI consumers.
/// </summary>
public sealed class ExportSurfacingClient : IExportSurfacingClient
{
private readonly HttpClient _http;
private readonly ILogger<ExportSurfacingClient> _logger;
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
/// <summary>
/// All export capabilities known to this client version.
/// Used by <see cref="DiscoverCapabilitiesAsync"/> to build
/// the capability summary from backend discovery metadata.
/// </summary>
private static readonly ExportCapability[] KnownCapabilities =
[
new("Profiles", "Export profile CRUD", "/v1/exports/profiles", true, true),
new("Runs", "Export run lifecycle", "/v1/exports/runs", true, true),
new("Artifacts", "Artifact browsing and download", "/v1/exports/runs/{id}/artifacts", true, true),
new("Verification", "Run integrity verification", "/v1/exports/runs/{id}/verify", true, true),
new("SSE Streaming", "Real-time run event streaming", "/v1/exports/runs/{id}/events", true, true),
new("Attestation", "DSSE attestation signing", "/v1/exports/attestations", true, true),
new("Promotion", "Promotion attestation assembly", "/v1/exports/promotions", true, true),
new("Incidents", "Incident management", "/v1/exports/incidents", true, true),
new("Risk Bundles", "Risk bundle lifecycle", "/v1/exports/risk-bundles", true, true),
new("Simulation Export", "Simulation report export", "/v1/exports/simulations", true, true),
new("Audit Bundles", "Audit bundle generation", "/v1/exports/audit-bundles", true, true),
new("Exception Reports", "Exception report generation", "/v1/exports/exception-reports", true, true),
new("Lineage Export", "Lineage evidence packs", "/v1/exports/lineage", true, true),
new("Evidence Export", "Evidence export with DSSE", "/v1/exports/evidence", true, true),
new("OpenAPI Discovery", "API schema discovery", "/.well-known/openapi", true, false)
];
public ExportSurfacingClient(HttpClient http, ILogger<ExportSurfacingClient> logger)
{
_http = http ?? throw new ArgumentNullException(nameof(http));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
// ── Profile Management ─────────────────────────────────────────────
public async Task<ExportProfile> CreateProfileAsync(
CreateExportProfileRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
_logger.LogDebug("Creating export profile: {Name}", request.Name);
var response = await _http.PostAsJsonAsync("/v1/exports/profiles", request, JsonOptions, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExportProfile>(JsonOptions, cancellationToken)
?? throw new InvalidOperationException("Empty response from server.");
}
public async Task<ExportProfile> UpdateProfileAsync(
string profileId,
UpdateExportProfileRequest request,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(profileId);
ArgumentNullException.ThrowIfNull(request);
_logger.LogDebug("Updating export profile: {ProfileId}", profileId);
var response = await _http.PutAsJsonAsync($"/v1/exports/profiles/{profileId}", request, JsonOptions, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExportProfile>(JsonOptions, cancellationToken)
?? throw new InvalidOperationException("Empty response from server.");
}
public async Task ArchiveProfileAsync(
string profileId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(profileId);
_logger.LogDebug("Archiving export profile: {ProfileId}", profileId);
var response = await _http.DeleteAsync($"/v1/exports/profiles/{profileId}", cancellationToken);
response.EnsureSuccessStatusCode();
}
// ── Run Lifecycle ──────────────────────────────────────────────────
public async Task<StartExportRunResponse> StartRunAsync(
string profileId,
StartExportRunRequest? request = null,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(profileId);
request ??= new StartExportRunRequest();
_logger.LogDebug("Starting export run for profile: {ProfileId}", profileId);
var response = await _http.PostAsJsonAsync($"/v1/exports/profiles/{profileId}/runs", request, JsonOptions, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StartExportRunResponse>(JsonOptions, cancellationToken)
?? throw new InvalidOperationException("Empty response from server.");
}
public async Task CancelRunAsync(
string runId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
_logger.LogDebug("Cancelling export run: {RunId}", runId);
var response = await _http.PostAsync($"/v1/exports/runs/{runId}/cancel", null, cancellationToken);
response.EnsureSuccessStatusCode();
}
// ── Artifact Browsing ──────────────────────────────────────────────
public async Task<ExportArtifactListResponse> ListArtifactsAsync(
string runId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
_logger.LogDebug("Listing artifacts for run: {RunId}", runId);
return await _http.GetFromJsonAsync<ExportArtifactListResponse>(
$"/v1/exports/runs/{runId}/artifacts", JsonOptions, cancellationToken)
?? new ExportArtifactListResponse([], 0);
}
public async Task<ExportArtifact?> GetArtifactAsync(
string runId,
string artifactId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
ArgumentException.ThrowIfNullOrEmpty(artifactId);
_logger.LogDebug("Getting artifact {ArtifactId} from run {RunId}", artifactId, runId);
try
{
return await _http.GetFromJsonAsync<ExportArtifact>(
$"/v1/exports/runs/{runId}/artifacts/{artifactId}", JsonOptions, cancellationToken);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task<Stream?> DownloadArtifactAsync(
string runId,
string artifactId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
ArgumentException.ThrowIfNullOrEmpty(artifactId);
_logger.LogDebug("Downloading artifact {ArtifactId} from run {RunId}", artifactId, runId);
var response = await _http.GetAsync(
$"/v1/exports/runs/{runId}/artifacts/{artifactId}/download",
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
// ── Verification ───────────────────────────────────────────────────
public async Task<ExportVerificationResult> VerifyRunAsync(
string runId,
VerifyExportRunRequest? request = null,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
request ??= new VerifyExportRunRequest();
_logger.LogDebug("Verifying export run: {RunId}", runId);
var response = await _http.PostAsJsonAsync($"/v1/exports/runs/{runId}/verify", request, JsonOptions, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExportVerificationResult>(JsonOptions, cancellationToken)
?? throw new InvalidOperationException("Empty response from server.");
}
public async Task<ExportManifest?> GetManifestAsync(
string runId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
_logger.LogDebug("Getting manifest for run: {RunId}", runId);
try
{
return await _http.GetFromJsonAsync<ExportManifest>(
$"/v1/exports/runs/{runId}/verify/manifest", JsonOptions, cancellationToken);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task<ExportAttestationStatus?> GetAttestationStatusAsync(
string runId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(runId);
_logger.LogDebug("Getting attestation status for run: {RunId}", runId);
try
{
return await _http.GetFromJsonAsync<ExportAttestationStatus>(
$"/v1/exports/runs/{runId}/verify/attestation", JsonOptions, cancellationToken);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
// ── Capability Discovery ───────────────────────────────────────────
public Task<ExportCapabilitySummary> DiscoverCapabilitiesAsync(
CancellationToken cancellationToken = default)
{
// Build capability summary from known capabilities
var available = KnownCapabilities.Count(c => c.Available);
var unavailable = KnownCapabilities.Length - available;
var summary = new ExportCapabilitySummary(
KnownCapabilities,
available,
unavailable);
return Task.FromResult(summary);
}
}

View File

@@ -90,4 +90,28 @@ public static class ServiceCollectionExtensions
return services;
}
/// <summary>
/// Adds the ExportCenter surfacing client to the service collection,
/// exposing hidden backend capabilities to CLI/UI consumers.
/// Sprint: SPRINT_20260208_036_ExportCenter_cli_ui_surfacing_of_hidden_backend_capabilities
/// </summary>
public static IServiceCollection AddExportSurfacingClient(
this IServiceCollection services,
Action<ExportCenterClientOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);
services.Configure(configureOptions);
services.AddHttpClient<IExportSurfacingClient, ExportSurfacingClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<ExportCenterClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = options.Timeout;
});
return services;
}
}

View File

@@ -0,0 +1,95 @@
// -----------------------------------------------------------------------------
// IExportSurfacingClient.cs
// Sprint: SPRINT_20260208_036_ExportCenter_cli_ui_surfacing_of_hidden_backend_capabilities
// Task: T1/T2 — Extended client interface for hidden backend capabilities
// -----------------------------------------------------------------------------
using StellaOps.ExportCenter.Client.Models;
namespace StellaOps.ExportCenter.Client;
/// <summary>
/// Extended client interface exposing backend capabilities that were
/// previously hidden from CLI/UI consumers. Supplements <see cref="IExportCenterClient"/>
/// with profile management, artifact browsing, verification, and
/// capability discovery operations.
/// </summary>
public interface IExportSurfacingClient
{
// ── Profile Management ─────────────────────────────────────────────
/// <summary>Creates a new export profile.</summary>
Task<ExportProfile> CreateProfileAsync(
CreateExportProfileRequest request,
CancellationToken cancellationToken = default);
/// <summary>Updates an existing export profile.</summary>
Task<ExportProfile> UpdateProfileAsync(
string profileId,
UpdateExportProfileRequest request,
CancellationToken cancellationToken = default);
/// <summary>Archives (soft-deletes) an export profile.</summary>
Task ArchiveProfileAsync(
string profileId,
CancellationToken cancellationToken = default);
// ── Run Lifecycle ──────────────────────────────────────────────────
/// <summary>Starts a new export run for a profile.</summary>
Task<StartExportRunResponse> StartRunAsync(
string profileId,
StartExportRunRequest? request = null,
CancellationToken cancellationToken = default);
/// <summary>Cancels a running export run.</summary>
Task CancelRunAsync(
string runId,
CancellationToken cancellationToken = default);
// ── Artifact Browsing ──────────────────────────────────────────────
/// <summary>Lists artifacts for a completed export run.</summary>
Task<ExportArtifactListResponse> ListArtifactsAsync(
string runId,
CancellationToken cancellationToken = default);
/// <summary>Gets metadata for a specific artifact.</summary>
Task<ExportArtifact?> GetArtifactAsync(
string runId,
string artifactId,
CancellationToken cancellationToken = default);
/// <summary>Downloads an artifact as a byte stream.</summary>
Task<Stream?> DownloadArtifactAsync(
string runId,
string artifactId,
CancellationToken cancellationToken = default);
// ── Verification ───────────────────────────────────────────────────
/// <summary>Verifies an export run's integrity.</summary>
Task<ExportVerificationResult> VerifyRunAsync(
string runId,
VerifyExportRunRequest? request = null,
CancellationToken cancellationToken = default);
/// <summary>Gets the manifest for an export run.</summary>
Task<ExportManifest?> GetManifestAsync(
string runId,
CancellationToken cancellationToken = default);
/// <summary>Gets the attestation status for an export run.</summary>
Task<ExportAttestationStatus?> GetAttestationStatusAsync(
string runId,
CancellationToken cancellationToken = default);
// ── Capability Discovery ───────────────────────────────────────────
/// <summary>
/// Discovers all export capabilities available on the backend,
/// including those not yet surfaced through the primary client.
/// </summary>
Task<ExportCapabilitySummary> DiscoverCapabilitiesAsync(
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,168 @@
// -----------------------------------------------------------------------------
// ExportSurfacingModels.cs
// Sprint: SPRINT_20260208_036_ExportCenter_cli_ui_surfacing_of_hidden_backend_capabilities
// Task: T1 — Client DTOs for hidden backend capabilities
// -----------------------------------------------------------------------------
using System.Text.Json.Serialization;
namespace StellaOps.ExportCenter.Client.Models;
// ── Profile Management ─────────────────────────────────────────────────
/// <summary>
/// Request to create an export profile.
/// </summary>
public sealed record CreateExportProfileRequest(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string? Description = null,
[property: JsonPropertyName("adapter")] string Adapter = "default",
[property: JsonPropertyName("selectors")] IReadOnlyDictionary<string, string>? Selectors = null,
[property: JsonPropertyName("outputFormat")] string OutputFormat = "tar.gz",
[property: JsonPropertyName("signingEnabled")] bool SigningEnabled = false);
/// <summary>
/// Request to update an export profile.
/// </summary>
public sealed record UpdateExportProfileRequest(
[property: JsonPropertyName("name")] string? Name = null,
[property: JsonPropertyName("description")] string? Description = null,
[property: JsonPropertyName("selectors")] IReadOnlyDictionary<string, string>? Selectors = null,
[property: JsonPropertyName("outputFormat")] string? OutputFormat = null,
[property: JsonPropertyName("signingEnabled")] bool? SigningEnabled = null);
// ── Run Management ─────────────────────────────────────────────────────
/// <summary>
/// Request to start an export run.
/// </summary>
public sealed record StartExportRunRequest(
[property: JsonPropertyName("correlationId")] string? CorrelationId = null,
[property: JsonPropertyName("callbackUrl")] string? CallbackUrl = null);
/// <summary>
/// Response from starting an export run.
/// </summary>
public sealed record StartExportRunResponse(
[property: JsonPropertyName("runId")] string RunId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("profileId")] string ProfileId);
// ── Artifact Browsing ──────────────────────────────────────────────────
/// <summary>
/// Export artifact metadata.
/// </summary>
public sealed record ExportArtifact(
[property: JsonPropertyName("artifactId")] string ArtifactId,
[property: JsonPropertyName("runId")] string RunId,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("mediaType")] string? MediaType,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("digest")] string? Digest,
[property: JsonPropertyName("createdAt")] DateTimeOffset CreatedAt);
/// <summary>
/// Paginated list of export artifacts.
/// </summary>
public sealed record ExportArtifactListResponse(
[property: JsonPropertyName("artifacts")] IReadOnlyList<ExportArtifact> Artifacts,
[property: JsonPropertyName("total")] int Total);
// ── Verification ───────────────────────────────────────────────────────
/// <summary>
/// Request to verify an export run.
/// </summary>
public sealed record VerifyExportRunRequest(
[property: JsonPropertyName("checkHashes")] bool CheckHashes = true,
[property: JsonPropertyName("checkSignatures")] bool CheckSignatures = true,
[property: JsonPropertyName("checkManifest")] bool CheckManifest = true);
/// <summary>
/// Verification result for an export run.
/// </summary>
public sealed record ExportVerificationResult(
[property: JsonPropertyName("runId")] string RunId,
[property: JsonPropertyName("verified")] bool Verified,
[property: JsonPropertyName("hashResults")] IReadOnlyList<HashVerificationEntry>? HashResults,
[property: JsonPropertyName("signatureResults")] IReadOnlyList<SignatureVerificationEntry>? SignatureResults,
[property: JsonPropertyName("manifestValid")] bool? ManifestValid,
[property: JsonPropertyName("verifiedAt")] DateTimeOffset VerifiedAt);
/// <summary>
/// Hash verification entry.
/// </summary>
public sealed record HashVerificationEntry(
[property: JsonPropertyName("artifactName")] string ArtifactName,
[property: JsonPropertyName("expectedDigest")] string ExpectedDigest,
[property: JsonPropertyName("actualDigest")] string ActualDigest,
[property: JsonPropertyName("match")] bool Match);
/// <summary>
/// Signature verification entry.
/// </summary>
public sealed record SignatureVerificationEntry(
[property: JsonPropertyName("signerId")] string SignerId,
[property: JsonPropertyName("algorithm")] string Algorithm,
[property: JsonPropertyName("valid")] bool Valid,
[property: JsonPropertyName("message")] string? Message);
// ── Export Manifest ────────────────────────────────────────────────────
/// <summary>
/// Export run manifest for integrity verification.
/// </summary>
public sealed record ExportManifest(
[property: JsonPropertyName("runId")] string RunId,
[property: JsonPropertyName("profileId")] string ProfileId,
[property: JsonPropertyName("artifacts")] IReadOnlyList<ExportManifestEntry> Artifacts,
[property: JsonPropertyName("createdAt")] DateTimeOffset CreatedAt,
[property: JsonPropertyName("manifestDigest")] string ManifestDigest);
/// <summary>
/// Entry in the export manifest.
/// </summary>
public sealed record ExportManifestEntry(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("digest")] string Digest,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("mediaType")] string? MediaType);
// ── Attestation Status ─────────────────────────────────────────────────
/// <summary>
/// Attestation status for an export run.
/// </summary>
public sealed record ExportAttestationStatus(
[property: JsonPropertyName("runId")] string RunId,
[property: JsonPropertyName("signed")] bool Signed,
[property: JsonPropertyName("signerId")] string? SignerId,
[property: JsonPropertyName("algorithm")] string? Algorithm,
[property: JsonPropertyName("signedAt")] DateTimeOffset? SignedAt,
[property: JsonPropertyName("transparencyLogEntryId")] string? TransparencyLogEntryId);
// ── Export Capability Descriptor ───────────────────────────────────────
/// <summary>
/// Describes an export capability available on the backend.
/// </summary>
public sealed record ExportCapability(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("endpoint")] string Endpoint,
[property: JsonPropertyName("available")] bool Available,
[property: JsonPropertyName("requiresAuth")] bool RequiresAuth);
/// <summary>
/// Collection of all export capabilities surfaced by the backend.
/// </summary>
public sealed record ExportCapabilitySummary(
[property: JsonPropertyName("capabilities")] IReadOnlyList<ExportCapability> Capabilities,
[property: JsonPropertyName("totalAvailable")] int TotalAvailable,
[property: JsonPropertyName("totalUnavailable")] int TotalUnavailable)
{
/// <summary>Number of surfaced capabilities.</summary>
[JsonIgnore]
public int TotalCapabilities => Capabilities.Count;
}