diff --git a/src/AdvisoryAI/README.md b/src/AdvisoryAI/README.md
index a2b4b9c5e..9e1ffce15 100644
--- a/src/AdvisoryAI/README.md
+++ b/src/AdvisoryAI/README.md
@@ -12,7 +12,7 @@ AdvisoryAI provides AI-assisted security advisory analysis, remediation planning
- `opsmemory` (via Router) — decision recording, playbook suggestions, similarity-based recall
## Storage
-PostgreSQL (OpsMemory: `ConnectionStrings:Default`; AdvisoryAI knowledge search: `KnowledgeSearch:ConnectionString`); file-system queue/plans/outputs for AdvisoryAI
+PostgreSQL (OpsMemory: `ConnectionStrings:Default`; AdvisoryAI knowledge search: `KnowledgeSearch:ConnectionString`; AdvisoryAI consent + attestation runtime state: `AdvisoryAI:Storage:ConnectionString` -> `ConnectionStrings:Default` -> `Database:ConnectionString`, fail-fast outside Development/Testing); file-system queue/plans/outputs for AdvisoryAI
## Background Workers
- `KnowledgeSearchStartupRebuildService` — rebuilds knowledge search index on startup
diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/AdvisoryAiCoreRuntimePersistenceExtensions.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/AdvisoryAiCoreRuntimePersistenceExtensions.cs
new file mode 100644
index 000000000..29c04b75e
--- /dev/null
+++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/AdvisoryAiCoreRuntimePersistenceExtensions.cs
@@ -0,0 +1,111 @@
+//
+// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
+//
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using StellaOps.AdvisoryAI.Chat;
+using StellaOps.AdvisoryAI.Chat.Settings;
+using StellaOps.AdvisoryAI.Explanation;
+using StellaOps.AdvisoryAI.PolicyStudio;
+using StellaOps.AdvisoryAI.Runs;
+using StellaOps.AdvisoryAI.Storage;
+using StellaOps.AdvisoryAI.Storage.Postgres;
+using StellaOps.Infrastructure.Postgres;
+using StellaOps.Infrastructure.Postgres.Migrations;
+using StellaOps.Infrastructure.Postgres.Options;
+
+namespace StellaOps.AdvisoryAI.Hosting;
+
+public static class AdvisoryAiCoreRuntimePersistenceExtensions
+{
+ private const string StorageSectionName = "AdvisoryAI:Storage";
+
+ public static IServiceCollection AddAdvisoryAiCoreRuntimePersistence(
+ this IServiceCollection services,
+ IConfiguration configuration,
+ IHostEnvironment environment)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(configuration);
+ ArgumentNullException.ThrowIfNull(environment);
+
+ var connectionString = ResolveConnectionString(configuration);
+ var allowInMemory = environment.IsEnvironment("Testing");
+
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ if (!allowInMemory)
+ {
+ throw new InvalidOperationException(
+ "AdvisoryAI requires PostgreSQL-backed runtime state outside Testing. Configure AdvisoryAI:Storage:ConnectionString, ConnectionStrings:Default, or Database:ConnectionString.");
+ }
+
+ return services;
+ }
+
+ services.AddPostgresOptions(configuration, StorageSectionName);
+ services.PostConfigure(options =>
+ {
+ options.ConnectionString = string.IsNullOrWhiteSpace(options.ConnectionString)
+ ? connectionString.Trim()
+ : options.ConnectionString.Trim();
+ options.SchemaName = string.IsNullOrWhiteSpace(options.SchemaName)
+ ? AdvisoryAiDataSource.DefaultSchemaName
+ : options.SchemaName.Trim();
+ });
+
+ services.AddStartupMigrations(
+ AdvisoryAiDataSource.DefaultSchemaName,
+ "AdvisoryAI",
+ typeof(AdvisoryAiDataSource).Assembly,
+ static options => options.ConnectionString);
+
+ services.TryAddSingleton();
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.RemoveAll();
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetRequiredService());
+ services.AddSingleton(provider => provider.GetRequiredService());
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ return services;
+ }
+
+ public static string? ResolveConnectionString(IConfiguration configuration)
+ {
+ var explicitConnectionString = configuration[$"{StorageSectionName}:ConnectionString"];
+ if (!string.IsNullOrWhiteSpace(explicitConnectionString))
+ {
+ return explicitConnectionString.Trim();
+ }
+
+ var defaultConnectionString = configuration.GetConnectionString("Default");
+ if (!string.IsNullOrWhiteSpace(defaultConnectionString))
+ {
+ return defaultConnectionString.Trim();
+ }
+
+ var legacyConnectionString = configuration["Database:ConnectionString"];
+ return string.IsNullOrWhiteSpace(legacyConnectionString)
+ ? null
+ : legacyConnectionString.Trim();
+ }
+}
diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs
index 1689b126e..5328a99cb 100644
--- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs
+++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs
@@ -86,12 +86,11 @@ builder.Services.AddSingleton();
builder.Services.AddSingleton();
// AI Attestations (Sprint: SPRINT_20260109_011_001 Task: AIAT-009)
builder.Services.AddAiAttestationServices();
-builder.Services.AddInMemoryAiAttestationStore();
+builder.Services.AddAdvisoryAiRuntimePersistence(builder.Configuration, builder.Environment);
// Evidence Packs (Sprint: SPRINT_20260109_011_005 Task: EVPK-010)
builder.Services.AddEvidencePack();
@@ -1929,4 +1928,3 @@ namespace StellaOps.AdvisoryAI.WebService
}
-
diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Services/AdvisoryAiRuntimePersistenceExtensions.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Services/AdvisoryAiRuntimePersistenceExtensions.cs
new file mode 100644
index 000000000..496cb4627
--- /dev/null
+++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Services/AdvisoryAiRuntimePersistenceExtensions.cs
@@ -0,0 +1,55 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using StellaOps.AdvisoryAI.Hosting;
+using StellaOps.AdvisoryAI.Attestation;
+using StellaOps.AdvisoryAI.Attestation.Storage;
+using StellaOps.AdvisoryAI.Storage.Postgres;
+
+namespace StellaOps.AdvisoryAI.WebService.Services;
+
+///
+/// Registers authoritative AdvisoryAI runtime persistence for consent and attestation state.
+///
+public static class AdvisoryAiRuntimePersistenceExtensions
+{
+ public static IServiceCollection AddAdvisoryAiRuntimePersistence(
+ this IServiceCollection services,
+ IConfiguration configuration,
+ IHostEnvironment environment)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(configuration);
+ ArgumentNullException.ThrowIfNull(environment);
+
+ services.AddAdvisoryAiCoreRuntimePersistence(configuration, environment);
+
+ var connectionString = AdvisoryAiCoreRuntimePersistenceExtensions.ResolveConnectionString(configuration);
+ var allowInMemory = environment.IsEnvironment("Testing");
+
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ if (!allowInMemory)
+ {
+ throw new InvalidOperationException(
+ "AdvisoryAI requires PostgreSQL-backed consent and attestation storage outside Testing. Configure AdvisoryAI:Storage:ConnectionString, ConnectionStrings:Default, or Database:ConnectionString.");
+ }
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.RemoveAll();
+ services.AddInMemoryAiAttestationStore();
+ return services;
+ }
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ services.RemoveAll();
+ services.AddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Services/PostgresAiAttestationStore.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Services/PostgresAiAttestationStore.cs
new file mode 100644
index 000000000..654d339d4
--- /dev/null
+++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Services/PostgresAiAttestationStore.cs
@@ -0,0 +1,348 @@
+using System.Collections.Immutable;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using StellaOps.AdvisoryAI.Attestation.Models;
+using StellaOps.AdvisoryAI.Attestation.Storage;
+using StellaOps.AdvisoryAI.Storage.Postgres;
+using StellaOps.Infrastructure.Postgres.Repositories;
+
+namespace StellaOps.AdvisoryAI.WebService.Services;
+
+///
+/// PostgreSQL-backed attestation store for AdvisoryAI runtime state.
+///
+public sealed class PostgresAiAttestationStore : RepositoryBase, IAiAttestationStore
+{
+ public PostgresAiAttestationStore(
+ AdvisoryAiDataSource dataSource,
+ ILogger logger)
+ : base(dataSource, logger)
+ {
+ }
+
+ public Task StoreRunAttestationAsync(AiRunAttestation attestation, CancellationToken ct)
+ {
+ ArgumentNullException.ThrowIfNull(attestation);
+
+ const string sql = """
+ INSERT INTO ai_run_attestations (
+ run_id,
+ tenant_id,
+ started_at,
+ completed_at,
+ created_at,
+ content_digest,
+ attestation_json
+ ) VALUES (
+ @run_id,
+ @tenant_id,
+ @started_at,
+ @completed_at,
+ @created_at,
+ @content_digest,
+ @attestation_json
+ )
+ ON CONFLICT (run_id) DO UPDATE
+ SET tenant_id = EXCLUDED.tenant_id,
+ started_at = EXCLUDED.started_at,
+ completed_at = EXCLUDED.completed_at,
+ created_at = EXCLUDED.created_at,
+ content_digest = EXCLUDED.content_digest,
+ attestation_json = EXCLUDED.attestation_json;
+ """;
+
+ var attestationJson = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiRunAttestation);
+ var contentDigest = attestation.ComputeDigest();
+
+ return ExecuteAsync(
+ attestation.TenantId,
+ sql,
+ command =>
+ {
+ AddParameter(command, "run_id", attestation.RunId);
+ AddParameter(command, "tenant_id", attestation.TenantId);
+ AddParameter(command, "started_at", attestation.StartedAt);
+ AddParameter(command, "completed_at", attestation.CompletedAt);
+ AddParameter(command, "created_at", attestation.CompletedAt);
+ AddParameter(command, "content_digest", contentDigest);
+ AddJsonbParameter(command, "attestation_json", attestationJson);
+ },
+ ct);
+ }
+
+ public Task StoreSignedEnvelopeAsync(string runId, object envelope, CancellationToken ct)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(runId);
+ ArgumentNullException.ThrowIfNull(envelope);
+
+ const string sql = """
+ UPDATE ai_run_attestations
+ SET signed_envelope_json = @signed_envelope_json
+ WHERE run_id = @run_id;
+ """;
+
+ var envelopeJson = SerializeEnvelope(envelope);
+
+ return ExecuteAsync(
+ string.Empty,
+ sql,
+ command =>
+ {
+ AddParameter(command, "run_id", runId.Trim());
+ AddJsonbParameter(command, "signed_envelope_json", envelopeJson);
+ },
+ ct);
+ }
+
+ public Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct)
+ {
+ ArgumentNullException.ThrowIfNull(attestation);
+
+ const string sql = """
+ INSERT INTO ai_claim_attestations (
+ claim_id,
+ run_id,
+ tenant_id,
+ turn_id,
+ content_digest,
+ "timestamp",
+ claim_json
+ ) VALUES (
+ @claim_id,
+ @run_id,
+ @tenant_id,
+ @turn_id,
+ @content_digest,
+ @timestamp,
+ @claim_json
+ )
+ ON CONFLICT (claim_id) DO UPDATE
+ SET run_id = EXCLUDED.run_id,
+ tenant_id = EXCLUDED.tenant_id,
+ turn_id = EXCLUDED.turn_id,
+ content_digest = EXCLUDED.content_digest,
+ "timestamp" = EXCLUDED."timestamp",
+ claim_json = EXCLUDED.claim_json;
+ """;
+
+ var claimJson = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiClaimAttestation);
+
+ return ExecuteAsync(
+ attestation.TenantId,
+ sql,
+ command =>
+ {
+ AddParameter(command, "claim_id", attestation.ClaimId);
+ AddParameter(command, "run_id", attestation.RunId);
+ AddParameter(command, "tenant_id", attestation.TenantId);
+ AddParameter(command, "turn_id", attestation.TurnId);
+ AddParameter(command, "content_digest", attestation.ContentDigest);
+ AddParameter(command, "timestamp", attestation.Timestamp);
+ AddJsonbParameter(command, "claim_json", claimJson);
+ },
+ ct);
+ }
+
+ public Task GetRunAttestationAsync(string runId, CancellationToken ct)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(runId);
+
+ const string sql = """
+ SELECT attestation_json::text AS attestation_json
+ FROM ai_run_attestations
+ WHERE run_id = @run_id
+ LIMIT 1;
+ """;
+
+ return QuerySingleOrDefaultAsync(
+ string.Empty,
+ sql,
+ command => AddParameter(command, "run_id", runId.Trim()),
+ reader => DeserializeRunAttestation(reader.GetString(reader.GetOrdinal("attestation_json"))),
+ ct);
+ }
+
+ public Task GetClaimAttestationAsync(string claimId, CancellationToken ct)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(claimId);
+
+ const string sql = """
+ SELECT claim_json::text AS claim_json
+ FROM ai_claim_attestations
+ WHERE claim_id = @claim_id
+ LIMIT 1;
+ """;
+
+ return QuerySingleOrDefaultAsync(
+ string.Empty,
+ sql,
+ command => AddParameter(command, "claim_id", claimId.Trim()),
+ reader => DeserializeClaimAttestation(reader.GetString(reader.GetOrdinal("claim_json"))),
+ ct);
+ }
+
+ public async Task> GetClaimAttestationsAsync(string runId, CancellationToken ct)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(runId);
+
+ const string sql = """
+ SELECT claim_json::text AS claim_json
+ FROM ai_claim_attestations
+ WHERE run_id = @run_id
+ ORDER BY "timestamp", claim_id;
+ """;
+
+ var results = await QueryAsync(
+ string.Empty,
+ sql,
+ command => AddParameter(command, "run_id", runId.Trim()),
+ reader => DeserializeClaimAttestation(reader.GetString(reader.GetOrdinal("claim_json"))),
+ ct).ConfigureAwait(false);
+
+ return results.ToImmutableArray();
+ }
+
+ public async Task> GetClaimAttestationsByTurnAsync(
+ string runId,
+ string turnId,
+ CancellationToken ct)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(runId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(turnId);
+
+ const string sql = """
+ SELECT claim_json::text AS claim_json
+ FROM ai_claim_attestations
+ WHERE run_id = @run_id
+ AND turn_id = @turn_id
+ ORDER BY "timestamp", claim_id;
+ """;
+
+ var results = await QueryAsync(
+ string.Empty,
+ sql,
+ command =>
+ {
+ AddParameter(command, "run_id", runId.Trim());
+ AddParameter(command, "turn_id", turnId.Trim());
+ },
+ reader => DeserializeClaimAttestation(reader.GetString(reader.GetOrdinal("claim_json"))),
+ ct).ConfigureAwait(false);
+
+ return results.ToImmutableArray();
+ }
+
+ public async Task