fix: filter domain assembly scans to Default ALC to prevent type identity mismatches

Plugin assemblies loaded via PluginHost into isolated AssemblyLoadContexts
produce distinct types even from the same DLL. When AppDomain.GetAssemblies()
returns both Default and plugin-ALC copies, DI registration and IOptions<T>
resolution silently fail (e.g. ValkeyTransportOptions defaulting to localhost).

Applied AssemblyLoadContext.Default filter to all 7 assembly discovery sites:
- MessagingServiceCollectionExtensions (transport plugin scan)
- StellaRouterIntegrationHelper (transport plugin loader)
- Gateway.WebService Program.cs (startup transport scan)
- GeneratedEndpointDiscoveryProvider (endpoint provider scan)
- ReflectionEndpointDiscoveryProvider (endpoint attribute scan)
- ServiceCollectionExtensions (schema provider scan)
- MigrationModulePluginDiscovery (migration plugin scan)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
master
2026-03-04 14:01:12 +02:00
parent aaad8104cb
commit 7bafcc3eef
27 changed files with 32 additions and 634 deletions

View File

@@ -0,0 +1,28 @@
# StellaOps.Cartographer — Agent Charter
## Mission
Build and operate the Cartographer service that materializes immutable SBOM property graphs, precomputes layout tiles, and hydrates policy/VEX overlays so other services (API, UI, CLI) can navigate and reason about dependency relationships with context.
## Responsibilities
- Ingest normalized SBOM projections (CycloneDX/SPDX) and generate versioned graph snapshots with tenant-aware storage.
- Maintain overlay workers that merge Policy Engine effective findings and VEX metadata onto graph nodes/edges, including path relevance computation.
- Serve graph APIs for viewport tiles, paths, filters, exports, simulation overlays, and diffing.
- Coordinate with Policy Engine, Scheduler, Conseiller, Excitor, and Authority to keep overlays current, respect RBAC, and uphold determinism guarantees.
- Deliver observability (metrics/traces/logs) and performance benchmarks for large graphs (≥50k nodes).
## Expectations
- Keep builds deterministic; snapshots are write-once and content-addressed.
- Tenancy and scope enforcement must match Authority policies (`graph:*`, `sbom:read`, `findings:read`).
- Update `TASKS.md`, `/docs/implplan/SPRINT_*.md` when status changes.
- Provide fixtures and documentation so UI/CLI teams can simulate graphs offline.
- Authority integration derives scope names from `StellaOps.Auth.Abstractions.StellaOpsScopes`; avoid hard-coded `graph:*` literals.
## Required Reading
- `docs/modules/platform/architecture-overview.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both correspoding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.

View File

@@ -0,0 +1,5 @@
namespace StellaOps.Cartographer;
public sealed class CartographerEntryPoint
{
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cartographer.Options;
/// <summary>
/// Configuration controlling Authority-backed authentication for the Cartographer service.
/// </summary>
public sealed class CartographerAuthorityOptions
{
/// <summary>
/// Enables Authority-backed authentication for Cartographer endpoints.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Allows anonymous access when Authority integration is enabled (development only).
/// </summary>
public bool AllowAnonymousFallback { get; set; }
/// <summary>
/// Authority issuer URL exposed via OpenID discovery.
/// </summary>
public string Issuer { get; set; } = string.Empty;
/// <summary>
/// Whether HTTPS metadata is required when fetching Authority discovery documents.
/// </summary>
public bool RequireHttpsMetadata { get; set; } = true;
/// <summary>
/// Optional explicit metadata endpoint for Authority discovery.
/// </summary>
public string? MetadataAddress { get; set; }
/// <summary>
/// Timeout (seconds) applied to Authority back-channel HTTP calls.
/// </summary>
public int BackchannelTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Allowed token clock skew (seconds) when validating Authority-issued tokens.
/// </summary>
public int TokenClockSkewSeconds { get; set; } = 60;
/// <summary>
/// Accepted audiences for Cartographer access tokens.
/// </summary>
public IList<string> Audiences { get; } = new List<string>();
/// <summary>
/// Scopes required for Cartographer operations.
/// </summary>
public IList<string> RequiredScopes { get; } = new List<string>();
/// <summary>
/// Tenants permitted to access Cartographer resources.
/// </summary>
public IList<string> RequiredTenants { get; } = new List<string>();
/// <summary>
/// Networks allowed to bypass authentication enforcement.
/// </summary>
public IList<string> BypassNetworks { get; } = new List<string>();
/// <summary>
/// Validates configured values and throws <see cref="InvalidOperationException"/> on failure.
/// </summary>
public void Validate()
{
if (!Enabled)
{
return;
}
if (string.IsNullOrWhiteSpace(Issuer))
{
throw new InvalidOperationException("Cartographer Authority issuer must be configured when Authority integration is enabled.");
}
if (!Uri.TryCreate(Issuer.Trim(), UriKind.Absolute, out var issuerUri))
{
throw new InvalidOperationException("Cartographer Authority issuer must be an absolute URI.");
}
if (RequireHttpsMetadata && !issuerUri.IsLoopback && !string.Equals(issuerUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Cartographer Authority issuer must use HTTPS unless running on loopback.");
}
if (BackchannelTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Cartographer Authority back-channel timeout must be greater than zero seconds.");
}
if (TokenClockSkewSeconds < 0 || TokenClockSkewSeconds > 300)
{
throw new InvalidOperationException("Cartographer Authority token clock skew must be between 0 and 300 seconds.");
}
}
}

View File

@@ -0,0 +1,38 @@
using StellaOps.Auth.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
namespace StellaOps.Cartographer.Options;
/// <summary>
/// Applies Cartographer-specific defaults to <see cref="CartographerAuthorityOptions"/>.
/// </summary>
internal static class CartographerAuthorityOptionsConfigurator
{
/// <summary>
/// Ensures required scopes are present and duplicates are removed case-insensitively.
/// </summary>
/// <param name="options">Target options.</param>
public static void ApplyDefaults(CartographerAuthorityOptions options)
{
ArgumentNullException.ThrowIfNull(options);
EnsureScope(options.RequiredScopes, StellaOpsScopes.GraphRead);
EnsureScope(options.RequiredScopes, StellaOpsScopes.GraphWrite);
}
private static void EnsureScope(ICollection<string> scopes, string scope)
{
ArgumentNullException.ThrowIfNull(scopes);
ArgumentException.ThrowIfNullOrEmpty(scope);
if (scopes.Any(existing => string.Equals(existing, scope, StringComparison.OrdinalIgnoreCase)))
{
return;
}
scopes.Add(scope);
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.Options;
namespace StellaOps.Cartographer.Options;
internal sealed class CartographerAuthorityOptionsValidator : IValidateOptions<CartographerAuthorityOptions>
{
public ValidateOptionsResult Validate(string? name, CartographerAuthorityOptions options)
{
try
{
CartographerAuthorityOptionsConfigurator.ApplyDefaults(options);
options.Validate();
return ValidateOptionsResult.Success;
}
catch (Exception ex)
{
return ValidateOptionsResult.Fail(ex.Message);
}
}
}

View File

@@ -0,0 +1,116 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Cartographer.Options;
using StellaOps.Router.AspNet;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables("CARTOGRAPHER_");
builder.Services.AddOptions();
builder.Services.AddLogging();
var authoritySection = builder.Configuration.GetSection("Cartographer:Authority");
builder.Services.AddOptions<CartographerAuthorityOptions>()
.Bind(authoritySection)
.PostConfigure(CartographerAuthorityOptionsConfigurator.ApplyDefaults)
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<CartographerAuthorityOptions>, CartographerAuthorityOptionsValidator>();
var authorityOptions = authoritySection.Get<CartographerAuthorityOptions>() ?? new CartographerAuthorityOptions();
CartographerAuthorityOptionsConfigurator.ApplyDefaults(authorityOptions);
authorityOptions.Validate();
if (authorityOptions.Enabled)
{
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
configure: resourceOptions =>
{
resourceOptions.Authority = authorityOptions.Issuer;
resourceOptions.RequireHttpsMetadata = authorityOptions.RequireHttpsMetadata;
resourceOptions.MetadataAddress = authorityOptions.MetadataAddress;
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(authorityOptions.BackchannelTimeoutSeconds);
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(authorityOptions.TokenClockSkewSeconds);
resourceOptions.Audiences.Clear();
foreach (var audience in authorityOptions.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
resourceOptions.RequiredScopes.Clear();
foreach (var scope in authorityOptions.RequiredScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
});
builder.Services.AddAuthorization(options =>
{
if (authorityOptions.AllowAnonymousFallback)
{
return;
}
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme)
.AddRequirements(new StellaOpsScopeRequirement(authorityOptions.RequiredScopes.ToArray()))
.Build();
});
}
// TODO: register Cartographer graph builders, overlay workers, and Authority client once implementations land.
builder.Services.AddHealthChecks()
.AddCheck("cartographer_ready", () => HealthCheckResult.Healthy(), tags: new[] { "ready" });
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
// Stella Router integration
var routerEnabled = builder.Services.AddRouterMicroservice(
builder.Configuration,
serviceName: "cartographer",
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
routerOptionsSection: "Router");
builder.TryAddStellaOpsLocalBinding("cartographer");
var app = builder.Build();
app.LogStellaOpsLocalHostname("cartographer");
if (!authorityOptions.Enabled)
{
app.Logger.LogWarning("Cartographer Authority authentication is disabled; enable it before production deployments.");
}
else if (authorityOptions.AllowAnonymousFallback)
{
app.Logger.LogWarning("Cartographer Authority allows anonymous fallback; disable fallback before production rollout.");
}
app.UseStellaOpsCors();
if (authorityOptions.Enabled)
{
app.UseAuthentication();
app.UseAuthorization();
app.TryUseStellaRouter(routerEnabled);
}
app.UseStellaOpsTenantMiddleware();
app.MapHealthChecks("/healthz").AllowAnonymous();
app.MapHealthChecks("/readyz", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
}).AllowAnonymous();
app.TryRefreshStellaRouterEndpoints(routerEnabled);
app.Run();
public partial class Program;

View File

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

View File

@@ -0,0 +1,14 @@
{
"profiles": {
"StellaOps.Cartographer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"STELLAOPS_WEBSERVICES_CORS": "true",
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
},
"applicationUrl": "https://localhost:10210;http://localhost:10211"
}
}
}

View File

@@ -0,0 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
</ItemGroup>
<PropertyGroup Label="StellaOpsReleaseVersion">
<Version>1.0.0-alpha1</Version>
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,5 @@
# Completed Tasks
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| CARTO-GRAPH-21-010 | DONE (2025-10-27) | Cartographer Guild | AUTH-GRAPH-21-001 | Replace hard-coded `graph:*` scope strings in Cartographer services/clients with `StellaOpsScopes` constants; document new dependency. | All scope checks reference `StellaOpsScopes`; documentation updated; unit tests adjusted if needed. |

View File

@@ -0,0 +1,11 @@
# Cartographer Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0134-M | DONE | Maintainability audit for StellaOps.Cartographer; revalidated 2026-01-06. |
| AUDIT-0134-T | DONE | Test coverage audit for StellaOps.Cartographer; revalidated 2026-01-06. |
| AUDIT-0134-A | TODO | Revalidated 2026-01-06; open findings pending apply. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -0,0 +1,27 @@
# Cartographer Tests Charter
## Mission
Own test coverage for Cartographer service configuration and behavior.
## Responsibilities
- Maintain `StellaOps.Cartographer.Tests`.
- Validate options defaults, validation, and integration wiring.
- Surface open work on `TASKS.md`; update statuses (TODO/DOING/DONE/BLOCKED/REVIEW).
## Key Paths
- `Options/CartographerAuthorityOptionsConfiguratorTests.cs`
## Coordination
- Cartographer service owners.
- Authority integration owners for scope contracts.
## Required Reading
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/graph/architecture.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.

View File

@@ -0,0 +1,46 @@
using System.Net;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Xunit;
namespace StellaOps.Cartographer.Tests;
public class CartographerProgramTests
{
[Fact]
public async Task HealthEndpoints_ReturnOk()
{
using var factory = new WebApplicationFactory<StellaOps.Cartographer.CartographerEntryPoint>();
using var client = factory.CreateClient();
var cancellationToken = TestContext.Current.CancellationToken;
var health = await client.GetAsync("/healthz", cancellationToken);
var ready = await client.GetAsync("/readyz", cancellationToken);
health.StatusCode.Should().Be(HttpStatusCode.OK);
ready.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public void AuthorityOptions_InvalidIssuer_ThrowsOnStart()
{
using var factory = new WebApplicationFactory<StellaOps.Cartographer.CartographerEntryPoint>().WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, config) =>
{
var settings = new Dictionary<string, string?>
{
["Cartographer:Authority:Enabled"] = "true",
["Cartographer:Authority:Issuer"] = "invalid"
};
config.AddInMemoryCollection(settings);
});
});
Action act = () => factory.CreateClient();
act.Should().Throw<OptionsValidationException>();
}
}

View File

@@ -0,0 +1,51 @@
using StellaOps.Auth.Abstractions;
using StellaOps.Cartographer.Options;
using Xunit;
namespace StellaOps.Cartographer.Tests.Options;
public class CartographerAuthorityOptionsConfiguratorTests
{
[Fact]
public void ApplyDefaults_AddsGraphScopes()
{
var options = new CartographerAuthorityOptions();
CartographerAuthorityOptionsConfigurator.ApplyDefaults(options);
Assert.Contains(StellaOpsScopes.GraphRead, options.RequiredScopes);
Assert.Contains(StellaOpsScopes.GraphWrite, options.RequiredScopes);
}
[Fact]
public void ApplyDefaults_DoesNotDuplicateScopes()
{
var options = new CartographerAuthorityOptions();
options.RequiredScopes.Add("GRAPH:READ");
options.RequiredScopes.Add(StellaOpsScopes.GraphWrite);
CartographerAuthorityOptionsConfigurator.ApplyDefaults(options);
Assert.Equal(2, options.RequiredScopes.Count);
}
[Fact]
public void Validate_AllowsDisabledConfiguration()
{
var options = new CartographerAuthorityOptions();
options.Validate(); // should not throw when disabled
}
[Fact]
public void Validate_ThrowsForInvalidIssuer()
{
var options = new CartographerAuthorityOptions
{
Enabled = true,
Issuer = "invalid"
};
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
}

View File

@@ -0,0 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Cartographer/StellaOps.Cartographer.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
# Cartographer Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0135-M | DONE | Maintainability audit for StellaOps.Cartographer.Tests; revalidated 2026-01-06. |
| AUDIT-0135-T | DONE | Test coverage audit for StellaOps.Cartographer.Tests; revalidated 2026-01-06. |
| AUDIT-0135-A | DONE | Waived (test project; revalidated 2026-01-06). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |