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:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Scanner.Surface.Env.Tests")]
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
/// <summary>
|
||||
/// Provides resolved surface environment settings for a component.
|
||||
/// </summary>
|
||||
public interface ISurfaceEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the resolved settings for the current component.
|
||||
/// </summary>
|
||||
SurfaceEnvironmentSettings Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw environment variables and configuration values that were used while building the settings.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, string> RawVariables { get; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddSurfaceEnvironment(
|
||||
this IServiceCollection services,
|
||||
Action<SurfaceEnvironmentOptions>? configure = null)
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services.AddSingleton<ISurfaceEnvironment>(sp => SurfaceEnvironmentFactory.Create(sp, configure));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<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.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
internal sealed class SurfaceEnvironment : ISurfaceEnvironment
|
||||
{
|
||||
public SurfaceEnvironment(SurfaceEnvironmentSettings settings, IReadOnlyDictionary<string, string> raw)
|
||||
{
|
||||
Settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
RawVariables = raw ?? throw new ArgumentNullException(nameof(raw));
|
||||
}
|
||||
|
||||
public SurfaceEnvironmentSettings Settings { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> RawVariables { get; }
|
||||
}
|
||||
|
||||
internal static class SurfaceEnvironmentFactory
|
||||
{
|
||||
public static ISurfaceEnvironment Create(IServiceProvider services, Action<SurfaceEnvironmentOptions>? configure = null)
|
||||
{
|
||||
var options = new SurfaceEnvironmentOptions();
|
||||
configure?.Invoke(options);
|
||||
|
||||
if (options.Prefixes.Count == 0)
|
||||
{
|
||||
options.AddPrefix("SCANNER");
|
||||
}
|
||||
|
||||
var configuration = services.GetRequiredService<IConfiguration>();
|
||||
var logger = services.GetRequiredService<ILogger<SurfaceEnvironmentBuilder>>();
|
||||
|
||||
var builder = new SurfaceEnvironmentBuilder(services, configuration, logger, options);
|
||||
var settings = builder.Build();
|
||||
var raw = builder.GetRawVariables();
|
||||
return new SurfaceEnvironment(settings, raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <see cref="SurfaceEnvironmentSettings"/> instances from configuration sources.
|
||||
/// </summary>
|
||||
public sealed class SurfaceEnvironmentBuilder
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<SurfaceEnvironmentBuilder> _logger;
|
||||
private readonly SurfaceEnvironmentOptions _options;
|
||||
private readonly Dictionary<string, string> _raw = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public SurfaceEnvironmentBuilder(
|
||||
IServiceProvider services,
|
||||
IConfiguration configuration,
|
||||
ILogger<SurfaceEnvironmentBuilder> logger,
|
||||
SurfaceEnvironmentOptions options)
|
||||
{
|
||||
_services = services ?? throw new ArgumentNullException(nameof(services));
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
if (_options.Prefixes.Count == 0)
|
||||
{
|
||||
_options.AddPrefix("SCANNER");
|
||||
}
|
||||
}
|
||||
|
||||
public SurfaceEnvironmentSettings Build()
|
||||
{
|
||||
var endpoint = ResolveUri("SURFACE_FS_ENDPOINT", required: _options.RequireSurfaceEndpoint);
|
||||
var bucket = ResolveString("SURFACE_FS_BUCKET", "surface-cache", required: endpoint is not null);
|
||||
var region = ResolveOptionalString("SURFACE_FS_REGION");
|
||||
var cacheRoot = ResolveDirectory("SURFACE_CACHE_ROOT", new DirectoryInfo(Path.Combine(Path.GetTempPath(), "stellaops", "surface")));
|
||||
var cacheQuota = ResolveInt("SURFACE_CACHE_QUOTA_MB", 4096, min: 64, max: 262144);
|
||||
var prefetch = ResolveBool("SURFACE_PREFETCH_ENABLED", defaultValue: false);
|
||||
var featureFlags = ResolveFeatureFlags();
|
||||
var secrets = ResolveSecretsConfiguration();
|
||||
var tls = ResolveTlsConfiguration();
|
||||
var tenant = ResolveTenant() ?? "default";
|
||||
|
||||
var settings = new SurfaceEnvironmentSettings(
|
||||
endpoint ?? new Uri("https://surface.invalid"),
|
||||
bucket,
|
||||
region,
|
||||
cacheRoot,
|
||||
cacheQuota,
|
||||
prefetch,
|
||||
featureFlags,
|
||||
secrets,
|
||||
tenant,
|
||||
tls);
|
||||
|
||||
return settings with { CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, string> GetRawVariables()
|
||||
=> new Dictionary<string, string>(_raw, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private Uri? ResolveUri(string suffix, bool required)
|
||||
{
|
||||
var value = ResolveString(suffix, required: required);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
|
||||
{
|
||||
throw new SurfaceEnvironmentException($"Value '{value}' for {suffix} is not a valid absolute URI.", suffix);
|
||||
}
|
||||
|
||||
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("Surface environment endpoint {Endpoint} is not HTTPS.", uri);
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string ResolveString(string suffix, string? defaultValue = null, bool required = false)
|
||||
{
|
||||
var value = ResolveOptionalString(suffix);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value!;
|
||||
}
|
||||
|
||||
if (required && defaultValue is null)
|
||||
{
|
||||
throw new SurfaceEnvironmentException($"Required surface environment variable {FormatNames(suffix)} was not provided.", suffix);
|
||||
}
|
||||
|
||||
return defaultValue ?? string.Empty;
|
||||
}
|
||||
|
||||
private string? ResolveOptionalString(string suffix)
|
||||
{
|
||||
foreach (var name in EnumerateNames(suffix))
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(name);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
_raw[name] = value!;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
var configKey = BuildConfigurationKey(suffix);
|
||||
var configured = _configuration[configKey];
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
_raw[configKey] = configured!;
|
||||
return configured;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DirectoryInfo ResolveDirectory(string suffix, DirectoryInfo fallback)
|
||||
{
|
||||
var path = ResolveOptionalString(suffix) ?? fallback.FullName;
|
||||
var directory = new DirectoryInfo(path);
|
||||
if (!directory.Exists)
|
||||
{
|
||||
directory.Create();
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private int ResolveInt(string suffix, int defaultValue, int min, int max)
|
||||
{
|
||||
var value = ResolveOptionalString(suffix);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
throw new SurfaceEnvironmentException($"Value '{value}' for {suffix} is not a valid integer.", suffix);
|
||||
}
|
||||
|
||||
if (parsed < min || parsed > max)
|
||||
{
|
||||
throw new SurfaceEnvironmentException($"Value '{parsed}' for {suffix} must be between {min} and {max}.", suffix);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private bool ResolveBool(string suffix, bool defaultValue)
|
||||
{
|
||||
var value = ResolveOptionalString(suffix);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (!bool.TryParse(value, out var parsed))
|
||||
{
|
||||
throw new SurfaceEnvironmentException($"Value '{value}' for {suffix} is not a valid boolean.", suffix);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<string> ResolveFeatureFlags()
|
||||
{
|
||||
var rawFlags = ResolveOptionalString("SURFACE_FEATURES");
|
||||
if (string.IsNullOrWhiteSpace(rawFlags))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var flags = rawFlags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(flag => flag.ToLowerInvariant())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
foreach (var flag in flags)
|
||||
{
|
||||
if (_options.KnownFeatureFlags.Count > 0 && !_options.KnownFeatureFlags.Contains(flag))
|
||||
{
|
||||
_logger.LogWarning("Unknown surface feature flag '{Flag}' detected for component {Component}.", flag, _options.ComponentName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Surface environment feature flag detected: {Flag}.", flag);
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
private SurfaceSecretsConfiguration ResolveSecretsConfiguration()
|
||||
{
|
||||
var provider = ResolveString("SURFACE_SECRETS_PROVIDER", "kubernetes");
|
||||
var root = ResolveOptionalString("SURFACE_SECRETS_ROOT");
|
||||
var ns = ResolveOptionalString("SURFACE_SECRETS_NAMESPACE");
|
||||
var fallback = ResolveOptionalString("SURFACE_SECRETS_FALLBACK_PROVIDER");
|
||||
var allowInline = ResolveBool("SURFACE_SECRETS_ALLOW_INLINE", defaultValue: false);
|
||||
var tenant = ResolveOptionalString("SURFACE_SECRETS_TENANT") ?? ResolveTenant() ?? "default";
|
||||
|
||||
return new SurfaceSecretsConfiguration(provider, tenant, root, ns, fallback, allowInline);
|
||||
}
|
||||
|
||||
private SurfaceTlsConfiguration ResolveTlsConfiguration()
|
||||
{
|
||||
var certPath = ResolveOptionalString("SURFACE_TLS_CERT_PATH");
|
||||
var keyPath = ResolveOptionalString("SURFACE_TLS_KEY_PATH");
|
||||
|
||||
X509Certificate2Collection? certificates = null;
|
||||
if (!string.IsNullOrWhiteSpace(certPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(certPath))
|
||||
{
|
||||
throw new FileNotFoundException("TLS certificate path not found.", certPath);
|
||||
}
|
||||
|
||||
var certificate = X509CertificateLoader.LoadCertificateFromFile(certPath);
|
||||
certificates = new X509Certificate2Collection { certificate };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new SurfaceEnvironmentException($"Failed to load TLS certificate from '{certPath}': {ex.Message}", "SURFACE_TLS_CERT_PATH", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return new SurfaceTlsConfiguration(certPath, keyPath, certificates);
|
||||
}
|
||||
|
||||
private string? ResolveTenant()
|
||||
{
|
||||
var tenant = ResolveOptionalString("SURFACE_TENANT");
|
||||
if (!string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return tenant;
|
||||
}
|
||||
|
||||
if (_options.TenantResolver is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
tenant = _options.TenantResolver(_services);
|
||||
if (!string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Tenant resolver for component {Component} threw an exception.", _options.ComponentName);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerable<string> EnumerateNames(string suffix)
|
||||
{
|
||||
foreach (var prefix in _options.Prefixes)
|
||||
{
|
||||
yield return $"{prefix}_{suffix}";
|
||||
}
|
||||
|
||||
yield return suffix;
|
||||
}
|
||||
|
||||
private string BuildConfigurationKey(string suffix)
|
||||
{
|
||||
var withoutPrefix = suffix.StartsWith("SURFACE_", StringComparison.OrdinalIgnoreCase)
|
||||
? suffix[8..]
|
||||
: suffix;
|
||||
|
||||
return $"Surface:{withoutPrefix.Replace('_', ':')}";
|
||||
}
|
||||
|
||||
private string FormatNames(string suffix)
|
||||
=> string.Join(", ", EnumerateNames(suffix));
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
public sealed class SurfaceEnvironmentException : Exception
|
||||
{
|
||||
public SurfaceEnvironmentException(string message, string variable)
|
||||
: base(message)
|
||||
{
|
||||
Variable = variable;
|
||||
}
|
||||
|
||||
public SurfaceEnvironmentException(string message, string variable, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Variable = variable;
|
||||
}
|
||||
|
||||
public string Variable { get; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
/// <summary>
|
||||
/// Options controlling how the surface environment is resolved.
|
||||
/// </summary>
|
||||
public sealed class SurfaceEnvironmentOptions
|
||||
{
|
||||
private readonly List<string> _prefixes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logical component name (e.g. "Scanner.Worker", "Zastava.Observer").
|
||||
/// </summary>
|
||||
public string ComponentName { get; set; } = "Scanner.Worker";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ordered list of environment variable prefixes that will be probed when resolving configuration values.
|
||||
/// The prefixes are evaluated in order; the first match wins.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Prefixes => _prefixes;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a prefix to the ordered prefix list.
|
||||
/// </summary>
|
||||
public void AddPrefix(string prefix)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefix))
|
||||
{
|
||||
throw new ArgumentException("Prefix cannot be null or whitespace.", nameof(prefix));
|
||||
}
|
||||
|
||||
if (!_prefixes.Contains(prefix, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_prefixes.Add(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When set to <c>true</c>, a missing Surface FS endpoint raises an exception.
|
||||
/// </summary>
|
||||
public bool RequireSurfaceEndpoint { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional delegate used to resolve the tenant when not explicitly provided via environment variables.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, string?>? TenantResolver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the set of recognised feature flags. Unknown flags produce validation warnings.
|
||||
/// </summary>
|
||||
public ISet<string> KnownFeatureFlags { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the resolved surface environment configuration for a component.
|
||||
/// </summary>
|
||||
public sealed record SurfaceEnvironmentSettings(
|
||||
Uri SurfaceFsEndpoint,
|
||||
string SurfaceFsBucket,
|
||||
string? SurfaceFsRegion,
|
||||
DirectoryInfo CacheRoot,
|
||||
int CacheQuotaMegabytes,
|
||||
bool PrefetchEnabled,
|
||||
IReadOnlyCollection<string> FeatureFlags,
|
||||
SurfaceSecretsConfiguration Secrets,
|
||||
string Tenant,
|
||||
SurfaceTlsConfiguration Tls)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the timestamp (UTC) when the configuration snapshot was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
/// <summary>
|
||||
/// Represents secret provider configuration resolved for the current component.
|
||||
/// </summary>
|
||||
public sealed record SurfaceSecretsConfiguration(
|
||||
string Provider,
|
||||
string Tenant,
|
||||
string? Root,
|
||||
string? Namespace,
|
||||
string? FallbackProvider,
|
||||
bool AllowInline)
|
||||
{
|
||||
public bool HasFallback => !string.IsNullOrWhiteSpace(FallbackProvider);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Env;
|
||||
|
||||
/// <summary>
|
||||
/// TLS configuration associated with the surface endpoints.
|
||||
/// </summary>
|
||||
public sealed record SurfaceTlsConfiguration(
|
||||
string? CertificatePath,
|
||||
string? PrivateKeyPath,
|
||||
X509Certificate2Collection? ClientCertificates)
|
||||
{
|
||||
public bool HasClientCertificates => ClientCertificates is { Count: > 0 };
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SURFACE-ENV-01 | TODO | Scanner Guild, Zastava Guild | ARCH-SURFACE-EPIC | Draft `docs/modules/scanner/design/surface-env.md` enumerating environment variables, defaults, and air-gap behaviour. | Spec merged; env matrix reviewed by Ops + Security. |
|
||||
| SURFACE-ENV-02 | TODO | Scanner Guild | SURFACE-ENV-01 | Implement strongly-typed env accessors in `StellaOps.Scanner.Surface.Env` with validation and deterministic logging. | Library published; unit tests cover parsing, fallbacks, and error paths. |
|
||||
| SURFACE-ENV-01 | DOING (2025-11-01) | Scanner Guild, Zastava Guild | ARCH-SURFACE-EPIC | Draft `docs/modules/scanner/design/surface-env.md` enumerating environment variables, defaults, and air-gap behaviour. | Spec merged; env matrix reviewed by Ops + Security. |
|
||||
| SURFACE-ENV-02 | DOING (2025-11-02) | Scanner Guild | SURFACE-ENV-01 | Implement strongly-typed env accessors in `StellaOps.Scanner.Surface.Env` with validation and deterministic logging. | Library published; unit tests cover parsing, fallbacks, and error paths. |
|
||||
| SURFACE-ENV-03 | TODO | Scanner Guild | SURFACE-ENV-02 | Adopt env helper across Scanner Worker/WebService/BuildX plug-ins. | Services use helper; manifests updated; smoke tests green. |
|
||||
| SURFACE-ENV-04 | TODO | Zastava Guild | SURFACE-ENV-02 | Wire env helper into Zastava Observer/Webhook containers. | Zastava builds reference env helper; admission tests validated. |
|
||||
| SURFACE-ENV-05 | TODO | Ops Guild | SURFACE-ENV-03..04 | Update Helm/Compose/offline kit templates with new env knobs and documentation. | Templates merged; docs include configuration table; air-gap scripts updated. |
|
||||
|
||||
Reference in New Issue
Block a user