Add Authority Advisory AI and API Lifecycle Configuration

- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings.
- Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations.
- Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration.
- Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options.
- Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations.
- Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client.
- Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
master
2025-11-02 13:40:38 +02:00
parent 66cb6c4b8a
commit f98cea3bcf
516 changed files with 68157 additions and 24754 deletions

View File

@@ -0,0 +1,9 @@
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Reports validation outcomes for observability purposes.
/// </summary>
public interface ISurfaceValidationReporter
{
void Report(SurfaceValidationContext context, SurfaceValidationResult result);
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Contract implemented by components that validate surface prerequisites.
/// </summary>
public interface ISurfaceValidator
{
ValueTask<SurfaceValidationResult> ValidateAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,16 @@
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Executes registered surface validators and aggregates their results.
/// </summary>
public interface ISurfaceValidatorRunner
{
ValueTask<SurfaceValidationResult> RunAllAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default);
ValueTask EnsureAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.Logging;
namespace StellaOps.Scanner.Surface.Validation;
internal sealed class LoggingSurfaceValidationReporter : ISurfaceValidationReporter
{
private readonly ILogger<LoggingSurfaceValidationReporter> _logger;
public LoggingSurfaceValidationReporter(ILogger<LoggingSurfaceValidationReporter> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void Report(SurfaceValidationContext context, SurfaceValidationResult result)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (result is null)
{
throw new ArgumentNullException(nameof(result));
}
if (result.IsSuccess)
{
_logger.LogInformation("Surface validation succeeded for component {Component}.", context.ComponentName);
return;
}
foreach (var issue in result.Issues)
{
var logLevel = issue.Severity switch
{
SurfaceValidationSeverity.Info => LogLevel.Information,
SurfaceValidationSeverity.Warning => LogLevel.Warning,
_ => LogLevel.Error
};
_logger.Log(logLevel,
"Surface validation issue for component {Component}: {Code} - {Message}. Hint: {Hint}",
context.ComponentName,
issue.Code,
issue.Message,
issue.Hint);
}
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Surface.Validation.Validators;
namespace StellaOps.Scanner.Surface.Validation;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSurfaceValidation(
this IServiceCollection services,
Action<SurfaceValidationBuilder>? configure = null)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAddSingleton<ISurfaceValidationReporter, LoggingSurfaceValidationReporter>();
services.TryAddSingleton<ISurfaceValidatorRunner, SurfaceValidatorRunner>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISurfaceValidator, SurfaceEndpointValidator>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISurfaceValidator, SurfaceCacheValidator>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISurfaceValidator, SurfaceSecretsValidator>());
services.TryAddSingleton<IConfigureOptions<SurfaceValidationOptions>, SurfaceValidationOptionsConfigurator>();
if (configure is not null)
{
var builder = new SurfaceValidationBuilder(services);
configure(builder);
}
return services;
}
private sealed class SurfaceValidationOptionsConfigurator : IConfigureOptions<SurfaceValidationOptions>
{
public void Configure(SurfaceValidationOptions options)
{
options ??= new SurfaceValidationOptions();
options.ThrowOnFailure = true;
options.ContinueOnError = false;
}
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" Exclude="obj\**;bin\**" />
<EmbeddedResource Include="**\*.json" Exclude="obj\**;bin\**" />
<None Include="**\*" Exclude="**\*.cs;**\*.json;bin\**;obj\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.Scanner.Surface.Validation;
public sealed class SurfaceValidationBuilder
{
private readonly IServiceCollection _services;
internal SurfaceValidationBuilder(IServiceCollection services)
{
_services = services;
}
public SurfaceValidationBuilder AddValidator<TValidator>()
where TValidator : class, ISurfaceValidator
{
_services.AddSingleton<ISurfaceValidator, TValidator>();
return this;
}
public SurfaceValidationBuilder AddValidator(Func<IServiceProvider, ISurfaceValidator> factory)
{
if (factory is null)
{
throw new ArgumentNullException(nameof(factory));
}
_services.AddSingleton(provider => factory(provider));
return this;
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using StellaOps.Scanner.Surface.Env;
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Context supplied to validation checks to describe the surface configuration.
/// </summary>
public sealed record SurfaceValidationContext(
IServiceProvider Services,
string ComponentName,
SurfaceEnvironmentSettings Environment,
IReadOnlyDictionary<string, object?> Properties)
{
public static SurfaceValidationContext Create(
IServiceProvider services,
string componentName,
SurfaceEnvironmentSettings environment,
IReadOnlyDictionary<string, object?>? properties = null)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
if (string.IsNullOrWhiteSpace(componentName))
{
throw new ArgumentException("Component name cannot be null or whitespace.", nameof(componentName));
}
if (environment is null)
{
throw new ArgumentNullException(nameof(environment));
}
return new SurfaceValidationContext(
services,
componentName,
environment,
properties ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase));
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Surface.Validation;
public sealed class SurfaceValidationException : Exception
{
public SurfaceValidationException(string message, IEnumerable<SurfaceValidationIssue> issues)
: base(message)
{
Issues = issues.ToImmutableArray();
}
public ImmutableArray<SurfaceValidationIssue> Issues { get; }
}

View File

@@ -0,0 +1,25 @@
using System.Diagnostics.CodeAnalysis;
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Represents a single validation finding produced by a surface validator.
/// </summary>
public sealed record SurfaceValidationIssue(
string Code,
string Message,
SurfaceValidationSeverity Severity,
string? Hint = null)
{
public static SurfaceValidationIssue Info(string code, string message, string? hint = null)
=> new(code, message, SurfaceValidationSeverity.Info, hint);
public static SurfaceValidationIssue Warning(string code, string message, string? hint = null)
=> new(code, message, SurfaceValidationSeverity.Warning, hint);
public static SurfaceValidationIssue Error(string code, string message, string? hint = null)
=> new(code, message, SurfaceValidationSeverity.Error, hint);
[MemberNotNullWhen(true, nameof(Hint))]
public bool HasHint => !string.IsNullOrWhiteSpace(Hint);
}

View File

@@ -0,0 +1,15 @@
namespace StellaOps.Scanner.Surface.Validation;
public static class SurfaceValidationIssueCodes
{
public const string SurfaceEndpointMissing = "SURFACE_ENV_MISSING_ENDPOINT";
public const string SurfaceEndpointInvalid = "SURFACE_ENV_ENDPOINT_INVALID";
public const string CacheDirectoryUnwritable = "SURFACE_ENV_CACHE_DIR_UNWRITABLE";
public const string CacheQuotaInvalid = "SURFACE_ENV_CACHE_QUOTA_INVALID";
public const string SecretsProviderUnknown = "SURFACE_SECRET_PROVIDER_UNKNOWN";
public const string SecretsConfigurationMissing = "SURFACE_SECRET_CONFIGURATION_MISSING";
public const string TenantMissing = "SURFACE_ENV_TENANT_MISSING";
public const string BucketMissing = "SURFACE_FS_BUCKET_MISSING";
public const string FeatureUnknown = "SURFACE_FEATURE_UNKNOWN";
public const string ValidatorException = "SURFACE_VALIDATOR_EXCEPTION";
}

View File

@@ -0,0 +1,18 @@
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Controls behaviour of the surface validation runner.
/// </summary>
public sealed class SurfaceValidationOptions
{
/// <summary>
/// Gets or sets a value indicating whether the runner should continue invoking validators after an error is recorded.
/// </summary>
public bool ContinueOnError { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the runner should throw a <see cref="SurfaceValidationException"/> when validation fails.
/// Defaults to <c>true</c> to align with fail-fast expectations.
/// </summary>
public bool ThrowOnFailure { get; set; } = true;
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Aggregate outcome emitted after running all registered validators.
/// </summary>
public sealed record SurfaceValidationResult
{
private SurfaceValidationResult(bool isSuccess, ImmutableArray<SurfaceValidationIssue> issues)
{
IsSuccess = isSuccess;
Issues = issues;
}
public bool IsSuccess { get; }
public ImmutableArray<SurfaceValidationIssue> Issues { get; }
public static SurfaceValidationResult Success()
=> new(true, ImmutableArray<SurfaceValidationIssue>.Empty);
public static SurfaceValidationResult FromIssues(IEnumerable<SurfaceValidationIssue> issues)
{
var immutable = issues.ToImmutableArray();
var success = immutable.All(issue => issue.Severity != SurfaceValidationSeverity.Error);
return new SurfaceValidationResult(success, immutable);
}
}

View File

@@ -0,0 +1,11 @@
namespace StellaOps.Scanner.Surface.Validation;
/// <summary>
/// Severity classification for surface validation issues.
/// </summary>
public enum SurfaceValidationSeverity
{
Info = 0,
Warning = 1,
Error = 2,
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Scanner.Surface.Validation;
internal sealed class SurfaceValidatorRunner : ISurfaceValidatorRunner
{
private readonly IReadOnlyList<ISurfaceValidator> _validators;
private readonly ILogger<SurfaceValidatorRunner> _logger;
private readonly ISurfaceValidationReporter _reporter;
private readonly SurfaceValidationOptions _options;
public SurfaceValidatorRunner(
IEnumerable<ISurfaceValidator> validators,
ILogger<SurfaceValidatorRunner> logger,
ISurfaceValidationReporter reporter,
IOptions<SurfaceValidationOptions> options)
{
_validators = validators?.ToArray() ?? Array.Empty<ISurfaceValidator>();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_reporter = reporter ?? throw new ArgumentNullException(nameof(reporter));
_options = options?.Value ?? new SurfaceValidationOptions();
}
public async ValueTask<SurfaceValidationResult> RunAllAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (_validators.Count == 0)
{
var success = SurfaceValidationResult.Success();
_reporter.Report(context, success);
return success;
}
var issues = new List<SurfaceValidationIssue>();
foreach (var validator in _validators)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var result = await validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
if (!result.IsSuccess)
{
issues.AddRange(result.Issues);
if (!_options.ContinueOnError && result.Issues.Any(issue => issue.Severity == SurfaceValidationSeverity.Error))
{
break;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Surface validator {Validator} threw an exception.", validator.GetType().FullName);
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.ValidatorException,
$"Validator '{validator.GetType().FullName}' threw an exception: {ex.Message}",
"Inspect logs for stack trace."));
if (!_options.ContinueOnError)
{
break;
}
}
}
var resultAggregate = issues.Count == 0
? SurfaceValidationResult.Success()
: SurfaceValidationResult.FromIssues(issues);
_reporter.Report(context, resultAggregate);
return resultAggregate;
}
public async ValueTask EnsureAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default)
{
var result = await RunAllAsync(context, cancellationToken).ConfigureAwait(false);
if (!result.IsSuccess && _options.ThrowOnFailure)
{
throw new SurfaceValidationException(
$"Surface validation failed for component '{context.ComponentName}'.",
result.Issues);
}
}
}

View File

@@ -2,7 +2,7 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SURFACE-VAL-01 | TODO | Scanner Guild, Security Guild | SURFACE-FS-01, SURFACE-ENV-01 | Define validation framework (design doc `surface-validation.md`) covering SOLID extension points and queryable checks for env/cache/secrets. | Spec merged; architecture sign-off from Scanner + Security; checklist of baseline validators established. |
| SURFACE-VAL-01 | DOING (2025-11-01) | Scanner Guild, Security Guild | SURFACE-FS-01, SURFACE-ENV-01 | Define validation framework (design doc `surface-validation.md`) covering SOLID extension points and queryable checks for env/cache/secrets. | Spec merged; architecture sign-off from Scanner + Security; checklist of baseline validators established. |
| SURFACE-VAL-02 | TODO | Scanner Guild | SURFACE-VAL-01, SURFACE-ENV-02, SURFACE-FS-02 | Implement base validation library (interfaces, check registry, default validators for env/cached manifests, secret refs) with unit tests. | Library published; validation registry supports DI; tests cover success/failure; XML docs added. |
| SURFACE-VAL-03 | TODO | Scanner Guild, Analyzer Guild | SURFACE-VAL-02 | Integrate validation pipeline into Scanner analyzers (Lang, EntryTrace, etc.) to ensure consistent checks before processing. | Analyzers call validation hooks; integration tests updated; performance baseline measured. |
| SURFACE-VAL-04 | TODO | Scanner Guild, Zastava Guild | SURFACE-VAL-02 | Expose validation helpers to Zastava and other runtime consumers (Observer/Webhook) for preflight checks. | Zastava uses shared validators; admission tests include validation failure scenarios. |

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using StellaOps.Scanner.Surface.Validation;
namespace StellaOps.Scanner.Surface.Validation.Validators;
internal sealed class SurfaceCacheValidator : ISurfaceValidator
{
public ValueTask<SurfaceValidationResult> ValidateAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var issues = new List<SurfaceValidationIssue>();
var directory = context.Environment.CacheRoot;
try
{
if (!directory.Exists)
{
directory.Create();
}
var testFile = Path.Combine(directory.FullName, ".validation");
using (File.Open(testFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
}
File.Delete(testFile);
}
catch (Exception ex)
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.CacheDirectoryUnwritable,
$"Surface cache directory '{directory.FullName}' is not writable: {ex.Message}",
"Ensure the cache directory exists and is writable by the process user."));
}
if (context.Environment.CacheQuotaMegabytes <= 0)
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.CacheQuotaInvalid,
"Surface cache quota must be greater than zero.",
"Set SCANNER_SURFACE_CACHE_QUOTA_MB to a positive value."));
}
return ValueTask.FromResult(issues.Count == 0
? SurfaceValidationResult.Success()
: SurfaceValidationResult.FromIssues(issues));
}
}

View File

@@ -0,0 +1,35 @@
using StellaOps.Scanner.Surface.Env;
namespace StellaOps.Scanner.Surface.Validation.Validators;
internal sealed class SurfaceEndpointValidator : ISurfaceValidator
{
public ValueTask<SurfaceValidationResult> ValidateAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var issues = new List<SurfaceValidationIssue>();
if (context.Environment.SurfaceFsEndpoint is null || string.Equals(context.Environment.SurfaceFsEndpoint.Host, "surface.invalid", StringComparison.Ordinal))
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.SurfaceEndpointMissing,
"Surface FS endpoint is missing or invalid.",
"Set SCANNER_SURFACE_FS_ENDPOINT to the RustFS/S3 endpoint."));
}
if (string.IsNullOrWhiteSpace(context.Environment.SurfaceFsBucket))
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.BucketMissing,
"Surface FS bucket must be provided.",
"Set SCANNER_SURFACE_FS_BUCKET"));
}
return ValueTask.FromResult(issues.Count == 0
? SurfaceValidationResult.Success()
: SurfaceValidationResult.FromIssues(issues));
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.Validation;
namespace StellaOps.Scanner.Surface.Validation.Validators;
internal sealed class SurfaceSecretsValidator : ISurfaceValidator
{
private static readonly HashSet<string> KnownProviders = new(StringComparer.OrdinalIgnoreCase)
{
"kubernetes",
"file",
"inline"
};
public ValueTask<SurfaceValidationResult> ValidateAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var issues = new List<SurfaceValidationIssue>();
var secrets = context.Environment.Secrets;
if (!KnownProviders.Contains(secrets.Provider))
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.SecretsProviderUnknown,
$"Surface secrets provider '{secrets.Provider}' is not recognised.",
"Set SCANNER_SURFACE_SECRETS_PROVIDER to 'kubernetes', 'file', or another supported provider."));
}
if (string.Equals(secrets.Provider, "kubernetes", StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrWhiteSpace(secrets.Namespace))
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.SecretsConfigurationMissing,
"Kubernetes secrets provider requires a namespace.",
"Set SCANNER_SURFACE_SECRETS_NAMESPACE to the target namespace."));
}
if (string.Equals(secrets.Provider, "file", StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrWhiteSpace(secrets.Root))
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.SecretsConfigurationMissing,
"File secrets provider requires a root directory.",
"Set SCANNER_SURFACE_SECRETS_ROOT to a directory path."));
}
if (string.IsNullOrWhiteSpace(secrets.Tenant))
{
issues.Add(SurfaceValidationIssue.Error(
SurfaceValidationIssueCodes.TenantMissing,
"Surface secrets tenant cannot be empty.",
"Set SCANNER_SURFACE_SECRETS_TENANT or ensure the tenant resolver provides a value."));
}
return ValueTask.FromResult(issues.Count == 0
? SurfaceValidationResult.Success()
: SurfaceValidationResult.FromIssues(issues));
}
}