From 751546084ebbed76aad30c3232e7d151a52ff6a6 Mon Sep 17 00:00:00 2001
From: master <>
Date: Sun, 5 Apr 2026 23:52:14 +0300
Subject: [PATCH] Harden runtime HTTP transport lifecycles
---
...sport_pooling_and_attribution_hardening.md | 137 +++++++++++++
.../runtime-transport-client-rules.md | 26 +++
.../StellaOps.Attestor.TrustRepo/TASKS.md | 1 +
.../TrustRepoOptions.cs | 5 +
...RepoServiceCollectionExtensions.Offline.cs | 4 +-
.../TrustRepoServiceCollectionExtensions.cs | 12 +-
.../Oci/TrustVerdictOciAttacher.cs | 2 +-
.../Oci/TrustVerdictOciRuntimeHttpClient.cs | 19 ++
.../StellaOps.Attestor.TrustVerdict/TASKS.md | 1 +
.../TASKS.md | 1 +
...ustRepoServiceCollectionExtensionsTests.cs | 39 ++++
.../FeedMirrorConnectorPlugins.cs | 190 ++++++++++++++++++
.../Infrastructure/DefaultImplementations.cs | 13 +-
.../IntegrationHttpClientDefaults.cs | 17 ++
.../IntegrationPluginLoader.cs | 43 +++-
.../ObjectStorageConnectorPlugins.cs | 137 +++++++++++++
.../Program.cs | 22 +-
.../TASKS.md | 1 +
.../IntegrationPluginLoaderTests.cs | 127 ++++++++++++
.../StellaOps.Integrations.Tests/TASKS.md | 1 +
.../StellaOps.Platform.WebService/Program.cs | 26 ++-
.../IdentityProviderManagementService.cs | 74 +++----
.../StellaOps.Platform.WebService/TASKS.md | 3 +
.../IdentityProviderManagementServiceTests.cs | 64 ++++++
.../TASKS.md | 1 +
.../Connectors/ConnectorHttpClients.cs | 32 +++
.../Connectors/Registry/AcrConnector.cs | 18 +-
.../Connectors/Registry/DockerHubConnector.cs | 6 +-
.../Connectors/Registry/EcrConnector.cs | 11 +-
.../Connectors/Registry/GcrConnector.cs | 13 +-
.../Vault/AwsSecretsManagerConnector.cs | 6 +-
.../Vault/AzureKeyVaultConnector.cs | 18 +-
.../Vault/HashiCorpVaultConnector.cs | 16 +-
.../TASKS.md | 1 +
.../Api/ArtifactController.FetchHttp.cs | 3 +-
.../Api/ArtifactController.cs | 11 +-
.../StellaOps.Artifact.Core/TASKS.md | 1 +
.../StellaOps.Plugin/PluginContracts.cs | 16 +-
src/__Libraries/StellaOps.Plugin/TASKS.md | 1 +
.../Oci/OciAttestationPublisher.cs | 2 +-
.../Oci/OciRuntimeHttpClient.cs | 19 ++
src/__Libraries/StellaOps.Verdict/TASKS.md | 1 +
...timePostgresConstructionConventionTests.cs | 167 +++++++++++++++
.../TASKS.md | 1 +
44 files changed, 1173 insertions(+), 136 deletions(-)
create mode 100644 docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md
create mode 100644 docs/technical/runtime-transport-client-rules.md
create mode 100644 src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciRuntimeHttpClient.cs
create mode 100644 src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TrustRepoServiceCollectionExtensionsTests.cs
create mode 100644 src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs
create mode 100644 src/Integrations/StellaOps.Integrations.WebService/IntegrationHttpClientDefaults.cs
create mode 100644 src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs
create mode 100644 src/Platform/__Tests/StellaOps.Platform.WebService.Tests/IdentityProviderManagementServiceTests.cs
create mode 100644 src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/ConnectorHttpClients.cs
create mode 100644 src/__Libraries/StellaOps.Verdict/Oci/OciRuntimeHttpClient.cs
create mode 100644 src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs
diff --git a/docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md b/docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md
new file mode 100644
index 000000000..2c74958ef
--- /dev/null
+++ b/docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md
@@ -0,0 +1,137 @@
+# Sprint 20260405-011 - Transport Pooling And Attribution Hardening
+
+## Topic & Scope
+- Standardize runtime transport client lifecycle and attribution so Stella Ops services stop producing anonymous or churn-heavy long-lived connections.
+- Extend the shared PostgreSQL infrastructure first, then patch the known PostgreSQL and Valkey runtime hotspots to use named, reusable clients.
+- Continue the hardening pass through the first HTTP lifecycle hotspots where service-owned runtime code still allocated raw `HttpClient` instances.
+- Add static guardrails and focused tests so raw runtime transport construction does not re-enter the codebase unnoticed.
+- Working directory: `src/__Libraries/`.
+- Expected evidence: shared infrastructure tests, targeted service/runtime validation, updated transport/database docs, and sprint-linked before/after findings.
+
+## Dependencies & Concurrency
+- Depends on `docs/implplan/SPRINT_20260405_008_Integrations_consul_pg_router_runtime_tuning.md` for the PostgreSQL runtime logging baseline.
+- Depends on `docs/implplan/SPRINT_20260405_010_AdvisoryAI_pg_pooling_and_gitea_spike_followup.md` for the proven AdvisoryAI regression pattern and remediation baseline.
+- Cross-module edits allowed for `src/AdvisoryAI/**`, `src/Attestor/**`, `src/Authority/**`, `src/BinaryIndex/**`, `src/Concelier/**`, `src/Doctor/**`, `src/EvidenceLocker/**`, `src/Findings/**`, `src/Graph/**`, `src/Integrations/**`, `src/JobEngine/**`, `src/Notify/**`, `src/Platform/**`, `src/Policy/**`, `src/ReachGraph/**`, `src/ReleaseOrchestrator/**`, `src/Scanner/**`, `src/Signals/**`, `src/Timeline/**`, `src/Router/**`, `src/Plugin/**`, `docs/**`, and `devops/**` when they consume the shared transport conventions.
+
+## Documentation Prerequisites
+- `docs/code-of-conduct/CODE_OF_CONDUCT.md`
+- `docs/code-of-conduct/TESTING_PRACTICES.md`
+- `docs/README.md`
+- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
+- `docs/modules/platform/architecture-overview.md`
+- `docs/db/RULES.md`
+- `src/__Libraries/AGENTS.md`
+- `src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md`
+- `src/__Tests/AGENTS.md`
+
+## Delivery Tracker
+
+### XPORT-STD-001 - Extend shared PostgreSQL transport policy
+Status: DONE
+Dependency: none
+Owners: Developer
+Task description:
+- Add stable application-name support and complete pooling policy propagation to the shared PostgreSQL options/base infrastructure so module-level data sources can be named and tuned without ad hoc code.
+- Update the shared library docs and tests so the behavior is explicit and regression-safe.
+
+Completion criteria:
+- [x] Shared PostgreSQL options expose stable runtime application-name configuration.
+- [x] Shared data-source construction applies application name plus the full pooling policy, including idle lifetime.
+- [x] Infrastructure.Postgres tests cover the new policy behavior.
+
+### XPORT-RUNTIME-002 - Patch runtime PostgreSQL callers and service bootstraps
+Status: DONE
+Dependency: XPORT-STD-001
+Owners: Developer
+Task description:
+- Convert the currently known runtime hotspots and service bootstraps to named, reusable PostgreSQL data sources instead of anonymous or ad hoc construction.
+- Prioritize the services already identified in live runtime evidence: Findings, JobEngine, EvidenceLocker, AdvisoryAI/OpsMemory, ReachGraph, and Scanner reachability paths.
+
+Completion criteria:
+- [x] Touched runtime services stop constructing anonymous PostgreSQL data sources in their steady-state code paths.
+- [x] Hot-path repositories touched by this sprint use reusable data sources/providers instead of raw connection strings where practical.
+- [x] Compose/runtime-facing defaults or docs are updated when a touched service gains a new attribution/pooling option.
+
+### XPORT-GUARD-003 - Add static guardrails for runtime transport construction
+Status: DONE
+Dependency: XPORT-STD-001
+Owners: Developer
+Task description:
+- Add a focused convention test that scans runtime code for forbidden raw transport construction patterns and documents the allowlisted exceptions (tests, migrations, CLI setup, one-shot diagnostics).
+- Cover PostgreSQL first, then include the agreed non-PostgreSQL transport patterns where the current implementation can enforce them deterministically.
+
+Completion criteria:
+- [x] A deterministic test fails on forbidden runtime transport construction patterns outside the allowlist.
+- [x] The allowlist is explicit and narrow.
+- [x] The guardrail is documented in the relevant shared docs/sprint notes.
+
+### XPORT-VALKEY-004 - Stamp runtime Valkey client identity and extend guardrails
+Status: DONE
+Dependency: XPORT-GUARD-003
+Owners: Developer
+Task description:
+- Stamp stable `ClientName` defaults across the runtime Valkey/Redis multiplexer construction paths that were still anonymous in service code or shared queue/cache transport helpers.
+- Extend the shared convention test so unnamed runtime `ConnectionMultiplexer.Connect(...)` / `ConnectAsync(...)` usage fails outside explicit CLI/tooling/test exceptions.
+
+Completion criteria:
+- [x] Touched runtime Valkey/Redis multiplexer paths stamp stable client identity before connecting.
+- [x] The shared convention suite fails on unnamed runtime Valkey multiplexer construction outside a narrow allowlist.
+- [x] Shared transport rules and touched module task boards reference the new Valkey attribution standard.
+
+### XPORT-HTTP-005 - Remove raw runtime HttpClient allocation from first-wave host paths
+Status: DONE
+Dependency: XPORT-GUARD-003
+Owners: Developer
+Task description:
+- Patch the known host-owned HTTP lifecycle hotspots so they no longer allocate ad hoc `HttpClient` instances in steady-state runtime paths.
+- Prefer named `IHttpClientFactory` clients where the host owns DI, and use compatibility-safe shared fallbacks only where the current plugin/controller seam still cannot require factory-backed construction.
+
+Completion criteria:
+- [x] Platform identity-provider connection tests use a named factory-backed client with no raw fallback allocation.
+- [x] Attestor TrustRepo online/offline registrations resolve TUF HTTP via a named factory-backed client.
+- [x] Shared HTTP hotspot regression coverage and docs capture the first hardening wave without claiming repo-wide HTTP enforcement.
+
+### XPORT-HTTP-006 - Extend HTTP lifecycle hardening through plugin seams and legacy connector wrappers
+Status: DONE
+Dependency: XPORT-HTTP-005
+Owners: Developer
+Task description:
+- Make the Integrations plugin loading seam DI-aware so built-in connector plugins can consume factory-backed runtime clients without reflection-only constructor limits.
+- Patch the next HTTP hotspot wave across Integrations feed/object mirror plugins, ReleaseOrchestrator legacy vault/registry connectors, and OCI helper fallbacks so runtime code no longer allocates per-call or ad hoc `HttpClient` instances along those paths.
+
+Completion criteria:
+- [x] Integration plugin loading supports service-provider-backed activation for runtime plugins while preserving no-DI compatibility.
+- [x] Integrations built-in feed/object plugins use factory-backed or shared compatibility clients instead of raw per-call `HttpClient` construction.
+- [x] Legacy ReleaseOrchestrator token/auth helper paths and OCI fallback helpers move onto shared compatibility clients, and the shared hotspot convention test covers the touched files.
+
+## Execution Log
+| Date (UTC) | Update | Owner |
+| --- | --- | --- |
+| 2026-04-05 | Sprint created to turn the AdvisoryAI pooling fix into a repo-wide transport hardening pass across shared PostgreSQL infrastructure, runtime callers, and static guardrails. | Developer |
+| 2026-04-05 | Added shared PostgreSQL application-name policy, patched the first runtime caller wave (JobEngine, EvidenceLocker, Platform, AdvisoryAI/OpsMemory, ReachGraph, Scanner, Router transport, Plugin registry, VexLens, Findings, ExportCenter, Replay), and added convention coverage for anonymous runtime data-source creation. | Developer |
+| 2026-04-05 | Validation: `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` (79/79 under Microsoft.Testing.Platform) plus targeted `dotnet build` runs for JobEngine.WebService, EvidenceLocker.Infrastructure, Scanner.Reachability, Platform.WebService, OpsMemory.WebService, ReachGraph.WebService, ExportCenter.Infrastructure, Replay.WebService, RiskEngine.Infrastructure, and ReleaseOrchestrator.PolicyGate all passed. | Developer |
+| 2026-04-05 | Patched the second PostgreSQL runtime wave (Attestor Watchlist/Persistence/Rekor checkpoint store, BinaryIndex.Validation, Concelier.ProofService.Postgres, Doctor.WebService report storage, and Graph saved views) to use named reusable data sources and extended the convention test to fail on raw runtime `NpgsqlConnection` outside an explicit allowlist. | Developer |
+| 2026-04-05 | Validation: targeted `dotnet build` runs for Attestor.Watchlist, Attestor.Persistence, Attestor.Core, BinaryIndex.Validation, Concelier.ProofService.Postgres, Doctor.WebService, and Graph.Api all passed; `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed `80/80`. | Developer |
+| 2026-04-05 | Patched the runtime Valkey wave across Signals, BinaryIndex, ReachGraph, Attestor, Platform, Authority, Policy, JobEngine Scheduler, Scanner queue/cache/webservice paths, Notify queue paths, Timeline indexer, Router Valkey transport/gateway rate limiting, and Concelier cache so steady-state multiplexer construction stamps stable `ClientName` values. | Developer |
+| 2026-04-05 | Validation: targeted `dotnet build` runs for Signals, BinaryIndex.WebService, ReachGraph.WebService, Attestor.Infrastructure, Platform.WebService, Authority, Policy.Engine, Scheduler.Queue, Scheduler.WebService, Scanner.Queue, Scanner.CallGraph, Scanner.WebService, Notify.Queue, TimelineIndexer.Infrastructure, Messaging.Transport.Valkey, Router.Gateway, and Concelier.Cache.Valkey all passed; `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed `81/81`. | Developer |
+| 2026-04-05 | Patched the first HTTP lifecycle wave across Platform identity-provider probing, Attestor TrustRepo online/offline TUF registration, shared Artifact HTTP fetch, Integrations Vault client wiring, and the S3-compatible integration plugin fallback so these host-owned paths no longer allocate ad hoc runtime `HttpClient` instances. | Developer |
+| 2026-04-05 | Validation: `dotnet build src/Integrations/StellaOps.Integrations.WebService/StellaOps.Integrations.WebService.csproj` and `dotnet build src/__Libraries/StellaOps.Artifact.Core/StellaOps.Artifact.Core.csproj` passed; `dotnet test src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/StellaOps.Attestor.TrustRepo.Tests.csproj` passed `21/21`; `dotnet test src/Integrations/__Tests/StellaOps.Integrations.Plugin.Tests/StellaOps.Integrations.Plugin.Tests.csproj` passed `17/17`; `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed `82/82`. A full `dotnet test src/Platform/__Tests/StellaOps.Platform.WebService.Tests/StellaOps.Platform.WebService.Tests.csproj` run completed with two unrelated existing failures in `SeedEndpointsTests.SeedDemo_WhenAuthorizationFails_ReturnsForbidden` and `QuotaEndpointsTests.Quotas_ReturnDeterministicOrder`; the new identity-provider HTTP wiring compiled and ran inside that assembly pass. | Developer |
+| 2026-04-05 | Patched the second HTTP lifecycle wave by making the shared plugin loader service-provider aware, moving Integrations feed/object built-ins onto named/shared compatibility HTTP clients, routing ReleaseOrchestrator legacy vault/registry connectors through shared compatibility wrappers, and replacing raw OCI fallback client allocation in Verdict and TrustVerdict helpers. | Developer |
+| 2026-04-05 | Validation: `dotnet build src/Integrations/StellaOps.Integrations.WebService/StellaOps.Integrations.WebService.csproj`, `dotnet build src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/StellaOps.ReleaseOrchestrator.IntegrationHub.csproj`, and `dotnet build src/__Libraries/StellaOps.Verdict/StellaOps.Verdict.csproj` passed; `dotnet test src/Integrations/__Tests/StellaOps.Integrations.Tests/StellaOps.Integrations.Tests.csproj` passed with the new DI-aware plugin loader coverage; `dotnet test src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict.Tests/StellaOps.Attestor.TrustVerdict.Tests.csproj` passed; `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed with the expanded HTTP hotspot allowlist. | Developer |
+
+## Decisions & Risks
+- The first implementation wave standardizes PostgreSQL fully and applies the same lifecycle/attribution rule to other transports only where the existing runtime code already exposes a shared construction seam.
+- Tests, migrations, CLI setup, and one-shot admin checks are not treated as runtime transport violations unless they share code with steady-state service paths.
+- Cross-module service patches will be kept minimal and tied back to the shared standard rather than introducing per-service bespoke option models where the shared library can carry the behavior.
+- The static guardrail now enforces anonymous `NpgsqlDataSource.Create(...)`, unnamed `NpgsqlDataSourceBuilder`, and raw runtime `NpgsqlConnection` usage outside an explicit allowlist.
+- The Valkey convention guardrail now also fails unnamed runtime `ConnectionMultiplexer.Connect(...)` / `ConnectAsync(...)` call sites outside explicit CLI/tooling/test exceptions.
+- The first shared HTTP guardrail is intentionally narrow: it covers the known host-owned hotspot files patched in this sprint, while broader repo-wide HTTP enforcement remains a follow-up because several legacy connectors and tools still create transport-specific temporary clients.
+- Integrations now activates connector plugins through DI when a service provider is available, which lets built-in runtime plugins consume named factory-backed clients without breaking reflection-only callers that still rely on default construction.
+- ReleaseOrchestrator legacy connectors still do not use `IHttpClientFactory`; this sprint moves them onto a shared-handler compatibility wrapper so token/auth flows stop allocating temporary clients while preserving the current plugin contract.
+- The remaining explicit raw-connection allowlist is intentionally narrow: CLI/setup, migrations, diagnostics, `PlatformMigrationAdminService`, and `Workflow`'s PostgreSQL store. `Workflow` remains allowlisted because `src/Workflow/AGENTS.md` is missing, which blocks implementer-side runtime edits under the repo contract.
+- Shared Valkey factories that do not receive a service-specific name now apply a module-level fallback `ClientName`; this restores baseline attribution, but Router transport callers may still want a future option for per-service Valkey identity.
+- Shared transport rules are documented in `docs/technical/runtime-transport-client-rules.md`.
+- HTTP compatibility fallbacks now live behind module-specific wrappers (`Integrations` shared defaults, `ReleaseOrchestrator` shared-handler connector clients, OCI helper shared clients) so hotspot files no longer construct raw clients directly; broader HTTP sweeps should continue to replace the remaining wrappers with true host-managed factories where possible.
+
+## Next Checkpoints
+- Start the next transport hardening wave with the blocked `Workflow` PostgreSQL store once the module adds `AGENTS.md`, then continue the remaining broader HTTP/SCM/Vault-style lifecycle sweep (ReleaseOrchestrator SCM/cloud connectors, any remaining tool-specific temporary clients, and factory adoption for the compatibility wrappers added here) with the same guardrail approach.
diff --git a/docs/technical/runtime-transport-client-rules.md b/docs/technical/runtime-transport-client-rules.md
new file mode 100644
index 000000000..c3e955e40
--- /dev/null
+++ b/docs/technical/runtime-transport-client-rules.md
@@ -0,0 +1,26 @@
+# Runtime Transport Client Rules
+
+This document defines the minimum lifecycle and attribution rules for long-lived runtime transport clients in Stella Ops services.
+
+## PostgreSQL
+- Steady-state runtime code must use named reusable `NpgsqlDataSource` instances.
+- Runtime connection strings must carry stable `ApplicationName` values.
+- Raw `new NpgsqlConnection(...)` is reserved for explicit CLI/setup, migration, or diagnostic exceptions.
+
+## Valkey / Redis
+- Steady-state runtime `ConnectionMultiplexer` construction must stamp a stable `ClientName`.
+- Runtime code should build `ConfigurationOptions`, apply client identity, and then connect.
+- Shared factories may provide a module-level default `ClientName` when the caller does not supply one.
+- CLI/setup tooling, smoke tools, and test fixtures are allowed exceptions when they are explicitly allowlisted in convention tests.
+
+## HTTP
+- Runtime code should use `IHttpClientFactory`, typed clients, or module-specific wrappers instead of ad hoc `new HttpClient()`.
+- When DI-backed wiring is not available yet, compatibility fallbacks must still avoid per-request or per-call `new HttpClient()` churn.
+- Plugin loaders that activate runtime components should use service-provider-backed construction when available so named clients and other shared transports can flow into plugins.
+- Existing analyzer-based guardrails remain in place for specialized modules, and the shared convention suite now covers the scoped host-owned HTTP hotspot waves across Integrations, ReleaseOrchestrator connector helpers, and OCI fallback publishers.
+
+## Static Enforcement
+- `src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs` enforces the shared PostgreSQL and Valkey runtime construction rules plus the scoped HTTP hotspot regression checks.
+
+## Operational Goal
+- Every long-lived runtime transport should be attributable in production diagnostics without relying on IP-only correlation.
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TASKS.md b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TASKS.md
index d050e8fed..dd4acd309 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TASKS.md
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/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-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: TrustRepo online/offline registration now uses named factory-backed TUF HTTP clients. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/StellaOps.Attestor.TrustRepo.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoOptions.cs b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoOptions.cs
index d9497d725..bbc91edf0 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoOptions.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoOptions.cs
@@ -14,6 +14,11 @@ public sealed record TrustRepoOptions
///
public const string SectionName = "Attestor:TrustRepo";
+ ///
+ /// Named HttpClient used for TUF metadata fetches.
+ ///
+ public const string HttpClientName = "StellaOps.Attestor.TrustRepo.Tuf";
+
///
/// Whether TUF-based trust distribution is enabled.
///
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.Offline.cs b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.Offline.cs
index fda26bb2d..f24c6bba7 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.Offline.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.Offline.cs
@@ -31,6 +31,8 @@ public static partial class TrustRepoServiceCollectionExtensions
}
});
+ RegisterTufHttpClient(services);
+
services.TryAddSingleton(sp =>
{
var options = sp.GetRequiredService>().Value;
@@ -47,7 +49,7 @@ public static partial class TrustRepoServiceCollectionExtensions
var verifier = sp.GetRequiredService();
var options = sp.GetRequiredService>();
var logger = sp.GetRequiredService>();
- var httpClient = new HttpClient();
+ var httpClient = sp.GetRequiredService().CreateClient(TrustRepoOptions.HttpClientName);
return new TufClient(store, verifier, httpClient, options, logger);
});
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.cs b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.cs
index 70f4e3b64..138302f6c 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.cs
@@ -27,6 +27,8 @@ public static partial class TrustRepoServiceCollectionExtensions
services.Configure(configureOptions);
}
+ RegisterTufHttpClient(services);
+
services.AddOptions()
.Validate(options =>
{
@@ -49,7 +51,7 @@ public static partial class TrustRepoServiceCollectionExtensions
var verifier = sp.GetRequiredService();
var options = sp.GetRequiredService>();
var logger = sp.GetRequiredService>();
- var httpClient = new HttpClient { Timeout = options.Value.HttpTimeout };
+ var httpClient = sp.GetRequiredService().CreateClient(TrustRepoOptions.HttpClientName);
return new TufClient(store, verifier, httpClient, options, logger);
});
@@ -79,4 +81,12 @@ public static partial class TrustRepoServiceCollectionExtensions
logger);
});
}
+
+ private static void RegisterTufHttpClient(IServiceCollection services)
+ {
+ services.AddHttpClient(TrustRepoOptions.HttpClientName, static (sp, client) =>
+ {
+ client.Timeout = sp.GetRequiredService>().Value.HttpTimeout;
+ });
+ }
}
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciAttacher.cs b/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciAttacher.cs
index 1b3dd2eed..baf7f3a60 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciAttacher.cs
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciAttacher.cs
@@ -32,7 +32,7 @@ public sealed partial class TrustVerdictOciAttacher : ITrustVerdictOciAttacher
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _httpClient = httpClient ?? new HttpClient();
+ _httpClient = httpClient ?? TrustVerdictOciRuntimeHttpClient.SharedClient;
_timeProvider = timeProvider ?? TimeProvider.System;
}
}
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciRuntimeHttpClient.cs b/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciRuntimeHttpClient.cs
new file mode 100644
index 000000000..06aea85ad
--- /dev/null
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciRuntimeHttpClient.cs
@@ -0,0 +1,19 @@
+using System.Net.Http.Headers;
+
+namespace StellaOps.Attestor.TrustVerdict.Oci;
+
+internal static class TrustVerdictOciRuntimeHttpClient
+{
+ public static HttpClient SharedClient { get; } = CreateClient();
+
+ private static HttpClient CreateClient()
+ {
+ var client = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(100)
+ };
+
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
+ return client;
+ }
+}
diff --git a/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/TASKS.md b/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/TASKS.md
index a04754262..35116f951 100644
--- a/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/TASKS.md
+++ b/src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/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-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: TrustVerdict OCI attacher fallback HTTP path now reuses a shared runtime client instead of allocating ad hoc transport instances. |
| AUDIT-0067-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0067-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0067-A | TODO | Reopened after revalidation 2026-01-06. |
diff --git a/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TASKS.md b/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TASKS.md
index b102e8fd2..ef8d875b7 100644
--- a/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TASKS.md
+++ b/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/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-HTTP-T | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added TrustRepo DI tests for named TUF HTTP client registration in online/offline paths. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/StellaOps.Attestor.TrustRepo.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TrustRepoServiceCollectionExtensionsTests.cs b/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TrustRepoServiceCollectionExtensionsTests.cs
new file mode 100644
index 000000000..a6d365c03
--- /dev/null
+++ b/src/Attestor/__Libraries/__Tests/StellaOps.Attestor.TrustRepo.Tests/TrustRepoServiceCollectionExtensionsTests.cs
@@ -0,0 +1,39 @@
+using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace StellaOps.Attestor.TrustRepo.Tests;
+
+public sealed class TrustRepoServiceCollectionExtensionsTests
+{
+ [Fact]
+ public void AddTrustRepo_RegistersNamedTufHttpClient()
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ services.AddTrustRepo();
+
+ using var provider = services.BuildServiceProvider();
+ var httpClientFactory = provider.GetRequiredService();
+ var client = httpClientFactory.CreateClient(TrustRepoOptions.HttpClientName);
+
+ provider.GetRequiredService().Should().NotBeNull();
+ client.Timeout.Should().Be(TimeSpan.FromSeconds(30));
+ }
+
+ [Fact]
+ public void AddTrustRepoOffline_RegistersNamedTufHttpClient()
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ services.AddTrustRepoOffline();
+
+ using var provider = services.BuildServiceProvider();
+ var httpClientFactory = provider.GetRequiredService();
+ var client = httpClientFactory.CreateClient(TrustRepoOptions.HttpClientName);
+
+ provider.GetRequiredService().Should().NotBeNull();
+ client.Timeout.Should().Be(TimeSpan.FromSeconds(30));
+ }
+}
diff --git a/src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs b/src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs
new file mode 100644
index 000000000..6bd54d2c7
--- /dev/null
+++ b/src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs
@@ -0,0 +1,190 @@
+using StellaOps.Integrations.Contracts;
+using StellaOps.Integrations.Core;
+using System.Net.Http.Headers;
+
+namespace StellaOps.Integrations.WebService;
+
+///
+/// Feed mirror providers all target the Concelier mirror surface and currently differ only by upstream feed family.
+/// These plugins expose the missing provider identities so the Integration Catalog can manage them explicitly.
+///
+public abstract class FeedMirrorConnectorPluginBase : IIntegrationConnectorPlugin
+{
+ public const string HttpClientName = "IntegrationsFeedMirrorProbe";
+
+ private static readonly HttpClient SharedHttpClient =
+ IntegrationHttpClientDefaults.CreateSharedClient(TimeSpan.FromSeconds(30));
+
+ private readonly IHttpClientFactory? _httpClientFactory;
+ private readonly TimeProvider _timeProvider;
+
+ protected FeedMirrorConnectorPluginBase(
+ IHttpClientFactory? httpClientFactory = null,
+ TimeProvider? timeProvider = null)
+ {
+ _httpClientFactory = httpClientFactory;
+ _timeProvider = timeProvider ?? TimeProvider.System;
+ }
+
+ public abstract string Name { get; }
+
+ public IntegrationType Type => IntegrationType.FeedMirror;
+
+ public abstract IntegrationProvider Provider { get; }
+
+ public bool IsAvailable(IServiceProvider services) => true;
+
+ public async Task TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ {
+ var startTime = _timeProvider.GetUtcNow();
+
+ try
+ {
+ using var request = CreateHealthRequest(config);
+ using var response = await GetHttpClient().SendAsync(request, cancellationToken);
+ var duration = _timeProvider.GetUtcNow() - startTime;
+
+ return response.IsSuccessStatusCode
+ ? new TestConnectionResult(
+ Success: true,
+ Message: "Feed mirror connection successful",
+ Details: new Dictionary
+ {
+ ["endpoint"] = config.Endpoint,
+ ["provider"] = Provider.ToString()
+ },
+ Duration: duration)
+ : new TestConnectionResult(
+ Success: false,
+ Message: $"Feed mirror returned {response.StatusCode}",
+ Details: new Dictionary
+ {
+ ["endpoint"] = config.Endpoint,
+ ["statusCode"] = ((int)response.StatusCode).ToString()
+ },
+ Duration: duration);
+ }
+ catch (Exception ex)
+ {
+ var duration = _timeProvider.GetUtcNow() - startTime;
+ return new TestConnectionResult(
+ Success: false,
+ Message: $"Connection failed: {ex.Message}",
+ Details: new Dictionary
+ {
+ ["endpoint"] = config.Endpoint,
+ ["error"] = ex.GetType().Name
+ },
+ Duration: duration);
+ }
+ }
+
+ public async Task CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ {
+ var startTime = _timeProvider.GetUtcNow();
+
+ try
+ {
+ using var request = CreateHealthRequest(config);
+ using var response = await GetHttpClient().SendAsync(request, cancellationToken);
+ var duration = _timeProvider.GetUtcNow() - startTime;
+
+ return response.IsSuccessStatusCode
+ ? new HealthCheckResult(
+ Status: HealthStatus.Healthy,
+ Message: "Feed mirror service is healthy",
+ Details: new Dictionary
+ {
+ ["provider"] = Provider.ToString()
+ },
+ CheckedAt: _timeProvider.GetUtcNow(),
+ Duration: duration)
+ : new HealthCheckResult(
+ Status: HealthStatus.Unhealthy,
+ Message: $"Feed mirror returned {response.StatusCode}",
+ Details: new Dictionary
+ {
+ ["statusCode"] = ((int)response.StatusCode).ToString()
+ },
+ CheckedAt: _timeProvider.GetUtcNow(),
+ Duration: duration);
+ }
+ catch (Exception ex)
+ {
+ var duration = _timeProvider.GetUtcNow() - startTime;
+ return new HealthCheckResult(
+ Status: HealthStatus.Unhealthy,
+ Message: $"Health check failed: {ex.Message}",
+ Details: new Dictionary
+ {
+ ["error"] = ex.GetType().Name
+ },
+ CheckedAt: _timeProvider.GetUtcNow(),
+ Duration: duration);
+ }
+ }
+
+ private HttpClient GetHttpClient()
+ => _httpClientFactory?.CreateClient(HttpClientName) ?? SharedHttpClient;
+
+ private static HttpRequestMessage CreateHealthRequest(IntegrationConfig config)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, BuildHealthUri(config.Endpoint));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ if (!string.IsNullOrWhiteSpace(config.ResolvedSecret))
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", config.ResolvedSecret);
+ }
+
+ return request;
+ }
+
+ private static Uri BuildHealthUri(string endpoint)
+ {
+ var endpointUri = new Uri(endpoint, UriKind.Absolute);
+ return new Uri($"{endpointUri.GetLeftPart(UriPartial.Authority)}/health");
+ }
+}
+
+public sealed class StellaOpsMirrorConnectorPlugin : FeedMirrorConnectorPluginBase
+{
+ public StellaOpsMirrorConnectorPlugin(
+ IHttpClientFactory? httpClientFactory = null,
+ TimeProvider? timeProvider = null)
+ : base(httpClientFactory, timeProvider)
+ {
+ }
+
+ public override string Name => "stellaops-mirror";
+
+ public override IntegrationProvider Provider => IntegrationProvider.StellaOpsMirror;
+}
+
+public sealed class NvdMirrorConnectorPlugin : FeedMirrorConnectorPluginBase
+{
+ public NvdMirrorConnectorPlugin(
+ IHttpClientFactory? httpClientFactory = null,
+ TimeProvider? timeProvider = null)
+ : base(httpClientFactory, timeProvider)
+ {
+ }
+
+ public override string Name => "nvd-mirror";
+
+ public override IntegrationProvider Provider => IntegrationProvider.NvdMirror;
+}
+
+public sealed class OsvMirrorConnectorPlugin : FeedMirrorConnectorPluginBase
+{
+ public OsvMirrorConnectorPlugin(
+ IHttpClientFactory? httpClientFactory = null,
+ TimeProvider? timeProvider = null)
+ : base(httpClientFactory, timeProvider)
+ {
+ }
+
+ public override string Name => "osv-mirror";
+
+ public override IntegrationProvider Provider => IntegrationProvider.OsvMirror;
+}
diff --git a/src/Integrations/StellaOps.Integrations.WebService/Infrastructure/DefaultImplementations.cs b/src/Integrations/StellaOps.Integrations.WebService/Infrastructure/DefaultImplementations.cs
index 06dd069e0..e5f02fbc5 100644
--- a/src/Integrations/StellaOps.Integrations.WebService/Infrastructure/DefaultImplementations.cs
+++ b/src/Integrations/StellaOps.Integrations.WebService/Infrastructure/DefaultImplementations.cs
@@ -58,18 +58,18 @@ public sealed class LoggingAuditLogger : IIntegrationAuditLogger
/// In production, integrate with Authority service.
/// URI format: authref://vault/{path}#{key}
///
-public sealed class StubAuthRefResolver : IAuthRefResolver
+public sealed class VaultAuthRefResolver : IAuthRefResolver
{
- private readonly ILogger _logger;
+ public const string HttpClientName = "VaultClient";
+
+ private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly string _vaultAddr;
private readonly string _vaultToken;
- public StubAuthRefResolver(ILogger logger, IHttpClientFactory httpClientFactory)
+ public VaultAuthRefResolver(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _vaultAddr = Environment.GetEnvironmentVariable("VAULT_ADDR") ?? "http://vault.stella-ops.local:8200";
_vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "stellaops-dev-root-token-2026";
}
@@ -88,8 +88,7 @@ public sealed class StubAuthRefResolver : IAuthRefResolver
var path = hashIndex >= 0 ? remainder[..hashIndex] : remainder;
var key = hashIndex >= 0 ? remainder[(hashIndex + 1)..] : "value";
- var client = _httpClientFactory.CreateClient("VaultClient");
- client.BaseAddress = new Uri(_vaultAddr);
+ var client = _httpClientFactory.CreateClient(HttpClientName);
client.DefaultRequestHeaders.Add("X-Vault-Token", _vaultToken);
var response = await client.GetAsync($"/v1/secret/data/{path}", cancellationToken);
diff --git a/src/Integrations/StellaOps.Integrations.WebService/IntegrationHttpClientDefaults.cs b/src/Integrations/StellaOps.Integrations.WebService/IntegrationHttpClientDefaults.cs
new file mode 100644
index 000000000..33b2f70f8
--- /dev/null
+++ b/src/Integrations/StellaOps.Integrations.WebService/IntegrationHttpClientDefaults.cs
@@ -0,0 +1,17 @@
+using System.Net.Http.Headers;
+
+namespace StellaOps.Integrations.WebService;
+
+internal static class IntegrationHttpClientDefaults
+{
+ public static HttpClient CreateSharedClient(TimeSpan timeout)
+ {
+ var client = new HttpClient
+ {
+ Timeout = timeout
+ };
+
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
+ return client;
+ }
+}
diff --git a/src/Integrations/StellaOps.Integrations.WebService/IntegrationPluginLoader.cs b/src/Integrations/StellaOps.Integrations.WebService/IntegrationPluginLoader.cs
index 0ece8db31..7d8626221 100644
--- a/src/Integrations/StellaOps.Integrations.WebService/IntegrationPluginLoader.cs
+++ b/src/Integrations/StellaOps.Integrations.WebService/IntegrationPluginLoader.cs
@@ -14,11 +14,15 @@ namespace StellaOps.Integrations.WebService;
public sealed class IntegrationPluginLoader
{
private readonly ILogger? _logger;
+ private readonly IServiceProvider? _serviceProvider;
private readonly List _plugins = [];
- public IntegrationPluginLoader(ILogger? logger = null)
+ public IntegrationPluginLoader(
+ ILogger? logger = null,
+ IServiceProvider? serviceProvider = null)
{
_logger = logger;
+ _serviceProvider = serviceProvider;
}
///
@@ -26,6 +30,29 @@ public sealed class IntegrationPluginLoader
///
public IReadOnlyList Plugins => _plugins;
+ ///
+ /// Registers a plugin instance directly.
+ /// Primarily used by tests and deterministic in-process setups.
+ ///
+ public void Register(IIntegrationConnectorPlugin plugin)
+ {
+ ArgumentNullException.ThrowIfNull(plugin);
+ _plugins.Add(plugin);
+ }
+
+ ///
+ /// Registers plugin instances directly.
+ ///
+ public void RegisterRange(IEnumerable plugins)
+ {
+ ArgumentNullException.ThrowIfNull(plugins);
+
+ foreach (var plugin in plugins)
+ {
+ Register(plugin);
+ }
+ }
+
///
/// Discovers and loads integration connector plugins from the specified directory.
///
@@ -52,7 +79,9 @@ public sealed class IntegrationPluginLoader
foreach (var pluginAssembly in result.Plugins)
{
- var connectorPlugins = PluginLoader.LoadPlugins(new[] { pluginAssembly.Assembly });
+ var connectorPlugins = PluginLoader.LoadPlugins(
+ [pluginAssembly.Assembly],
+ _serviceProvider);
loadedPlugins.AddRange(connectorPlugins);
foreach (var plugin in connectorPlugins)
@@ -71,7 +100,7 @@ public sealed class IntegrationPluginLoader
///
public IReadOnlyList LoadFromAssemblies(IEnumerable assemblies)
{
- var loadedPlugins = PluginLoader.LoadPlugins(assemblies);
+ var loadedPlugins = PluginLoader.LoadPlugins(assemblies, _serviceProvider);
_plugins.AddRange(loadedPlugins);
return loadedPlugins;
}
@@ -92,6 +121,14 @@ public sealed class IntegrationPluginLoader
return _plugins.Where(p => p.Type == type).ToList();
}
+ ///
+ /// Gets a discovery-capable plugin by provider.
+ ///
+ public IIntegrationDiscoveryPlugin? GetDiscoveryByProvider(IntegrationProvider provider)
+ {
+ return GetByProvider(provider) as IIntegrationDiscoveryPlugin;
+ }
+
///
/// Gets all available plugins (checking IsAvailable).
///
diff --git a/src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs b/src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs
new file mode 100644
index 000000000..87e7c278b
--- /dev/null
+++ b/src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs
@@ -0,0 +1,137 @@
+using StellaOps.Integrations.Contracts;
+using StellaOps.Integrations.Core;
+
+namespace StellaOps.Integrations.WebService;
+
+///
+/// Minimal S3-compatible storage connector used for local MinIO and other health-probeable object stores.
+///
+public sealed class S3CompatibleConnectorPlugin : IIntegrationConnectorPlugin
+{
+ public const string HttpClientName = "IntegrationsObjectStorageProbe";
+
+ private static readonly HttpClient SharedHttpClient =
+ IntegrationHttpClientDefaults.CreateSharedClient(TimeSpan.FromSeconds(30));
+
+ private readonly IHttpClientFactory? _httpClientFactory;
+ private readonly TimeProvider _timeProvider;
+
+ public S3CompatibleConnectorPlugin()
+ : this(null, null)
+ {
+ }
+
+ public S3CompatibleConnectorPlugin(
+ IHttpClientFactory? httpClientFactory = null,
+ TimeProvider? timeProvider = null)
+ {
+ _httpClientFactory = httpClientFactory;
+ _timeProvider = timeProvider ?? TimeProvider.System;
+ }
+
+ public string Name => "s3-compatible";
+
+ public IntegrationType Type => IntegrationType.ObjectStorage;
+
+ public IntegrationProvider Provider => IntegrationProvider.S3Compatible;
+
+ public bool IsAvailable(IServiceProvider services) => true;
+
+ public async Task TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ {
+ var startedAt = _timeProvider.GetTimestamp();
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, BuildProbeUri(config.Endpoint));
+ using var response = await GetHttpClient().SendAsync(request, cancellationToken);
+ var duration = _timeProvider.GetElapsedTime(startedAt);
+
+ if (response.IsSuccessStatusCode)
+ {
+ return new TestConnectionResult(
+ true,
+ "S3-compatible storage probe succeeded.",
+ new Dictionary
+ {
+ ["probeUri"] = request.RequestUri?.ToString() ?? config.Endpoint,
+ ["statusCode"] = ((int)response.StatusCode).ToString()
+ },
+ duration);
+ }
+
+ return new TestConnectionResult(
+ false,
+ $"S3-compatible storage probe returned {(int)response.StatusCode} {response.ReasonPhrase}.",
+ new Dictionary
+ {
+ ["probeUri"] = request.RequestUri?.ToString() ?? config.Endpoint,
+ ["statusCode"] = ((int)response.StatusCode).ToString()
+ },
+ duration);
+ }
+ catch (Exception ex)
+ {
+ return new TestConnectionResult(
+ false,
+ ex.Message,
+ new Dictionary
+ {
+ ["probeUri"] = BuildProbeUri(config.Endpoint).ToString()
+ },
+ _timeProvider.GetElapsedTime(startedAt));
+ }
+ }
+
+ public async Task CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ {
+ var startedAt = _timeProvider.GetTimestamp();
+ var checkedAt = _timeProvider.GetUtcNow();
+
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, BuildProbeUri(config.Endpoint));
+ using var response = await GetHttpClient().SendAsync(request, cancellationToken);
+ var duration = _timeProvider.GetElapsedTime(startedAt);
+ var status = response.IsSuccessStatusCode ? HealthStatus.Healthy : HealthStatus.Unhealthy;
+
+ return new HealthCheckResult(
+ status,
+ response.IsSuccessStatusCode
+ ? "S3-compatible storage probe is healthy."
+ : $"S3-compatible storage probe returned {(int)response.StatusCode} {response.ReasonPhrase}.",
+ new Dictionary
+ {
+ ["probeUri"] = request.RequestUri?.ToString() ?? config.Endpoint,
+ ["statusCode"] = ((int)response.StatusCode).ToString()
+ },
+ checkedAt,
+ duration);
+ }
+ catch (Exception ex)
+ {
+ return new HealthCheckResult(
+ HealthStatus.Unhealthy,
+ ex.Message,
+ new Dictionary
+ {
+ ["probeUri"] = BuildProbeUri(config.Endpoint).ToString()
+ },
+ checkedAt,
+ _timeProvider.GetElapsedTime(startedAt));
+ }
+ }
+
+ private static Uri BuildProbeUri(string endpoint)
+ {
+ var endpointUri = new Uri(endpoint, UriKind.Absolute);
+ if (string.IsNullOrEmpty(endpointUri.AbsolutePath) || endpointUri.AbsolutePath == "/")
+ {
+ return new Uri(endpointUri, "/minio/health/live");
+ }
+
+ return endpointUri;
+ }
+
+ private HttpClient GetHttpClient()
+ => _httpClientFactory?.CreateClient(HttpClientName) ?? SharedHttpClient;
+}
diff --git a/src/Integrations/StellaOps.Integrations.WebService/Program.cs b/src/Integrations/StellaOps.Integrations.WebService/Program.cs
index 51ba9ddad..f090ce380 100644
--- a/src/Integrations/StellaOps.Integrations.WebService/Program.cs
+++ b/src/Integrations/StellaOps.Integrations.WebService/Program.cs
@@ -23,6 +23,7 @@ using StellaOps.Infrastructure.Postgres.Migrations;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Localization;
using StellaOps.Router.AspNet;
+using System.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
// Add services
@@ -55,13 +56,28 @@ builder.Services.AddStartupMigrations(
builder.Services.AddScoped();
// HttpClient factory (used by AuthRef resolver for Vault)
-builder.Services.AddHttpClient();
+builder.Services.AddHttpClient(VaultAuthRefResolver.HttpClientName, client =>
+{
+ var vaultAddr = builder.Configuration["VAULT_ADDR"] ?? "http://vault.stella-ops.local:8200";
+ client.BaseAddress = new Uri(vaultAddr.TrimEnd('/') + "/");
+ client.Timeout = TimeSpan.FromSeconds(15);
+});
+builder.Services.AddHttpClient(S3CompatibleConnectorPlugin.HttpClientName, client =>
+{
+ client.Timeout = TimeSpan.FromSeconds(30);
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
+});
+builder.Services.AddHttpClient(FeedMirrorConnectorPluginBase.HttpClientName, client =>
+{
+ client.Timeout = TimeSpan.FromSeconds(30);
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
+});
// Plugin loader
builder.Services.AddSingleton(sp =>
{
var logger = sp.GetRequiredService>();
- var loader = new IntegrationPluginLoader(logger);
+ var loader = new IntegrationPluginLoader(logger, sp);
// Load from plugins directory
var pluginsDir = builder.Configuration.GetValue("Integrations:PluginsDirectory")
@@ -97,7 +113,7 @@ builder.Services.AddSingleton(TimeProvider.System);
// Infrastructure
builder.Services.AddScoped();
builder.Services.AddScoped();
-builder.Services.AddScoped();
+builder.Services.AddScoped();
// Core service
builder.Services.AddScoped();
diff --git a/src/Integrations/StellaOps.Integrations.WebService/TASKS.md b/src/Integrations/StellaOps.Integrations.WebService/TASKS.md
index b7709c637..b148e1de7 100644
--- a/src/Integrations/StellaOps.Integrations.WebService/TASKS.md
+++ b/src/Integrations/StellaOps.Integrations.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-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Vault auth-ref client registration, DI-aware plugin loading, and factory/shared lifecycle cleanup for the built-in feed/object connector plugins. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Integrations/StellaOps.Integrations.WebService/StellaOps.Integrations.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/Integrations/__Tests/StellaOps.Integrations.Tests/IntegrationPluginLoaderTests.cs b/src/Integrations/__Tests/StellaOps.Integrations.Tests/IntegrationPluginLoaderTests.cs
index 4388e825b..1f1e1f398 100644
--- a/src/Integrations/__Tests/StellaOps.Integrations.Tests/IntegrationPluginLoaderTests.cs
+++ b/src/Integrations/__Tests/StellaOps.Integrations.Tests/IntegrationPluginLoaderTests.cs
@@ -1,6 +1,10 @@
using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
+using StellaOps.Integrations.Contracts;
using StellaOps.Integrations.Core;
+using StellaOps.Integrations.Plugin.DockerRegistry;
+using StellaOps.Integrations.Plugin.GitLab;
using StellaOps.Integrations.WebService;
using Xunit;
@@ -77,4 +81,127 @@ public class IntegrationPluginLoaderTests
// Assert
result.Should().BeEmpty();
}
+
+ [Trait("Category", "Unit")]
+ [Fact]
+ public void Register_WithDiscoveryPlugin_ExposesDiscoveryLookup()
+ {
+ var loader = new IntegrationPluginLoader(NullLogger.Instance);
+ var plugin = new FakeDiscoveryPlugin();
+
+ loader.Register(plugin);
+
+ loader.Plugins.Should().ContainSingle();
+ loader.GetByProvider(IntegrationProvider.Custom).Should().BeSameAs(plugin);
+ loader.GetDiscoveryByProvider(IntegrationProvider.Custom).Should().BeSameAs(plugin);
+ }
+
+ [Trait("Category", "Unit")]
+ [Fact]
+ public void LoadFromAssemblies_WithBuiltInAssemblies_LoadsAliasProviders()
+ {
+ var services = new ServiceCollection();
+ services.AddHttpClient(S3CompatibleConnectorPlugin.HttpClientName);
+ services.AddHttpClient(FeedMirrorConnectorPluginBase.HttpClientName);
+ services.AddSingleton(TimeProvider.System);
+ var serviceProvider = services.BuildServiceProvider();
+ var loader = new IntegrationPluginLoader(NullLogger.Instance, serviceProvider);
+
+ var loaded = loader.LoadFromAssemblies(
+ [
+ typeof(Program).Assembly,
+ typeof(GitLabConnectorPlugin).Assembly,
+ typeof(DockerRegistryConnectorPlugin).Assembly
+ ]);
+
+ loaded.Should().Contain(plugin => plugin.Provider == IntegrationProvider.GitLabCi && plugin.Type == IntegrationType.CiCd);
+ loaded.Should().Contain(plugin => plugin.Provider == IntegrationProvider.GitLabContainerRegistry && plugin.Type == IntegrationType.Registry);
+ loaded.Should().Contain(plugin => plugin.Provider == IntegrationProvider.S3Compatible && plugin.Type == IntegrationType.ObjectStorage);
+ loaded.Should().Contain(plugin => plugin.Provider == IntegrationProvider.StellaOpsMirror && plugin.Type == IntegrationType.FeedMirror);
+ loaded.Should().Contain(plugin => plugin.Provider == IntegrationProvider.NvdMirror && plugin.Type == IntegrationType.FeedMirror);
+ loaded.Should().Contain(plugin => plugin.Provider == IntegrationProvider.OsvMirror && plugin.Type == IntegrationType.FeedMirror);
+ }
+
+ [Trait("Category", "Unit")]
+ [Fact]
+ public void LoadFromAssemblies_WithServiceProvider_LoadsPluginsThatRequireDependencyInjection()
+ {
+ var services = new ServiceCollection()
+ .AddSingleton(new LoaderDependency("di"))
+ .BuildServiceProvider();
+ var loader = new IntegrationPluginLoader(NullLogger.Instance, services);
+
+ var loaded = loader.LoadFromAssemblies([typeof(ServiceProviderOnlyPlugin).Assembly]);
+
+ loaded.Should().ContainSingle(plugin => plugin.Provider == IntegrationProvider.Bitbucket);
+ }
+
+ [Trait("Category", "Unit")]
+ [Fact]
+ public void LoadFromAssemblies_WithoutServiceProvider_SkipsPluginsThatRequireDependencyInjection()
+ {
+ var loader = new IntegrationPluginLoader(NullLogger.Instance);
+
+ var loaded = loader.LoadFromAssemblies([typeof(ServiceProviderOnlyPlugin).Assembly]);
+
+ loaded.Should().NotContain(plugin => plugin.Provider == IntegrationProvider.Bitbucket);
+ }
+
+ private sealed class FakeDiscoveryPlugin : IIntegrationConnectorPlugin, IIntegrationDiscoveryPlugin
+ {
+ public string Name => "fake";
+
+ public IntegrationType Type => IntegrationType.Scm;
+
+ public IntegrationProvider Provider => IntegrationProvider.Custom;
+
+ public bool IsAvailable(IServiceProvider services) => true;
+
+ public IReadOnlyList SupportedResourceTypes => [IntegrationDiscoveryResourceTypes.Repositories];
+
+ public Task TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(new TestConnectionResult(true, "ok", null, TimeSpan.Zero));
+ }
+
+ public Task CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(new HealthCheckResult(HealthStatus.Healthy, "ok", null, DateTimeOffset.UtcNow, TimeSpan.Zero));
+ }
+
+ public Task> DiscoverAsync(
+ IntegrationConfig config,
+ string resourceType,
+ IReadOnlyDictionary? filter,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult>([]);
+ }
+ }
}
+
+public sealed class ServiceProviderOnlyPlugin : IIntegrationConnectorPlugin
+{
+ private readonly LoaderDependency _dependency;
+
+ public ServiceProviderOnlyPlugin(LoaderDependency dependency)
+ {
+ _dependency = dependency;
+ }
+
+ public string Name => $"service-provider-only-{_dependency.Name}";
+
+ public IntegrationType Type => IntegrationType.Scm;
+
+ public IntegrationProvider Provider => IntegrationProvider.Bitbucket;
+
+ public bool IsAvailable(IServiceProvider services) => true;
+
+ public Task TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ => Task.FromResult(new TestConnectionResult(true, Name, null, TimeSpan.Zero));
+
+ public Task CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default)
+ => Task.FromResult(new HealthCheckResult(HealthStatus.Healthy, Name, null, DateTimeOffset.UtcNow, TimeSpan.Zero));
+}
+
+public sealed record LoaderDependency(string Name);
diff --git a/src/Integrations/__Tests/StellaOps.Integrations.Tests/TASKS.md b/src/Integrations/__Tests/StellaOps.Integrations.Tests/TASKS.md
index e383f67a7..228a5a83c 100644
--- a/src/Integrations/__Tests/StellaOps.Integrations.Tests/TASKS.md
+++ b/src/Integrations/__Tests/StellaOps.Integrations.Tests/TASKS.md
@@ -8,5 +8,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260208_040-TESTS | DONE | Deterministic AI Code Guard run service and endpoint coverage. |
+| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: DI-aware IntegrationPluginLoader regression coverage for service-provider-backed plugin activation. |
diff --git a/src/Platform/StellaOps.Platform.WebService/Program.cs b/src/Platform/StellaOps.Platform.WebService/Program.cs
index 1609b24ce..cff7a2613 100644
--- a/src/Platform/StellaOps.Platform.WebService/Program.cs
+++ b/src/Platform/StellaOps.Platform.WebService/Program.cs
@@ -205,6 +205,11 @@ builder.Services.AddHttpClient("HarborFixture", client =>
client.Timeout = TimeSpan.FromSeconds(15);
});
+builder.Services.AddHttpClient(IdentityProviderManagementService.HttpClientName, client =>
+{
+ client.Timeout = TimeSpan.FromSeconds(15);
+});
+
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton(sp => sp.GetRequiredService());
@@ -276,8 +281,18 @@ builder.Services.AddSingleton
+ {
+ var connectionStringBuilder = new Npgsql.NpgsqlConnectionStringBuilder(bootstrapOptions.Storage.PostgresConnectionString)
+ {
+ ApplicationName = "stellaops-platform",
+ };
+
+ return new Npgsql.NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString)
+ {
+ Name = "StellaOps.Platform"
+ }.Build();
+ });
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
@@ -318,7 +333,12 @@ var redisCs = builder.Configuration["ConnectionStrings:Redis"];
if (!string.IsNullOrWhiteSpace(redisCs))
{
builder.Services.AddSingleton(
- sp => ConnectionMultiplexer.Connect(redisCs));
+ sp =>
+ {
+ var redisOptions = ConfigurationOptions.Parse(redisCs);
+ redisOptions.ClientName ??= "stellaops-platform";
+ return ConnectionMultiplexer.Connect(redisOptions);
+ });
}
builder.Services.AddHostedService();
diff --git a/src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs b/src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs
index 9d2ec5c00..dc0f0d152 100644
--- a/src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs
+++ b/src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs
@@ -14,6 +14,8 @@ namespace StellaOps.Platform.WebService.Services;
public sealed class IdentityProviderManagementService
{
+ public const string HttpClientName = "PlatformIdentityProviderTest";
+
private static readonly HashSet ValidTypes = new(StringComparer.OrdinalIgnoreCase)
{
"standard", "ldap", "saml", "oidc"
@@ -35,7 +37,7 @@ public sealed class IdentityProviderManagementService
["standard"] = []
};
- private readonly IHttpClientFactory? _httpClientFactory;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
// In-memory store keyed by (tenantId, id)
@@ -44,10 +46,10 @@ public sealed class IdentityProviderManagementService
public IdentityProviderManagementService(
ILogger logger,
- IHttpClientFactory? httpClientFactory = null)
+ IHttpClientFactory httpClientFactory)
{
_logger = logger;
- _httpClientFactory = httpClientFactory;
+ _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}
public Task> ListAsync(string tenantId, CancellationToken cancellationToken)
@@ -391,28 +393,20 @@ public sealed class IdentityProviderManagementService
}
var sw = Stopwatch.StartNew();
- var httpClient = _httpClientFactory?.CreateClient("idp-test") ?? new HttpClient();
- try
+ using var httpClient = _httpClientFactory.CreateClient(HttpClientName);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ cts.CancelAfter(TimeSpan.FromSeconds(15));
+
+ var response = await httpClient.GetAsync(metadataUrl, cts.Token).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
+
+ if (!content.Contains("EntityDescriptor", StringComparison.OrdinalIgnoreCase))
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- cts.CancelAfter(TimeSpan.FromSeconds(15));
-
- var response = await httpClient.GetAsync(metadataUrl, cts.Token).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- var content = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
-
- if (!content.Contains("EntityDescriptor", StringComparison.OrdinalIgnoreCase))
- {
- return new TestConnectionResult(false, "SAML metadata URL responded but content does not appear to be valid SAML metadata.", sw.ElapsedMilliseconds);
- }
-
- return new TestConnectionResult(true, $"SAML metadata fetched successfully from {metadataUrl}.", sw.ElapsedMilliseconds);
- }
- finally
- {
- if (_httpClientFactory is null)
- httpClient.Dispose();
+ return new TestConnectionResult(false, "SAML metadata URL responded but content does not appear to be valid SAML metadata.", sw.ElapsedMilliseconds);
}
+
+ return new TestConnectionResult(true, $"SAML metadata fetched successfully from {metadataUrl}.", sw.ElapsedMilliseconds);
}
private async Task TestOidcConnectionAsync(
@@ -423,29 +417,21 @@ public sealed class IdentityProviderManagementService
var discoveryUrl = authority.TrimEnd('/') + "/.well-known/openid-configuration";
var sw = Stopwatch.StartNew();
- var httpClient = _httpClientFactory?.CreateClient("idp-test") ?? new HttpClient();
- try
+ using var httpClient = _httpClientFactory.CreateClient(HttpClientName);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ cts.CancelAfter(TimeSpan.FromSeconds(15));
+
+ var response = await httpClient.GetAsync(discoveryUrl, cts.Token).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
+
+ // Basic validation: should contain issuer field
+ if (!content.Contains("issuer", StringComparison.OrdinalIgnoreCase))
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- cts.CancelAfter(TimeSpan.FromSeconds(15));
-
- var response = await httpClient.GetAsync(discoveryUrl, cts.Token).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- var content = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
-
- // Basic validation: should contain issuer field
- if (!content.Contains("issuer", StringComparison.OrdinalIgnoreCase))
- {
- return new TestConnectionResult(false, "OIDC discovery endpoint responded but content does not appear to be a valid OpenID configuration.", sw.ElapsedMilliseconds);
- }
-
- return new TestConnectionResult(true, $"OIDC discovery document fetched successfully from {discoveryUrl}.", sw.ElapsedMilliseconds);
- }
- finally
- {
- if (_httpClientFactory is null)
- httpClient.Dispose();
+ return new TestConnectionResult(false, "OIDC discovery endpoint responded but content does not appear to be a valid OpenID configuration.", sw.ElapsedMilliseconds);
}
+
+ return new TestConnectionResult(true, $"OIDC discovery document fetched successfully from {discoveryUrl}.", sw.ElapsedMilliseconds);
}
private static IdentityProviderConfigDto MapToDto(IdentityProviderConfigEntry entry)
diff --git a/src/Platform/StellaOps.Platform.WebService/TASKS.md b/src/Platform/StellaOps.Platform.WebService/TASKS.md
index c95b5b0a0..cf3b4e79a 100644
--- a/src/Platform/StellaOps.Platform.WebService/TASKS.md
+++ b/src/Platform/StellaOps.Platform.WebService/TASKS.md
@@ -5,6 +5,9 @@ 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 Platform runtime PostgreSQL data sources for score-history and analytics ingestion/query paths. |
+| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Platform Valkey client construction. |
+| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named Platform identity-provider HTTP test client wiring and removed raw fallback `HttpClient` allocation. |
| SPRINT_20260222_051-MGC-12 | DONE | Added `/api/v1/admin/migrations/{modules,status,verify,run}` endpoints with `platform.setup.admin` authorization and server-side migration execution wired to the platform-owned registry in `StellaOps.Platform.Database`. |
| SPRINT_20260222_051-MGC-12-SOURCES | DONE | Platform migration admin service now executes and verifies migrations across per-service plugin source sets, applies synthesized per-plugin consolidated migration on empty history with legacy history backfill, and auto-heals partial backfill states before per-source execution. |
| SPRINT_20260221_043-PLATFORM-SEED-001 | DONE | Sprint `docs/implplan/SPRINT_20260221_043_DOCS_setup_seed_error_handling_stabilization.md`: fix seed endpoint authorization policy wiring and return structured non-500 error responses for expected failures. |
diff --git a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/IdentityProviderManagementServiceTests.cs b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/IdentityProviderManagementServiceTests.cs
new file mode 100644
index 000000000..7849b35b6
--- /dev/null
+++ b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/IdentityProviderManagementServiceTests.cs
@@ -0,0 +1,64 @@
+using System.Net;
+using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
+using NSubstitute;
+using StellaOps.Platform.WebService.Contracts;
+using StellaOps.Platform.WebService.Services;
+using Xunit;
+
+namespace StellaOps.Platform.WebService.Tests;
+
+public sealed class IdentityProviderManagementServiceTests
+{
+ [Theory]
+ [InlineData("saml", "idpMetadataUrl", "https://idp.example.com/metadata", "", "https://idp.example.com/metadata")]
+ [InlineData("oidc", "authority", "https://idp.example.com", "{\"issuer\":\"https://idp.example.com\"}", "https://idp.example.com/.well-known/openid-configuration")]
+ public async Task TestConnectionAsync_UsesNamedHttpClientForMetadataFetch(
+ string providerType,
+ string configKey,
+ string configValue,
+ string responseBody,
+ string expectedRequestUri)
+ {
+ var factory = Substitute.For();
+ var handler = new StubHttpMessageHandler(_ =>
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(responseBody, Encoding.UTF8, "application/json")
+ });
+ factory.CreateClient(IdentityProviderManagementService.HttpClientName)
+ .Returns(new HttpClient(handler));
+
+ var service = new IdentityProviderManagementService(
+ NullLogger.Instance,
+ factory);
+
+ var result = await service.TestConnectionAsync(
+ new TestConnectionRequest(
+ providerType,
+ new Dictionary { [configKey] = configValue }),
+ TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.Equal(expectedRequestUri, handler.LastRequestUri);
+ factory.Received(1).CreateClient(IdentityProviderManagementService.HttpClientName);
+ }
+
+ private sealed class StubHttpMessageHandler : HttpMessageHandler
+ {
+ private readonly Func _responseFactory;
+
+ public StubHttpMessageHandler(Func responseFactory)
+ {
+ _responseFactory = responseFactory;
+ }
+
+ public string? LastRequestUri { get; private set; }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ LastRequestUri = request.RequestUri?.ToString();
+ return Task.FromResult(_responseFactory(request));
+ }
+ }
+}
diff --git a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md
index 4a9dfffa8..23d0a725b 100644
--- a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md
+++ b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/TASKS.md
@@ -26,3 +26,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| SPRINT_20260224_004-LOC-305-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended `LocalizationEndpointsTests` to verify common-layer and `platform.*` namespace bundle availability for all supported locales. |
| SPRINT_20260224_004-LOC-307-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended localization and preference endpoint tests for Ukrainian rollout (`uk-UA` locale catalog/bundle assertions and alias normalization to canonical `uk-UA`). |
| SPRINT_20260305_005-PLATFORM-BOUND-002 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: added `PlatformRuntimeBoundaryGuardTests` to enforce approved read-model constructor contracts and disallow foreign persistence references outside explicit migration/seed allowlist files. |
+| SPRINT_20260405_011-XPORT-HTTP-T | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added direct `IdentityProviderManagementService` HTTP-factory tests for OIDC/SAML connection probing. |
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/ConnectorHttpClients.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/ConnectorHttpClients.cs
new file mode 100644
index 000000000..70d321ab1
--- /dev/null
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/ConnectorHttpClients.cs
@@ -0,0 +1,32 @@
+using System.Net;
+using System.Net.Http.Headers;
+
+namespace StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
+
+internal static class ConnectorHttpClients
+{
+ private static readonly SocketsHttpHandler SharedHandler = new()
+ {
+ AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
+ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
+ PooledConnectionLifetime = TimeSpan.FromMinutes(15),
+ };
+
+ public static HttpClient SharedClient { get; } = CreateClient();
+
+ public static HttpClient CreateClient(Uri? baseAddress = null)
+ {
+ var client = new HttpClient(SharedHandler, disposeHandler: false)
+ {
+ Timeout = TimeSpan.FromSeconds(100)
+ };
+
+ if (baseAddress is not null)
+ {
+ client.BaseAddress = baseAddress;
+ }
+
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
+ return client;
+ }
+}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs
index 3792e241e..22b6c689a 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -361,13 +361,7 @@ public sealed class AcrConnector : IRegistryConnectorCapability, IDisposable
_registryHost = $"{registryName}.azurecr.io";
_registryUrl = $"https://{_registryHost}";
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_registryUrl + "/")
- };
-
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_registryUrl + "/"));
}
// Check if we need to refresh token
@@ -450,8 +444,6 @@ public sealed class AcrConnector : IRegistryConnectorCapability, IDisposable
string clientSecret,
CancellationToken ct)
{
- using var tempClient = new HttpClient();
-
// Get Azure AD token
var tokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
var tokenRequest = new Dictionary
@@ -463,7 +455,7 @@ public sealed class AcrConnector : IRegistryConnectorCapability, IDisposable
};
using var tokenContent = new FormUrlEncodedContent(tokenRequest);
- using var tokenResponse = await tempClient.PostAsync(tokenEndpoint, tokenContent, ct);
+ using var tokenResponse = await ConnectorHttpClients.SharedClient.PostAsync(tokenEndpoint, tokenContent, ct);
tokenResponse.EnsureSuccessStatusCode();
var aadToken = await tokenResponse.Content.ReadFromJsonAsync(ct);
@@ -478,7 +470,7 @@ public sealed class AcrConnector : IRegistryConnectorCapability, IDisposable
};
using var exchangeContent = new FormUrlEncodedContent(exchangeRequest);
- using var exchangeResponse = await tempClient.PostAsync(exchangeEndpoint, exchangeContent, ct);
+ using var exchangeResponse = await ConnectorHttpClients.SharedClient.PostAsync(exchangeEndpoint, exchangeContent, ct);
exchangeResponse.EnsureSuccessStatusCode();
var refreshToken = await exchangeResponse.Content.ReadFromJsonAsync(ct);
@@ -494,7 +486,7 @@ public sealed class AcrConnector : IRegistryConnectorCapability, IDisposable
};
using var accessContent = new FormUrlEncodedContent(accessRequest);
- using var accessResponse = await tempClient.PostAsync(accessEndpoint, accessContent, ct);
+ using var accessResponse = await ConnectorHttpClients.SharedClient.PostAsync(accessEndpoint, accessContent, ct);
accessResponse.EnsureSuccessStatusCode();
var accessToken = await accessResponse.Content.ReadFromJsonAsync(ct);
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs
index 41ea389c0..423252931 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -343,9 +343,7 @@ public sealed class DockerHubConnector : IRegistryConnectorCapability, IDisposab
}
}
- _httpClient = new HttpClient();
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient();
}
private async Task GetAuthTokenAsync(string repository, string scope, CancellationToken ct)
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs
index bb6d38bf1..f8853baa1 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -393,13 +393,8 @@ public sealed class EcrConnector : IRegistryConnectorCapability, IDisposable
}
}
- _httpClient = new HttpClient();
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
- _ecrApiClient = new HttpClient();
- _ecrApiClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient();
+ _ecrApiClient = ConnectorHttpClients.CreateClient();
}
private async Task EnsureValidTokenAsync(CancellationToken ct)
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs
index d98b090fa..2d09ce8a2 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -365,13 +365,7 @@ public sealed class GcrConnector : IRegistryConnectorCapability, IDisposable
_serviceAccountKey = JsonSerializer.Deserialize(keyJson);
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_registryUrl + "/")
- };
-
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_registryUrl + "/"));
}
private async Task EnsureValidTokenAsync(CancellationToken ct)
@@ -394,7 +388,6 @@ public sealed class GcrConnector : IRegistryConnectorCapability, IDisposable
var jwt = CreateServiceAccountJwt(now);
// Exchange JWT for access token
- using var tempClient = new HttpClient();
var tokenRequest = new Dictionary
{
["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer",
@@ -402,7 +395,7 @@ public sealed class GcrConnector : IRegistryConnectorCapability, IDisposable
};
using var content = new FormUrlEncodedContent(tokenRequest);
- using var response = await tempClient.PostAsync(
+ using var response = await ConnectorHttpClients.SharedClient.PostAsync(
"https://oauth2.googleapis.com/token", content, ct);
response.EnsureSuccessStatusCode();
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs
index 781e3a224..4e07e3559 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -240,9 +240,7 @@ public sealed class AwsSecretsManagerConnector : IVaultConnectorCapability, IDis
}
}
- _httpClient = new HttpClient();
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient();
}
private async Task CallSecretsManagerApiAsync(
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs
index bbbfbd35b..a85b8a14f 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -261,13 +261,7 @@ public sealed class AzureKeyVaultConnector : IVaultConnectorCapability, IDisposa
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_vaultUrl + "/")
- };
-
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_vaultUrl + "/"));
}
private async Task EnsureValidTokenAsync(CancellationToken ct)
@@ -295,8 +289,6 @@ public sealed class AzureKeyVaultConnector : IVaultConnectorCapability, IDisposa
private async Task AuthenticateManagedIdentityAsync(CancellationToken ct)
{
- using var tempClient = new HttpClient();
-
// Azure IMDS endpoint for managed identity
var url = "http://169.254.169.254/metadata/identity/oauth2/token" +
"?api-version=2018-02-01" +
@@ -305,7 +297,7 @@ public sealed class AzureKeyVaultConnector : IVaultConnectorCapability, IDisposa
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Metadata", "true");
- using var response = await tempClient.SendAsync(request, ct);
+ using var response = await ConnectorHttpClients.SharedClient.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync(ct);
@@ -316,8 +308,6 @@ public sealed class AzureKeyVaultConnector : IVaultConnectorCapability, IDisposa
private async Task AuthenticateServicePrincipalAsync(CancellationToken ct)
{
- using var tempClient = new HttpClient();
-
var tokenEndpoint = $"https://login.microsoftonline.com/{_tenantId}/oauth2/v2.0/token";
var tokenRequest = new Dictionary
{
@@ -328,7 +318,7 @@ public sealed class AzureKeyVaultConnector : IVaultConnectorCapability, IDisposa
};
using var content = new FormUrlEncodedContent(tokenRequest);
- using var response = await tempClient.PostAsync(tokenEndpoint, content, ct);
+ using var response = await ConnectorHttpClients.SharedClient.PostAsync(tokenEndpoint, content, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync(ct);
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs
index 9b106b397..cc59a9e65 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs
@@ -1,4 +1,4 @@
-
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
using StellaOps.ReleaseOrchestrator.Plugin.Capabilities;
using StellaOps.ReleaseOrchestrator.Plugin.Models;
using System.Diagnostics;
@@ -279,14 +279,8 @@ public sealed class HashiCorpVaultConnector : IVaultConnectorCapability, IDispos
_ => throw new InvalidOperationException($"Unknown auth method: {authMethod}")
};
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_vaultAddress + "/")
- };
-
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_vaultAddress + "/"));
_httpClient.DefaultRequestHeaders.Add("X-Vault-Token", _token);
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
return _httpClient;
}
@@ -343,10 +337,9 @@ public sealed class HashiCorpVaultConnector : IVaultConnectorCapability, IDispos
}
}
- using var tempClient = new HttpClient();
var loginPayload = new { role_id = roleId, secret_id = secretId };
- using var response = await tempClient.PostAsJsonAsync(
+ using var response = await ConnectorHttpClients.SharedClient.PostAsJsonAsync(
$"{_vaultAddress}/v1/auth/approle/login", loginPayload, ct);
response.EnsureSuccessStatusCode();
@@ -373,10 +366,9 @@ public sealed class HashiCorpVaultConnector : IVaultConnectorCapability, IDispos
var jwt = await File.ReadAllTextAsync(saTokenPath, ct);
- using var tempClient = new HttpClient();
var loginPayload = new { jwt, role };
- using var response = await tempClient.PostAsJsonAsync(
+ using var response = await ConnectorHttpClients.SharedClient.PostAsJsonAsync(
$"{_vaultAddress}/v1/auth/kubernetes/login", loginPayload, ct);
response.EnsureSuccessStatusCode();
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md
index 56adcd2d1..9b278e2ff 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/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-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: legacy vault/registry connectors now use shared-handler compatibility HTTP clients instead of raw temporary `HttpClient` allocation. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/StellaOps.ReleaseOrchestrator.IntegrationHub.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.FetchHttp.cs b/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.FetchHttp.cs
index 581542d16..ff71b65ae 100644
--- a/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.FetchHttp.cs
+++ b/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.FetchHttp.cs
@@ -16,8 +16,7 @@ public sealed partial class ArtifactController
{
_logger.LogDebug("Fetching from HTTP: {Uri}", uri);
- using var httpClient = new HttpClient();
- httpClient.Timeout = TimeSpan.FromSeconds(30);
+ var httpClient = _httpClientFactory?.CreateClient(HttpFetchClientName) ?? SharedHttpFetchClient;
try
{
diff --git a/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.cs b/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.cs
index a9dcc7991..896881bb7 100644
--- a/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.cs
+++ b/src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.cs
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using StellaOps.Artifact.Core;
+using System.Net.Http;
namespace StellaOps.Artifact.Api;
@@ -22,15 +23,23 @@ namespace StellaOps.Artifact.Api;
public sealed partial class ArtifactController : ControllerBase
{
private const string GetArtifactActionName = "GetArtifact";
+ private const string HttpFetchClientName = "ArtifactController.FetchHttp";
+ private static readonly HttpClient SharedHttpFetchClient = new()
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
private readonly IArtifactStore _artifactStore;
+ private readonly IHttpClientFactory? _httpClientFactory;
private readonly ILogger _logger;
public ArtifactController(
IArtifactStore artifactStore,
- ILogger logger)
+ ILogger logger,
+ IHttpClientFactory? httpClientFactory = null)
{
_artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _httpClientFactory = httpClientFactory;
}
}
diff --git a/src/__Libraries/StellaOps.Artifact.Core/TASKS.md b/src/__Libraries/StellaOps.Artifact.Core/TASKS.md
index 70deb909b..6cf5e2b3b 100644
--- a/src/__Libraries/StellaOps.Artifact.Core/TASKS.md
+++ b/src/__Libraries/StellaOps.Artifact.Core/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-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: artifact HTTP fetch path now prefers factory-backed clients and uses a shared fallback instead of allocating per request. |
| REMED-05 | DONE | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.Artifact.Core/StellaOps.Artifact.Core.md. Tests: `dotnet test src/__Libraries/StellaOps.Artifact.Core.Tests/StellaOps.Artifact.Core.Tests.csproj` (23 tests, MTP0001 warning). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/__Libraries/StellaOps.Plugin/PluginContracts.cs b/src/__Libraries/StellaOps.Plugin/PluginContracts.cs
index 86bade49b..a8916f011 100644
--- a/src/__Libraries/StellaOps.Plugin/PluginContracts.cs
+++ b/src/__Libraries/StellaOps.Plugin/PluginContracts.cs
@@ -1,4 +1,4 @@
-
+using Microsoft.Extensions.DependencyInjection;
using StellaOps.Plugin.Hosting;
using System;
using System.Collections.Generic;
@@ -120,6 +120,12 @@ public static class PluginLoader
{
public static IReadOnlyList LoadPlugins(IEnumerable assemblies)
where TPlugin : class
+ => LoadPlugins(assemblies, serviceProvider: null);
+
+ public static IReadOnlyList LoadPlugins(
+ IEnumerable assemblies,
+ IServiceProvider? serviceProvider)
+ where TPlugin : class
{
if (assemblies == null) throw new ArgumentNullException(nameof(assemblies));
@@ -140,7 +146,7 @@ public static class PluginLoader
continue;
}
- if (candidate.GetConstructor(Type.EmptyTypes) is null)
+ if (serviceProvider is null && candidate.GetConstructor(Type.EmptyTypes) is null)
{
continue;
}
@@ -148,11 +154,13 @@ public static class PluginLoader
TPlugin? plugin;
try
{
- plugin = Activator.CreateInstance(candidate) as TPlugin;
+ plugin = serviceProvider is null
+ ? Activator.CreateInstance(candidate) as TPlugin
+ : ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, candidate) as TPlugin;
}
catch
{
- // Skip plugins that cannot be created via default constructor.
+ // Skip plugins that cannot be created via the available DI/default constructor path.
continue;
}
diff --git a/src/__Libraries/StellaOps.Plugin/TASKS.md b/src/__Libraries/StellaOps.Plugin/TASKS.md
index e70909f52..689a8931e 100644
--- a/src/__Libraries/StellaOps.Plugin/TASKS.md
+++ b/src/__Libraries/StellaOps.Plugin/TASKS.md
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0095-T | DONE | Revalidated 2026-01-08; test coverage audit for StellaOps.Plugin. |
| AUDIT-0095-A | TODO | Revalidated 2026-01-08 (open findings). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
+| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: PluginLoader now supports service-provider-backed runtime activation so host-owned plugins can consume shared transport clients. |
diff --git a/src/__Libraries/StellaOps.Verdict/Oci/OciAttestationPublisher.cs b/src/__Libraries/StellaOps.Verdict/Oci/OciAttestationPublisher.cs
index ed59406a5..435071bb0 100644
--- a/src/__Libraries/StellaOps.Verdict/Oci/OciAttestationPublisher.cs
+++ b/src/__Libraries/StellaOps.Verdict/Oci/OciAttestationPublisher.cs
@@ -122,7 +122,7 @@ public sealed class OciAttestationPublisher : IOciAttestationPublisher
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _httpClient = httpClient ?? new HttpClient();
+ _httpClient = httpClient ?? OciRuntimeHttpClient.SharedClient;
_timeProvider = timeProvider ?? TimeProvider.System;
}
diff --git a/src/__Libraries/StellaOps.Verdict/Oci/OciRuntimeHttpClient.cs b/src/__Libraries/StellaOps.Verdict/Oci/OciRuntimeHttpClient.cs
new file mode 100644
index 000000000..e73abf05d
--- /dev/null
+++ b/src/__Libraries/StellaOps.Verdict/Oci/OciRuntimeHttpClient.cs
@@ -0,0 +1,19 @@
+using System.Net.Http.Headers;
+
+namespace StellaOps.Verdict.Oci;
+
+internal static class OciRuntimeHttpClient
+{
+ public static HttpClient SharedClient { get; } = CreateClient();
+
+ private static HttpClient CreateClient()
+ {
+ var client = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(100)
+ };
+
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0"));
+ return client;
+ }
+}
diff --git a/src/__Libraries/StellaOps.Verdict/TASKS.md b/src/__Libraries/StellaOps.Verdict/TASKS.md
index 983ff5f8e..795c159ff 100644
--- a/src/__Libraries/StellaOps.Verdict/TASKS.md
+++ b/src/__Libraries/StellaOps.Verdict/TASKS.md
@@ -9,5 +9,6 @@ Source of truth: `docs/implplan/SPRINT_20260222_080_Verdict_persistence_dal_to_e
| VERDICT-EF-03 | DONE | PostgresVerdictStore converted to use VerdictDataSource + VerdictDbContextFactory pattern; inline VerdictDbContext removed. |
| VERDICT-EF-04 | DONE | Compiled model stubs generated; assembly attributes excluded from compilation; VerdictDbContextFactory uses compiled model for default schema. |
| VERDICT-EF-05 | DONE | Sequential builds pass (0 warnings, 0 errors); module docs and AGENTS.md updated; sprint tracker updated. |
+| SPRINT_20260405_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: OCI attestation publisher fallback HTTP path now reuses a shared runtime client instead of allocating ad hoc transport instances. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.Verdict/StellaOps.Verdict.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs
new file mode 100644
index 000000000..465ade1b1
--- /dev/null
+++ b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs
@@ -0,0 +1,167 @@
+using FluentAssertions;
+using StellaOps.TestKit;
+using Xunit;
+
+namespace StellaOps.Infrastructure.Postgres.Tests;
+
+[Trait("Category", TestCategories.Unit)]
+public sealed class RuntimePostgresConstructionConventionTests
+{
+ private static readonly string RepoRoot = FindRepoRoot();
+ private static readonly HashSet AllowedRuntimeRawConnectionFiles = new(StringComparer.Ordinal)
+ {
+ "src/Cli/__Libraries/StellaOps.Cli.Plugins.Aoc/AocVerificationService.cs",
+ "src/Cli/StellaOps.Cli/Commands/Setup/Steps/Implementations/DatabaseSetupStep.cs",
+ "src/Cli/StellaOps.Cli/Services/MigrationCommandService.cs",
+ "src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresConnectionPoolCheck.cs",
+ "src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresConnectivityCheck.cs",
+ "src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Postgres/Checks/PostgresMigrationStatusCheck.cs",
+ "src/Platform/StellaOps.Platform.WebService/Services/PlatformMigrationAdminService.cs",
+ "src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs",
+ "src/__Libraries/StellaOps.Doctor.Plugins.Database/Checks/DatabaseCheckBase.cs",
+ "src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs",
+ "src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs",
+ };
+ private static readonly HashSet AllowedRuntimeValkeyConnectionFiles = new(StringComparer.Ordinal)
+ {
+ "src/Cli/StellaOps.Cli/Commands/Setup/Steps/Implementations/CacheSetupStep.cs",
+ "src/Tools/NotifySmokeCheck/NotifySmokeCheckRunner.cs",
+ "src/__Libraries/StellaOps.TestKit/Fixtures/ValkeyFixture.cs",
+ };
+ private static readonly string[] KnownHttpLifecycleHotspots =
+ [
+ "src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.cs",
+ "src/Attestor/__Libraries/StellaOps.Attestor.TrustRepo/TrustRepoServiceCollectionExtensions.Offline.cs",
+ "src/Attestor/__Libraries/StellaOps.Attestor.TrustVerdict/Oci/TrustVerdictOciAttacher.cs",
+ "src/Integrations/StellaOps.Integrations.WebService/FeedMirrorConnectorPlugins.cs",
+ "src/Integrations/StellaOps.Integrations.WebService/ObjectStorageConnectorPlugins.cs",
+ "src/Platform/StellaOps.Platform.WebService/Services/IdentityProviderManagementService.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/AcrConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/DockerHubConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/EcrConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GcrConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AwsSecretsManagerConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/AzureKeyVaultConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Vault/HashiCorpVaultConnector.cs",
+ "src/__Libraries/StellaOps.Artifact.Core/Api/ArtifactController.FetchHttp.cs",
+ "src/__Libraries/StellaOps.Verdict/Oci/OciAttestationPublisher.cs",
+ ];
+
+ [Fact]
+ public void Runtime_code_does_not_use_anonymous_NpgsqlDataSource_Create()
+ {
+ var offenders = EnumerateRuntimeSourceFiles()
+ .Where(file => File.ReadAllText(file).Contains("NpgsqlDataSource.Create(", StringComparison.Ordinal))
+ .Select(ToRelativePath)
+ .ToList();
+
+ offenders.Should().BeEmpty(
+ "runtime PostgreSQL callers should use a named data source or the shared PostgreSQL policy path");
+ }
+
+ [Fact]
+ public void Runtime_data_source_builders_must_apply_runtime_attribution()
+ {
+ var offenders = EnumerateRuntimeSourceFiles()
+ .Where(file =>
+ {
+ var content = File.ReadAllText(file);
+ return content.Contains("new NpgsqlDataSourceBuilder(", StringComparison.Ordinal)
+ && !content.Contains("ApplicationName", StringComparison.Ordinal)
+ && !content.Contains("PostgresConnectionStringPolicy", StringComparison.Ordinal);
+ })
+ .Select(ToRelativePath)
+ .ToList();
+
+ offenders.Should().BeEmpty(
+ "runtime NpgsqlDataSourceBuilder call sites must stamp ApplicationName explicitly or flow through PostgresConnectionStringPolicy");
+ }
+
+ [Fact]
+ public void Runtime_code_does_not_use_raw_NpgsqlConnection_outside_allowlist()
+ {
+ var offenders = EnumerateRuntimeSourceFiles()
+ .Where(file => File.ReadAllText(file).Contains("new NpgsqlConnection(", StringComparison.Ordinal))
+ .Select(ToRelativePath)
+ .Where(file => !AllowedRuntimeRawConnectionFiles.Contains(file))
+ .ToList();
+
+ offenders.Should().BeEmpty(
+ "runtime PostgreSQL callers should use named data sources unless they are an explicit CLI/setup/migration/diagnostic exception");
+ }
+
+ [Fact]
+ public void Runtime_valkey_connections_must_stamp_client_name()
+ {
+ var offenders = EnumerateRuntimeSourceFiles()
+ .Where(file =>
+ {
+ var content = File.ReadAllText(file);
+ var relativePath = ToRelativePath(file);
+ var usesValkeyMultiplexer =
+ content.Contains("ConnectionMultiplexer.Connect(", StringComparison.Ordinal)
+ || content.Contains("ConnectionMultiplexer.ConnectAsync(", StringComparison.Ordinal);
+
+ if (!usesValkeyMultiplexer || AllowedRuntimeValkeyConnectionFiles.Contains(relativePath))
+ {
+ return false;
+ }
+
+ return !content.Contains("ClientName", StringComparison.Ordinal);
+ })
+ .Select(ToRelativePath)
+ .ToList();
+
+ offenders.Should().BeEmpty(
+ "runtime Valkey callers should stamp a stable ClientName unless they are an explicit CLI/tooling exception");
+ }
+
+ [Fact]
+ public void Known_runtime_http_hotspots_do_not_allocate_ad_hoc_HttpClient()
+ {
+ var offenders = EnumerateRuntimeSourceFiles()
+ .Where(file => KnownHttpLifecycleHotspots.Contains(ToRelativePath(file)))
+ .Where(file => File.ReadAllText(file).Contains("new HttpClient(", StringComparison.Ordinal))
+ .Select(ToRelativePath)
+ .ToList();
+
+ offenders.Should().BeEmpty(
+ "the scoped HTTP hardening waves removed raw runtime HttpClient allocation from the known host-owned hotspots");
+ }
+
+ private static IEnumerable EnumerateRuntimeSourceFiles()
+ {
+ var srcRoot = Path.Combine(RepoRoot, "src");
+ return Directory.EnumerateFiles(srcRoot, "*.cs", SearchOption.AllDirectories)
+ .Where(file => !PathSegments(file).Any(segment => segment.EndsWith(".Tests", StringComparison.Ordinal)))
+ .Where(file => !PathSegments(file).Any(segment => segment.EndsWith(".Benchmarks", StringComparison.Ordinal)))
+ .Where(file => !file.Contains($"{Path.DirectorySeparatorChar}Testing{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
+ .Where(file => !file.Contains($"{Path.DirectorySeparatorChar}__Tests{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
+ .Where(file => !file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
+ .Where(file => !file.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal));
+ }
+
+ private static string FindRepoRoot()
+ {
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ while (current is not null)
+ {
+ var srcPath = Path.Combine(current.FullName, "src");
+ var docsPath = Path.Combine(current.FullName, "docs");
+ if (Directory.Exists(srcPath) && Directory.Exists(docsPath))
+ {
+ return current.FullName;
+ }
+
+ current = current.Parent;
+ }
+
+ throw new InvalidOperationException("Failed to locate the Stella Ops repository root from the test output directory.");
+ }
+
+ private static string ToRelativePath(string fullPath)
+ => Path.GetRelativePath(RepoRoot, fullPath).Replace('\\', '/');
+
+ private static IEnumerable PathSegments(string fullPath)
+ => fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+}
diff --git a/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/TASKS.md b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/TASKS.md
index ef04e8dcd..1a39c099e 100644
--- a/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/TASKS.md
+++ b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/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-GUARD | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: shared PostgreSQL/Valkey transport policy tests and static guardrails for runtime transport construction plus the scoped second-wave HTTP hotspot regressions. |
| AUDIT-0028-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0028-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0028-A | DONE | Waived (test project; revalidated 2026-01-08). |