using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Policy.Snapshots; namespace StellaOps.Policy.Replay; /// /// Resolves knowledge sources from snapshot descriptors. /// public sealed class KnowledgeSourceResolver : IKnowledgeSourceResolver { private readonly ISnapshotStore _snapshotStore; private readonly ILogger _logger; public KnowledgeSourceResolver( ISnapshotStore snapshotStore, ILogger? logger = null) { _snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore)); _logger = logger ?? NullLogger.Instance; } /// /// Resolves a knowledge source to its actual content. /// public async Task ResolveAsync( KnowledgeSourceDescriptor descriptor, bool allowNetworkFetch, CancellationToken ct = default) { _logger.LogDebug("Resolving source {Name} ({Type})", descriptor.Name, descriptor.Type); // Try bundled content first if (descriptor.InclusionMode != SourceInclusionMode.Referenced && descriptor.BundlePath is not null) { var bundled = await ResolveBundledAsync(descriptor, ct).ConfigureAwait(false); if (bundled is not null) return bundled; } // Try local store by digest var local = await ResolveFromLocalStoreAsync(descriptor, ct).ConfigureAwait(false); if (local is not null) return local; // Network fetch not implemented yet (air-gap safe default) if (allowNetworkFetch && descriptor.Origin is not null) { _logger.LogWarning("Network fetch not implemented for {Name}", descriptor.Name); } _logger.LogWarning("Failed to resolve source {Name} with digest {Digest}", descriptor.Name, descriptor.Digest); return null; } private async Task ResolveBundledAsync( KnowledgeSourceDescriptor descriptor, CancellationToken ct) { try { var content = await _snapshotStore.GetBundledContentAsync(descriptor.BundlePath!, ct) .ConfigureAwait(false); if (content is null) return null; // Verify digest var actualDigest = ComputeDigest(content); if (actualDigest != descriptor.Digest) { _logger.LogWarning( "Bundled source {Name} digest mismatch: expected {Expected}, got {Actual}", descriptor.Name, descriptor.Digest, actualDigest); return null; } return new ResolvedSource( descriptor.Name, descriptor.Type, content, SourceResolutionMethod.Bundled); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to resolve bundled source {Name}", descriptor.Name); return null; } } private async Task ResolveFromLocalStoreAsync( KnowledgeSourceDescriptor descriptor, CancellationToken ct) { try { var content = await _snapshotStore.GetByDigestAsync(descriptor.Digest, ct) .ConfigureAwait(false); if (content is null) return null; return new ResolvedSource( descriptor.Name, descriptor.Type, content, SourceResolutionMethod.LocalStore); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to resolve source {Name} from local store", descriptor.Name); return null; } } private static string ComputeDigest(byte[] content) { var hash = SHA256.HashData(content); return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; } } /// /// Resolved knowledge source with content. /// public sealed record ResolvedSource( string Name, string Type, byte[] Content, SourceResolutionMethod Method); /// /// Method used to resolve a source. /// public enum SourceResolutionMethod { Bundled, LocalStore, NetworkFetch } /// /// Interface for source resolution. /// public interface IKnowledgeSourceResolver { Task ResolveAsync( KnowledgeSourceDescriptor descriptor, bool allowNetworkFetch, CancellationToken ct = default); } /// /// Frozen inputs for replay. /// public sealed class FrozenInputs { public Dictionary ResolvedSources { get; } = new(); public IReadOnlyList MissingSources { get; init; } = []; public bool IsComplete => MissingSources.Count == 0; } /// /// Builder for frozen inputs. /// public sealed class FrozenInputsBuilder { private readonly Dictionary _sources = new(); public FrozenInputsBuilder AddSource(string name, ResolvedSource source) { _sources[name] = source; return this; } public FrozenInputs Build(IReadOnlyList missingSources) { var inputs = new FrozenInputs { MissingSources = missingSources }; // Copy resolved sources foreach (var kvp in _sources) { inputs.ResolvedSources[kvp.Key] = kvp.Value; } return inputs; } }