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,151 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Scanner.Surface.FS;
public sealed class FileSurfaceCache : ISurfaceCache
{
private readonly string _root;
private readonly ILogger<FileSurfaceCache> _logger;
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new(StringComparer.Ordinal);
public FileSurfaceCache(
IOptions<SurfaceCacheOptions> options,
ILogger<FileSurfaceCache> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var root = options?.Value?.ResolveRoot();
if (string.IsNullOrWhiteSpace(root))
{
throw new ArgumentException("Surface cache root directory must be provided.", nameof(options));
}
_root = root!;
}
public async Task<T> GetOrCreateAsync<T>(
SurfaceCacheKey key,
Func<CancellationToken, Task<T>> factory,
Func<T, ReadOnlyMemory<byte>> serializer,
Func<ReadOnlyMemory<byte>, T> deserializer,
CancellationToken cancellationToken = default)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
cancellationToken.ThrowIfCancellationRequested();
var path = ResolvePath(key);
if (TryRead(path, deserializer, out var value))
{
_logger.LogTrace("Surface cache hit for {Key}.", key);
return value!;
}
var gate = _locks.GetOrAdd(path, _ => new SemaphoreSlim(1, 1));
await gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (TryRead(path, deserializer, out value))
{
_logger.LogTrace("Surface cache race recovered for {Key}.", key);
return value!;
}
value = await factory(cancellationToken).ConfigureAwait(false);
var payload = serializer(value);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllBytesAsync(path, payload.ToArray(), cancellationToken).ConfigureAwait(false);
return value;
}
finally
{
gate.Release();
}
}
public Task<T?> TryGetAsync<T>(
SurfaceCacheKey key,
Func<ReadOnlyMemory<byte>, T> deserializer,
CancellationToken cancellationToken = default)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
cancellationToken.ThrowIfCancellationRequested();
var path = ResolvePath(key);
return Task.FromResult(TryRead(path, deserializer, out var value) ? value : default);
}
public async Task SetAsync(
SurfaceCacheKey key,
ReadOnlyMemory<byte> payload,
CancellationToken cancellationToken = default)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
var path = ResolvePath(key);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllBytesAsync(path, payload.ToArray(), cancellationToken).ConfigureAwait(false);
}
private string ResolvePath(SurfaceCacheKey key)
{
var hash = ComputeHash(key.ContentKey);
var tenant = Sanitize(key.Tenant);
var ns = Sanitize(key.Namespace);
return Path.Combine(_root, ns, tenant, hash[..2], hash[2..4], $"{hash}.bin");
}
private static string ComputeHash(string input)
{
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static string Sanitize(string value)
=> string.IsNullOrWhiteSpace(value)
? "default"
: value.Replace('/', '_').Replace('\\', '_');
private static bool TryRead<T>(string path, Func<ReadOnlyMemory<byte>, T> deserializer, out T? value)
{
value = default;
if (!File.Exists(path))
{
return false;
}
try
{
var bytes = File.ReadAllBytes(path);
value = deserializer(bytes);
return true;
}
catch
{
try
{
File.Delete(path);
}
catch
{
// ignore
}
return false;
}
}
}

View File

@@ -0,0 +1,24 @@
namespace StellaOps.Scanner.Surface.FS;
/// <summary>
/// Provides content-addressable storage for surface artefacts.
/// </summary>
public interface ISurfaceCache
{
Task<T> GetOrCreateAsync<T>(
SurfaceCacheKey key,
Func<CancellationToken, Task<T>> factory,
Func<T, ReadOnlyMemory<byte>> serializer,
Func<ReadOnlyMemory<byte>, T> deserializer,
CancellationToken cancellationToken = default);
Task<T?> TryGetAsync<T>(
SurfaceCacheKey key,
Func<ReadOnlyMemory<byte>, T> deserializer,
CancellationToken cancellationToken = default);
Task SetAsync(
SurfaceCacheKey key,
ReadOnlyMemory<byte> payload,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,59 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace StellaOps.Scanner.Surface.FS;
public static class ServiceCollectionExtensions
{
private const string ConfigurationSection = "Surface:Cache";
public static IServiceCollection AddSurfaceFileCache(
this IServiceCollection services,
Action<SurfaceCacheOptions>? configure = null)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddOptions<SurfaceCacheOptions>()
.BindConfiguration(ConfigurationSection);
if (configure is not null)
{
services.Configure(configure);
}
services.TryAddSingleton<ISurfaceCache, FileSurfaceCache>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<SurfaceCacheOptions>, SurfaceCacheOptionsValidator>());
return services;
}
private sealed class SurfaceCacheOptionsValidator : IValidateOptions<SurfaceCacheOptions>
{
public ValidateOptionsResult Validate(string? name, SurfaceCacheOptions options)
{
if (options is null)
{
return ValidateOptionsResult.Fail("Options cannot be null.");
}
try
{
var root = options.ResolveRoot();
if (string.IsNullOrWhiteSpace(root))
{
return ValidateOptionsResult.Fail("Root directory cannot be empty.");
}
}
catch (Exception ex)
{
return ValidateOptionsResult.Fail($"Failed to resolve surface cache root: {ex.Message}");
}
return ValidateOptionsResult.Success;
}
}
}

View File

@@ -0,0 +1,26 @@
<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>
<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" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.7.25380.108" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,31 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Surface.FS;
internal static class SurfaceCacheJsonSerializer
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
static SurfaceCacheJsonSerializer()
{
Options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
}
public static ReadOnlyMemory<byte> Serialize<T>(T value)
{
return JsonSerializer.SerializeToUtf8Bytes(value, Options);
}
public static T Deserialize<T>(ReadOnlyMemory<byte> payload)
{
return JsonSerializer.Deserialize<T>(payload.Span, Options)!;
}
}

View File

@@ -0,0 +1,10 @@
namespace StellaOps.Scanner.Surface.FS;
/// <summary>
/// Identifies a cached artefact within the surface file store.
/// </summary>
public sealed record SurfaceCacheKey(string Namespace, string Tenant, string ContentKey)
{
public override string ToString()
=> $"{Namespace}/{Tenant}/{ContentKey}";
}

View File

@@ -0,0 +1,22 @@
namespace StellaOps.Scanner.Surface.FS;
/// <summary>
/// Configures the on-disk storage used by the surface cache.
/// </summary>
public sealed class SurfaceCacheOptions
{
/// <summary>
/// Root directory where cached payloads are stored. Defaults to a deterministic path under the temporary directory.
/// </summary>
public string? RootDirectory { get; set; }
internal string ResolveRoot()
{
if (!string.IsNullOrWhiteSpace(RootDirectory))
{
return RootDirectory!;
}
return Path.Combine(Path.GetTempPath(), "stellaops", "surface-cache");
}
}

View File

@@ -2,8 +2,8 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SURFACE-FS-01 | TODO | Scanner Guild, Zastava Guild | ARCH-SURFACE-EPIC | Author `docs/modules/scanner/design/surface-fs.md` defining cache layout, pointer schema, tenancy, and offline handling. | Spec merged; reviewers from Scanner/Zastava sign off; component map cross-link drafted. |
| SURFACE-FS-02 | TODO | Scanner Guild | SURFACE-FS-01 | Implement `StellaOps.Scanner.Surface.FS` core abstractions (writer, reader, manifest models) with deterministic serialization + unit tests. | Library compiles; tests pass; XML docs cover public types. |
| SURFACE-FS-01 | DOING (2025-11-02) | Scanner Guild, Zastava Guild | ARCH-SURFACE-EPIC | Author `docs/modules/scanner/design/surface-fs.md` defining cache layout, pointer schema, tenancy, and offline handling. | Spec merged; reviewers from Scanner/Zastava sign off; component map cross-link drafted. |
| SURFACE-FS-02 | DOING (2025-11-02) | Scanner Guild | SURFACE-FS-01 | Implement `StellaOps.Scanner.Surface.FS` core abstractions (writer, reader, manifest models) with deterministic serialization + unit tests. | Library compiles; tests pass; XML docs cover public types. |
| SURFACE-FS-03 | TODO | Scanner Guild | SURFACE-FS-02 | Integrate Surface.FS writer into Scanner Worker analyzer pipeline to persist layer + entry-trace fragments. | Worker produces cache entries in integration tests; observability counters emitted. |
| SURFACE-FS-04 | TODO | Zastava Guild | SURFACE-FS-02 | Integrate Surface.FS reader into Zastava Observer runtime drift loop. | Observer validates runtime artefacts via cache; regression tests updated. |
| SURFACE-FS-05 | TODO | Scanner Guild, Scheduler Guild | SURFACE-FS-03 | Expose Surface.FS pointers via Scanner WebService reports and coordinate rescan planning with Scheduler. | API contracts updated; Scheduler consumes pointers; docs refreshed. |