From fc798a15730f6a0f7c387625984df4d029a198bc Mon Sep 17 00:00:00 2001
From: master <>
Date: Mon, 6 Apr 2026 00:24:16 +0300
Subject: [PATCH] Harden remaining runtime transport lifecycles
---
...sport_pooling_and_attribution_hardening.md | 64 ++++++++++++++-
docs/modules/airgap/guides/airgap-mode.md | 1 +
.../runtime-transport-client-rules.md | 4 +-
.../EgressPolicyTests.HttpClientFactory.cs | 25 ++++++
.../EgressHttpClientFactory.cs | 22 ++++--
.../StellaOps.AirGap.Policy/TASKS.md | 2 +-
.../Registry/GenericOciConnector.cs | 8 +-
.../Connectors/Registry/HarborConnector.cs | 8 +-
.../Registry/JfrogArtifactoryConnector.cs | 8 +-
.../Connectors/Registry/QuayConnector.cs | 8 +-
.../Connectors/Scm/AzureDevOpsConnector.cs | 7 +-
.../Connectors/Scm/GitHubConnector.cs | 7 +-
.../Connectors/Scm/GitLabConnector.cs | 7 +-
.../Connectors/Scm/GiteaConnector.cs | 7 +-
.../SettingsStore/AwsAppConfigConnector.cs | 12 +--
.../AwsParameterStoreConnector.cs | 10 +--
.../SettingsStore/AzureAppConfigConnector.cs | 9 +--
.../SettingsStore/ConsulKvConnector.cs | 10 +--
.../Connectors/SettingsStore/EtcdConnector.cs | 10 +--
.../InternalsVisibleTo.cs | 3 +
.../TASKS.md | 1 +
.../ConnectorHttpClientsTests.cs | 32 ++++++++
.../TASKS.md | 1 +
src/Workflow/AGENTS.md | 23 ++++++
.../InternalsVisibleTo.cs | 3 +
.../PostgresWorkflowBackendOptions.cs | 5 ++
.../PostgresWorkflowDatabase.cs | 29 ++++++-
.../PostgresWorkflowDatabaseTests.cs | 78 +++++++++++++++++++
...timePostgresConstructionConventionTests.cs | 14 +++-
29 files changed, 311 insertions(+), 107 deletions(-)
create mode 100644 src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/InternalsVisibleTo.cs
create mode 100644 src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/ConnectorHttpClientsTests.cs
create mode 100644 src/Workflow/AGENTS.md
create mode 100644 src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/InternalsVisibleTo.cs
create mode 100644 src/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/PostgresWorkflowDatabaseTests.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
index 2c74958ef..f18500893 100644
--- 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
@@ -11,7 +11,7 @@
## 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.
+- Cross-module edits allowed for `src/AdvisoryAI/**`, `src/AirGap/**`, `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/**`, `src/Workflow/**`, `docs/**`, and `devops/**` when they consume the shared transport conventions.
## Documentation Prerequisites
- `docs/code-of-conduct/CODE_OF_CONDUCT.md`
@@ -23,6 +23,10 @@
- `src/__Libraries/AGENTS.md`
- `src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md`
- `src/__Tests/AGENTS.md`
+- `src/AirGap/StellaOps.AirGap.Policy/AGENTS.md`
+- `src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/AGENTS.md`
+- `src/ReleaseOrchestrator/AGENTS.md`
+- `src/Workflow/AGENTS.md`
## Delivery Tracker
@@ -104,6 +108,46 @@ Completion criteria:
- [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.
+### XPORT-WORKFLOW-007 - Remove the remaining Workflow PostgreSQL runtime exception
+Status: DONE
+Dependency: XPORT-GUARD-003
+Owners: Developer
+Task description:
+- Add the missing Workflow module instructions so runtime storage edits are no longer blocked by repo governance.
+- Normalize the Workflow PostgreSQL backend connection string with stable application-name and pooling settings, add focused regression coverage, and remove the backend from the shared raw-connection allowlist.
+
+Completion criteria:
+- [x] `src/Workflow/AGENTS.md` exists and documents the module rules needed for runtime storage changes.
+- [x] Workflow's PostgreSQL backend applies stable runtime attribution/pooling before opening raw `NpgsqlConnection` instances.
+- [x] The shared convention suite no longer allowlists the Workflow PostgreSQL backend.
+
+### XPORT-HTTP-008 - Harden AirGap egress HTTP fallback lifecycle
+Status: DONE
+Dependency: XPORT-HTTP-005
+Owners: Developer
+Task description:
+- Replace the raw default `new HttpClient()` fallback inside `EgressHttpClientFactory` with a shared-handler compatibility path so repeated policy-approved calls do not create independent default connection pools.
+- Keep the public helper contract unchanged, document the fallback behavior, and preserve per-call client isolation for callers that apply custom headers or base addresses.
+
+Completion criteria:
+- [x] `EgressHttpClientFactory` no longer uses the default parameterless `new HttpClient()` fallback path.
+- [x] Unit coverage proves the fallback still returns isolated client instances for caller-specific configuration.
+- [x] AirGap module docs and task board reflect the hardened fallback behavior.
+
+### XPORT-HTTP-009 - Eliminate default-handler churn across ReleaseOrchestrator IntegrationHub connectors
+Status: DONE
+Dependency: XPORT-HTTP-006
+Owners: Developer
+Task description:
+- Add the missing ReleaseOrchestrator module instructions needed for autonomous connector/runtime transport edits.
+- Move the remaining IntegrationHub SCM, settings-store, and registry connectors off raw default-handler `new HttpClient()` construction and onto the shared-handler compatibility wrapper while preserving per-connector client isolation for auth headers and base addresses.
+- Extend the scoped HTTP guardrail and add focused helper regression coverage so the shared compatibility path stays isolated and pooled.
+
+Completion criteria:
+- [x] `src/ReleaseOrchestrator/AGENTS.md` exists and covers connector/runtime transport work.
+- [x] The remaining raw IntegrationHub connector `HttpClient` constructions route through `ConnectorHttpClients.CreateClient(...)` instead of the default handler path.
+- [x] The shared convention suite and targeted IntegrationHub tests cover the broadened ReleaseOrchestrator connector hotspot set.
+
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
@@ -118,6 +162,12 @@ Completion criteria:
| 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 |
+| 2026-04-05 | Added `src/Workflow/AGENTS.md`, normalized the Workflow PostgreSQL backend connection string with stable application name and pooling defaults, added focused Workflow regression coverage, and removed the backend from the shared raw-connection allowlist. | Developer |
+| 2026-04-05 | Validation: `dotnet build src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/StellaOps.Workflow.DataStore.PostgreSQL.csproj`, `dotnet test src/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests.csproj`, and `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed. | Developer |
+| 2026-04-05 | Hardened the AirGap `EgressHttpClientFactory` fallback to use a shared handler instead of raw default `new HttpClient()` allocation, added isolation coverage for the fallback path, and updated the module task board plus air-gap mode guidance. | Developer |
+| 2026-04-05 | Validation: `dotnet build src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj` and `dotnet test src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/StellaOps.AirGap.Policy.Tests.csproj` passed. | Developer |
+| 2026-04-06 | Added `src/ReleaseOrchestrator/AGENTS.md`, routed the remaining IntegrationHub SCM, settings-store, and registry connectors through `ConnectorHttpClients.CreateClient(...)`, and added focused helper coverage for isolated shared-handler client creation. | Developer |
+| 2026-04-06 | Validation: `dotnet build src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/StellaOps.ReleaseOrchestrator.IntegrationHub.csproj`, `dotnet test src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests.csproj`, and `dotnet test src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/StellaOps.Infrastructure.Postgres.Tests.csproj` passed. | 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.
@@ -126,12 +176,18 @@ Completion criteria:
- 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.
+- AirGap's fallback egress wrapper now uses a shared handler while still returning isolated `HttpClient` instances per call, preserving caller-specific header/base-address configuration without paying the raw default-handler churn cost.
- 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.
+- ReleaseOrchestrator IntegrationHub connectors still do not use `IHttpClientFactory`; this sprint broadens the shared-handler compatibility path across SCM, settings-store, and registry connectors so they stop allocating default-handler clients while preserving per-connector client isolation.
+- ReleaseOrchestrator's compatibility wrapper is still not safe to client-cache broadly because many connectors mutate `DefaultRequestHeaders` with per-connector auth state; a future refactor needs request-scoped headers or typed/factory clients before shared client instances can be introduced there.
+- Workflow now has module-local instructions, and its PostgreSQL store normalizes `ApplicationName` plus pooling before opening raw `NpgsqlConnection` instances; it remains a direct-connection implementation for now, but it is no longer an anonymous runtime exception.
+- The remaining explicit raw-connection allowlist is intentionally narrow: CLI/setup, migrations, diagnostics, and `PlatformMigrationAdminService`.
- 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.
+- Continue the 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.
+- Continue the broader HTTP/SCM/Vault-style lifecycle sweep with special focus on connector stacks that still mutate `DefaultRequestHeaders` on shared compatibility clients, because those need request-scoped auth/header refactors before client caching is safe.
+- Continue the connector HTTP sweep with request-scoped auth/header refactors for ReleaseOrchestrator and the remaining CLI fallbacks, because those are now the main sources of duplicated runtime client setup after the shared-handler migration.
+- Evaluate whether Workflow should move from normalized raw `NpgsqlConnection` usage to a module-scoped `NpgsqlDataSource` wrapper in a future storage refactor, but it is no longer a blocker for the shared convention suite.
diff --git a/docs/modules/airgap/guides/airgap-mode.md b/docs/modules/airgap/guides/airgap-mode.md
index b651670a9..e5eb506f4 100644
--- a/docs/modules/airgap/guides/airgap-mode.md
+++ b/docs/modules/airgap/guides/airgap-mode.md
@@ -62,6 +62,7 @@ Air-Gapped Mode is the supported operating profile for deployments with **zero e
- **CLI guard:** the CLI now routes outbound HTTP through the shared egress policy. When sealed, commands that would dial external endpoints (for example, `scanner download` or remote `sources ingest` URIs) are refused with `AIRGAP_EGRESS_BLOCKED` messaging and remediation guidance instead of attempting the network call.
- **Observability exporters:** `StellaOps.Telemetry.Core` now binds OTLP exporters to the configured egress policy. When sealed, any collector endpoint that is not loopback or allow-listed is skipped at startup and a structured warning is written so operators see the remediation guidance without leaving sealed mode.
- **Linting/CI:** enable the `StellaOps.AirGap.Policy.Analyzers` package in solution-level analyzers so CI fails on raw `HttpClient` usage. The analyzer emits `AIRGAP001` and the bundled code fix rewrites to `EgressHttpClientFactory.Create(...)`; treat analyzer warnings as errors in sealed-mode pipelines.
+- **Egress wrapper fallback:** when DI-managed `IHttpClientFactory` wiring is unavailable, `EgressHttpClientFactory.Create(...)` now falls back to a shared-handler HTTP client path instead of creating a brand-new default handler/connection pool for each request. Service-owned hosts should still prefer the overload that accepts a caller-supplied factory client so naming, retries, and other host policy can flow through.
## Testing & verification
diff --git a/docs/technical/runtime-transport-client-rules.md b/docs/technical/runtime-transport-client-rules.md
index c3e955e40..08c81cf11 100644
--- a/docs/technical/runtime-transport-client-rules.md
+++ b/docs/technical/runtime-transport-client-rules.md
@@ -6,6 +6,7 @@ This document defines the minimum lifecycle and attribution rules for long-lived
- 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.
+- Any temporary runtime exception that still uses raw `NpgsqlConnection` must normalize `ApplicationName` and pooling policy before opening the connection.
## Valkey / Redis
- Steady-state runtime `ConnectionMultiplexer` construction must stamp a stable `ClientName`.
@@ -16,8 +17,9 @@ This document defines the minimum lifecycle and attribution rules for long-lived
## 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.
+- Compatibility wrappers may still return per-call `HttpClient` instances when callers need isolated headers or base addresses, but those wrappers should share the underlying handler/pool rather than constructing default-handler clients repeatedly.
- 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.
+- 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 plus the broadened SCM/settings-store/registry connector set, 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.
diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.HttpClientFactory.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.HttpClientFactory.cs
index 24bcd0b6a..ac14a39ed 100644
--- a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.HttpClientFactory.cs
+++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Tests/EgressPolicyTests.HttpClientFactory.cs
@@ -19,4 +19,29 @@ public sealed partial class EgressPolicyTests
Assert.True(recordingPolicy.EnsureAllowedCalled);
Assert.NotNull(client);
}
+
+ [Trait("Category", TestCategories.Unit)]
+ [Fact]
+ public void EgressHttpClientFactory_Create_UsingFallbackFactory_ReturnsIsolatedClients()
+ {
+ var recordingPolicy = new RecordingPolicy();
+ var request = new EgressRequest("Component", new Uri("https://allowed.internal"), "mirror-sync");
+
+ using var first = EgressHttpClientFactory.Create(
+ recordingPolicy,
+ request,
+ client =>
+ {
+ client.BaseAddress = new Uri("https://first.internal/");
+ client.DefaultRequestHeaders.Add("X-Test", "one");
+ });
+ using var second = EgressHttpClientFactory.Create(recordingPolicy, request);
+
+ Assert.True(recordingPolicy.EnsureAllowedCalled);
+ Assert.Equal(new Uri("https://first.internal/"), first.BaseAddress);
+ Assert.Null(second.BaseAddress);
+ Assert.True(first.DefaultRequestHeaders.Contains("X-Test"));
+ Assert.False(second.DefaultRequestHeaders.Contains("X-Test"));
+ Assert.NotEmpty(second.DefaultRequestHeaders.UserAgent);
+ }
}
diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs
index 45b077d4e..1ce4f75fa 100644
--- a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs
+++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/EgressHttpClientFactory.cs
@@ -1,5 +1,7 @@
using System;
+using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
namespace StellaOps.AirGap.Policy;
@@ -8,6 +10,13 @@ namespace StellaOps.AirGap.Policy;
///
public static class EgressHttpClientFactory
{
+ private static readonly SocketsHttpHandler SharedHandler = new()
+ {
+ AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
+ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
+ PooledConnectionLifetime = TimeSpan.FromMinutes(15),
+ };
+
///
/// Creates an after validating the supplied egress request against the policy.
///
@@ -19,11 +28,7 @@ public static class EgressHttpClientFactory
{
ArgumentNullException.ThrowIfNull(egressPolicy);
- egressPolicy.EnsureAllowed(request);
-
- var client = new HttpClient();
- configure?.Invoke(client);
- return client;
+ return Create(egressPolicy, request, CreateFallbackClient, configure);
}
///
@@ -90,4 +95,11 @@ public static class EgressHttpClientFactory
Func clientFactory,
Action? configure = null)
=> Create(egressPolicy, new EgressRequest(component, destination, intent), clientFactory, configure);
+
+ private static HttpClient CreateFallbackClient()
+ {
+ var client = new HttpClient(SharedHandler, disposeHandler: false);
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOpsAirGapPolicy", "1.0"));
+ return client;
+ }
}
diff --git a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/TASKS.md b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/TASKS.md
index a3d623d82..093f872d5 100644
--- a/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/TASKS.md
+++ b/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/TASKS.md
@@ -7,6 +7,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| AUDIT-0030-M | DONE | Revalidated 2026-01-06; new findings recorded in audit report. |
| AUDIT-0030-T | DONE | Revalidated 2026-01-06; test coverage tracked in AUDIT-0033. |
-| AUDIT-0030-A | TODO | Replace direct new HttpClient usage in EgressHttpClientFactory. |
+| AUDIT-0030-A | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: `EgressHttpClientFactory` now uses a shared-handler fallback instead of raw default `new HttpClient()` allocation. |
| REMED-05 | DONE | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.md. |
| REMED-06 | DONE | SOLID review notes updated for SPRINT_20260130_002. |
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GenericOciConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GenericOciConnector.cs
index 76ce6394e..49ac42f24 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GenericOciConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/GenericOciConnector.cs
@@ -362,10 +362,7 @@ public sealed class GenericOciConnector : IRegistryConnectorCapability, IDisposa
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_registryUrl + "/")
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_registryUrl + "/"));
if (!string.IsNullOrEmpty(_username))
{
@@ -375,9 +372,6 @@ public sealed class GenericOciConnector : IRegistryConnectorCapability, IDisposa
new AuthenticationHeaderValue("Basic", credentials);
}
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs
index 79f9cdc83..53bc95229 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs
@@ -375,10 +375,7 @@ public sealed class HarborConnector : IRegistryConnectorCapability, IDisposable
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_harborUrl + "/")
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_harborUrl + "/"));
if (!string.IsNullOrEmpty(_username))
{
@@ -388,9 +385,6 @@ public sealed class HarborConnector : IRegistryConnectorCapability, IDisposable
new AuthenticationHeaderValue("Basic", credentials);
}
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs
index d10d4c71c..1c48045b9 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs
@@ -523,10 +523,7 @@ public sealed class JfrogArtifactoryConnector : IRegistryConnectorCapability, ID
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_artifactoryUrl + "/")
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_artifactoryUrl + "/"));
// Set authorization header based on available auth
if (!string.IsNullOrEmpty(_accessToken))
@@ -546,9 +543,6 @@ public sealed class JfrogArtifactoryConnector : IRegistryConnectorCapability, ID
new AuthenticationHeaderValue("Basic", credentials);
}
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.cs
index 509aa954c..cba72b0f0 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.cs
@@ -417,10 +417,7 @@ public sealed class QuayConnector : IRegistryConnectorCapability, IDisposable
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_quayUrl + "/")
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_quayUrl + "/"));
// Set authorization header
if (!string.IsNullOrEmpty(_oauth2Token))
@@ -436,9 +433,6 @@ public sealed class QuayConnector : IRegistryConnectorCapability, IDisposable
new AuthenticationHeaderValue("Basic", credentials);
}
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/AzureDevOpsConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/AzureDevOpsConnector.cs
index e17214cc5..de16b966d 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/AzureDevOpsConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/AzureDevOpsConnector.cs
@@ -350,16 +350,11 @@ public sealed class AzureDevOpsConnector : IScmConnectorCapability, IDisposable
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($":{pat}"));
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(baseUrl)
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(baseUrl));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", authValue);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs
index 08305b9a4..000945300 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs
@@ -285,16 +285,11 @@ public sealed class GitHubConnector : IScmConnectorCapability, IDisposable
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(baseUrl)
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(baseUrl));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
_httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
return _httpClient;
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs
index 54bdb936b..6e0ee7339 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs
@@ -283,15 +283,10 @@ public sealed class GitLabConnector : IScmConnectorCapability, IDisposable
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(baseUrl)
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(baseUrl));
_httpClient.DefaultRequestHeaders.Add("PRIVATE-TOKEN", token);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs
index 89c71ac61..b73f084d8 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs
@@ -292,16 +292,11 @@ public sealed class GiteaConnector : IScmConnectorCapability, IDisposable
_baseUrl = baseUrlProp.GetString()!.TrimEnd('/');
var apiUrl = _baseUrl + "/api/v1/";
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(apiUrl)
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(apiUrl));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("token", token);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs
index 91088a474..d0db88909 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs
@@ -454,7 +454,7 @@ public sealed class AwsAppConfigConnector : ISettingsStoreConnectorCapability, I
// Use the AppConfig Data API for configuration retrieval
var dataEndpoint = $"https://appconfigdata.{_region}.amazonaws.com";
- using var dataClient = new HttpClient { BaseAddress = new Uri(dataEndpoint) };
+ using var dataClient = ConnectorHttpClients.CreateClient(new Uri(dataEndpoint));
// Start a configuration session
var sessionRequest = CreateAppConfigDataRequest(
@@ -561,14 +561,8 @@ public sealed class AwsAppConfigConnector : ISettingsStoreConnectorCapability, I
var endpoint = $"https://appconfig.{_region}.amazonaws.com";
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(endpoint + "/"),
- Timeout = TimeSpan.FromSeconds(30)
- };
-
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(endpoint + "/"));
+ _httpClient.Timeout = TimeSpan.FromSeconds(30);
}
private HttpRequestMessage CreateAppConfigRequest(HttpMethod method, string path, string? payload)
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs
index 613867ff8..7c007e5e2 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs
@@ -480,14 +480,8 @@ public sealed class AwsParameterStoreConnector : ISettingsStoreConnectorCapabili
var endpoint = $"https://ssm.{_region}.amazonaws.com";
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(endpoint + "/"),
- Timeout = TimeSpan.FromSeconds(30)
- };
-
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(endpoint + "/"));
+ _httpClient.Timeout = TimeSpan.FromSeconds(30);
}
private HttpRequestMessage CreateSsmRequest(string action, string payload)
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs
index 579d4b0b8..c646d4c40 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs
@@ -528,16 +528,11 @@ public sealed class AzureAppConfigConnector : ISettingsStoreConnectorCapability,
_label = labelProp.GetString();
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_endpoint + "/"),
- Timeout = TimeSpan.FromSeconds(30)
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_endpoint + "/"));
+ _httpClient.Timeout = TimeSpan.FromSeconds(30);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.microsoft.appconfig.kv+json"));
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs
index c7965e2d8..375ff052a 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs
@@ -462,20 +462,14 @@ public sealed class ConsulKvConnector : ISettingsStoreConnectorCapability, IDisp
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_consulAddress + "/"),
- Timeout = TimeSpan.FromMinutes(6) // Allow for blocking queries
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_consulAddress + "/"));
+ _httpClient.Timeout = TimeSpan.FromMinutes(6); // Allow for blocking queries
if (!string.IsNullOrEmpty(_token))
{
_httpClient.DefaultRequestHeaders.Add("X-Consul-Token", _token);
}
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.cs
index b62cc6eaf..f62b2456f 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.cs
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.cs
@@ -473,11 +473,8 @@ public sealed class EtcdConnector : ISettingsStoreConnectorCapability, IDisposab
}
}
- _httpClient = new HttpClient
- {
- BaseAddress = new Uri(_etcdAddress + "/"),
- Timeout = TimeSpan.FromMinutes(6)
- };
+ _httpClient = ConnectorHttpClients.CreateClient(new Uri(_etcdAddress + "/"));
+ _httpClient.Timeout = TimeSpan.FromMinutes(6);
// Authenticate if credentials provided
if (!string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_password))
@@ -485,9 +482,6 @@ public sealed class EtcdConnector : ISettingsStoreConnectorCapability, IDisposab
await AuthenticateAsync(ct);
}
- _httpClient.DefaultRequestHeaders.UserAgent.Add(
- new ProductInfoHeaderValue("StellaOps", "1.0"));
-
return _httpClient;
}
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/InternalsVisibleTo.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/InternalsVisibleTo.cs
new file mode 100644
index 000000000..f7d5212ad
--- /dev/null
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("StellaOps.ReleaseOrchestrator.IntegrationHub.Tests")]
diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md
index 9b278e2ff..f7bc16b0f 100644
--- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md
+++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/TASKS.md
@@ -5,5 +5,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. |
+| SPRINT_20260406_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: SCM, settings-store, and remaining registry connectors now source their per-connector `HttpClient` instances from `ConnectorHttpClients.CreateClient(...)` so they reuse the shared pooled handler without leaking auth headers across integrations. |
| 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/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/ConnectorHttpClientsTests.cs b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/ConnectorHttpClientsTests.cs
new file mode 100644
index 000000000..12c26e8d1
--- /dev/null
+++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/ConnectorHttpClientsTests.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Linq;
+
+using StellaOps.ReleaseOrchestrator.IntegrationHub.Connectors;
+
+namespace StellaOps.ReleaseOrchestrator.IntegrationHub.Tests;
+
+public sealed class ConnectorHttpClientsTests
+{
+ [Fact]
+ public void CreateClient_AssignsBaseAddressAndSharedUserAgent()
+ {
+ using var client = ConnectorHttpClients.CreateClient(new Uri("https://connector.example/api/"));
+
+ Assert.Equal(new Uri("https://connector.example/api/"), client.BaseAddress);
+ Assert.Contains(client.DefaultRequestHeaders.UserAgent, agent => agent.Product?.Name == "StellaOps");
+ }
+
+ [Fact]
+ public void CreateClient_ReturnsIsolatedClientInstances()
+ {
+ using var first = ConnectorHttpClients.CreateClient();
+ using var second = ConnectorHttpClients.CreateClient();
+
+ first.DefaultRequestHeaders.Add("X-Test-Header", "one");
+
+ Assert.NotSame(first, second);
+ Assert.True(first.DefaultRequestHeaders.Contains("X-Test-Header"));
+ Assert.False(second.DefaultRequestHeaders.Contains("X-Test-Header"));
+ Assert.True(second.DefaultRequestHeaders.UserAgent.Any());
+ }
+}
diff --git a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/TASKS.md b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/TASKS.md
index cb6bf102a..c36768c54 100644
--- a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/TASKS.md
+++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/TASKS.md
@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
+| SPRINT_20260406_011-XPORT-HTTP | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: added `ConnectorHttpClients` regression coverage for isolated per-connector clients on the shared pooled handler path. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests/StellaOps.ReleaseOrchestrator.IntegrationHub.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
diff --git a/src/Workflow/AGENTS.md b/src/Workflow/AGENTS.md
new file mode 100644
index 000000000..2411aad8d
--- /dev/null
+++ b/src/Workflow/AGENTS.md
@@ -0,0 +1,23 @@
+# AGENTS - Workflow Module
+
+## Working Directory
+- `src/Workflow/**` (workflow abstractions, engine, storage backends, signaling, and tests).
+
+## Required Reading
+- `docs/README.md`
+- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
+- `docs/modules/platform/architecture-overview.md`
+- `docs/modules/release-orchestrator/modules/workflow-engine.md`
+
+## Engineering Rules
+- Preserve deterministic workflow state transitions, event ordering, and projection behavior across storage backends.
+- Keep PostgreSQL, signaling, and engine changes compatible at the contract level unless the sprint explicitly scopes a cross-backend divergence.
+- Runtime storage code must follow the shared transport attribution rules: stable client identity, pooled connections, and no anonymous long-lived transports.
+- Avoid cross-module edits unless the active sprint explicitly allows them.
+
+## Testing & Verification
+- Workflow backend/storage changes must run the targeted backend test project, not just the broader module solution.
+- Prefer focused integration coverage for persistence, signaling, wake-outbox, and projection flows when touching storage or transport behavior.
+
+## Sprint Discipline
+- Track task state in the active sprint file and record blockers or contract changes in `Decisions & Risks`.
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/InternalsVisibleTo.cs b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/InternalsVisibleTo.cs
new file mode 100644
index 000000000..890a80759
--- /dev/null
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("StellaOps.Workflow.DataStore.PostgreSQL.Tests")]
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowBackendOptions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowBackendOptions.cs
index 284af2297..256fb6ff3 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowBackendOptions.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowBackendOptions.cs
@@ -7,6 +7,11 @@ public sealed class PostgresWorkflowBackendOptions
public const string SectionName = $"{WorkflowBackendOptions.SectionName}:Postgres";
public string ConnectionStringName { get; set; } = "WorkflowPostgres";
+ public string? ApplicationName { get; set; }
+ public bool Pooling { get; set; } = true;
+ public int MinPoolSize { get; set; } = 1;
+ public int MaxPoolSize { get; set; } = 100;
+ public int? ConnectionIdleLifetimeSeconds { get; set; } = 300;
public string SchemaName { get; set; } = "srd_wfklw";
public string RuntimeStatesTableName { get; set; } = "wf_runtime_states";
public string HostedJobLocksTableName { get; set; } = "wf_host_locks";
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs
index 8cefd0992..7186922a7 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDatabase.cs
@@ -18,6 +18,7 @@ public sealed class PostgresWorkflowDatabase(
PostgresWorkflowMutationSessionAccessor sessionAccessor,
IWorkflowMutationScopeAccessor scopeAccessor)
{
+ private const string DefaultApplicationName = "stellaops-workflow";
private readonly PostgresWorkflowBackendOptions postgres = options.Value;
internal PostgresWorkflowBackendOptions Options => postgres;
@@ -86,14 +87,36 @@ public sealed class PostgresWorkflowDatabase(
}
internal async Task OpenConnectionAsync(CancellationToken cancellationToken = default)
+ {
+ var connection = new NpgsqlConnection(BuildConnectionString());
+ await connection.OpenAsync(cancellationToken);
+ return connection;
+ }
+
+ internal string BuildConnectionString()
{
var connectionString = configuration.GetConnectionString(postgres.ConnectionStringName)
?? throw new InvalidOperationException(
$"PostgreSQL workflow backend requires connection string '{postgres.ConnectionStringName}'.");
- var connection = new NpgsqlConnection(connectionString);
- await connection.OpenAsync(cancellationToken);
- return connection;
+ var normalizedMinPoolSize = Math.Max(postgres.MinPoolSize, 0);
+ var normalizedMaxPoolSize = Math.Max(postgres.MaxPoolSize, normalizedMinPoolSize);
+ var builder = new NpgsqlConnectionStringBuilder(connectionString)
+ {
+ ApplicationName = string.IsNullOrWhiteSpace(postgres.ApplicationName)
+ ? DefaultApplicationName
+ : postgres.ApplicationName.Trim(),
+ Pooling = postgres.Pooling,
+ MinPoolSize = normalizedMinPoolSize,
+ MaxPoolSize = normalizedMaxPoolSize,
+ };
+
+ if (postgres.ConnectionIdleLifetimeSeconds.HasValue && postgres.ConnectionIdleLifetimeSeconds.Value > 0)
+ {
+ builder.ConnectionIdleLifetime = postgres.ConnectionIdleLifetimeSeconds.Value;
+ }
+
+ return builder.ToString();
}
}
diff --git a/src/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/PostgresWorkflowDatabaseTests.cs b/src/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/PostgresWorkflowDatabaseTests.cs
new file mode 100644
index 000000000..03c347af0
--- /dev/null
+++ b/src/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/PostgresWorkflowDatabaseTests.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+
+using StellaOps.Workflow.DataStore.PostgreSQL;
+using StellaOps.Workflow.Engine.Services;
+
+using FluentAssertions;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+using Npgsql;
+
+using NUnit.Framework;
+
+namespace StellaOps.Workflow.DataStore.PostgreSQL.Tests;
+
+[TestFixture]
+public class PostgresWorkflowDatabaseTests
+{
+ [Test]
+ public void BuildConnectionString_WhenApplicationNameIsOmitted_UsesWorkflowDefaults()
+ {
+ var options = new PostgresWorkflowBackendOptions
+ {
+ ConnectionStringName = "WorkflowPostgres",
+ MinPoolSize = 2,
+ MaxPoolSize = 9,
+ ConnectionIdleLifetimeSeconds = 123,
+ };
+
+ var connectionString = CreateDatabase(options).BuildConnectionString();
+ var builder = new NpgsqlConnectionStringBuilder(connectionString);
+
+ builder.ApplicationName.Should().Be("stellaops-workflow");
+ builder.Pooling.Should().BeTrue();
+ builder.MinPoolSize.Should().Be(2);
+ builder.MaxPoolSize.Should().Be(9);
+ builder.ConnectionIdleLifetime.Should().Be(123);
+ }
+
+ [Test]
+ public void BuildConnectionString_WhenExplicitSettingsAreProvided_AppliesNormalizedValues()
+ {
+ var options = new PostgresWorkflowBackendOptions
+ {
+ ConnectionStringName = "WorkflowPostgres",
+ ApplicationName = "workflow-projection-tests",
+ Pooling = false,
+ MinPoolSize = 8,
+ MaxPoolSize = 3,
+ };
+
+ var connectionString = CreateDatabase(options).BuildConnectionString();
+ var builder = new NpgsqlConnectionStringBuilder(connectionString);
+
+ builder.ApplicationName.Should().Be("workflow-projection-tests");
+ builder.Pooling.Should().BeFalse();
+ builder.MinPoolSize.Should().Be(8);
+ builder.MaxPoolSize.Should().Be(8);
+ }
+
+ private static PostgresWorkflowDatabase CreateDatabase(PostgresWorkflowBackendOptions options)
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ [$"ConnectionStrings:{options.ConnectionStringName}"] =
+ "Host=localhost;Database=workflow;Username=workflow;Password=workflow",
+ })
+ .Build();
+
+ return new PostgresWorkflowDatabase(
+ configuration,
+ Options.Create(options),
+ new PostgresWorkflowMutationSessionAccessor(),
+ new WorkflowMutationScopeAccessor());
+ }
+}
diff --git a/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs
index 465ade1b1..854ac5002 100644
--- a/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs
+++ b/src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests/RuntimePostgresConstructionConventionTests.cs
@@ -17,7 +17,6 @@ public sealed class RuntimePostgresConstructionConventionTests
"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",
@@ -36,10 +35,23 @@ public sealed class RuntimePostgresConstructionConventionTests
"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/GenericOciConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/HarborConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/JfrogArtifactoryConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Registry/QuayConnector.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/Scm/AzureDevOpsConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GiteaConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitHubConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/Scm/GitLabConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsAppConfigConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AwsParameterStoreConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/AzureAppConfigConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/ConsulKvConnector.cs",
+ "src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.IntegrationHub/Connectors/SettingsStore/EtcdConnector.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",