using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http.Headers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using StellaOps.AdvisoryAI.Caching; using StellaOps.AdvisoryAI.Chat; using StellaOps.AdvisoryAI.DependencyInjection; using StellaOps.AdvisoryAI.Explanation; using StellaOps.AdvisoryAI.Inference; using StellaOps.AdvisoryAI.Metrics; using StellaOps.AdvisoryAI.Guardrails; using StellaOps.AdvisoryAI.Outputs; using StellaOps.AdvisoryAI.PolicyStudio; using StellaOps.AdvisoryAI.Providers; using StellaOps.AdvisoryAI.Queue; using StellaOps.AdvisoryAI.Remediation; using StellaOps.OpsMemory.Storage; namespace StellaOps.AdvisoryAI.Hosting; public static class ServiceCollectionExtensions { public static IServiceCollection AddAdvisoryAiCore( this IServiceCollection services, IConfiguration configuration, Action? configure = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); services.AddOptions() .Bind(configuration.GetSection("AdvisoryAI")) .PostConfigure(options => { configure?.Invoke(options); AdvisoryAiServiceOptionsValidator.Validate(options); }) .ValidateOnStart(); services.AddOptions() .Configure>((target, source) => { var advisoryOptions = source.Value; target.BaseAddress = advisoryOptions.SbomBaseAddress; target.Tenant = advisoryOptions.SbomTenant; target.TenantHeaderName = advisoryOptions.SbomTenantHeaderName; }) .Validate(opt => opt.BaseAddress is null || opt.BaseAddress.IsAbsoluteUri, "SBOM base address must be absolute when provided."); services.AddOptions() .Configure>((target, source) => { var inference = source.Value.Inference ?? new AdvisoryAiInferenceOptions(); target.Mode = inference.Mode; target.Remote = inference.Remote ?? new AdvisoryAiRemoteInferenceOptions(); }); services.AddHttpClient((provider, client) => { var inference = provider.GetRequiredService>().Value ?? new AdvisoryAiInferenceOptions(); var remote = inference.Remote ?? new AdvisoryAiRemoteInferenceOptions(); if (remote.BaseAddress is not null) { client.BaseAddress = remote.BaseAddress; } if (remote.Timeout > TimeSpan.Zero) { client.Timeout = remote.Timeout; } if (!string.IsNullOrWhiteSpace(remote.ApiKey)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", remote.ApiKey); } }); services.TryAddSingleton(); services.TryAddSingleton(); services.AddSingleton(provider => { var inference = provider.GetRequiredService>().Value ?? new AdvisoryAiInferenceOptions(); return inference.Mode == AdvisoryAiInferenceMode.Remote ? provider.GetRequiredService() : provider.GetRequiredService(); }); services.AddSbomContext(); services.AddAdvisoryPipeline(); services.AddAdvisoryPipelineInfrastructure(); services.AddOptions() .Configure, IHostEnvironment>((options, aiOptions, environment) => { ApplyGuardrailConfiguration(options, aiOptions.Value.Guardrails, environment); }); // Register deterministic providers (allow test injection) services.TryAddSingleton(SystemGuidProvider.Instance); services.TryAddSingleton( StellaOps.Determinism.SystemGuidProvider.Instance); services.TryAddSingleton(TimeProvider.System); services.Replace(ServiceDescriptor.Singleton()); services.Replace(ServiceDescriptor.Singleton()); services.Replace(ServiceDescriptor.Singleton()); services.TryAddSingleton(); // Explanation services (SPRINT_20251226_015_AI_zastava_companion) services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); // Remediation services (SPRINT_20251226_016_AI_remedy_autopilot) services.TryAddSingleton(); // Policy studio services (SPRINT_20251226_017_AI_policy_copilot) services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); // Chat services (SPRINT_20260107_006_003 CH-005) services.AddOptions() .Bind(configuration.GetSection("AdvisoryAI:Chat")) .ValidateOnStart(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); // Action policy gate and audit defaults (SPRINT_20260109_011_004_BE) services.AddDefaultActionPolicyIntegration(); // Object link resolvers (SPRINT_20260109_011_002 OMCI-005) services.TryAddSingleton(); services.TryAddSingleton(); return services; } private static void ApplyGuardrailConfiguration( AdvisoryGuardrailOptions target, AdvisoryAiGuardrailOptions? source, IHostEnvironment? environment) { if (source is null) { return; } if (source.MaxPromptLength.HasValue && source.MaxPromptLength.Value > 0) { target.MaxPromptLength = source.MaxPromptLength.Value; } target.RequireCitations = source.RequireCitations; if (source.EntropyThreshold.HasValue && source.EntropyThreshold.Value >= 0) { target.EntropyThreshold = source.EntropyThreshold.Value; } if (source.EntropyMinLength.HasValue && source.EntropyMinLength.Value >= 0) { target.EntropyMinLength = source.EntropyMinLength.Value; } var defaults = target.BlockedPhrases.ToList(); var merged = new SortedSet(defaults, StringComparer.OrdinalIgnoreCase); if (source.BlockedPhrases is { Count: > 0 }) { foreach (var phrase in source.BlockedPhrases) { if (!string.IsNullOrWhiteSpace(phrase)) { merged.Add(phrase.Trim()); } } } if (!string.IsNullOrWhiteSpace(source.BlockedPhraseFile)) { var resolvedPath = ResolveGuardrailPath(source.BlockedPhraseFile!, environment); foreach (var phrase in GuardrailPhraseLoader.Load(resolvedPath)) { if (!string.IsNullOrWhiteSpace(phrase)) { merged.Add(phrase.Trim()); } } } if (merged.Count > 0) { target.BlockedPhrases.Clear(); foreach (var phrase in merged) { target.BlockedPhrases.Add(phrase); } } var allowlistDefaults = target.AllowlistPatterns.ToList(); var allowlist = new SortedSet(allowlistDefaults, StringComparer.OrdinalIgnoreCase); if (source.AllowlistPatterns is { Count: > 0 }) { foreach (var pattern in source.AllowlistPatterns) { if (!string.IsNullOrWhiteSpace(pattern)) { allowlist.Add(pattern.Trim()); } } } if (!string.IsNullOrWhiteSpace(source.AllowlistFile)) { var resolvedPath = ResolveGuardrailPath(source.AllowlistFile!, environment); foreach (var pattern in GuardrailAllowlistLoader.Load(resolvedPath)) { if (!string.IsNullOrWhiteSpace(pattern)) { allowlist.Add(pattern.Trim()); } } } if (allowlist.Count > 0) { target.AllowlistPatterns.Clear(); foreach (var pattern in allowlist) { target.AllowlistPatterns.Add(pattern); } } } private static string ResolveGuardrailPath(string configuredPath, IHostEnvironment? environment) { var trimmed = configuredPath.Trim(); if (Path.IsPathRooted(trimmed)) { if (!File.Exists(trimmed)) { throw new FileNotFoundException($"Guardrail phrase file {trimmed} was not found.", trimmed); } return trimmed; } var root = environment?.ContentRootPath; if (string.IsNullOrWhiteSpace(root)) { root = AppContext.BaseDirectory; } var resolved = Path.GetFullPath(Path.Combine(root!, trimmed)); if (!File.Exists(resolved)) { throw new FileNotFoundException($"Guardrail phrase file {resolved} was not found.", resolved); } return resolved; } }