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;
}
}