sprints work
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
// <copyright file="AiAttestationService.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of AI attestation service.
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-003
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation stores attestations in memory. For production,
|
||||
/// use a database-backed implementation with signing integration.
|
||||
/// </remarks>
|
||||
public sealed class AiAttestationService : IAiAttestationService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<AiAttestationService> _logger;
|
||||
private readonly ConcurrentDictionary<string, StoredAttestation> _runAttestations = new();
|
||||
private readonly ConcurrentDictionary<string, StoredAttestation> _claimAttestations = new();
|
||||
|
||||
public AiAttestationService(
|
||||
TimeProvider timeProvider,
|
||||
ILogger<AiAttestationService> logger)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiAttestationResult> CreateRunAttestationAsync(
|
||||
AiRunAttestation attestation,
|
||||
bool sign = true,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var digest = attestation.ComputeDigest();
|
||||
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiRunAttestation);
|
||||
|
||||
// In production, this would call the signer service
|
||||
string? dsseEnvelope = null;
|
||||
if (sign)
|
||||
{
|
||||
// Placeholder - real implementation would use StellaOps.Signer
|
||||
dsseEnvelope = CreateMockDsseEnvelope(AiRunAttestation.PredicateType, json);
|
||||
}
|
||||
|
||||
var stored = new StoredAttestation(
|
||||
attestation.RunId,
|
||||
AiRunAttestation.PredicateType,
|
||||
json,
|
||||
digest,
|
||||
dsseEnvelope,
|
||||
now);
|
||||
|
||||
_runAttestations[attestation.RunId] = stored;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created run attestation {RunId} with digest {Digest}, signed={Signed}",
|
||||
attestation.RunId,
|
||||
digest,
|
||||
sign);
|
||||
|
||||
return Task.FromResult(new AiAttestationResult
|
||||
{
|
||||
AttestationId = attestation.RunId,
|
||||
Digest = digest,
|
||||
Signed = sign,
|
||||
DsseEnvelope = dsseEnvelope,
|
||||
StorageUri = $"stella://ai-attestation/run/{attestation.RunId}",
|
||||
CreatedAt = now
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiAttestationResult> CreateClaimAttestationAsync(
|
||||
AiClaimAttestation attestation,
|
||||
bool sign = true,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var digest = attestation.ComputeDigest();
|
||||
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiClaimAttestation);
|
||||
|
||||
string? dsseEnvelope = null;
|
||||
if (sign)
|
||||
{
|
||||
dsseEnvelope = CreateMockDsseEnvelope(AiClaimAttestation.PredicateType, json);
|
||||
}
|
||||
|
||||
var stored = new StoredAttestation(
|
||||
attestation.ClaimId,
|
||||
AiClaimAttestation.PredicateType,
|
||||
json,
|
||||
digest,
|
||||
dsseEnvelope,
|
||||
now);
|
||||
|
||||
_claimAttestations[attestation.ClaimId] = stored;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Created claim attestation {ClaimId} for run {RunId}",
|
||||
attestation.ClaimId,
|
||||
attestation.RunId);
|
||||
|
||||
return Task.FromResult(new AiAttestationResult
|
||||
{
|
||||
AttestationId = attestation.ClaimId,
|
||||
Digest = digest,
|
||||
Signed = sign,
|
||||
DsseEnvelope = dsseEnvelope,
|
||||
StorageUri = $"stella://ai-attestation/claim/{attestation.ClaimId}",
|
||||
CreatedAt = now
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiAttestationVerificationResult> VerifyRunAttestationAsync(
|
||||
string runId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
if (!_runAttestations.TryGetValue(runId, out var stored))
|
||||
{
|
||||
return Task.FromResult(AiAttestationVerificationResult.Failure(
|
||||
now,
|
||||
$"Run attestation {runId} not found"));
|
||||
}
|
||||
|
||||
// Verify digest
|
||||
var attestation = JsonSerializer.Deserialize(
|
||||
stored.Json,
|
||||
AiAttestationJsonContext.Default.AiRunAttestation);
|
||||
|
||||
if (attestation == null)
|
||||
{
|
||||
return Task.FromResult(AiAttestationVerificationResult.Failure(
|
||||
now,
|
||||
"Failed to deserialize attestation"));
|
||||
}
|
||||
|
||||
var computedDigest = attestation.ComputeDigest();
|
||||
if (computedDigest != stored.Digest)
|
||||
{
|
||||
return Task.FromResult(AiAttestationVerificationResult.Failure(
|
||||
now,
|
||||
"Digest mismatch",
|
||||
digestValid: false));
|
||||
}
|
||||
|
||||
// In production, verify signature via signer service
|
||||
bool? signatureValid = stored.DsseEnvelope != null ? true : null;
|
||||
|
||||
_logger.LogDebug("Verified run attestation {RunId}", runId);
|
||||
|
||||
return Task.FromResult(AiAttestationVerificationResult.Success(
|
||||
now,
|
||||
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiAttestationVerificationResult> VerifyClaimAttestationAsync(
|
||||
string claimId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
if (!_claimAttestations.TryGetValue(claimId, out var stored))
|
||||
{
|
||||
return Task.FromResult(AiAttestationVerificationResult.Failure(
|
||||
now,
|
||||
$"Claim attestation {claimId} not found"));
|
||||
}
|
||||
|
||||
var attestation = JsonSerializer.Deserialize(
|
||||
stored.Json,
|
||||
AiAttestationJsonContext.Default.AiClaimAttestation);
|
||||
|
||||
if (attestation == null)
|
||||
{
|
||||
return Task.FromResult(AiAttestationVerificationResult.Failure(
|
||||
now,
|
||||
"Failed to deserialize attestation"));
|
||||
}
|
||||
|
||||
var computedDigest = attestation.ComputeDigest();
|
||||
if (computedDigest != stored.Digest)
|
||||
{
|
||||
return Task.FromResult(AiAttestationVerificationResult.Failure(
|
||||
now,
|
||||
"Digest mismatch",
|
||||
digestValid: false));
|
||||
}
|
||||
|
||||
return Task.FromResult(AiAttestationVerificationResult.Success(
|
||||
now,
|
||||
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiRunAttestation?> GetRunAttestationAsync(
|
||||
string runId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!_runAttestations.TryGetValue(runId, out var stored))
|
||||
{
|
||||
return Task.FromResult<AiRunAttestation?>(null);
|
||||
}
|
||||
|
||||
var attestation = JsonSerializer.Deserialize(
|
||||
stored.Json,
|
||||
AiAttestationJsonContext.Default.AiRunAttestation);
|
||||
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IReadOnlyList<AiClaimAttestation>> GetClaimAttestationsAsync(
|
||||
string runId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var claims = _claimAttestations.Values
|
||||
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiClaimAttestation))
|
||||
.Where(c => c != null && c.RunId == runId)
|
||||
.Cast<AiClaimAttestation>()
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AiClaimAttestation>>(claims);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IReadOnlyList<AiRunAttestation>> ListRecentAttestationsAsync(
|
||||
string tenantId,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var attestations = _runAttestations.Values
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiRunAttestation))
|
||||
.Where(a => a != null && a.TenantId == tenantId)
|
||||
.Cast<AiRunAttestation>()
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AiRunAttestation>>(attestations);
|
||||
}
|
||||
|
||||
private static string CreateMockDsseEnvelope(string predicateType, string payload)
|
||||
{
|
||||
// Mock DSSE envelope - real implementation would use StellaOps.Signer
|
||||
var payloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload));
|
||||
return $$"""
|
||||
{
|
||||
"payloadType": "{{predicateType}}",
|
||||
"payload": "{{payloadBase64}}",
|
||||
"signatures": [{"sig": "mock-signature"}]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private sealed record StoredAttestation(
|
||||
string Id,
|
||||
string PredicateType,
|
||||
string Json,
|
||||
string Digest,
|
||||
string? DsseEnvelope,
|
||||
DateTimeOffset CreatedAt);
|
||||
}
|
||||
Reference in New Issue
Block a user