Rename Vexer to Excititor

This commit is contained in:
2025-10-18 20:00:46 +03:00
parent fbd1826ef3
commit 7e1b10d3b2
263 changed files with 848 additions and 848 deletions

View 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`.

View 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; }
}

View File

@@ -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;
}
}

View File

@@ -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!;
}

View 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;

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Excititor.Worker.Tests")]

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.Excititor.Worker.Scheduling;
internal interface IVexProviderRunner
{
ValueTask RunAsync(string providerId, CancellationToken cancellationToken);
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,3 @@
namespace StellaOps.Excititor.Worker.Scheduling;
internal sealed record VexWorkerSchedule(string ProviderId, TimeSpan Interval, TimeSpan InitialDelay);

View File

@@ -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>

View 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 2448h dampers before flipping published status/score, and trigger re-resolve when base image or kernel fingerprints change.|