using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using StellaOps.Policy.Engine.Domain; namespace StellaOps.Policy.Engine.Services; /// /// Deterministic runtime evaluator with per-digest caching. /// internal sealed class PolicyRuntimeEvaluator { private readonly IPolicyPackRepository _repository; private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); public PolicyRuntimeEvaluator(IPolicyPackRepository repository) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } public async Task EvaluateAsync(PolicyEvaluationRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); if (string.IsNullOrWhiteSpace(request.PackId)) { throw new ArgumentException("packId required", nameof(request)); } if (request.Version <= 0) { throw new ArgumentException("version must be positive", nameof(request)); } if (string.IsNullOrWhiteSpace(request.Subject)) { throw new ArgumentException("subject required", nameof(request)); } var bundle = await _repository.GetBundleAsync(request.PackId, request.Version, cancellationToken).ConfigureAwait(false); if (bundle is null) { throw new InvalidOperationException("Bundle not found for requested revision."); } var cacheKey = $"{bundle.Digest}|{request.Subject}"; if (_cache.TryGetValue(cacheKey, out var cached)) { return cached with { Cached = true }; } var decision = ComputeDecision(bundle.Digest, request.Subject); var correlationId = ComputeCorrelationId(cacheKey); var response = new PolicyEvaluationResponse( request.PackId, request.Version, bundle.Digest, decision, correlationId, Cached: false); _cache.TryAdd(cacheKey, response); return response; } private static string ComputeDecision(string digest, string subject) { Span hash = stackalloc byte[32]; SHA256.HashData(Encoding.UTF8.GetBytes($"{digest}|{subject}"), hash); return (hash[0] & 1) == 0 ? "allow" : "deny"; } private static string ComputeCorrelationId(string value) { Span hash = stackalloc byte[32]; SHA256.HashData(Encoding.UTF8.GetBytes(value), hash); return Convert.ToHexString(hash); } }