diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md index f096f62bc..6409cceda 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/TASKS.md @@ -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. | diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs index 751c8fabc..b62daccf7 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs @@ -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(sp => diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md index 3d3e1a761..8b11c8fb1 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/TASKS.md @@ -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. | diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Storage/Rekor/PostgresRekorCheckpointStore.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Storage/Rekor/PostgresRekorCheckpointStore.cs index 8190c20c4..1cc82b13e 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Storage/Rekor/PostgresRekorCheckpointStore.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Storage/Rekor/PostgresRekorCheckpointStore.cs @@ -19,9 +19,9 @@ namespace StellaOps.Attestor.Core.Rekor; /// /// PostgreSQL implementation of the Rekor checkpoint store. /// -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 _logger; @@ -30,8 +30,9 @@ public sealed class PostgresRekorCheckpointStore : IRekorCheckpointStore ILogger 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 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 diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresPredicateTypeRegistryRepository.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresPredicateTypeRegistryRepository.cs index ada9a7728..21f9b9c74 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresPredicateTypeRegistryRepository.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresPredicateTypeRegistryRepository.cs @@ -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. /// -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 /// 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(); + } } diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresVerdictLedgerRepository.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresVerdictLedgerRepository.cs index c1a1bac70..0b7299653 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresVerdictLedgerRepository.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/Repositories/PostgresVerdictLedgerRepository.cs @@ -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. /// -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 /// 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 /// public async Task 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 /// public async Task 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 /// public async Task 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(); + } } diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/TASKS.md b/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/TASKS.md index 3b0203e0c..75154f0f8 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/TASKS.md +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Persistence/TASKS.md @@ -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. | diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.CheckAndUpdate.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.CheckAndUpdate.cs index 5769247b5..aea118ad3 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.CheckAndUpdate.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.CheckAndUpdate.cs @@ -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); diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.cs index 6ea6dfa8b..97928487b 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresAlertDedupRepository.cs @@ -8,15 +8,15 @@ namespace StellaOps.Attestor.Watchlist.Storage; /// /// PostgreSQL implementation of the alert deduplication repository. /// -public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository +public sealed partial class PostgresAlertDedupRepository : IAlertDedupRepository, IAsyncDisposable { - private readonly string _connectionString; + private readonly NpgsqlDataSource _dataSource; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public PostgresAlertDedupRepository(string connectionString, ILogger 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 /// public async Task 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(); + } } diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.List.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.List.cs index b46a16ef8..4dadfedaa 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.List.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.List.cs @@ -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); diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.Upsert.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.Upsert.cs index 9c1683f4f..de9019615 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.Upsert.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.Upsert.cs @@ -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 /// public async Task 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); diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.cs b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.cs index 2def8ba2a..c3be28f48 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.cs +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/Storage/PostgresWatchlistRepository.cs @@ -11,9 +11,9 @@ namespace StellaOps.Attestor.Watchlist.Storage; /// /// PostgreSQL implementation of the watchlist repository with caching. /// -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 _logger; private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(5); @@ -28,7 +28,7 @@ public sealed partial class PostgresWatchlistRepository : IWatchlistRepository IMemoryCache cache, ILogger 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 /// public async Task 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 /// public async Task 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(); + } } diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/TASKS.md b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/TASKS.md index 06c10dd0e..3f2dda794 100644 --- a/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/TASKS.md +++ b/src/Attestor/__Libraries/StellaOps.Attestor.Watchlist/TASKS.md @@ -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. | diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs index 10802f8a8..a6c4d3200 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Program.cs @@ -176,7 +176,11 @@ builder.Services.TryAddSingleton(); if (string.Equals(senderConstraints.Dpop.Nonce.Store, "redis", StringComparison.OrdinalIgnoreCase)) { builder.Services.TryAddSingleton(_ => - 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(provider => { diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/TASKS.md b/src/Authority/StellaOps.Authority/StellaOps.Authority/TASKS.md index 38d79ff6c..d74ad620c 100644 --- a/src/Authority/StellaOps.Authority/StellaOps.Authority/TASKS.md +++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/TASKS.md @@ -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. | diff --git a/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs b/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs index b3c8da768..de67a1f1d 100644 --- a/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs +++ b/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs @@ -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(); diff --git a/src/BinaryIndex/StellaOps.BinaryIndex.WebService/TASKS.md b/src/BinaryIndex/StellaOps.BinaryIndex.WebService/TASKS.md index 0a093f6a7..6b893a8b2 100644 --- a/src/BinaryIndex/StellaOps.BinaryIndex.WebService/TASKS.md +++ b/src/BinaryIndex/StellaOps.BinaryIndex.WebService/TASKS.md @@ -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`. | diff --git a/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/MatchResultRepository.cs b/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/MatchResultRepository.cs index e7e3a232e..3331c1e61 100644 --- a/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/MatchResultRepository.cs +++ b/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/MatchResultRepository.cs @@ -10,14 +10,14 @@ namespace StellaOps.BinaryIndex.Validation.Persistence; /// /// PostgreSQL repository for match results. /// -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 /// public async Task> 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(); + } } diff --git a/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/ValidationRunRepository.cs b/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/ValidationRunRepository.cs index 7ac918970..ccc86d213 100644 --- a/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/ValidationRunRepository.cs +++ b/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/Persistence/ValidationRunRepository.cs @@ -10,14 +10,14 @@ namespace StellaOps.BinaryIndex.Validation.Persistence; /// /// PostgreSQL repository for validation runs. /// -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 /// 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 /// public async Task 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(); + } } diff --git a/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/TASKS.md b/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/TASKS.md index 9da5d3321..fdd2140be 100644 --- a/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/TASKS.md +++ b/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Validation/TASKS.md @@ -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. | diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresDistroAdvisoryRepository.cs b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresDistroAdvisoryRepository.cs index af4980643..cc72d2d2a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresDistroAdvisoryRepository.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresDistroAdvisoryRepository.cs @@ -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. /// -public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository +public sealed class PostgresDistroAdvisoryRepository : IDistroAdvisoryRepository, IAsyncDisposable { - private readonly string _connectionString; + private readonly NpgsqlDataSource _dataSource; private readonly ILogger _logger; public PostgresDistroAdvisoryRepository( string connectionString, ILogger 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( 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(); + } } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresPatchRepository.cs b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresPatchRepository.cs index 263d56f31..d13ea1813 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresPatchRepository.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresPatchRepository.cs @@ -11,16 +11,16 @@ namespace StellaOps.Concelier.ProofService.Postgres; /// PostgreSQL implementation of patch repository. /// Queries vuln.patch_evidence and feedser.binary_fingerprints tables. /// -public sealed class PostgresPatchRepository : IPatchRepository +public sealed class PostgresPatchRepository : IPatchRepository, IAsyncDisposable { - private readonly string _connectionString; + private readonly NpgsqlDataSource _dataSource; private readonly ILogger _logger; public PostgresPatchRepository( string connectionString, ILogger 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( 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( 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( 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(); + } } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresSourceArtifactRepository.cs b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresSourceArtifactRepository.cs index 00a31890e..eb5532dea 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresSourceArtifactRepository.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/PostgresSourceArtifactRepository.cs @@ -10,16 +10,16 @@ namespace StellaOps.Concelier.ProofService.Postgres; /// PostgreSQL implementation of source artifact repository. /// Queries vuln.changelog_evidence for CVE mentions in changelogs. /// -public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository +public sealed class PostgresSourceArtifactRepository : ISourceArtifactRepository, IAsyncDisposable { - private readonly string _connectionString; + private readonly NpgsqlDataSource _dataSource; private readonly ILogger _logger; public PostgresSourceArtifactRepository( string connectionString, ILogger 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( 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(); + } } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/TASKS.md b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/TASKS.md index 17da14ee2..924281b08 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/TASKS.md +++ b/src/Concelier/__Libraries/StellaOps.Concelier.ProofService.Postgres/TASKS.md @@ -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). | diff --git a/src/Doctor/StellaOps.Doctor.WebService/Services/PostgresReportStorageService.cs b/src/Doctor/StellaOps.Doctor.WebService/Services/PostgresReportStorageService.cs index d7d7d3b0c..436ace62a 100644 --- a/src/Doctor/StellaOps.Doctor.WebService/Services/PostgresReportStorageService.cs +++ b/src/Doctor/StellaOps.Doctor.WebService/Services/PostgresReportStorageService.cs @@ -23,7 +23,7 @@ namespace StellaOps.Doctor.WebService.Services; /// public sealed class PostgresReportStorageService : IReportStorageService, IDisposable { - private readonly string _connectionString; + private readonly NpgsqlDataSource _dataSource; private readonly DoctorServiceOptions _options; private readonly ILogger _logger; private readonly Timer? _cleanupTimer; @@ -37,9 +37,10 @@ public sealed class PostgresReportStorageService : IReportStorageService, IDispo IOptions options, ILogger 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 /// public async Task 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 /// public async Task> 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 /// public async Task 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 /// public async Task 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(); + } } diff --git a/src/Doctor/StellaOps.Doctor.WebService/TASKS.md b/src/Doctor/StellaOps.Doctor.WebService/TASKS.md index abf036890..801cf20bd 100644 --- a/src/Doctor/StellaOps.Doctor.WebService/TASKS.md +++ b/src/Doctor/StellaOps.Doctor.WebService/TASKS.md @@ -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 diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/EvidenceLockerDataSource.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/EvidenceLockerDataSource.cs index dc003e453..399eff1ed 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/EvidenceLockerDataSource.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/EvidenceLockerDataSource.cs @@ -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(); } diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/DependencyInjection/EvidenceLockerInfrastructureServiceCollectionExtensions.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/DependencyInjection/EvidenceLockerInfrastructureServiceCollectionExtensions.cs index b3df5119a..c4d9224bd 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/DependencyInjection/EvidenceLockerInfrastructureServiceCollectionExtensions.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/DependencyInjection/EvidenceLockerInfrastructureServiceCollectionExtensions.cs @@ -83,10 +83,10 @@ public static class EvidenceLockerInfrastructureServiceCollectionExtensions // Verdict attestation repository services.AddScoped(provider => { - var options = provider.GetRequiredService>().Value; + var dataSource = provider.GetRequiredService(); var logger = provider.GetRequiredService>(); 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(provider => { - var options = provider.GetRequiredService>().Value; + var dataSource = provider.GetRequiredService(); var logger = provider.GetRequiredService>(); return new StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository( - options.Database.ConnectionString, + cancellationToken => dataSource.OpenConnectionAsync(cancellationToken), logger); }); diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/TASKS.md b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/TASKS.md index 691c8e73e..dcb5b78f8 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/TASKS.md +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/TASKS.md @@ -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). | diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresEvidenceThreadRepository.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresEvidenceThreadRepository.cs index a798b5971..2ecb60bb1 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresEvidenceThreadRepository.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresEvidenceThreadRepository.cs @@ -11,14 +11,14 @@ namespace StellaOps.EvidenceLocker.Storage; /// public sealed class PostgresEvidenceThreadRepository : IEvidenceThreadRepository { - private readonly string _connectionString; + private readonly Func> _openConnectionAsync; private readonly ILogger _logger; public PostgresEvidenceThreadRepository( - string connectionString, + Func> openConnectionAsync, ILogger 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( 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( new CommandDefinition( diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresVerdictRepository.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresVerdictRepository.cs index ec9c90599..3db9103f1 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresVerdictRepository.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/Storage/PostgresVerdictRepository.cs @@ -9,16 +9,16 @@ namespace StellaOps.EvidenceLocker.Storage; /// public sealed class PostgresVerdictRepository : IVerdictRepository { - private readonly string _connectionString; + private readonly Func> _openConnectionAsync; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public PostgresVerdictRepository( - string connectionString, + Func> openConnectionAsync, ILogger 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( @@ -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( 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( 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( 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( new CommandDefinition( diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs index c9ef9bc87..5db8e06b2 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/ExportCenterDataSource.cs @@ -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(); } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/TASKS.md b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/TASKS.md index 0b0f666cb..936c9483f 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/TASKS.md +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/TASKS.md @@ -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). | diff --git a/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/LedgerDataSource.cs b/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/LedgerDataSource.cs index 1eb1deebb..20cc7e77f 100644 --- a/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/LedgerDataSource.cs +++ b/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/LedgerDataSource.cs @@ -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(); } diff --git a/src/Findings/StellaOps.Findings.Ledger/TASKS.md b/src/Findings/StellaOps.Findings.Ledger/TASKS.md index 2c132620d..25ed51768 100644 --- a/src/Findings/StellaOps.Findings.Ledger/TASKS.md +++ b/src/Findings/StellaOps.Findings.Ledger/TASKS.md @@ -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). | diff --git a/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/Stores/PostgresRiskScoreResultStore.cs b/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/Stores/PostgresRiskScoreResultStore.cs index 0fc50abc4..5ca27c146 100644 --- a/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/Stores/PostgresRiskScoreResultStore.cs +++ b/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/Stores/PostgresRiskScoreResultStore.cs @@ -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) diff --git a/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/TASKS.md b/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/TASKS.md index 39803a5c6..302cc360e 100644 --- a/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/TASKS.md +++ b/src/Findings/__Libraries/StellaOps.RiskEngine.Infrastructure/TASKS.md @@ -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. | diff --git a/src/Platform/StellaOps.Platform.Analytics/Services/AnalyticsIngestionDataSource.cs b/src/Platform/StellaOps.Platform.Analytics/Services/AnalyticsIngestionDataSource.cs index 4e1d90f46..51013e49c 100644 --- a/src/Platform/StellaOps.Platform.Analytics/Services/AnalyticsIngestionDataSource.cs +++ b/src/Platform/StellaOps.Platform.Analytics/Services/AnalyticsIngestionDataSource.cs @@ -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(); + } } diff --git a/src/Platform/StellaOps.Platform.Analytics/TASKS.md b/src/Platform/StellaOps.Platform.Analytics/TASKS.md index dc4e9ef77..ae73dd5c6 100644 --- a/src/Platform/StellaOps.Platform.Analytics/TASKS.md +++ b/src/Platform/StellaOps.Platform.Analytics/TASKS.md @@ -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/`. | diff --git a/src/Platform/StellaOps.Platform.WebService/Services/PlatformAnalyticsDataSource.cs b/src/Platform/StellaOps.Platform.WebService/Services/PlatformAnalyticsDataSource.cs index 672a4b872..8e51f90fd 100644 --- a/src/Platform/StellaOps.Platform.WebService/Services/PlatformAnalyticsDataSource.cs +++ b/src/Platform/StellaOps.Platform.WebService/Services/PlatformAnalyticsDataSource.cs @@ -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(); + } } diff --git a/src/Plugin/StellaOps.Plugin.Registry/Extensions/ServiceCollectionExtensions.cs b/src/Plugin/StellaOps.Plugin.Registry/Extensions/ServiceCollectionExtensions.cs index 057676559..9d95f88ce 100644 --- a/src/Plugin/StellaOps.Plugin.Registry/Extensions/ServiceCollectionExtensions.cs +++ b/src/Plugin/StellaOps.Plugin.Registry/Extensions/ServiceCollectionExtensions.cs @@ -30,7 +30,12 @@ public static class ServiceCollectionExtensions services.AddSingleton(sp => { var options = sp.GetRequiredService>().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(sp => { var options = sp.GetRequiredService>().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(); }); diff --git a/src/Plugin/StellaOps.Plugin.Registry/TASKS.md b/src/Plugin/StellaOps.Plugin.Registry/TASKS.md index 5cc284b6d..6c6abc6c4 100644 --- a/src/Plugin/StellaOps.Plugin.Registry/TASKS.md +++ b/src/Plugin/StellaOps.Plugin.Registry/TASKS.md @@ -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. | diff --git a/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs b/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs index fbba690a2..c433f5ac5 100644 --- a/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs +++ b/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs @@ -272,7 +272,11 @@ public static class PolicyEngineServiceCollectionExtensions string connectionString) { services.TryAddSingleton(sp => - ConnectionMultiplexer.Connect(connectionString)); + { + var redisOptions = ConfigurationOptions.Parse(connectionString); + redisOptions.ClientName ??= "stellaops-policy-engine"; + return ConnectionMultiplexer.Connect(redisOptions); + }); return services; } diff --git a/src/Policy/StellaOps.Policy.Engine/TASKS.md b/src/Policy/StellaOps.Policy.Engine/TASKS.md index 4086f89c3..18ae93b49 100644 --- a/src/Policy/StellaOps.Policy.Engine/TASKS.md +++ b/src/Policy/StellaOps.Policy.Engine/TASKS.md @@ -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). | diff --git a/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs b/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs index b7ad5e79e..746d4be5a 100644 --- a/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs +++ b/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs @@ -35,7 +35,13 @@ builder.Services.AddSingleton(sp => var config = sp.GetRequiredService(); 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(sp => { var config = sp.GetRequiredService(); 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 diff --git a/src/ReachGraph/StellaOps.ReachGraph.WebService/TASKS.md b/src/ReachGraph/StellaOps.ReachGraph.WebService/TASKS.md index 9dff0f39f..a0ab8d9b6 100644 --- a/src/ReachGraph/StellaOps.ReachGraph.WebService/TASKS.md +++ b/src/ReachGraph/StellaOps.ReachGraph.WebService/TASKS.md @@ -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. | diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Extensions/ServiceCollectionExtensions.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Extensions/ServiceCollectionExtensions.cs index ea9609c16..16d4c5e3a 100644 --- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Extensions/ServiceCollectionExtensions.cs +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Extensions/ServiceCollectionExtensions.cs @@ -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 diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/TASKS.md b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/TASKS.md index bf9eb34cd..453023388 100644 --- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/TASKS.md +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/TASKS.md @@ -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. | diff --git a/src/Replay/StellaOps.Replay.WebService/ReplayFeedSnapshotStores.cs b/src/Replay/StellaOps.Replay.WebService/ReplayFeedSnapshotStores.cs index e04f7aec6..67862d8ed 100644 --- a/src/Replay/StellaOps.Replay.WebService/ReplayFeedSnapshotStores.cs +++ b/src/Replay/StellaOps.Replay.WebService/ReplayFeedSnapshotStores.cs @@ -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) diff --git a/src/Replay/StellaOps.Replay.WebService/TASKS.md b/src/Replay/StellaOps.Replay.WebService/TASKS.md index 7ef596939..30e232289 100644 --- a/src/Replay/StellaOps.Replay.WebService/TASKS.md +++ b/src/Replay/StellaOps.Replay.WebService/TASKS.md @@ -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. | diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PostgresReachabilityCache.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PostgresReachabilityCache.cs index f607508df..35a949449 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PostgresReachabilityCache.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PostgresReachabilityCache.cs @@ -20,7 +20,7 @@ namespace StellaOps.Scanner.Reachability.Cache; /// public sealed class PostgresReachabilityCache : IReachabilityCache { - private readonly string _connectionString; + private readonly Services.PostgresReachabilityDataSourceProvider _dataSourceProvider; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; @@ -28,8 +28,16 @@ public sealed class PostgresReachabilityCache : IReachabilityCache string connectionString, ILogger logger, TimeProvider? timeProvider = null) + : this(new Services.PostgresReachabilityDataSourceProvider(connectionString), logger, timeProvider) { - _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + } + + internal PostgresReachabilityCache( + Services.PostgresReachabilityDataSourceProvider dataSourceProvider, + ILogger 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 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 OpenConnectionAsync(CancellationToken cancellationToken) + => _dataSourceProvider.OpenConnectionAsync(cancellationToken); } diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ServiceCollectionExtensions.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ServiceCollectionExtensions.cs index f22255bec..d6723319e 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ServiceCollectionExtensions.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ServiceCollectionExtensions.cs @@ -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(sp => new PostgresCveSymbolMappingRepository( - connectionString, + sp.GetRequiredService(), sp.GetRequiredService>())); // Stack Evaluator (already exists, ensure registered) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresCveSymbolMappingRepository.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresCveSymbolMappingRepository.cs index c044d6797..71f507149 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresCveSymbolMappingRepository.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresCveSymbolMappingRepository.cs @@ -15,14 +15,21 @@ namespace StellaOps.Scanner.Reachability.Services; /// public sealed class PostgresCveSymbolMappingRepository : ICveSymbolMappingService { - private readonly string _connectionString; + private readonly PostgresReachabilityDataSourceProvider _dataSourceProvider; private readonly ILogger _logger; public PostgresCveSymbolMappingRepository( string connectionString, ILogger logger) + : this(new PostgresReachabilityDataSourceProvider(connectionString), logger) { - _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + } + + internal PostgresCveSymbolMappingRepository( + PostgresReachabilityDataSourceProvider dataSourceProvider, + ILogger 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 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) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresReachabilityDataSourceProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresReachabilityDataSourceProvider.cs new file mode 100644 index 000000000..df3d6a268 --- /dev/null +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Services/PostgresReachabilityDataSourceProvider.cs @@ -0,0 +1,37 @@ +using Npgsql; + +namespace StellaOps.Scanner.Reachability.Services; + +internal sealed class PostgresReachabilityDataSourceProvider : IAsyncDisposable +{ + private readonly Lazy _dataSource; + + public PostgresReachabilityDataSourceProvider(string connectionString) + { + ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); + _dataSource = new Lazy( + () => CreateDataSource(connectionString), + isThreadSafe: true); + } + + public ValueTask 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(); + } +} diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/TASKS.md b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/TASKS.md index 5a8ef8da2..206b32cb8 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/TASKS.md +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/TASKS.md @@ -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. | diff --git a/src/VexLens/StellaOps.VexLens/Extensions/VexLensServiceCollectionExtensions.cs b/src/VexLens/StellaOps.VexLens/Extensions/VexLensServiceCollectionExtensions.cs index 315634d9b..a6e4edaaf 100644 --- a/src/VexLens/StellaOps.VexLens/Extensions/VexLensServiceCollectionExtensions.cs +++ b/src/VexLens/StellaOps.VexLens/Extensions/VexLensServiceCollectionExtensions.cs @@ -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); diff --git a/src/VexLens/StellaOps.VexLens/TASKS.md b/src/VexLens/StellaOps.VexLens/TASKS.md index 3a6b6a6ea..a0a0d1443 100644 --- a/src/VexLens/StellaOps.VexLens/TASKS.md +++ b/src/VexLens/StellaOps.VexLens/TASKS.md @@ -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. | diff --git a/src/__Libraries/StellaOps.Eventing/ServiceCollectionExtensions.cs b/src/__Libraries/StellaOps.Eventing/ServiceCollectionExtensions.cs index 28000ee2e..cd596e95d 100644 --- a/src/__Libraries/StellaOps.Eventing/ServiceCollectionExtensions.cs +++ b/src/__Libraries/StellaOps.Eventing/ServiceCollectionExtensions.cs @@ -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(); @@ -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(); services.TryAddSingleton(); @@ -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(); + } } diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md b/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md index b8d3bd0bb..9c4ca815d 100644 --- a/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md @@ -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. - diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs b/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs index 6ef1ad92e..af429d156 100644 --- a/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/DataSourceBase.cs @@ -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)); } diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/PostgresConnectionStringPolicy.cs b/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/PostgresConnectionStringPolicy.cs new file mode 100644 index 000000000..f94c668fa --- /dev/null +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/Connections/PostgresConnectionStringPolicy.cs @@ -0,0 +1,103 @@ +using Npgsql; +using StellaOps.Infrastructure.Postgres.Options; + +namespace StellaOps.Infrastructure.Postgres.Connections; + +/// +/// Applies StellaOps runtime defaults to PostgreSQL connection strings. +/// +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(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('-'); + } +} diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/Options/PostgresOptions.cs b/src/__Libraries/StellaOps.Infrastructure.Postgres/Options/PostgresOptions.cs index 5508d5087..bb7b38ef1 100644 --- a/src/__Libraries/StellaOps.Infrastructure.Postgres/Options/PostgresOptions.cs +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/Options/PostgresOptions.cs @@ -20,6 +20,11 @@ public sealed class PostgresOptions /// public int MaxPoolSize { get; set; } = 100; + /// + /// Stable PostgreSQL application name used for runtime attribution. + /// + public string? ApplicationName { get; set; } + /// /// Minimum number of connections in the pool. Default is 1. /// diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/TASKS.md b/src/__Libraries/StellaOps.Infrastructure.Postgres/TASKS.md index 67ebcd7e7..e90808f6c 100644 --- a/src/__Libraries/StellaOps.Infrastructure.Postgres/TASKS.md +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/TASKS.md @@ -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). | diff --git a/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresConnectionStringPolicyTests.cs b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresConnectionStringPolicyTests.cs new file mode 100644 index 000000000..8a33895da --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/PostgresConnectionStringPolicyTests.cs @@ -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); + } +}