Centralize Postgres connection string policy across all modules

Extract connection string building into PostgresConnectionStringPolicy so all
services use consistent pooling, application_name, and timeout settings.
Adopt the new policy in 20+ module DataSource/ServiceCollectionExtensions classes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-06 08:51:04 +03:00
parent 517fa0a92d
commit ccdfd41e4f
64 changed files with 625 additions and 178 deletions

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Rekor checkpoint PostgreSQL store and aligned it with reusable runtime data-source conventions. |
| AUDIT-0049-M | DONE | Revalidated maintainability for StellaOps.Attestor.Core. |
| AUDIT-0049-T | DONE | Revalidated test coverage for StellaOps.Attestor.Core. |
| AUDIT-0049-A | TODO | Reopened on revalidation; address canonicalization, time/ID determinism, and Ed25519 gaps. |

View File

@@ -180,7 +180,9 @@ public static class ServiceCollectionExtensions
throw new InvalidOperationException("Redis connection string is required when redis dedupe is enabled.");
}
return ConnectionMultiplexer.Connect(options.Redis.Url);
var redisOptions = ConfigurationOptions.Parse(options.Redis.Url);
redisOptions.ClientName ??= "stellaops-attestor-dedupe";
return ConnectionMultiplexer.Connect(redisOptions);
});
services.AddSingleton<IAttestorArchiveStore>(sp =>

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Attestor Redis dedupe client construction path. |
| AUDIT-0055-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0055-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0055-A | DONE | Applied determinism, backend resolver, and Rekor client fixes 2026-01-08. |

View File

@@ -19,9 +19,9 @@ namespace StellaOps.Attestor.Core.Rekor;
/// <summary>
/// PostgreSQL implementation of the Rekor checkpoint store.
/// </summary>
public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore
public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly PostgresCheckpointStoreOptions _options;
private readonly ILogger<PostgresRekorCheckpointStore> _logger;
@@ -30,8 +30,9 @@ public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore
ILogger<PostgresRekorCheckpointStore> logger)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_connectionString = _options.ConnectionString
var connectionString = _options.ConnectionString
?? throw new InvalidOperationException("ConnectionString is required");
_dataSource = CreateDataSource(connectionString);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -285,11 +286,23 @@ public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore
private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
{
var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
return conn;
return await _dataSource.OpenConnectionAsync(cancellationToken);
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-rekor-checkpoint"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static StoredCheckpoint MapCheckpoint(NpgsqlDataReader reader)
{
return new StoredCheckpoint

View File

@@ -15,9 +15,9 @@ namespace StellaOps.Attestor.Persistence.Repositories;
/// EF Core implementation of the predicate type registry repository.
/// Preserves idempotent registration via ON CONFLICT DO NOTHING semantics.
/// </summary>
public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegistryRepository
public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegistryRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly string _schemaName;
private const int DefaultCommandTimeoutSeconds = 30;
@@ -26,7 +26,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
/// </summary>
public PostgresPredicateTypeRegistryRepository(string connectionString, string? schemaName = null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_dataSource = CreateDataSource(connectionString);
_schemaName = schemaName ?? AttestorDbContextFactory.DefaultSchemaName;
}
@@ -38,8 +38,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
int limit = 100,
CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
var query = dbContext.PredicateTypeRegistry.AsNoTracking().AsQueryable();
@@ -67,8 +66,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
string predicateTypeUri,
CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.PredicateTypeRegistry
@@ -85,8 +83,7 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
{
ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
dbContext.PredicateTypeRegistry.Add(entry);
@@ -115,4 +112,18 @@ public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegi
}
return false;
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-predicate-registry"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -16,9 +16,9 @@ namespace StellaOps.Attestor.Persistence.Repositories;
/// EF Core implementation of the verdict ledger repository.
/// Enforces append-only semantics with hash chain validation.
/// </summary>
public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly string _schemaName;
private const int DefaultCommandTimeoutSeconds = 30;
@@ -27,7 +27,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// </summary>
public PostgresVerdictLedgerRepository(string connectionString, string? schemaName = null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_dataSource = CreateDataSource(connectionString);
_schemaName = schemaName ?? AttestorDbContextFactory.DefaultSchemaName;
}
@@ -47,8 +47,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
// Use raw SQL for the INSERT with RETURNING and enum cast, which is cleaner
// for the verdict_decision custom enum type
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
dbContext.VerdictLedger.Add(entry);
@@ -74,8 +73,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// <inheritdoc />
public async Task<VerdictLedgerEntry?> GetByHashAsync(string verdictHash, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger
@@ -89,8 +87,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
Guid tenantId,
CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger
@@ -103,8 +100,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// <inheritdoc />
public async Task<VerdictLedgerEntry?> GetLatestAsync(Guid tenantId, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger
@@ -151,8 +147,7 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
/// <inheritdoc />
public async Task<long> CountAsync(Guid tenantId, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var dbContext = AttestorDbContextFactory.Create(conn, DefaultCommandTimeoutSeconds, _schemaName);
return await dbContext.VerdictLedger
@@ -172,4 +167,18 @@ public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
}
return false;
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-verdict-ledger"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -5,6 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20260222_092_Attestor_dal_to_efcore.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named reusable PostgreSQL data sources for verdict-ledger and predicate-registry runtime repositories. |
| AUDIT-0060-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0060-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0060-A | TODO | Reopened after revalidation 2026-01-06. |

View File

@@ -13,8 +13,7 @@ public sealed partial class PostgresAlertDedupRepository
int dedupWindowMinutes,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
var now = _timeProvider.GetUtcNow();
var windowStart = now.AddMinutes(-dedupWindowMinutes);

View File

@@ -8,15 +8,15 @@ namespace StellaOps.Attestor.Watchlist.Storage;
/// <summary>
/// PostgreSQL implementation of the alert deduplication repository.
/// </summary>
public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresAlertDedupRepository> _logger;
private readonly TimeProvider _timeProvider;
public PostgresAlertDedupRepository(string connectionString, ILogger<PostgresAlertDedupRepository> logger, TimeProvider? timeProvider = null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_dataSource = CreateDataSource(connectionString);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -27,8 +27,7 @@ public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
string identityHash,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT alert_count FROM attestor.identity_alert_dedup
@@ -45,8 +44,7 @@ public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
/// <inheritdoc />
public async Task<int> CleanupExpiredAsync(CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
var cutoff = _timeProvider.GetUtcNow().AddDays(-7);
const string sql = @"
@@ -57,4 +55,18 @@ public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository
cmd.Parameters.AddWithValue("cutoff", cutoff);
return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-alert-dedup"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -14,8 +14,7 @@ public sealed partial class PostgresWatchlistRepository
bool includeGlobal = true,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
var sql = includeGlobal
? SqlQueries.SelectByTenantIncludingGlobal
@@ -46,8 +45,7 @@ public sealed partial class PostgresWatchlistRepository
return cached;
}
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.SelectActiveByTenant, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);

View File

@@ -13,8 +13,7 @@ public sealed partial class PostgresWatchlistRepository
{
ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.Upsert, conn);
AddUpsertParameters(cmd, entry);
@@ -38,8 +37,7 @@ public sealed partial class PostgresWatchlistRepository
/// <inheritdoc />
public async Task<bool> DeleteAsync(Guid id, string tenantId, CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.Delete, conn);
cmd.Parameters.AddWithValue("id", id);

View File

@@ -11,9 +11,9 @@ namespace StellaOps.Attestor.Watchlist.Storage;
/// <summary>
/// PostgreSQL implementation of the watchlist repository with caching.
/// </summary>
public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
public sealed partial class PostgresWatchlistRepository : IWatchlistRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly IMemoryCache _cache;
private readonly ILogger<PostgresWatchlistRepository> _logger;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(5);
@@ -28,7 +28,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
IMemoryCache cache,
ILogger<PostgresWatchlistRepository> logger)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_dataSource = CreateDataSource(connectionString);
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -36,8 +36,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
/// <inheritdoc />
public async Task<WatchedIdentity?> GetAsync(Guid id, CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(SqlQueries.SelectById, conn);
cmd.Parameters.AddWithValue("id", id);
@@ -54,8 +53,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
/// <inheritdoc />
public async Task<int> GetCountAsync(string tenantId, CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = "SELECT COUNT(*) FROM attestor.identity_watchlist WHERE tenant_id = @tenant_id";
@@ -65,4 +63,18 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository
var result = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return Convert.ToInt32(result);
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-attestor-watchlist"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: replaced raw PostgreSQL connection construction with named reusable data sources for watchlist and alert-dedup runtime paths. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/StellaOps.Attestor.Watchlist.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -176,7 +176,11 @@ builder.Services.TryAddSingleton<IDpopProofValidator, DpopProofValidator>();
if (string.Equals(senderConstraints.Dpop.Nonce.Store, "redis", StringComparison.OrdinalIgnoreCase))
{
builder.Services.TryAddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect(senderConstraints.Dpop.Nonce.RedisConnectionString!));
{
var redisOptions = ConfigurationOptions.Parse(senderConstraints.Dpop.Nonce.RedisConnectionString!);
redisOptions.ClientName ??= "stellaops-authority-dpop-nonce";
return ConnectionMultiplexer.Connect(redisOptions);
});
builder.Services.TryAddSingleton<IDpopNonceStore>(provider =>
{

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Authority DPoP nonce-store Valkey client construction path. |
| AUDIT-0085-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0085-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0085-A | TODO | Reopened 2026-01-06: remove Guid.NewGuid/DateTimeOffset.UtcNow, fix branding error messages, and modularize Program.cs. |

View File

@@ -123,6 +123,7 @@ static IResolutionCacheService CreateResolutionCacheService(IServiceProvider ser
redisOptions.ConnectTimeout = 500;
redisOptions.SyncTimeout = 500;
redisOptions.AsyncTimeout = 500;
redisOptions.ClientName ??= "stellaops-binaryindex-resolution-cache";
var multiplexer = ConnectionMultiplexer.Connect(redisOptions);
_ = multiplexer.GetDatabase().Ping();

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the BinaryIndex resolution-cache Valkey client. |
| QA-BINARYINDEX-VERIFY-029 | DONE | SPRINT_20260211_033 run-001: `patch-coverage-tracking` passed Tier 0/1/2 with patch-coverage controller behavioral evidence (including post-create coverage updates) and was moved to `docs/features/checked/binaryindex/patch-coverage-tracking.md`. |
| QA-BINARYINDEX-VERIFY-028 | BLOCKED | SPRINT_20260211_033: `ml-function-embedding-service` is actively owned by another lane (`run-001` in progress); this lane terminalized collision as `skipped` (`owned_by_other_agent`) per FLOW 0.1. |
| QA-BINARYINDEX-VERIFY-026 | DONE | SPRINT_20260211_033 run-002: `known-build-binary-catalog` passed Tier 0/1/2 with Build-ID/SHA256/assertion/cache/method-mapping behavioral evidence; dossier verified in `docs/features/checked/binaryindex/known-build-binary-catalog.md`. |

View File

@@ -10,14 +10,14 @@ namespace StellaOps.BinaryIndex.Validation.Persistence;
/// <summary>
/// PostgreSQL repository for match results.
/// </summary>
public sealed class MatchResultRepository : IMatchResultRepository
public sealed class MatchResultRepository : IMatchResultRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly JsonSerializerOptions _jsonOptions;
public MatchResultRepository(string connectionString)
{
_connectionString = connectionString;
_dataSource = CreateDataSource(connectionString);
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
@@ -30,8 +30,7 @@ public sealed class MatchResultRepository : IMatchResultRepository
{
if (results.Count == 0) return;
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var transaction = await conn.BeginTransactionAsync(ct);
@@ -105,8 +104,7 @@ public sealed class MatchResultRepository : IMatchResultRepository
/// <inheritdoc/>
public async Task<IReadOnlyList<MatchResult>> GetForRunAsync(Guid runId, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
const string query = """
SELECT
@@ -215,4 +213,18 @@ public sealed class MatchResultRepository : IMatchResultRepository
string ExpectedName, string? ExpectedDemangledName, long ExpectedAddress, long? ExpectedSize, string ExpectedBuildId, string ExpectedBinaryName,
string? ActualName, string? ActualDemangledName, long? ActualAddress, long? ActualSize, string? ActualBuildId, string? ActualBinaryName,
string Outcome, double? MatchScore, string? Confidence, string? InferredCause, string? MismatchDetail, double? MatchDurationMs);
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-binaryindex-match-results"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -10,14 +10,14 @@ namespace StellaOps.BinaryIndex.Validation.Persistence;
/// <summary>
/// PostgreSQL repository for validation runs.
/// </summary>
public sealed class ValidationRunRepository : IValidationRunRepository
public sealed class ValidationRunRepository : IValidationRunRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly JsonSerializerOptions _jsonOptions;
public ValidationRunRepository(string connectionString)
{
_connectionString = connectionString;
_dataSource = CreateDataSource(connectionString);
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
@@ -28,8 +28,7 @@ public sealed class ValidationRunRepository : IValidationRunRepository
/// <inheritdoc/>
public async Task SaveAsync(ValidationRun run, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
const string upsert = """
INSERT INTO groundtruth.validation_runs (
@@ -107,8 +106,7 @@ public sealed class ValidationRunRepository : IValidationRunRepository
/// <inheritdoc/>
public async Task<ValidationRun?> GetAsync(Guid runId, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
const string query = """
SELECT
@@ -132,8 +130,7 @@ public sealed class ValidationRunRepository : IValidationRunRepository
ValidationRunFilter? filter,
CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
var sql = new System.Text.StringBuilder("""
SELECT id, name, status, created_at, completed_at,
@@ -264,4 +261,18 @@ public sealed class ValidationRunRepository : IValidationRunRepository
private sealed record ValidationRunSummaryRow(
Guid Id, string Name, string Status, DateTimeOffset CreatedAt, DateTimeOffset? CompletedAt,
double? MatchRate, double? F1Score, int? PairCount, int? FunctionCount, string[]? Tags);
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-binaryindex-validation-runs"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named reusable PostgreSQL data sources for validation-run and match-result persistence. |
| QA-BINARYINDEX-VERIFY-023 | BLOCKED | SPRINT_20260211_033 run-001: blocked because `AGENTS.md` is missing in this module and related validation modules/tests (repo AGENTS rule 5). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/StellaOps.BinaryIndex.Validation.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -11,16 +11,16 @@ namespace StellaOps.Concelier.ProofService.Postgres;
/// PostgreSQL implementation of distro advisory repository.
/// Queries the vuln.distro_advisories table for CVE + package evidence.
/// </summary>
public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository
public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresDistroAdvisoryRepository> _logger;
public PostgresDistroAdvisoryRepository(
string connectionString,
ILogger<PostgresDistroAdvisoryRepository> logger)
{
_connectionString = connectionString;
_dataSource = CreateDataSource(connectionString);
_logger = logger;
}
@@ -48,8 +48,7 @@ public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
var result = await connection.QuerySingleOrDefaultAsync<DistroAdvisoryDto>(
new CommandDefinition(sql, new { CveId = cveId, PackagePurl = packagePurl }, cancellationToken: ct));
@@ -71,4 +70,18 @@ public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository
throw;
}
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-concelier-proofservice-distro-advisories"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -11,16 +11,16 @@ namespace StellaOps.Concelier.ProofService.Postgres;
/// PostgreSQL implementation of patch repository.
/// Queries vuln.patch_evidence and feedser.binary_fingerprints tables.
/// </summary>
public sealed class PostgresPatchRepository : IPatchRepository
public sealed class PostgresPatchRepository : IPatchRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresPatchRepository> _logger;
public PostgresPatchRepository(
string connectionString,
ILogger<PostgresPatchRepository> logger)
{
_connectionString = connectionString;
_dataSource = CreateDataSource(connectionString);
_logger = logger;
}
@@ -45,8 +45,7 @@ public sealed class PostgresPatchRepository : IPatchRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
var results = await connection.QueryAsync<PatchHeaderDto>(
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
@@ -89,8 +88,7 @@ public sealed class PostgresPatchRepository : IPatchRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
var results = await connection.QueryAsync<PatchSigDto>(
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
@@ -144,8 +142,7 @@ public sealed class PostgresPatchRepository : IPatchRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
var results = await connection.QueryAsync<BinaryFingerprintRow>(
new CommandDefinition(sql, new { CveId = cveId }, cancellationToken: ct));
@@ -206,4 +203,18 @@ public sealed class PostgresPatchRepository : IPatchRepository
public required DateTimeOffset ExtractedAt { get; init; }
public required string ExtractorVersion { get; init; }
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-concelier-proofservice-patches"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -10,16 +10,16 @@ namespace StellaOps.Concelier.ProofService.Postgres;
/// PostgreSQL implementation of source artifact repository.
/// Queries vuln.changelog_evidence for CVE mentions in changelogs.
/// </summary>
public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository
public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository, IAsyncDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<PostgresSourceArtifactRepository> _logger;
public PostgresSourceArtifactRepository(
string connectionString,
ILogger<PostgresSourceArtifactRepository> logger)
{
_connectionString = connectionString;
_dataSource = CreateDataSource(connectionString);
_logger = logger;
}
@@ -46,8 +46,7 @@ public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
var results = await connection.QueryAsync<ChangelogDto>(
new CommandDefinition(sql, new { CveId = cveId, PackagePurl = packagePurl }, cancellationToken: ct));
@@ -68,4 +67,18 @@ public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository
throw;
}
}
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-concelier-proofservice-source-artifacts"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: replaced raw PostgreSQL connection construction with named reusable proof-service data sources. |
| AUDIT-0233-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0233-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0233-A | TODO | Revalidated 2026-01-07 (open findings). |

View File

@@ -23,7 +23,7 @@ namespace StellaOps.Doctor.WebService.Services;
/// </summary>
public sealed class PostgresReportStorageService : IReportStorageService, IDisposable
{
private readonly string _connectionString;
private readonly NpgsqlDataSource _dataSource;
private readonly DoctorServiceOptions _options;
private readonly ILogger<PostgresReportStorageService> _logger;
private readonly Timer? _cleanupTimer;
@@ -37,9 +37,10 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
IOptions<DoctorServiceOptions> options,
ILogger<PostgresReportStorageService> logger)
{
_connectionString = configuration.GetConnectionString("StellaOps")
var connectionString = configuration.GetConnectionString("StellaOps")
?? configuration["Database:ConnectionString"]
?? throw new InvalidOperationException("Database connection string not configured");
_dataSource = CreateDataSource(connectionString);
_options = options.Value;
_logger = logger;
@@ -60,8 +61,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
var json = JsonSerializer.Serialize(report, JsonSerializerOptions.Default);
var compressed = CompressJson(json);
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
const string sql = """
INSERT INTO doctor_reports (run_id, started_at, completed_at, overall_severity,
@@ -104,8 +104,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc />
public async Task<DoctorReport?> GetReportAsync(string runId, CancellationToken ct)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
const string sql = "SELECT report_json_compressed FROM doctor_reports WHERE run_id = @runId";
@@ -126,8 +125,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc />
public async Task<IReadOnlyList<ReportSummaryDto>> ListReportsAsync(int limit, int offset, CancellationToken ct)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
const string sql = """
SELECT run_id, started_at, completed_at, overall_severity,
@@ -170,8 +168,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc />
public async Task<bool> DeleteReportAsync(string runId, CancellationToken ct)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
const string sql = "DELETE FROM doctor_reports WHERE run_id = @runId";
@@ -185,8 +182,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
/// <inheritdoc />
public async Task<int> GetCountAsync(CancellationToken ct)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
const string sql = "SELECT COUNT(*) FROM doctor_reports";
@@ -207,8 +203,7 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
var cutoff = DateTimeOffset.UtcNow.AddDays(-_options.ReportRetentionDays);
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(ct);
await using var connection = await _dataSource.OpenConnectionAsync(ct);
const string sql = "DELETE FROM doctor_reports WHERE created_at < @cutoff";
@@ -261,7 +256,20 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo
if (!_disposed)
{
_cleanupTimer?.Dispose();
_dataSource.Dispose();
_disposed = true;
}
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-doctor-report-storage"
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -3,6 +3,7 @@
## Completed
### 2026-01-12 - Sprint 001_007 Implementation
- [x] SPRINT_20260405_011-XPORT: named the PostgreSQL report-storage data source and aligned the runtime path with the shared transport attribution guardrail.
- [x] Created project structure following Platform WebService pattern
- [x] Implemented DoctorServiceOptions with authority configuration
- [x] Defined DoctorPolicies and DoctorScopes for authorization

View File

@@ -38,7 +38,12 @@ public sealed class EvidenceLockerDataSource : IAsyncDisposable
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var builder = new NpgsqlDataSourceBuilder(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-evidence-locker",
};
var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
builder.EnableDynamicJson();
return builder.Build();
}

View File

@@ -83,10 +83,10 @@ public static class EvidenceLockerInfrastructureServiceCollectionExtensions
// Verdict attestation repository
services.AddScoped<StellaOps.EvidenceLocker.Storage.IVerdictRepository>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
var dataSource = provider.GetRequiredService<EvidenceLockerDataSource>();
var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository>>();
return new StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository(
options.Database.ConnectionString,
cancellationToken => dataSource.OpenConnectionAsync(cancellationToken),
logger);
});
@@ -94,10 +94,10 @@ public static class EvidenceLockerInfrastructureServiceCollectionExtensions
// Sprint: SPRINT_20260219_009 (CID-04)
services.AddScoped<StellaOps.EvidenceLocker.Storage.IEvidenceThreadRepository>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
var dataSource = provider.GetRequiredService<EvidenceLockerDataSource>();
var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository>>();
return new StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository(
options.Database.ConnectionString,
cancellationToken => dataSource.OpenConnectionAsync(cancellationToken),
logger);
});

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the Evidence Locker PostgreSQL datasource and switched runtime repositories onto the shared open-connection path. |
| AUDIT-0289-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
| AUDIT-0289-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
| AUDIT-0289-A | TODO | Revalidated 2026-01-07 (open findings). |

View File

@@ -11,14 +11,14 @@ namespace StellaOps.EvidenceLocker.Storage;
/// </summary>
public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository
{
private readonly string _connectionString;
private readonly Func<CancellationToken, Task<NpgsqlConnection>> _openConnectionAsync;
private readonly ILogger<PostgresEvidenceThreadRepository> _logger;
public PostgresEvidenceThreadRepository(
string connectionString,
Func<CancellationToken, Task<NpgsqlConnection>> openConnectionAsync,
ILogger<PostgresEvidenceThreadRepository> logger)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_openConnectionAsync = openConnectionAsync ?? throw new ArgumentNullException(nameof(openConnectionAsync));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -45,8 +45,7 @@ public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var record = await connection.QuerySingleOrDefaultAsync<ArtifactCanonicalRecord>(
new CommandDefinition(
@@ -88,8 +87,7 @@ public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var results = await connection.QueryAsync<ArtifactCanonicalRecord>(
new CommandDefinition(

View File

@@ -9,16 +9,16 @@ namespace StellaOps.EvidenceLocker.Storage;
/// </summary>
public sealed class PostgresVerdictRepository : IVerdictRepository
{
private readonly string _connectionString;
private readonly Func<CancellationToken, Task<NpgsqlConnection>> _openConnectionAsync;
private readonly ILogger<PostgresVerdictRepository> _logger;
private readonly TimeProvider _timeProvider;
public PostgresVerdictRepository(
string connectionString,
Func<CancellationToken, Task<NpgsqlConnection>> openConnectionAsync,
ILogger<PostgresVerdictRepository> logger,
TimeProvider? timeProvider = null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_openConnectionAsync = openConnectionAsync ?? throw new ArgumentNullException(nameof(openConnectionAsync));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -74,8 +74,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var now = _timeProvider.GetUtcNow();
var verdictId = await connection.ExecuteScalarAsync<string>(
@@ -153,8 +152,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var record = await connection.QuerySingleOrDefaultAsync<VerdictAttestationRecord>(
new CommandDefinition(
@@ -231,8 +229,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var results = await connection.QueryAsync<VerdictAttestationSummary>(
new CommandDefinition(
@@ -309,8 +306,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var results = await connection.QueryAsync<VerdictAttestationSummary>(
new CommandDefinition(
@@ -365,8 +361,7 @@ public sealed class PostgresVerdictRepository : IVerdictRepository
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var connection = await _openConnectionAsync(cancellationToken).ConfigureAwait(false);
var count = await connection.ExecuteScalarAsync<int>(
new CommandDefinition(

View File

@@ -40,7 +40,12 @@ public sealed class ExportCenterDataSource : IAsyncDisposable
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var builder = new NpgsqlDataSourceBuilder(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-export-center",
};
var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
builder.EnableDynamicJson();
return builder.Build();
}

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Export Center PostgreSQL datasource sessions for runtime attribution. |
| QA-EXPORTCENTER-VERIFY-001 | DONE | `cli-ui-surfacing-of-hidden-backend-capabilities` verified in run-002 (Tier 0/1/2 pass; Policy blocker remediated; client 62/62 and service 920/920). |
| QA-EXPORTCENTER-VERIFY-002 | DONE | `export-center-risk-bundle-builder` verified in run-001 (Tier 0/1/2 pass; service suite 920/920). |
| QA-EXPORTCENTER-VERIFY-003 | DONE | `export-telemetry-and-worker` verified in run-001 (Tier 0/1/2 pass; service suite 920/920). |

View File

@@ -22,7 +22,12 @@ public sealed class LedgerDataSource : IAsyncDisposable
_options = options.Value.Database;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var builder = new NpgsqlDataSourceBuilder(_options.ConnectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(_options.ConnectionString)
{
ApplicationName = "stellaops-findings-ledger",
};
var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
_dataSource = builder.Build();
}

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Findings Ledger PostgreSQL datasource sessions for runtime attribution. |
| AUDIT-0342-M | DONE | Revalidated 2026-01-07; maintainability audit for Findings Ledger. |
| AUDIT-0342-T | DONE | Revalidated 2026-01-07; test coverage audit for Findings Ledger. |
| AUDIT-0342-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). |

View File

@@ -20,7 +20,13 @@ public sealed class PostgresRiskScoreResultStore : IRiskScoreResultStore, IAsync
public PostgresRiskScoreResultStore(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = NpgsqlDataSource.Create(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-riskengine",
};
_dataSource = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
public async Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken)

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the risk-engine PostgreSQL result-store datasource. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Infrastructure/StellaOps.RiskEngine.Infrastructure.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-312-005 | DONE | Added `PostgresRiskScoreResultStore` with schema/bootstrap and deterministic upsert/read behavior. |

View File

@@ -32,10 +32,7 @@ public sealed class AnalyticsIngestionDataSource : IAsyncDisposable
return null;
}
_dataSource ??= new NpgsqlDataSourceBuilder(_connectionString!)
{
Name = "StellaOps.Platform.Analytics.Ingestion"
}.Build();
_dataSource ??= CreateDataSource(_connectionString!);
var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await ConfigureSessionAsync(connection, cancellationToken).ConfigureAwait(false);
@@ -62,4 +59,17 @@ public sealed class AnalyticsIngestionDataSource : IAsyncDisposable
_logger.LogDebug("Configured analytics ingestion session for PostgreSQL connection.");
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-platform-analytics-ingestion",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString)
{
Name = "StellaOps.Platform.Analytics.Ingestion"
}.Build();
}
}

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named analytics ingestion PostgreSQL datasource construction for runtime attribution. |
| QA-PLATFORM-VERIFY-001 | DONE | run-002 verification captured analytics rollup/materialized-view behavior evidence; feature terminalized as `not_implemented` due missing advisory lock/LISTEN-NOTIFY parity. |
| QA-PLATFORM-VERIFY-002 | DONE | run-001 verification passed with maintenance, endpoint paths, analytics service behavior, and Docker schema integration (`38/38` scoped tests). |
| QA-PLATFORM-VERIFY-003 | DONE | `platform-service-aggregation-layer` verified with run-001 Tier 0/1/2 endpoint evidence and moved to `docs/features/checked/platform/`. |

View File

@@ -32,10 +32,7 @@ public sealed class PlatformAnalyticsDataSource : IAsyncDisposable
return null;
}
_dataSource ??= new NpgsqlDataSourceBuilder(_connectionString!)
{
Name = "StellaOps.Platform.Analytics"
}.Build();
_dataSource ??= CreateDataSource(_connectionString!);
var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await ConfigureSessionAsync(connection, cancellationToken).ConfigureAwait(false);
@@ -64,4 +61,17 @@ public sealed class PlatformAnalyticsDataSource : IAsyncDisposable
_logger.LogDebug("Configured analytics session for PostgreSQL connection.");
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-platform-analytics",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString)
{
Name = "StellaOps.Platform.Analytics"
}.Build();
}
}

View File

@@ -30,7 +30,12 @@ public static class ServiceCollectionExtensions
services.AddSingleton<NpgsqlDataSource>(sp =>
{
var options = sp.GetRequiredService<IOptions<PluginRegistryOptions>>().Value;
var dataSourceBuilder = new NpgsqlDataSourceBuilder(options.ConnectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
ApplicationName = "stellaops-plugin-registry",
};
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
return dataSourceBuilder.Build();
});
@@ -62,7 +67,12 @@ public static class ServiceCollectionExtensions
services.AddSingleton<NpgsqlDataSource>(sp =>
{
var options = sp.GetRequiredService<IOptions<PluginRegistryOptions>>().Value;
var dataSourceBuilder = new NpgsqlDataSourceBuilder(options.ConnectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
ApplicationName = "stellaops-plugin-registry",
};
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
return dataSourceBuilder.Build();
});

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named plugin registry PostgreSQL datasource construction. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Plugin/StellaOps.Plugin.Registry/StellaOps.Plugin.Registry.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -272,7 +272,11 @@ public static class PolicyEngineServiceCollectionExtensions
string connectionString)
{
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(connectionString));
{
var redisOptions = ConfigurationOptions.Parse(connectionString);
redisOptions.ClientName ??= "stellaops-policy-engine";
return ConnectionMultiplexer.Connect(redisOptions);
});
return services;
}

View File

@@ -5,6 +5,7 @@ Source of truth: `docs/implplan/SPRINT_20260119_021_Policy_license_compliance.md
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Policy Engine Valkey client construction. |
| AUDIT-0440-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Engine. |
| AUDIT-0440-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Engine. |
| AUDIT-0440-A | DOING | Revalidated 2026-01-07 (open findings). |

View File

@@ -35,7 +35,13 @@ builder.Services.AddSingleton<NpgsqlDataSource>(sp =>
var config = sp.GetRequiredService<IConfiguration>();
var connStr = config.GetConnectionString("PostgreSQL")
?? throw new InvalidOperationException("PostgreSQL connection string not configured");
return new NpgsqlDataSourceBuilder(connStr).Build();
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connStr)
{
ApplicationName = "stellaops-reachgraph",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
});
// Redis/Valkey (lazy so integration tests can replace before first resolve)
@@ -43,7 +49,9 @@ builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var redisConnStr = config.GetConnectionString("Redis") ?? "localhost:6379";
return ConnectionMultiplexer.Connect(redisConnStr);
var redisOptions = ConfigurationOptions.Parse(redisConnStr);
redisOptions.ClientName ??= "stellaops-reachgraph";
return ConnectionMultiplexer.Connect(redisOptions);
});
// Core services

View File

@@ -4,5 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the ReachGraph PostgreSQL datasource for runtime attribution. |
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the ReachGraph Valkey client construction path. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReachGraph/StellaOps.ReachGraph.WebService/StellaOps.ReachGraph.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -29,7 +29,11 @@ public static class ServiceCollectionExtensions
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
// Create NpgsqlDataSource from connection string
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-release-orchestrator-policygate",
};
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString);
var dataSource = dataSourceBuilder.Build();
// Register stores

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Policy Gate PostgreSQL datasource construction. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/StellaOps.ReleaseOrchestrator.PolicyGate.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -14,7 +14,13 @@ public sealed class PostgresFeedSnapshotIndexStore : IFeedSnapshotIndexStore, IA
public PostgresFeedSnapshotIndexStore(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = NpgsqlDataSource.Create(connectionString);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-replay",
};
_dataSource = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
public async Task IndexSnapshotAsync(FeedSnapshotIndexEntry entry, CancellationToken ct = default)

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Replay PostgreSQL snapshot-index datasource construction. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Replay/StellaOps.Replay.WebService/StellaOps.Replay.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-312-006 | DONE | Added Postgres snapshot index + seed-fs snapshot blob stores and wired storage-driver registration in webservice startup. |

View File

@@ -20,7 +20,7 @@ namespace StellaOps.Scanner.Reachability.Cache;
/// </summary>
public sealed class PostgresReachabilityCache : IReachabilityCache
{
private readonly string _connectionString;
private readonly Services.PostgresReachabilityDataSourceProvider _dataSourceProvider;
private readonly ILogger<PostgresReachabilityCache> _logger;
private readonly TimeProvider _timeProvider;
@@ -28,8 +28,16 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string connectionString,
ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null)
: this(new Services.PostgresReachabilityDataSourceProvider(connectionString), logger, timeProvider)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
internal PostgresReachabilityCache(
Services.PostgresReachabilityDataSourceProvider dataSourceProvider,
ILogger<PostgresReachabilityCache> logger,
TimeProvider? timeProvider = null)
{
_dataSourceProvider = dataSourceProvider ?? throw new ArgumentNullException(nameof(dataSourceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -40,8 +48,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string graphHash,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
// Get cache entry
const string entrySql = """
@@ -120,8 +127,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
{
ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var tx = await conn.BeginTransactionAsync(cancellationToken);
try
@@ -202,8 +208,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string sinkMethodKey,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT p.is_reachable, p.path_length, p.confidence, p.computed_at
@@ -246,8 +251,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
IEnumerable<string> affectedMethodKeys,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
// For now, invalidate entire cache for service
// More granular invalidation would require additional indices
@@ -283,8 +287,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
string serviceId,
CancellationToken cancellationToken = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT total_hits, total_misses, full_recomputes, incremental_computes,
@@ -392,4 +395,7 @@ public sealed class PostgresReachabilityCache : IReachabilityCache
await cmd.ExecuteNonQueryAsync(cancellationToken);
}
private ValueTask<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
=> _dataSourceProvider.OpenConnectionAsync(cancellationToken);
}

View File

@@ -34,10 +34,12 @@ public static class ServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
services.TryAddSingleton(_ => new PostgresReachabilityDataSourceProvider(connectionString));
// CVE-Symbol Mapping Service
services.TryAddSingleton<ICveSymbolMappingService>(sp =>
new PostgresCveSymbolMappingRepository(
connectionString,
sp.GetRequiredService<PostgresReachabilityDataSourceProvider>(),
sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<PostgresCveSymbolMappingRepository>>()));
// Stack Evaluator (already exists, ensure registered)

View File

@@ -15,14 +15,21 @@ namespace StellaOps.Scanner.Reachability.Services;
/// </summary>
public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingService
{
private readonly string _connectionString;
private readonly PostgresReachabilityDataSourceProvider _dataSourceProvider;
private readonly ILogger<PostgresCveSymbolMappingRepository> _logger;
public PostgresCveSymbolMappingRepository(
string connectionString,
ILogger<PostgresCveSymbolMappingRepository> logger)
: this(new PostgresReachabilityDataSourceProvider(connectionString), logger)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
internal PostgresCveSymbolMappingRepository(
PostgresReachabilityDataSourceProvider dataSourceProvider,
ILogger<PostgresCveSymbolMappingRepository> logger)
{
_dataSourceProvider = dataSourceProvider ?? throw new ArgumentNullException(nameof(dataSourceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -206,9 +213,7 @@ public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingServic
private async Task<NpgsqlConnection> OpenConnectionAsync(CancellationToken ct)
{
var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
return conn;
return await _dataSourceProvider.OpenConnectionAsync(ct).ConfigureAwait(false);
}
private static CveSinkMapping MapFromReader(NpgsqlDataReader reader)

View File

@@ -0,0 +1,37 @@
using Npgsql;
namespace StellaOps.Scanner.Reachability.Services;
internal sealed class PostgresReachabilityDataSourceProvider : IAsyncDisposable
{
private readonly Lazy<NpgsqlDataSource> _dataSource;
public PostgresReachabilityDataSourceProvider(string connectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_dataSource = new Lazy<NpgsqlDataSource>(
() => CreateDataSource(connectionString),
isThreadSafe: true);
}
public ValueTask<NpgsqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
=> _dataSource.Value.OpenConnectionAsync(cancellationToken);
public async ValueTask DisposeAsync()
{
if (_dataSource.IsValueCreated)
{
await _dataSource.Value.DisposeAsync().ConfigureAwait(false);
}
}
private static NpgsqlDataSource CreateDataSource(string connectionString)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = "stellaops-scanner-reachability",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added a named reachability datasource provider and routed PostgreSQL-backed reachability services through it. |
| QA-SCANNER-VERIFY-005 | DONE | SPRINT_20260212_002 run-001: `api-gateway-boundary-extractor` passed Tier 0/1/2 and moved to `docs/features/checked/scanner/`. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -236,6 +236,7 @@ public static class VexLensServiceCollectionExtensions
{
var connStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
ApplicationName = "stellaops-vexlens",
CommandTimeout = options.CommandTimeoutSeconds
};
var builder = new NpgsqlDataSourceBuilder(connStringBuilder.ConnectionString);
@@ -274,6 +275,7 @@ public static class VexLensServiceCollectionExtensions
{
var connStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
ApplicationName = "stellaops-vexlens",
CommandTimeout = options.CommandTimeoutSeconds
};
var builder = new NpgsqlDataSourceBuilder(connStringBuilder.ConnectionString);

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named VexLens PostgreSQL consensus-store data sources. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/VexLens/StellaOps.VexLens/StellaOps.VexLens.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -62,7 +62,7 @@ public static class ServiceCollectionExtensions
"Eventing:ConnectionString must be configured when Eventing:UseInMemoryStore is false.");
}
return NpgsqlDataSource.Create(configuredOptions.ConnectionString);
return CreateNamedDataSource(configuredOptions.ConnectionString, configuredOptions.ServiceName);
});
services.TryAddSingleton<ITimelineEventStore, PostgresTimelineEventStore>();
@@ -104,7 +104,7 @@ public static class ServiceCollectionExtensions
TryAddHybridLogicalClock(services, serviceName);
// Register NpgsqlDataSource
services.TryAddSingleton(_ => NpgsqlDataSource.Create(connectionString));
services.TryAddSingleton(_ => CreateNamedDataSource(connectionString, serviceName));
services.TryAddSingleton<ITimelineEventStore, PostgresTimelineEventStore>();
services.TryAddSingleton<ITimelineEventEmitter, TimelineEventEmitter>();
@@ -186,4 +186,15 @@ public static class ServiceCollectionExtensions
return nodeId.Trim().Replace(' ', '-').ToLowerInvariant();
}
private static NpgsqlDataSource CreateNamedDataSource(string connectionString, string? serviceName)
{
var normalizedServiceName = ResolveNodeId(serviceName);
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = $"stellaops-eventing-{normalizedServiceName}",
};
return new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString).Build();
}
}

View File

@@ -19,6 +19,8 @@
## Determinism & Guardrails
- Target runtime: .NET 10, Npgsql 9.x; keep options defaults deterministic (UTC timezone, statement timeout, stable pagination ordering).
- Tenant context must be set via `set_config('app.current_tenant', ...)` on every connection before use; never bypass DataSourceBase.
- Runtime `NpgsqlDataSource` creation must carry a stable `application_name`; prefer `PostgresConnectionStringPolicy` or an explicit `NpgsqlConnectionStringBuilder.ApplicationName`.
- Anonymous `NpgsqlDataSource.Create(...)` is forbidden in steady-state runtime code. Allow exceptions only for tests, migrations, CLI/setup paths, or sprint-documented one-shot diagnostics.
- Migrations ship as embedded resources; MigrationRunner uses SHA256 checksums and `RunFromAssemblyAsync`???do not execute ad-hoc SQL outside tracked migrations.
- Respect air-gap posture: no external downloads at runtime; pin Postgres/Testcontainers images (`postgres:16-alpine` or later) in tests.
@@ -30,4 +32,3 @@
## Handoff Notes
- Align configuration defaults with the provisioning values under `devops/database/postgres` (ports, pool sizes, SSL/TLS).
- Update this AGENTS file whenever connection/session rules or provisioning defaults change; record updates in the sprint Execution Log.

View File

@@ -241,15 +241,8 @@ public abstract class DataSourceBase : IAsyncDisposable
return connection;
}
private static string BuildConnectionString(PostgresOptions options)
{
var builder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
{
Pooling = options.Pooling,
MaxPoolSize = options.MaxPoolSize,
MinPoolSize = options.MinPoolSize
};
return builder.ToString();
}
private string BuildConnectionString(PostgresOptions options)
=> PostgresConnectionStringPolicy.Build(
options,
PostgresConnectionStringPolicy.BuildDefaultApplicationName(ModuleName));
}

View File

@@ -0,0 +1,103 @@
using Npgsql;
using StellaOps.Infrastructure.Postgres.Options;
namespace StellaOps.Infrastructure.Postgres.Connections;
/// <summary>
/// Applies StellaOps runtime defaults to PostgreSQL connection strings.
/// </summary>
public static class PostgresConnectionStringPolicy
{
public static string Build(PostgresOptions options, string defaultApplicationName)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(defaultApplicationName);
return Build(
options.ConnectionString,
string.IsNullOrWhiteSpace(options.ApplicationName)
? defaultApplicationName.Trim()
: options.ApplicationName.Trim(),
options.Pooling,
options.MinPoolSize,
options.MaxPoolSize,
options.ConnectionIdleLifetimeSeconds);
}
public static string Build(
string connectionString,
string applicationName,
bool pooling = true,
int minPoolSize = 1,
int maxPoolSize = 100,
int? connectionIdleLifetimeSeconds = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
ArgumentException.ThrowIfNullOrWhiteSpace(applicationName);
var normalizedMinPoolSize = Math.Max(minPoolSize, 0);
var normalizedMaxPoolSize = Math.Max(maxPoolSize, normalizedMinPoolSize);
var builder = new NpgsqlConnectionStringBuilder(connectionString)
{
ApplicationName = applicationName.Trim(),
Pooling = pooling,
MinPoolSize = normalizedMinPoolSize,
MaxPoolSize = normalizedMaxPoolSize,
};
if (connectionIdleLifetimeSeconds.HasValue && connectionIdleLifetimeSeconds.Value > 0)
{
builder.ConnectionIdleLifetime = connectionIdleLifetimeSeconds.Value;
}
return builder.ToString();
}
public static string BuildDefaultApplicationName(string moduleName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(moduleName);
return $"stellaops-{ToKebabCase(moduleName.Trim())}";
}
private static string ToKebabCase(string value)
{
var chars = new List<char>(value.Length + 8);
for (var i = 0; i < value.Length; i++)
{
var current = value[i];
if (char.IsWhiteSpace(current) || current is '_' or '-')
{
if (chars.Count > 0 && chars[^1] != '-')
{
chars.Add('-');
}
continue;
}
if (char.IsUpper(current))
{
var hasPrevious = chars.Count > 0;
var previous = hasPrevious ? value[i - 1] : '\0';
var hasNext = i + 1 < value.Length;
var next = hasNext ? value[i + 1] : '\0';
if (hasPrevious
&& chars[^1] != '-'
&& (!char.IsUpper(previous) || (hasNext && char.IsLower(next))))
{
chars.Add('-');
}
chars.Add(char.ToLowerInvariant(current));
continue;
}
chars.Add(char.ToLowerInvariant(current));
}
return new string(chars.ToArray()).Trim('-');
}
}

View File

@@ -20,6 +20,11 @@ public sealed class PostgresOptions
/// </summary>
public int MaxPoolSize { get; set; } = 100;
/// <summary>
/// Stable PostgreSQL application name used for runtime attribution.
/// </summary>
public string? ApplicationName { get; set; }
/// <summary>
/// Minimum number of connections in the pool. Default is 1.
/// </summary>

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| SPRINT_20260405_011-XPORT-STD | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: shared transport hardening, PostgreSQL application-name policy, runtime caller remediation, and static guardrails. |
| AUDIT-0089-M | DONE | Revalidated 2026-01-08; maintainability audit for Infrastructure.Postgres. |
| AUDIT-0089-T | DONE | Revalidated 2026-01-08; test coverage audit for Infrastructure.Postgres. |
| AUDIT-0089-A | TODO | Pending approval (non-test project; revalidated 2026-01-08). |

View File

@@ -0,0 +1,66 @@
using FluentAssertions;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Connections;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Infrastructure.Postgres.Tests;
[Trait("Category", TestCategories.Unit)]
public sealed class PostgresConnectionStringPolicyTests
{
[Fact]
public void Build_applies_runtime_attribution_and_pooling_policy()
{
var options = new PostgresOptions
{
ConnectionString = "Host=localhost;Database=stellaops;Username=stellaops;Password=stellaops",
ApplicationName = "custom-ledger-app",
Pooling = false,
MinPoolSize = 3,
MaxPoolSize = 12,
ConnectionIdleLifetimeSeconds = 900,
};
var connectionString = PostgresConnectionStringPolicy.Build(options, "stellaops-findings-ledger");
var builder = new NpgsqlConnectionStringBuilder(connectionString);
builder.ApplicationName.Should().Be("custom-ledger-app");
builder.Pooling.Should().BeFalse();
builder.MinPoolSize.Should().Be(3);
builder.MaxPoolSize.Should().Be(12);
builder.ConnectionIdleLifetime.Should().Be(900);
}
[Fact]
public void Build_uses_default_application_name_when_none_is_configured()
{
var options = new PostgresOptions
{
ConnectionString = "Host=localhost;Database=stellaops;Username=stellaops;Password=stellaops",
MinPoolSize = 0,
MaxPoolSize = 1,
};
var connectionString = PostgresConnectionStringPolicy.Build(options, "stellaops-policy");
var builder = new NpgsqlConnectionStringBuilder(connectionString);
builder.ApplicationName.Should().Be("stellaops-policy");
builder.MinPoolSize.Should().Be(0);
builder.MaxPoolSize.Should().Be(1);
builder.ConnectionIdleLifetime.Should().Be(300);
}
[Theory]
[InlineData("Policy", "stellaops-policy")]
[InlineData("PacksRegistry", "stellaops-packs-registry")]
[InlineData("IssuerDirectory", "stellaops-issuer-directory")]
[InlineData("ReachGraph", "stellaops-reach-graph")]
public void BuildDefaultApplicationName_normalizes_module_names(string moduleName, string expected)
{
var result = PostgresConnectionStringPolicy.BuildDefaultApplicationName(moduleName);
result.Should().Be(expected);
}
}