part #2
This commit is contained in:
23
src/__Libraries/StellaOps.AdvisoryAI.Attestation/AGENTS.md
Normal file
23
src/__Libraries/StellaOps.AdvisoryAI.Attestation/AGENTS.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# AdvisoryAI Attestation Charter
|
||||
|
||||
## Mission
|
||||
- Provide deterministic, offline-safe attestation models and helpers for Advisory AI workflows.
|
||||
|
||||
## Responsibilities
|
||||
- Maintain stable attestation contracts and digest behavior.
|
||||
- Keep in-memory implementations deterministic and test-friendly.
|
||||
- Update local `TASKS.md` and sprint status when starting or finishing work.
|
||||
|
||||
## Required Reading
|
||||
- `docs/README.md`
|
||||
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/modules/concelier/advisory-ai-api.md`
|
||||
|
||||
## Working Directory & Scope
|
||||
- Primary: `src/__Libraries/StellaOps.AdvisoryAI.Attestation`
|
||||
- Allowed shared projects: `src/__Libraries/__Tests/StellaOps.AdvisoryAI.Attestation.Tests`
|
||||
|
||||
## Testing Expectations
|
||||
- Add unit tests for digest and template registry behaviors.
|
||||
- Keep tests deterministic and offline-friendly.
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Result of creating an attestation.
|
||||
/// </summary>
|
||||
public sealed record AiAttestationResult
|
||||
{
|
||||
/// <summary>Attestation ID.</summary>
|
||||
public required string AttestationId { get; init; }
|
||||
|
||||
/// <summary>Content digest.</summary>
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>Whether the attestation was signed.</summary>
|
||||
public bool Signed { get; init; }
|
||||
|
||||
/// <summary>DSSE envelope if signed.</summary>
|
||||
public string? DsseEnvelope { get; init; }
|
||||
|
||||
/// <summary>Storage URI.</summary>
|
||||
public string? StorageUri { get; init; }
|
||||
|
||||
/// <summary>Creation timestamp.</summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
public sealed partial class AiAttestationService
|
||||
{
|
||||
/// <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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
public sealed partial class AiAttestationService
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
public sealed partial class AiAttestationService
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
public sealed partial class AiAttestationService
|
||||
{
|
||||
/// <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));
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,8 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
@@ -18,7 +15,7 @@ namespace StellaOps.AdvisoryAI.Attestation;
|
||||
/// This implementation stores attestations in memory. For production,
|
||||
/// use a database-backed implementation with signing integration.
|
||||
/// </remarks>
|
||||
public sealed class AiAttestationService : IAiAttestationService
|
||||
public sealed partial class AiAttestationService : IAiAttestationService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<AiAttestationService> _logger;
|
||||
@@ -32,244 +29,4 @@ public sealed class AiAttestationService : IAiAttestationService
|
||||
_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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying an attestation.
|
||||
/// </summary>
|
||||
public sealed record AiAttestationVerificationResult
|
||||
{
|
||||
/// <summary>Whether verification succeeded.</summary>
|
||||
public required bool Valid { get; init; }
|
||||
|
||||
/// <summary>Verification timestamp.</summary>
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
/// <summary>Signing key ID if signed.</summary>
|
||||
public string? SigningKeyId { get; init; }
|
||||
|
||||
/// <summary>Key expiration if applicable.</summary>
|
||||
public DateTimeOffset? KeyExpiresAt { get; init; }
|
||||
|
||||
/// <summary>Digest verification result.</summary>
|
||||
public bool DigestValid { get; init; }
|
||||
|
||||
/// <summary>Signature verification result.</summary>
|
||||
public bool? SignatureValid { get; init; }
|
||||
|
||||
/// <summary>Verification failure reason if invalid.</summary>
|
||||
public string? FailureReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful verification result.
|
||||
/// </summary>
|
||||
public static AiAttestationVerificationResult Success(
|
||||
DateTimeOffset verifiedAt,
|
||||
string? signingKeyId = null,
|
||||
DateTimeOffset? keyExpiresAt = null) => new()
|
||||
{
|
||||
Valid = true,
|
||||
VerifiedAt = verifiedAt,
|
||||
SigningKeyId = signingKeyId,
|
||||
KeyExpiresAt = keyExpiresAt,
|
||||
DigestValid = true,
|
||||
SignatureValid = signingKeyId != null ? true : null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed verification result.
|
||||
/// </summary>
|
||||
public static AiAttestationVerificationResult Failure(
|
||||
DateTimeOffset verifiedAt,
|
||||
string reason,
|
||||
bool digestValid = false,
|
||||
bool? signatureValid = null) => new()
|
||||
{
|
||||
Valid = false,
|
||||
VerifiedAt = verifiedAt,
|
||||
DigestValid = digestValid,
|
||||
SignatureValid = signatureValid,
|
||||
FailureReason = reason
|
||||
};
|
||||
}
|
||||
@@ -88,86 +88,3 @@ public interface IAiAttestationService
|
||||
int limit = 100,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of creating an attestation.
|
||||
/// </summary>
|
||||
public sealed record AiAttestationResult
|
||||
{
|
||||
/// <summary>Attestation ID.</summary>
|
||||
public required string AttestationId { get; init; }
|
||||
|
||||
/// <summary>Content digest.</summary>
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>Whether the attestation was signed.</summary>
|
||||
public bool Signed { get; init; }
|
||||
|
||||
/// <summary>DSSE envelope if signed.</summary>
|
||||
public string? DsseEnvelope { get; init; }
|
||||
|
||||
/// <summary>Storage URI.</summary>
|
||||
public string? StorageUri { get; init; }
|
||||
|
||||
/// <summary>Creation timestamp.</summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying an attestation.
|
||||
/// </summary>
|
||||
public sealed record AiAttestationVerificationResult
|
||||
{
|
||||
/// <summary>Whether verification succeeded.</summary>
|
||||
public required bool Valid { get; init; }
|
||||
|
||||
/// <summary>Verification timestamp.</summary>
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
/// <summary>Signing key ID if signed.</summary>
|
||||
public string? SigningKeyId { get; init; }
|
||||
|
||||
/// <summary>Key expiration if applicable.</summary>
|
||||
public DateTimeOffset? KeyExpiresAt { get; init; }
|
||||
|
||||
/// <summary>Digest verification result.</summary>
|
||||
public bool DigestValid { get; init; }
|
||||
|
||||
/// <summary>Signature verification result.</summary>
|
||||
public bool? SignatureValid { get; init; }
|
||||
|
||||
/// <summary>Verification failure reason if invalid.</summary>
|
||||
public string? FailureReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful verification result.
|
||||
/// </summary>
|
||||
public static AiAttestationVerificationResult Success(
|
||||
DateTimeOffset verifiedAt,
|
||||
string? signingKeyId = null,
|
||||
DateTimeOffset? keyExpiresAt = null) => new()
|
||||
{
|
||||
Valid = true,
|
||||
VerifiedAt = verifiedAt,
|
||||
SigningKeyId = signingKeyId,
|
||||
KeyExpiresAt = keyExpiresAt,
|
||||
DigestValid = true,
|
||||
SignatureValid = signingKeyId != null ? true : null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed verification result.
|
||||
/// </summary>
|
||||
public static AiAttestationVerificationResult Failure(
|
||||
DateTimeOffset verifiedAt,
|
||||
string reason,
|
||||
bool digestValid = false,
|
||||
bool? signatureValid = null) => new()
|
||||
{
|
||||
Valid = false,
|
||||
VerifiedAt = verifiedAt,
|
||||
DigestValid = digestValid,
|
||||
SignatureValid = signatureValid,
|
||||
FailureReason = reason
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// <copyright file="IPromptTemplateRegistry.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for prompt template registry.
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-004
|
||||
/// </summary>
|
||||
public interface IPromptTemplateRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a prompt template with version.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <param name="version">Template version.</param>
|
||||
/// <param name="template">Template content.</param>
|
||||
void Register(string name, string version, string template);
|
||||
|
||||
/// <summary>
|
||||
/// Gets template info including hash.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <returns>Template info or null if not found.</returns>
|
||||
PromptTemplateInfo? GetTemplateInfo(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets template info for a specific version.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <param name="version">Template version.</param>
|
||||
/// <returns>Template info or null if not found.</returns>
|
||||
PromptTemplateInfo? GetTemplateInfo(string name, string version);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a template hash matches registered version.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <param name="expectedHash">Expected hash.</param>
|
||||
/// <returns>True if hash matches.</returns>
|
||||
bool VerifyHash(string name, string expectedHash);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered templates.
|
||||
/// </summary>
|
||||
/// <returns>All template info records.</returns>
|
||||
IReadOnlyList<PromptTemplateInfo> GetAllTemplates();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
public sealed partial record AiClaimAttestation
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the content digest for this attestation.
|
||||
/// </summary>
|
||||
public string ComputeDigest()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiClaimAttestation);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
public sealed partial record AiClaimAttestation
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a claim attestation from a claim evidence.
|
||||
/// </summary>
|
||||
public static AiClaimAttestation FromClaimEvidence(
|
||||
ClaimEvidence evidence,
|
||||
string runId,
|
||||
string turnId,
|
||||
string tenantId,
|
||||
DateTimeOffset timestamp,
|
||||
AiRunContext? context = null)
|
||||
{
|
||||
var claimDigest = ComputeClaimDigest(evidence.Text);
|
||||
var claimId = $"claim-{Guid.NewGuid():N}";
|
||||
|
||||
var attestation = new AiClaimAttestation
|
||||
{
|
||||
ClaimId = claimId,
|
||||
RunId = runId,
|
||||
TurnId = turnId,
|
||||
TenantId = tenantId,
|
||||
ClaimText = evidence.Text,
|
||||
ClaimDigest = claimDigest,
|
||||
Category = evidence.Category,
|
||||
GroundedBy = evidence.GroundedBy,
|
||||
GroundingScore = evidence.GroundingScore,
|
||||
Verified = evidence.Verified,
|
||||
Timestamp = timestamp,
|
||||
Context = context,
|
||||
ContentDigest = "" // Placeholder, computed below
|
||||
};
|
||||
|
||||
// Now compute the actual content digest
|
||||
return attestation with { ContentDigest = attestation.ComputeDigest() };
|
||||
}
|
||||
|
||||
private static string ComputeClaimDigest(string claimText)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(claimText));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
@@ -21,7 +18,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
/// - Claim-specific evidence linkage
|
||||
/// - Selective claim citation in reports
|
||||
/// </remarks>
|
||||
public sealed record AiClaimAttestation
|
||||
public sealed partial record AiClaimAttestation
|
||||
{
|
||||
/// <summary>Attestation type URI.</summary>
|
||||
public const string PredicateType = "https://stellaops.org/attestation/ai-claim/v1";
|
||||
@@ -86,54 +83,4 @@ public sealed record AiClaimAttestation
|
||||
[JsonPropertyName("claimType")]
|
||||
public string? ClaimType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Computes the content digest for this attestation.
|
||||
/// </summary>
|
||||
public string ComputeDigest()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiClaimAttestation);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a claim attestation from a claim evidence.
|
||||
/// </summary>
|
||||
public static AiClaimAttestation FromClaimEvidence(
|
||||
ClaimEvidence evidence,
|
||||
string runId,
|
||||
string turnId,
|
||||
string tenantId,
|
||||
DateTimeOffset timestamp,
|
||||
AiRunContext? context = null)
|
||||
{
|
||||
var claimDigest = ComputeClaimDigest(evidence.Text);
|
||||
var claimId = $"claim-{Guid.NewGuid():N}";
|
||||
|
||||
var attestation = new AiClaimAttestation
|
||||
{
|
||||
ClaimId = claimId,
|
||||
RunId = runId,
|
||||
TurnId = turnId,
|
||||
TenantId = tenantId,
|
||||
ClaimText = evidence.Text,
|
||||
ClaimDigest = claimDigest,
|
||||
Category = evidence.Category,
|
||||
GroundedBy = evidence.GroundedBy,
|
||||
GroundingScore = evidence.GroundingScore,
|
||||
Verified = evidence.Verified,
|
||||
Timestamp = timestamp,
|
||||
Context = context,
|
||||
ContentDigest = "" // Placeholder, computed below
|
||||
};
|
||||
|
||||
// Now compute the actual content digest
|
||||
return attestation with { ContentDigest = attestation.ComputeDigest() };
|
||||
}
|
||||
|
||||
private static string ComputeClaimDigest(string claimText)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(claimText));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
public sealed partial record AiRunAttestation
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the content digest for this attestation.
|
||||
/// </summary>
|
||||
public string ComputeDigest()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiRunAttestation);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
@@ -23,7 +20,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
/// - What was said (content digests)
|
||||
/// - What claims were made and their grounding evidence
|
||||
/// </remarks>
|
||||
public sealed record AiRunAttestation
|
||||
public sealed partial record AiRunAttestation
|
||||
{
|
||||
/// <summary>Attestation type URI.</summary>
|
||||
public const string PredicateType = "https://stellaops.org/attestation/ai-run/v1";
|
||||
@@ -84,35 +81,4 @@ public sealed record AiRunAttestation
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Computes the content digest for this attestation.
|
||||
/// </summary>
|
||||
public string ComputeDigest()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiRunAttestation);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI run status.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<AiRunStatus>))]
|
||||
public enum AiRunStatus
|
||||
{
|
||||
/// <summary>Run completed successfully.</summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>Run failed.</summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>Run was cancelled.</summary>
|
||||
Cancelled,
|
||||
|
||||
/// <summary>Run timed out.</summary>
|
||||
TimedOut,
|
||||
|
||||
/// <summary>Run was blocked by guardrails.</summary>
|
||||
Blocked
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
/// <summary>
|
||||
/// AI run status.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<AiRunStatus>))]
|
||||
public enum AiRunStatus
|
||||
{
|
||||
/// <summary>Run completed successfully.</summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>Run failed.</summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>Run was cancelled.</summary>
|
||||
Cancelled,
|
||||
|
||||
/// <summary>Run timed out.</summary>
|
||||
TimedOut,
|
||||
|
||||
/// <summary>Run was blocked by guardrails.</summary>
|
||||
Blocked
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
public sealed partial class PromptTemplateRegistry
|
||||
{
|
||||
private static string ComputeDigest(string content)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
public sealed partial class PromptTemplateRegistry
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public PromptTemplateInfo? GetTemplateInfo(string name)
|
||||
{
|
||||
return _latestVersions.TryGetValue(name, out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PromptTemplateInfo? GetTemplateInfo(string name, string version)
|
||||
{
|
||||
return _allVersions.TryGetValue((name, version), out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool VerifyHash(string name, string expectedHash)
|
||||
{
|
||||
if (!_latestVersions.TryGetValue(name, out var info))
|
||||
{
|
||||
_logger.LogWarning("Template {Name} not found for hash verification", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
var matches = string.Equals(info.Digest, expectedHash, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Hash mismatch for template {Name}: expected {Expected}, got {Actual}",
|
||||
name,
|
||||
expectedHash,
|
||||
info.Digest);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<PromptTemplateInfo> GetAllTemplates()
|
||||
{
|
||||
return [.. _latestVersions.Values];
|
||||
}
|
||||
}
|
||||
@@ -2,65 +2,17 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for prompt template registry.
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-004
|
||||
/// </summary>
|
||||
public interface IPromptTemplateRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a prompt template with version.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <param name="version">Template version.</param>
|
||||
/// <param name="template">Template content.</param>
|
||||
void Register(string name, string version, string template);
|
||||
|
||||
/// <summary>
|
||||
/// Gets template info including hash.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <returns>Template info or null if not found.</returns>
|
||||
PromptTemplateInfo? GetTemplateInfo(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets template info for a specific version.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <param name="version">Template version.</param>
|
||||
/// <returns>Template info or null if not found.</returns>
|
||||
PromptTemplateInfo? GetTemplateInfo(string name, string version);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a template hash matches registered version.
|
||||
/// </summary>
|
||||
/// <param name="name">Template name.</param>
|
||||
/// <param name="expectedHash">Expected hash.</param>
|
||||
/// <returns>True if hash matches.</returns>
|
||||
bool VerifyHash(string name, string expectedHash);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered templates.
|
||||
/// </summary>
|
||||
/// <returns>All template info records.</returns>
|
||||
IReadOnlyList<PromptTemplateInfo> GetAllTemplates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of prompt template registry.
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-004
|
||||
/// </summary>
|
||||
public sealed class PromptTemplateRegistry : IPromptTemplateRegistry
|
||||
public sealed partial class PromptTemplateRegistry : IPromptTemplateRegistry
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<PromptTemplateRegistry> _logger;
|
||||
@@ -101,51 +53,4 @@ public sealed class PromptTemplateRegistry : IPromptTemplateRegistry
|
||||
version,
|
||||
digest);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PromptTemplateInfo? GetTemplateInfo(string name)
|
||||
{
|
||||
return _latestVersions.TryGetValue(name, out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PromptTemplateInfo? GetTemplateInfo(string name, string version)
|
||||
{
|
||||
return _allVersions.TryGetValue((name, version), out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool VerifyHash(string name, string expectedHash)
|
||||
{
|
||||
if (!_latestVersions.TryGetValue(name, out var info))
|
||||
{
|
||||
_logger.LogWarning("Template {Name} not found for hash verification", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
var matches = string.Equals(info.Digest, expectedHash, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Hash mismatch for template {Name}: expected {Expected}, got {Actual}",
|
||||
name,
|
||||
expectedHash,
|
||||
info.Digest);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<PromptTemplateInfo> GetAllTemplates()
|
||||
{
|
||||
return [.. _latestVersions.Values];
|
||||
}
|
||||
|
||||
private static string ComputeDigest(string content)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
|
||||
public partial interface IAiAttestationStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a run attestation by run ID.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The attestation or null if not found.</returns>
|
||||
Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get all claim attestations for a run.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of claim attestations.</returns>
|
||||
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get claim attestations for a specific turn.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="turnId">The turn ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of claim attestations for the turn.</returns>
|
||||
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
|
||||
string runId,
|
||||
string turnId,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get the signed envelope for a run.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The signed envelope or null if not found.</returns>
|
||||
Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a run attestation exists.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if the attestation exists.</returns>
|
||||
Task<bool> ExistsAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get attestations by tenant within a time range.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">The tenant ID.</param>
|
||||
/// <param name="from">Start time.</param>
|
||||
/// <param name="to">End time.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of run attestations.</returns>
|
||||
Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
|
||||
string tenantId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get attestation by content digest.
|
||||
/// </summary>
|
||||
/// <param name="contentDigest">The content digest.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The claim attestation or null if not found.</returns>
|
||||
Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
/// Interface for storing and retrieving AI attestations.
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-006
|
||||
/// </summary>
|
||||
public interface IAiAttestationStore
|
||||
public partial interface IAiAttestationStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Store a run attestation.
|
||||
@@ -35,70 +35,4 @@ public interface IAiAttestationStore
|
||||
/// <param name="attestation">The attestation to store.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get a run attestation by run ID.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The attestation or null if not found.</returns>
|
||||
Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get all claim attestations for a run.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of claim attestations.</returns>
|
||||
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get claim attestations for a specific turn.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="turnId">The turn ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of claim attestations for the turn.</returns>
|
||||
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
|
||||
string runId,
|
||||
string turnId,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get the signed envelope for a run.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The signed envelope or null if not found.</returns>
|
||||
Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a run attestation exists.
|
||||
/// </summary>
|
||||
/// <param name="runId">The run ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if the attestation exists.</returns>
|
||||
Task<bool> ExistsAsync(string runId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get attestations by tenant within a time range.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">The tenant ID.</param>
|
||||
/// <param name="from">Start time.</param>
|
||||
/// <param name="to">End time.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of run attestations.</returns>
|
||||
Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
|
||||
string tenantId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get attestation by content digest.
|
||||
/// </summary>
|
||||
/// <param name="contentDigest">The content digest.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The claim attestation or null if not found.</returns>
|
||||
Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Clear all stored attestations. Useful for testing.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_runAttestations.Clear();
|
||||
_signedEnvelopes.Clear();
|
||||
_claimAttestations.Clear();
|
||||
_digestIndex.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get count of run attestations. Useful for testing.
|
||||
/// </summary>
|
||||
public int RunAttestationCount => _runAttestations.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Get count of all claim attestations. Useful for testing.
|
||||
/// </summary>
|
||||
public int ClaimAttestationCount => _claimAttestations.Values.Sum(c =>
|
||||
{
|
||||
lock (c) { return c.Count; }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStore
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
_runAttestations.TryGetValue(runId, out var attestation);
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
if (_claimAttestations.TryGetValue(runId, out var claims))
|
||||
{
|
||||
lock (claims)
|
||||
{
|
||||
return Task.FromResult(claims.ToImmutableArray());
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
|
||||
string runId,
|
||||
string turnId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_claimAttestations.TryGetValue(runId, out var claims))
|
||||
{
|
||||
lock (claims)
|
||||
{
|
||||
var filtered = claims
|
||||
.Where(c => c.TurnId == turnId)
|
||||
.ToImmutableArray();
|
||||
return Task.FromResult(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
_signedEnvelopes.TryGetValue(runId, out var envelope);
|
||||
return Task.FromResult(envelope);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> ExistsAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(_runAttestations.ContainsKey(runId));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
|
||||
string tenantId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var results = _runAttestations.Values
|
||||
.Where(a => a.TenantId == tenantId &&
|
||||
a.StartedAt >= from &&
|
||||
a.StartedAt <= to)
|
||||
.OrderBy(a => a.StartedAt)
|
||||
.ToImmutableArray();
|
||||
|
||||
return Task.FromResult(results);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct)
|
||||
{
|
||||
_digestIndex.TryGetValue(contentDigest, out var attestation);
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStore
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task StoreRunAttestationAsync(AiRunAttestation attestation, CancellationToken ct)
|
||||
{
|
||||
_runAttestations[attestation.RunId] = attestation;
|
||||
_logger.LogDebug("Stored run attestation for RunId {RunId}", attestation.RunId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoreSignedEnvelopeAsync(string runId, object envelope, CancellationToken ct)
|
||||
{
|
||||
_signedEnvelopes[runId] = envelope;
|
||||
_logger.LogDebug("Stored signed envelope for RunId {RunId}", runId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct)
|
||||
{
|
||||
var claims = _claimAttestations.GetOrAdd(attestation.RunId, _ => []);
|
||||
lock (claims)
|
||||
{
|
||||
claims.Add(attestation);
|
||||
}
|
||||
|
||||
_digestIndex[attestation.ContentDigest] = attestation;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Stored claim attestation for RunId {RunId}, TurnId {TurnId}",
|
||||
attestation.RunId,
|
||||
attestation.TurnId);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
|
||||
@@ -15,7 +13,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
/// Useful for testing and development.
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-006
|
||||
/// </summary>
|
||||
public sealed class InMemoryAiAttestationStore : IAiAttestationStore
|
||||
public sealed partial class InMemoryAiAttestationStore : IAiAttestationStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, AiRunAttestation> _runAttestations = new();
|
||||
private readonly ConcurrentDictionary<string, object> _signedEnvelopes = new();
|
||||
@@ -27,141 +25,4 @@ public sealed class InMemoryAiAttestationStore : IAiAttestationStore
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoreRunAttestationAsync(AiRunAttestation attestation, CancellationToken ct)
|
||||
{
|
||||
_runAttestations[attestation.RunId] = attestation;
|
||||
_logger.LogDebug("Stored run attestation for RunId {RunId}", attestation.RunId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoreSignedEnvelopeAsync(string runId, object envelope, CancellationToken ct)
|
||||
{
|
||||
_signedEnvelopes[runId] = envelope;
|
||||
_logger.LogDebug("Stored signed envelope for RunId {RunId}", runId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct)
|
||||
{
|
||||
var claims = _claimAttestations.GetOrAdd(attestation.RunId, _ => []);
|
||||
lock (claims)
|
||||
{
|
||||
claims.Add(attestation);
|
||||
}
|
||||
|
||||
_digestIndex[attestation.ContentDigest] = attestation;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Stored claim attestation for RunId {RunId}, TurnId {TurnId}",
|
||||
attestation.RunId,
|
||||
attestation.TurnId);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
_runAttestations.TryGetValue(runId, out var attestation);
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
if (_claimAttestations.TryGetValue(runId, out var claims))
|
||||
{
|
||||
lock (claims)
|
||||
{
|
||||
return Task.FromResult(claims.ToImmutableArray());
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
|
||||
string runId,
|
||||
string turnId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_claimAttestations.TryGetValue(runId, out var claims))
|
||||
{
|
||||
lock (claims)
|
||||
{
|
||||
var filtered = claims
|
||||
.Where(c => c.TurnId == turnId)
|
||||
.ToImmutableArray();
|
||||
return Task.FromResult(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
_signedEnvelopes.TryGetValue(runId, out var envelope);
|
||||
return Task.FromResult(envelope);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> ExistsAsync(string runId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult(_runAttestations.ContainsKey(runId));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
|
||||
string tenantId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var results = _runAttestations.Values
|
||||
.Where(a => a.TenantId == tenantId &&
|
||||
a.StartedAt >= from &&
|
||||
a.StartedAt <= to)
|
||||
.OrderBy(a => a.StartedAt)
|
||||
.ToImmutableArray();
|
||||
|
||||
return Task.FromResult(results);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct)
|
||||
{
|
||||
_digestIndex.TryGetValue(contentDigest, out var attestation);
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all stored attestations. Useful for testing.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_runAttestations.Clear();
|
||||
_signedEnvelopes.Clear();
|
||||
_claimAttestations.Clear();
|
||||
_digestIndex.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get count of run attestations. Useful for testing.
|
||||
/// </summary>
|
||||
public int RunAttestationCount => _runAttestations.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Get count of all claim attestations. Useful for testing.
|
||||
/// </summary>
|
||||
public int ClaimAttestationCount => _claimAttestations.Values.Sum(c =>
|
||||
{
|
||||
lock (c) { return c.Count; }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.AdvisoryAI.Attestation/StellaOps.AdvisoryAI.Attestation.md. |
|
||||
| REMED-05 | DONE | Split service/registry/models/store into <=100-line partials; `dotnet test src/__Libraries/__Tests/StellaOps.AdvisoryAI.Attestation.Tests/StellaOps.AdvisoryAI.Attestation.Tests.csproj -p:BuildInParallel=false -p:UseSharedCompilation=false` passed (58 tests) 2026-02-04. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
Reference in New Issue
Block a user