Files
git.stella-ops.org/src/Policy/__Libraries/StellaOps.Policy/Snapshots/SnapshotService.cs

290 lines
9.3 KiB
C#

using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Cryptography;
namespace StellaOps.Policy.Snapshots;
/// <summary>
/// Service for managing knowledge snapshots.
/// </summary>
public sealed class SnapshotService : ISnapshotService
{
private readonly ISnapshotIdGenerator _idGenerator;
private readonly ICryptoSigner? _signer;
private readonly ISnapshotStore _store;
private readonly ILogger<SnapshotService> _logger;
public SnapshotService(
ISnapshotIdGenerator idGenerator,
ISnapshotStore store,
ILogger<SnapshotService> logger,
ICryptoSigner? signer = null)
{
_idGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator));
_store = store ?? throw new ArgumentNullException(nameof(store));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_signer = signer;
}
/// <summary>
/// Creates and persists a new snapshot.
/// </summary>
public async Task<KnowledgeSnapshotManifest> CreateSnapshotAsync(
SnapshotBuilder builder,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(builder);
var manifest = builder.Build();
// Validate ID before storing
if (!_idGenerator.ValidateId(manifest))
throw new InvalidOperationException("Snapshot ID validation failed");
await _store.SaveAsync(manifest, ct).ConfigureAwait(false);
_logger.LogInformation("Created snapshot {SnapshotId}", manifest.SnapshotId);
return manifest;
}
/// <summary>
/// Seals a snapshot with a DSSE signature.
/// </summary>
public async Task<KnowledgeSnapshotManifest> SealSnapshotAsync(
KnowledgeSnapshotManifest manifest,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(manifest);
if (_signer is null)
throw new InvalidOperationException("No signer configured for sealing snapshots");
var payload = JsonSerializer.SerializeToUtf8Bytes(manifest with { Signature = null },
SnapshotSerializerOptions.Canonical);
var signatureBytes = await _signer.SignAsync(payload, ct).ConfigureAwait(false);
var signature = Convert.ToBase64String(signatureBytes);
var sealedManifest = manifest with { Signature = signature };
await _store.SaveAsync(sealedManifest, ct).ConfigureAwait(false);
_logger.LogInformation("Sealed snapshot {SnapshotId}", manifest.SnapshotId);
return sealedManifest;
}
/// <summary>
/// Verifies a snapshot's integrity and signature.
/// </summary>
public async Task<SnapshotVerificationResult> VerifySnapshotAsync(
KnowledgeSnapshotManifest manifest,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(manifest);
// Verify content-addressed ID
if (!_idGenerator.ValidateId(manifest))
{
return SnapshotVerificationResult.Fail("Snapshot ID does not match content");
}
// Verify signature if present
if (manifest.Signature is not null)
{
if (_signer is null)
{
return SnapshotVerificationResult.Fail("No signer configured for signature verification");
}
var payload = JsonSerializer.SerializeToUtf8Bytes(manifest with { Signature = null },
SnapshotSerializerOptions.Canonical);
var signatureBytes = Convert.FromBase64String(manifest.Signature);
var sigValid = await _signer.VerifyAsync(payload, signatureBytes, ct).ConfigureAwait(false);
if (!sigValid)
{
return SnapshotVerificationResult.Fail("Signature verification failed");
}
}
return SnapshotVerificationResult.Success();
}
/// <summary>
/// Retrieves a snapshot by ID.
/// </summary>
public async Task<KnowledgeSnapshotManifest?> GetSnapshotAsync(
string snapshotId,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(snapshotId))
return null;
return await _store.GetAsync(snapshotId, ct).ConfigureAwait(false);
}
/// <summary>
/// Lists all snapshots in the store.
/// </summary>
public async Task<IReadOnlyList<KnowledgeSnapshotManifest>> ListSnapshotsAsync(
int skip = 0,
int take = 100,
CancellationToken ct = default)
{
return await _store.ListAsync(skip, take, ct).ConfigureAwait(false);
}
}
/// <summary>
/// Result of snapshot verification.
/// </summary>
public sealed record SnapshotVerificationResult(bool IsValid, string? Error)
{
public static SnapshotVerificationResult Success() => new(true, null);
public static SnapshotVerificationResult Fail(string error) => new(false, error);
}
/// <summary>
/// Interface for snapshot management operations.
/// </summary>
public interface ISnapshotService
{
/// <summary>
/// Creates and persists a new snapshot.
/// </summary>
Task<KnowledgeSnapshotManifest> CreateSnapshotAsync(SnapshotBuilder builder, CancellationToken ct = default);
/// <summary>
/// Seals a snapshot with a DSSE signature.
/// </summary>
Task<KnowledgeSnapshotManifest> SealSnapshotAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default);
/// <summary>
/// Verifies a snapshot's integrity and signature.
/// </summary>
Task<SnapshotVerificationResult> VerifySnapshotAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default);
/// <summary>
/// Retrieves a snapshot by ID.
/// </summary>
Task<KnowledgeSnapshotManifest?> GetSnapshotAsync(string snapshotId, CancellationToken ct = default);
/// <summary>
/// Lists all snapshots in the store.
/// </summary>
Task<IReadOnlyList<KnowledgeSnapshotManifest>> ListSnapshotsAsync(int skip = 0, int take = 100, CancellationToken ct = default);
}
/// <summary>
/// Interface for snapshot persistence.
/// </summary>
public interface ISnapshotStore
{
/// <summary>
/// Saves a snapshot manifest.
/// </summary>
Task SaveAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default);
/// <summary>
/// Retrieves a snapshot manifest by ID.
/// </summary>
Task<KnowledgeSnapshotManifest?> GetAsync(string snapshotId, CancellationToken ct = default);
/// <summary>
/// Lists snapshot manifests.
/// </summary>
Task<IReadOnlyList<KnowledgeSnapshotManifest>> ListAsync(int skip = 0, int take = 100, CancellationToken ct = default);
/// <summary>
/// Deletes a snapshot manifest by ID.
/// </summary>
Task<bool> DeleteAsync(string snapshotId, CancellationToken ct = default);
/// <summary>
/// Gets bundled content by path.
/// </summary>
Task<byte[]?> GetBundledContentAsync(string bundlePath, CancellationToken ct = default);
/// <summary>
/// Gets content by digest.
/// </summary>
Task<byte[]?> GetByDigestAsync(string digest, CancellationToken ct = default);
}
/// <summary>
/// In-memory implementation of <see cref="ISnapshotStore"/> for testing.
/// </summary>
public sealed class InMemorySnapshotStore : ISnapshotStore
{
private readonly Dictionary<string, KnowledgeSnapshotManifest> _snapshots = new();
private readonly object _lock = new();
public Task SaveAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
lock (_lock)
{
_snapshots[manifest.SnapshotId] = manifest;
}
return Task.CompletedTask;
}
public Task<KnowledgeSnapshotManifest?> GetAsync(string snapshotId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
lock (_lock)
{
return Task.FromResult(_snapshots.TryGetValue(snapshotId, out var manifest) ? manifest : null);
}
}
public Task<IReadOnlyList<KnowledgeSnapshotManifest>> ListAsync(int skip = 0, int take = 100, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
lock (_lock)
{
var result = _snapshots.Values
.OrderByDescending(s => s.CreatedAt)
.Skip(skip)
.Take(take)
.ToList();
return Task.FromResult<IReadOnlyList<KnowledgeSnapshotManifest>>(result);
}
}
public Task<bool> DeleteAsync(string snapshotId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
lock (_lock)
{
return Task.FromResult(_snapshots.Remove(snapshotId));
}
}
public Task<byte[]?> GetBundledContentAsync(string bundlePath, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
// In-memory implementation doesn't support bundled content
return Task.FromResult<byte[]?>(null);
}
public Task<byte[]?> GetByDigestAsync(string digest, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
// In-memory implementation doesn't support digest-based lookup
return Task.FromResult<byte[]?>(null);
}
/// <summary>
/// Clears all snapshots from the store (for testing only).
/// </summary>
public void Clear()
{
lock (_lock)
{
_snapshots.Clear();
}
}
}