Harden remaining runtime transport lifecycles

This commit is contained in:
master
2026-04-06 00:24:16 +03:00
parent 751546084e
commit fc798a1573
29 changed files with 311 additions and 107 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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;
/// </summary>
public static class EgressHttpClientFactory
{
private static readonly SocketsHttpHandler SharedHandler = new()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
};
/// <summary>
/// Creates an <see cref="HttpClient"/> after validating the supplied egress request against the policy.
/// </summary>
@@ -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);
}
/// <summary>
@@ -90,4 +95,11 @@ public static class EgressHttpClientFactory
Func<HttpClient> clientFactory,
Action<HttpClient>? 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;
}
}

View File

@@ -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. |

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.ReleaseOrchestrator.IntegrationHub.Tests")]

View File

@@ -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. |

View File

@@ -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());
}
}

View File

@@ -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. |

23
src/Workflow/AGENTS.md Normal file
View File

@@ -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`.

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Workflow.DataStore.PostgreSQL.Tests")]

View File

@@ -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";

View File

@@ -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<NpgsqlConnection> 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();
}
}

View File

@@ -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<string, string?>
{
[$"ConnectionStrings:{options.ConnectionStringName}"] =
"Host=localhost;Database=workflow;Username=workflow;Password=workflow",
})
.Build();
return new PostgresWorkflowDatabase(
configuration,
Options.Create(options),
new PostgresWorkflowMutationSessionAccessor(),
new WorkflowMutationScopeAccessor());
}
}

View File

@@ -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",