Rename Vexer to Excititor
This commit is contained in:
		
							
								
								
									
										23
									
								
								src/StellaOps.Excititor.Worker/AGENTS.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/StellaOps.Excititor.Worker/AGENTS.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # AGENTS | ||||
| ## Role | ||||
| Background processing host coordinating scheduled pulls, retries, reconciliation, verification, and cache maintenance for Excititor. | ||||
| ## Scope | ||||
| - Hosted service (Worker Service) wiring timers/queues for provider pulls and reconciliation cycles. | ||||
| - Resume token management, retry policies, and failure quarantines for connectors. | ||||
| - Re-verification of stored attestations and cache garbage collection routines. | ||||
| - Operational metrics and structured logging for offline-friendly monitoring. | ||||
| ## Participants | ||||
| - Triggered by WebService job requests or internal schedules to run connector pulls. | ||||
| - Collaborates with Storage.Mongo repositories and Attestation verification utilities. | ||||
| - Emits telemetry consumed by observability stack and CLI status queries. | ||||
| ## Interfaces & contracts | ||||
| - Scheduler abstractions, provider run controllers, retry/backoff strategies, and queue processors. | ||||
| - Hooks for policy revision changes and cache GC thresholds. | ||||
| ## In/Out of scope | ||||
| In: background orchestration, job lifecycle management, observability for worker operations. | ||||
| Out: HTTP endpoint definitions, domain modeling, connector-specific parsing logic. | ||||
| ## Observability & security expectations | ||||
| - Publish metrics for pull latency, failure counts, retry depth, cache size, and verification outcomes. | ||||
| - Log correlation IDs & provider IDs; avoid leaking secret config values. | ||||
| ## Tests | ||||
| - Worker orchestration tests, timer controls, and retry behavior will live in `../StellaOps.Excititor.Worker.Tests`. | ||||
							
								
								
									
										62
									
								
								src/StellaOps.Excititor.Worker/Options/VexWorkerOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/StellaOps.Excititor.Worker/Options/VexWorkerOptions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using System.Collections.Generic; | ||||
| using StellaOps.Excititor.Worker.Scheduling; | ||||
|  | ||||
| namespace StellaOps.Excititor.Worker.Options; | ||||
|  | ||||
| public sealed class VexWorkerOptions | ||||
| { | ||||
|     public TimeSpan DefaultInterval { get; set; } = TimeSpan.FromHours(1); | ||||
|  | ||||
|     public TimeSpan OfflineInterval { get; set; } = TimeSpan.FromHours(6); | ||||
|  | ||||
|     public TimeSpan DefaultInitialDelay { get; set; } = TimeSpan.FromMinutes(5); | ||||
|  | ||||
|     public bool OfflineMode { get; set; } | ||||
|  | ||||
|     public IList<VexWorkerProviderOptions> Providers { get; } = new List<VexWorkerProviderOptions>(); | ||||
|  | ||||
|     internal IReadOnlyList<VexWorkerSchedule> ResolveSchedules() | ||||
|     { | ||||
|         var schedules = new List<VexWorkerSchedule>(); | ||||
|         foreach (var provider in Providers) | ||||
|         { | ||||
|             if (!provider.Enabled) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var providerId = provider.ProviderId?.Trim(); | ||||
|             if (string.IsNullOrWhiteSpace(providerId)) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var interval = provider.Interval ?? (OfflineMode ? OfflineInterval : DefaultInterval); | ||||
|             if (interval <= TimeSpan.Zero) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var initialDelay = provider.InitialDelay ?? DefaultInitialDelay; | ||||
|             if (initialDelay < TimeSpan.Zero) | ||||
|             { | ||||
|                 initialDelay = TimeSpan.Zero; | ||||
|             } | ||||
|  | ||||
|             schedules.Add(new VexWorkerSchedule(providerId, interval, initialDelay)); | ||||
|         } | ||||
|  | ||||
|         return schedules; | ||||
|     } | ||||
| } | ||||
|  | ||||
| public sealed class VexWorkerProviderOptions | ||||
| { | ||||
|     public string ProviderId { get; set; } = string.Empty; | ||||
|  | ||||
|     public bool Enabled { get; set; } = true; | ||||
|  | ||||
|     public TimeSpan? Interval { get; set; } | ||||
|  | ||||
|     public TimeSpan? InitialDelay { get; set; } | ||||
| } | ||||
| @@ -0,0 +1,50 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.Extensions.Options; | ||||
|  | ||||
| namespace StellaOps.Excititor.Worker.Options; | ||||
|  | ||||
| internal sealed class VexWorkerOptionsValidator : IValidateOptions<VexWorkerOptions> | ||||
| { | ||||
|     public ValidateOptionsResult Validate(string? name, VexWorkerOptions options) | ||||
|     { | ||||
|         var failures = new List<string>(); | ||||
|  | ||||
|         if (options.DefaultInterval <= TimeSpan.Zero) | ||||
|         { | ||||
|             failures.Add("Excititor.Worker.DefaultInterval must be greater than zero."); | ||||
|         } | ||||
|  | ||||
|         if (options.OfflineInterval <= TimeSpan.Zero) | ||||
|         { | ||||
|             failures.Add("Excititor.Worker.OfflineInterval must be greater than zero."); | ||||
|         } | ||||
|  | ||||
|         if (options.DefaultInitialDelay < TimeSpan.Zero) | ||||
|         { | ||||
|             failures.Add("Excititor.Worker.DefaultInitialDelay cannot be negative."); | ||||
|         } | ||||
|  | ||||
|         for (var i = 0; i < options.Providers.Count; i++) | ||||
|         { | ||||
|             var provider = options.Providers[i]; | ||||
|             if (string.IsNullOrWhiteSpace(provider.ProviderId)) | ||||
|             { | ||||
|                 failures.Add($"Excititor.Worker.Providers[{i}].ProviderId must be set."); | ||||
|             } | ||||
|  | ||||
|             if (provider.Interval is { } interval && interval <= TimeSpan.Zero) | ||||
|             { | ||||
|                 failures.Add($"Excititor.Worker.Providers[{i}].Interval must be greater than zero when specified."); | ||||
|             } | ||||
|  | ||||
|             if (provider.InitialDelay is { } delay && delay < TimeSpan.Zero) | ||||
|             { | ||||
|                 failures.Add($"Excititor.Worker.Providers[{i}].InitialDelay cannot be negative."); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return failures.Count > 0 | ||||
|             ? ValidateOptionsResult.Fail(failures) | ||||
|             : ValidateOptionsResult.Success; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| using System; | ||||
| using System.IO; | ||||
|  | ||||
| namespace StellaOps.Excititor.Worker.Options; | ||||
|  | ||||
| public sealed class VexWorkerPluginOptions | ||||
| { | ||||
|     public string? Directory { get; set; } | ||||
|  | ||||
|     public string? SearchPattern { get; set; } | ||||
|  | ||||
|     internal string ResolveDirectory() | ||||
|         => string.IsNullOrWhiteSpace(Directory) | ||||
|             ? Path.Combine(AppContext.BaseDirectory, "plugins") | ||||
|             : Path.GetFullPath(Directory); | ||||
|  | ||||
|     internal string ResolveSearchPattern() | ||||
|         => string.IsNullOrWhiteSpace(SearchPattern) | ||||
|             ? "StellaOps.Excititor.Connectors.*.dll" | ||||
|             : SearchPattern!; | ||||
| } | ||||
							
								
								
									
										59
									
								
								src/StellaOps.Excititor.Worker/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/StellaOps.Excititor.Worker/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
| using StellaOps.Plugin; | ||||
| using StellaOps.Excititor.Connectors.RedHat.CSAF.DependencyInjection; | ||||
| using StellaOps.Excititor.Worker.Options; | ||||
| using StellaOps.Excititor.Worker.Scheduling; | ||||
|  | ||||
| var builder = Host.CreateApplicationBuilder(args); | ||||
| var services = builder.Services; | ||||
| var configuration = builder.Configuration; | ||||
| services.AddOptions<VexWorkerOptions>() | ||||
|     .Bind(configuration.GetSection("Excititor:Worker")) | ||||
|     .ValidateOnStart(); | ||||
|  | ||||
| services.Configure<VexWorkerPluginOptions>(configuration.GetSection("Excititor:Worker:Plugins")); | ||||
| services.AddRedHatCsafConnector(); | ||||
|  | ||||
| services.AddSingleton<IValidateOptions<VexWorkerOptions>, VexWorkerOptionsValidator>(); | ||||
| services.AddSingleton(TimeProvider.System); | ||||
| services.PostConfigure<VexWorkerOptions>(options => | ||||
| { | ||||
|     if (!options.Providers.Any(provider => string.Equals(provider.ProviderId, "excititor:redhat", StringComparison.OrdinalIgnoreCase))) | ||||
|     { | ||||
|         options.Providers.Add(new VexWorkerProviderOptions | ||||
|         { | ||||
|             ProviderId = "excititor:redhat", | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
| services.AddSingleton<PluginCatalog>(provider => | ||||
| { | ||||
|     var pluginOptions = provider.GetRequiredService<IOptions<VexWorkerPluginOptions>>().Value; | ||||
|     var catalog = new PluginCatalog(); | ||||
|  | ||||
|     var directory = pluginOptions.ResolveDirectory(); | ||||
|     if (Directory.Exists(directory)) | ||||
|     { | ||||
|         catalog.AddFromDirectory(directory, pluginOptions.ResolveSearchPattern()); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         var logger = provider.GetRequiredService<ILogger<Program>>(); | ||||
|         logger.LogWarning("Excititor worker plugin directory '{Directory}' does not exist; proceeding without external connectors.", directory); | ||||
|     } | ||||
|  | ||||
|     return catalog; | ||||
| }); | ||||
|  | ||||
| services.AddSingleton<IVexProviderRunner, DefaultVexProviderRunner>(); | ||||
| services.AddHostedService<VexWorkerHostedService>(); | ||||
|  | ||||
| var host = builder.Build(); | ||||
| await host.RunAsync(); | ||||
|  | ||||
| public partial class Program; | ||||
| @@ -0,0 +1,3 @@ | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| [assembly: InternalsVisibleTo("StellaOps.Excititor.Worker.Tests")] | ||||
| @@ -0,0 +1,47 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using StellaOps.Plugin; | ||||
|  | ||||
| namespace StellaOps.Excititor.Worker.Scheduling; | ||||
|  | ||||
| internal sealed class DefaultVexProviderRunner : IVexProviderRunner | ||||
| { | ||||
|     private readonly IServiceProvider _serviceProvider; | ||||
|     private readonly PluginCatalog _pluginCatalog; | ||||
|     private readonly ILogger<DefaultVexProviderRunner> _logger; | ||||
|  | ||||
|     public DefaultVexProviderRunner( | ||||
|         IServiceProvider serviceProvider, | ||||
|         PluginCatalog pluginCatalog, | ||||
|         ILogger<DefaultVexProviderRunner> logger) | ||||
|     { | ||||
|         _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); | ||||
|         _pluginCatalog = pluginCatalog ?? throw new ArgumentNullException(nameof(pluginCatalog)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|     } | ||||
|  | ||||
|     public ValueTask RunAsync(string providerId, CancellationToken cancellationToken) | ||||
|     { | ||||
|         ArgumentException.ThrowIfNullOrWhiteSpace(providerId); | ||||
|  | ||||
|         using var scope = _serviceProvider.CreateScope(); | ||||
|         var availablePlugins = _pluginCatalog.GetAvailableConnectorPlugins(scope.ServiceProvider); | ||||
|         var matched = availablePlugins.FirstOrDefault(plugin => | ||||
|             string.Equals(plugin.Name, providerId, StringComparison.OrdinalIgnoreCase)); | ||||
|  | ||||
|         if (matched is null) | ||||
|         { | ||||
|             _logger.LogInformation("No connector plugin registered for provider {ProviderId}; nothing to execute.", providerId); | ||||
|             return ValueTask.CompletedTask; | ||||
|         } | ||||
|  | ||||
|         _logger.LogInformation( | ||||
|             "Connector plugin {PluginName} ({ProviderId}) is available. Execution hooks will be added in subsequent tasks.", | ||||
|             matched.Name, | ||||
|             providerId); | ||||
|  | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| namespace StellaOps.Excititor.Worker.Scheduling; | ||||
|  | ||||
| internal interface IVexProviderRunner | ||||
| { | ||||
|     ValueTask RunAsync(string providerId, CancellationToken cancellationToken); | ||||
| } | ||||
| @@ -0,0 +1,110 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
| using StellaOps.Excititor.Worker.Options; | ||||
|  | ||||
| namespace StellaOps.Excititor.Worker.Scheduling; | ||||
|  | ||||
| internal sealed class VexWorkerHostedService : BackgroundService | ||||
| { | ||||
|     private readonly IOptions<VexWorkerOptions> _options; | ||||
|     private readonly IVexProviderRunner _runner; | ||||
|     private readonly ILogger<VexWorkerHostedService> _logger; | ||||
|     private readonly TimeProvider _timeProvider; | ||||
|  | ||||
|     public VexWorkerHostedService( | ||||
|         IOptions<VexWorkerOptions> options, | ||||
|         IVexProviderRunner runner, | ||||
|         ILogger<VexWorkerHostedService> logger, | ||||
|         TimeProvider timeProvider) | ||||
|     { | ||||
|         _options = options ?? throw new ArgumentNullException(nameof(options)); | ||||
|         _runner = runner ?? throw new ArgumentNullException(nameof(runner)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|         _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); | ||||
|     } | ||||
|  | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         var schedules = _options.Value.ResolveSchedules(); | ||||
|         if (schedules.Count == 0) | ||||
|         { | ||||
|             _logger.LogWarning("Excititor worker has no configured provider schedules; the service will remain idle."); | ||||
|             await Task.CompletedTask; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         _logger.LogInformation("Excititor worker starting with {ProviderCount} provider schedule(s).", schedules.Count); | ||||
|  | ||||
|         var tasks = new List<Task>(schedules.Count); | ||||
|         foreach (var schedule in schedules) | ||||
|         { | ||||
|             tasks.Add(RunScheduleAsync(schedule, stoppingToken)); | ||||
|         } | ||||
|  | ||||
|         await Task.WhenAll(tasks); | ||||
|     } | ||||
|  | ||||
|     private async Task RunScheduleAsync(VexWorkerSchedule schedule, CancellationToken cancellationToken) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (schedule.InitialDelay > TimeSpan.Zero) | ||||
|             { | ||||
|                 _logger.LogInformation( | ||||
|                     "Provider {ProviderId} initial delay of {InitialDelay} before first execution.", | ||||
|                     schedule.ProviderId, | ||||
|                     schedule.InitialDelay); | ||||
|  | ||||
|                 await Task.Delay(schedule.InitialDelay, cancellationToken).ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|             using var timer = new PeriodicTimer(schedule.Interval); | ||||
|             do | ||||
|             { | ||||
|                 var startedAt = _timeProvider.GetUtcNow(); | ||||
|                 _logger.LogInformation( | ||||
|                     "Provider {ProviderId} run started at {StartedAt}. Interval={Interval}.", | ||||
|                     schedule.ProviderId, | ||||
|                     startedAt, | ||||
|                     schedule.Interval); | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     await _runner.RunAsync(schedule.ProviderId, cancellationToken).ConfigureAwait(false); | ||||
|  | ||||
|                     var completedAt = _timeProvider.GetUtcNow(); | ||||
|                     var elapsed = completedAt - startedAt; | ||||
|  | ||||
|                     _logger.LogInformation( | ||||
|                         "Provider {ProviderId} run completed at {CompletedAt} (duration {Duration}).", | ||||
|                         schedule.ProviderId, | ||||
|                         completedAt, | ||||
|                         elapsed); | ||||
|                 } | ||||
|                 catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) | ||||
|                 { | ||||
|                     _logger.LogInformation("Provider {ProviderId} run cancelled.", schedule.ProviderId); | ||||
|                     break; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     _logger.LogError( | ||||
|                         ex, | ||||
|                         "Provider {ProviderId} run failed: {Message}", | ||||
|                         schedule.ProviderId, | ||||
|                         ex.Message); | ||||
|                 } | ||||
|             } | ||||
|             while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)); | ||||
|         } | ||||
|         catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) | ||||
|         { | ||||
|             _logger.LogInformation("Provider {ProviderId} schedule cancelled.", schedule.ProviderId); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| namespace StellaOps.Excititor.Worker.Scheduling; | ||||
|  | ||||
| internal sealed record VexWorkerSchedule(string ProviderId, TimeSpan Interval, TimeSpan InitialDelay); | ||||
| @@ -0,0 +1,19 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk.Worker"> | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net10.0</TargetFramework> | ||||
|     <LangVersion>preview</LangVersion> | ||||
|     <Nullable>enable</Nullable> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|     <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||||
|   </PropertyGroup> | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" /> | ||||
|     <ProjectReference Include="..\StellaOps.Excititor.Connectors.Abstractions\StellaOps.Excititor.Connectors.Abstractions.csproj" /> | ||||
|     <ProjectReference Include="..\StellaOps.Excititor.Connectors.RedHat.CSAF\StellaOps.Excititor.Connectors.RedHat.CSAF.csproj" /> | ||||
|     <ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" /> | ||||
|     <ProjectReference Include="..\StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
							
								
								
									
										8
									
								
								src/StellaOps.Excititor.Worker/TASKS.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/StellaOps.Excititor.Worker/TASKS.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md). | ||||
| # TASKS | ||||
| | Task | Owner(s) | Depends on | Notes | | ||||
| |---|---|---|---| | ||||
| |EXCITITOR-WORKER-01-001 – Worker host & scheduling|Team Excititor Worker|EXCITITOR-STORAGE-01-003, EXCITITOR-WEB-01-001|**DONE (2025-10-17)** – Worker project bootstraps provider schedules from configuration, integrates plugin catalog discovery, and emits structured logs/metrics-ready events via `VexWorkerHostedService`; scheduling logic covered by `VexWorkerOptionsTests`.| | ||||
| |EXCITITOR-WORKER-01-002 – Resume tokens & retry policy|Team Excititor Worker|EXCITITOR-WORKER-01-001|TODO – Implement durable resume markers, exponential backoff with jitter, and quarantine for failing connectors per architecture spec.| | ||||
| |EXCITITOR-WORKER-01-003 – Verification & cache GC loops|Team Excititor Worker|EXCITITOR-WORKER-01-001, EXCITITOR-ATTEST-01-003, EXCITITOR-EXPORT-01-002|TODO – Add scheduled attestation re-verification and cache pruning routines, surfacing metrics for export reuse ratios.| | ||||
| |EXCITITOR-WORKER-01-004 – TTL refresh & stability damper|Team Excititor Worker|EXCITITOR-WORKER-01-001, EXCITITOR-CORE-02-001|TODO – Monitor consensus/VEX TTLs, apply 24–48h dampers before flipping published status/score, and trigger re-resolve when base image or kernel fingerprints change.| | ||||
		Reference in New Issue
	
	Block a user