Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Plugin.Manifest;
/// <summary>
/// Unified plugin manifest schema (v2.0).
/// All modules use this schema for plugin.json files.
/// </summary>
public sealed record PluginManifest
{
/// <summary>
/// Schema version. Current version is "2.0".
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "2.0";
/// <summary>
/// Unique plugin identifier in format: stellaops.{module}.{plugin}
/// Examples: stellaops.router.tcp, stellaops.scanner.lang.dotnet
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Human-readable display name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Plugin version (SemVer).
/// </summary>
[JsonPropertyName("version")]
public string Version { get; init; } = "1.0.0";
/// <summary>
/// Assembly descriptor for loading the plugin.
/// </summary>
[JsonPropertyName("assembly")]
public required PluginAssemblyDescriptor Assembly { get; init; }
/// <summary>
/// Entry point type for plugin initialization (optional).
/// If not specified, the loader will scan for plugin interfaces.
/// </summary>
[JsonPropertyName("type")]
public string? Type { get; init; }
/// <summary>
/// Plugin capabilities (e.g., "signing:ES256", "transport:tcp", "analyzer:dotnet").
/// </summary>
[JsonPropertyName("capabilities")]
public IReadOnlyList<string> Capabilities { get; init; } = [];
/// <summary>
/// Supported platforms (e.g., "linux-x64", "win-x64", "osx-arm64").
/// Empty list means all platforms.
/// </summary>
[JsonPropertyName("platforms")]
public IReadOnlyList<string> Platforms { get; init; } = [];
/// <summary>
/// Compliance standards (e.g., "NIST", "FIPS-140-3", "GOST", "eIDAS").
/// </summary>
[JsonPropertyName("compliance")]
public IReadOnlyList<string> Compliance { get; init; } = [];
/// <summary>
/// Jurisdiction restriction (e.g., "world", "russia", "china", "eu", "korea").
/// </summary>
[JsonPropertyName("jurisdiction")]
public string? Jurisdiction { get; init; }
/// <summary>
/// Loading priority (0-100). Higher priority plugins are loaded first.
/// Default is 100 for user plugins, 50 for built-in plugins.
/// </summary>
[JsonPropertyName("priority")]
public int Priority { get; init; } = 100;
/// <summary>
/// Whether the plugin is enabled by default.
/// </summary>
[JsonPropertyName("enabled")]
public bool Enabled { get; init; } = true;
/// <summary>
/// Whether the plugin is enabled by default in new installations.
/// </summary>
[JsonPropertyName("enabledByDefault")]
public bool EnabledByDefault { get; init; } = false;
/// <summary>
/// Plugin-specific configuration options.
/// </summary>
[JsonPropertyName("options")]
public IReadOnlyDictionary<string, object?> Options { get; init; } = new Dictionary<string, object?>();
/// <summary>
/// Additional metadata (e.g., author, license, homepage).
/// </summary>
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, string> Metadata { get; init; } = new Dictionary<string, string>();
/// <summary>
/// Plugin dependencies (other plugin IDs).
/// </summary>
[JsonPropertyName("dependencies")]
public IReadOnlyList<string> Dependencies { get; init; } = [];
/// <summary>
/// Conditional compilation symbol required to build this plugin.
/// </summary>
[JsonPropertyName("conditionalCompilation")]
public string? ConditionalCompilation { get; init; }
}
/// <summary>
/// Descriptor for the plugin assembly location.
/// </summary>
public sealed record PluginAssemblyDescriptor
{
/// <summary>
/// Relative path to the assembly DLL from the plugin directory.
/// </summary>
[JsonPropertyName("path")]
public required string Path { get; init; }
/// <summary>
/// Optional SHA256 hash of the assembly for integrity verification.
/// </summary>
[JsonPropertyName("sha256")]
public string? Sha256 { get; init; }
/// <summary>
/// Optional signature file path (.sig) for cosign verification.
/// </summary>
[JsonPropertyName("signaturePath")]
public string? SignaturePath { get; init; }
}
/// <summary>
/// Runtime configuration for a plugin loaded from config.yaml.
/// </summary>
public sealed record PluginRuntimeConfig
{
/// <summary>
/// Plugin ID (must match manifest ID).
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Display name override (optional).
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>
/// Whether the plugin is enabled.
/// </summary>
[JsonPropertyName("enabled")]
public bool Enabled { get; init; } = true;
/// <summary>
/// Priority override (optional).
/// </summary>
[JsonPropertyName("priority")]
public int? Priority { get; init; }
/// <summary>
/// Plugin-specific runtime configuration.
/// Supports environment variable substitution: ${VAR:-default}
/// </summary>
[JsonPropertyName("config")]
public IReadOnlyDictionary<string, object?> Config { get; init; } = new Dictionary<string, object?>();
}
/// <summary>
/// Loaded plugin combining manifest, config, and assembly.
/// </summary>
public sealed class LoadedPlugin
{
/// <summary>
/// Creates a new loaded plugin instance.
/// </summary>
public LoadedPlugin(
PluginManifest manifest,
PluginRuntimeConfig? config,
Hosting.PluginAssembly? assembly)
{
Manifest = manifest ?? throw new ArgumentNullException(nameof(manifest));
Config = config;
Assembly = assembly;
}
/// <summary>
/// The plugin manifest from plugin.json.
/// </summary>
public PluginManifest Manifest { get; }
/// <summary>
/// The runtime configuration from config.yaml (may be null).
/// </summary>
public PluginRuntimeConfig? Config { get; }
/// <summary>
/// The loaded assembly (null if not yet loaded or load failed).
/// </summary>
public Hosting.PluginAssembly? Assembly { get; internal set; }
/// <summary>
/// Whether the plugin is effectively enabled (manifest + config).
/// </summary>
public bool IsEnabled => Config?.Enabled ?? Manifest.Enabled;
/// <summary>
/// Effective priority (config override or manifest default).
/// </summary>
public int EffectivePriority => Config?.Priority ?? Manifest.Priority;
/// <summary>
/// Plugin ID from manifest.
/// </summary>
public string Id => Manifest.Id;
/// <summary>
/// Effective display name (config override or manifest default).
/// </summary>
public string Name => Config?.Name ?? Manifest.Name;
}

View File

@@ -0,0 +1,524 @@
using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace StellaOps.Plugin.Manifest;
/// <summary>
/// Unified plugin loader that discovers plugins from manifest files.
/// Replaces module-specific loaders with a standardized approach.
/// </summary>
public sealed class PluginManifestLoader
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
private readonly ILogger? _logger;
/// <summary>
/// Creates a new plugin manifest loader.
/// </summary>
public PluginManifestLoader(ILogger? logger = null)
{
_logger = logger;
}
/// <summary>
/// Loads plugins from the specified configuration directory.
/// </summary>
/// <param name="options">Loading options including base directory and filters.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Registry result with loaded plugins and any errors.</returns>
public async Task<PluginRegistryResult> LoadAsync(
PluginRegistryOptions options,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(options);
var configDir = options.BaseDirectory;
var registryPath = Path.Combine(configDir, "registry.yaml");
var errors = new List<PluginLoadError>();
var loadedPlugins = new List<LoadedPlugin>();
// Load registry.yaml if it exists
PluginRegistry? registry = null;
if (File.Exists(registryPath))
{
try
{
var yaml = await File.ReadAllTextAsync(registryPath, cancellationToken);
registry = YamlDeserializer.Deserialize<PluginRegistry>(yaml);
_logger?.LogDebug("Loaded plugin registry from {Path}", registryPath);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to load plugin registry from {Path}", registryPath);
return new PluginRegistryResult
{
RegistryPath = registryPath,
ErrorMessage = $"Failed to load registry: {ex.Message}"
};
}
}
else
{
_logger?.LogDebug("No registry.yaml found at {Path}, will discover plugins from subdirectories", registryPath);
}
// Discover plugin directories
var pluginDirectories = DiscoverPluginDirectories(configDir);
_logger?.LogDebug("Discovered {Count} plugin directories in {Directory}", pluginDirectories.Count, configDir);
// Load each plugin
foreach (var pluginDir in pluginDirectories)
{
cancellationToken.ThrowIfCancellationRequested();
var pluginId = Path.GetFileName(pluginDir);
var manifestPath = Path.Combine(pluginDir, "plugin.json");
// Load manifest
PluginManifest? manifest;
try
{
manifest = await LoadManifestAsync(manifestPath, cancellationToken);
if (manifest is null)
{
errors.Add(new PluginLoadError
{
PluginId = pluginId,
Category = PluginLoadErrorCategory.ManifestNotFound,
Message = $"Manifest not found at {manifestPath}"
});
continue;
}
}
catch (Exception ex)
{
errors.Add(new PluginLoadError
{
PluginId = pluginId,
Category = PluginLoadErrorCategory.ManifestInvalid,
Message = $"Failed to load manifest: {ex.Message}",
Exception = ex
});
continue;
}
// Check platform filter
if (!IsPlatformSupported(manifest, options.Platform))
{
_logger?.LogDebug("Plugin {PluginId} skipped: platform mismatch", manifest.Id);
errors.Add(new PluginLoadError
{
PluginId = manifest.Id,
Category = PluginLoadErrorCategory.PlatformMismatch,
Message = $"Plugin does not support platform {options.Platform}"
});
continue;
}
// Check jurisdiction filter
if (!IsJurisdictionSupported(manifest, options.Jurisdiction))
{
_logger?.LogDebug("Plugin {PluginId} skipped: jurisdiction mismatch", manifest.Id);
errors.Add(new PluginLoadError
{
PluginId = manifest.Id,
Category = PluginLoadErrorCategory.JurisdictionMismatch,
Message = $"Plugin jurisdiction '{manifest.Jurisdiction}' does not match required '{options.Jurisdiction}'"
});
continue;
}
// Check compliance filter
if (!IsComplianceSupported(manifest, options.RequiredCompliance))
{
_logger?.LogDebug("Plugin {PluginId} skipped: compliance mismatch", manifest.Id);
errors.Add(new PluginLoadError
{
PluginId = manifest.Id,
Category = PluginLoadErrorCategory.ComplianceMismatch,
Message = "Plugin does not meet required compliance standards"
});
continue;
}
// Load runtime config
PluginRuntimeConfig? config = null;
var configPath = Path.Combine(pluginDir, "config.yaml");
if (File.Exists(configPath))
{
try
{
config = await LoadConfigAsync(configPath, cancellationToken);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to load config for plugin {PluginId}", manifest.Id);
errors.Add(new PluginLoadError
{
PluginId = manifest.Id,
Category = PluginLoadErrorCategory.ConfigInvalid,
Message = $"Failed to load config: {ex.Message}",
Exception = ex
});
continue;
}
}
// Apply registry overrides
config = ApplyRegistryOverrides(manifest.Id, config, registry);
var loadedPlugin = new LoadedPlugin(manifest, config, null);
// Skip disabled plugins
if (!loadedPlugin.IsEnabled)
{
_logger?.LogDebug("Plugin {PluginId} is disabled, skipping assembly load", manifest.Id);
loadedPlugins.Add(loadedPlugin);
continue;
}
// Load assembly if requested
if (options.LoadAssemblies)
{
try
{
var assembly = await LoadAssemblyAsync(manifest, pluginDir, options, cancellationToken);
loadedPlugin.Assembly = assembly;
_logger?.LogInformation("Loaded plugin {PluginId} from {Assembly}", manifest.Id, manifest.Assembly.Path);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to load assembly for plugin {PluginId}", manifest.Id);
errors.Add(new PluginLoadError
{
PluginId = manifest.Id,
Category = PluginLoadErrorCategory.AssemblyLoadFailed,
Message = $"Failed to load assembly: {ex.Message}",
Exception = ex
});
}
}
loadedPlugins.Add(loadedPlugin);
}
// Sort by priority (higher first)
loadedPlugins.Sort((a, b) => b.EffectivePriority.CompareTo(a.EffectivePriority));
return new PluginRegistryResult
{
Registry = registry,
RegistryPath = registryPath,
PluginDirectories = pluginDirectories,
LoadedPlugins = loadedPlugins,
Errors = errors
};
}
/// <summary>
/// Loads a single plugin manifest from a JSON file.
/// </summary>
public async Task<PluginManifest?> LoadManifestAsync(string path, CancellationToken cancellationToken = default)
{
if (!File.Exists(path))
{
return null;
}
var json = await File.ReadAllTextAsync(path, cancellationToken);
return JsonSerializer.Deserialize<PluginManifest>(json, JsonOptions);
}
/// <summary>
/// Loads a plugin runtime configuration from a YAML file.
/// </summary>
public async Task<PluginRuntimeConfig?> LoadConfigAsync(string path, CancellationToken cancellationToken = default)
{
if (!File.Exists(path))
{
return null;
}
var yaml = await File.ReadAllTextAsync(path, cancellationToken);
var expanded = ExpandEnvironmentVariables(yaml);
return YamlDeserializer.Deserialize<PluginRuntimeConfig>(expanded);
}
private static IReadOnlyList<string> DiscoverPluginDirectories(string baseDirectory)
{
if (!Directory.Exists(baseDirectory))
{
return [];
}
var directories = new List<string>();
foreach (var dir in Directory.EnumerateDirectories(baseDirectory))
{
var dirName = Path.GetFileName(dir);
// Skip hidden directories and common non-plugin directories
if (dirName.StartsWith('.') || dirName.Equals("bin", StringComparison.OrdinalIgnoreCase) ||
dirName.Equals("obj", StringComparison.OrdinalIgnoreCase))
{
continue;
}
// A plugin directory should have either a plugin.json or config.yaml
if (File.Exists(Path.Combine(dir, "plugin.json")) || File.Exists(Path.Combine(dir, "config.yaml")))
{
directories.Add(dir);
}
}
return directories;
}
private static bool IsPlatformSupported(PluginManifest manifest, string? currentPlatform)
{
// No platform filter or no platforms specified = supported
if (string.IsNullOrEmpty(currentPlatform) || manifest.Platforms.Count == 0)
{
return true;
}
// Check exact match or wildcard
foreach (var platform in manifest.Platforms)
{
if (platform.Equals("*", StringComparison.Ordinal) ||
platform.Equals(currentPlatform, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Check partial match (e.g., "linux" matches "linux-x64")
if (currentPlatform.StartsWith(platform, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool IsJurisdictionSupported(PluginManifest manifest, string? requiredJurisdiction)
{
// No jurisdiction filter = supported
if (string.IsNullOrEmpty(requiredJurisdiction))
{
return true;
}
// "world" jurisdiction is always supported
if (manifest.Jurisdiction is null || manifest.Jurisdiction.Equals("world", StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Check exact match
return manifest.Jurisdiction.Equals(requiredJurisdiction, StringComparison.OrdinalIgnoreCase);
}
private static bool IsComplianceSupported(PluginManifest manifest, IReadOnlyList<string> requiredCompliance)
{
// No compliance filter = supported
if (requiredCompliance.Count == 0)
{
return true;
}
// Plugin must declare at least one of the required compliance standards
foreach (var required in requiredCompliance)
{
if (manifest.Compliance.Any(c => c.Equals(required, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
private PluginRuntimeConfig? ApplyRegistryOverrides(string pluginId, PluginRuntimeConfig? config, PluginRegistry? registry)
{
if (registry?.Plugins is null || !registry.Plugins.TryGetValue(GetShortPluginId(pluginId), out var entry))
{
return config;
}
// Create config if it doesn't exist
config ??= new PluginRuntimeConfig { Id = pluginId };
// Apply overrides
var newConfig = new Dictionary<string, object?>(config.Config);
return config with
{
Enabled = entry.Enabled ?? config.Enabled,
Priority = entry.Priority ?? config.Priority
};
}
private static string GetShortPluginId(string fullId)
{
// Extract short name from "stellaops.module.name" format
var parts = fullId.Split('.');
return parts.Length > 0 ? parts[^1] : fullId;
}
private async Task<PluginAssembly?> LoadAssemblyAsync(
PluginManifest manifest,
string pluginDir,
PluginRegistryOptions options,
CancellationToken cancellationToken)
{
// Determine assembly path
var assemblyPath = manifest.Assembly.Path;
// First, check in the plugin config directory
var fullPath = Path.IsPathRooted(assemblyPath)
? assemblyPath
: Path.Combine(pluginDir, assemblyPath);
// If not found there, check in the assembly output directory
if (!File.Exists(fullPath) && options.AssemblyDirectory is not null)
{
var assemblyFileName = Path.GetFileName(assemblyPath);
var altPath = Path.Combine(options.AssemblyDirectory, assemblyFileName);
if (File.Exists(altPath))
{
fullPath = altPath;
}
// Also check in a subdirectory matching the plugin short name
var shortId = GetShortPluginId(manifest.Id);
var subDirPath = Path.Combine(options.AssemblyDirectory, shortId, assemblyFileName);
if (File.Exists(subDirPath))
{
fullPath = subDirPath;
}
}
if (!File.Exists(fullPath))
{
throw new FileNotFoundException($"Assembly not found: {fullPath}", fullPath);
}
// Verify SHA256 if specified
if (!string.IsNullOrEmpty(manifest.Assembly.Sha256))
{
var actualHash = await ComputeSha256Async(fullPath, cancellationToken);
if (!actualHash.Equals(manifest.Assembly.Sha256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Assembly hash mismatch: expected {manifest.Assembly.Sha256}, got {actualHash}");
}
}
// Load the assembly
var loadContext = new PluginLoadContext(fullPath);
var assembly = loadContext.LoadFromAssemblyPath(fullPath);
return new PluginAssembly(fullPath, assembly, loadContext);
}
private static async Task<string> ComputeSha256Async(string filePath, CancellationToken cancellationToken)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
await using var stream = File.OpenRead(filePath);
var hash = await sha256.ComputeHashAsync(stream, cancellationToken);
return Convert.ToHexString(hash).ToLowerInvariant();
}
/// <summary>
/// Expands environment variable references in a string.
/// Supports ${VAR} and ${VAR:-default} syntax.
/// </summary>
public static string ExpandEnvironmentVariables(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
// Match ${VAR} or ${VAR:-default}
return Regex.Replace(input, @"\$\{([^}:]+)(?::-([^}]*))?\}", match =>
{
var varName = match.Groups[1].Value;
var defaultValue = match.Groups[2].Success ? match.Groups[2].Value : string.Empty;
var envValue = Environment.GetEnvironmentVariable(varName);
return string.IsNullOrEmpty(envValue) ? defaultValue : envValue;
});
}
/// <summary>
/// Gets the current platform identifier (e.g., "linux-x64", "win-x64", "osx-arm64").
/// </summary>
public static string GetCurrentPlatform()
{
var os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win" :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" : "unknown";
var arch = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
Architecture.X86 => "x86",
Architecture.Arm => "arm",
_ => "unknown"
};
return $"{os}-{arch}";
}
}
/// <summary>
/// Extension methods for plugin manifest loading.
/// </summary>
public static class PluginManifestLoaderExtensions
{
/// <summary>
/// Loads plugins from etc/plugins/{module} with assembly output from plugins/{module}.
/// </summary>
public static async Task<PluginRegistryResult> LoadModulePluginsAsync(
this PluginManifestLoader loader,
string repoRoot,
string moduleName,
ILogger? logger = null,
CancellationToken cancellationToken = default)
{
var configDir = Path.Combine(repoRoot, "etc", "plugins", moduleName);
var assemblyDir = Path.Combine(repoRoot, "plugins", moduleName);
var options = new PluginRegistryOptions
{
BaseDirectory = configDir,
ModuleName = moduleName,
AssemblyDirectory = Directory.Exists(assemblyDir) ? assemblyDir : null,
Platform = PluginManifestLoader.GetCurrentPlatform()
};
return await loader.LoadAsync(options, cancellationToken);
}
}

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Plugin.Manifest;
/// <summary>
/// Plugin registry schema for registry.yaml files.
/// Each module has a registry that defines plugin defaults and overrides.
/// </summary>
public sealed record PluginRegistry
{
/// <summary>
/// Registry schema version.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; init; } = "1.0";
/// <summary>
/// Module category (e.g., "router.plugins", "scanner.plugins").
/// </summary>
[JsonPropertyName("category")]
public required string Category { get; init; }
/// <summary>
/// Default settings for all plugins in this registry.
/// </summary>
[JsonPropertyName("defaults")]
public PluginRegistryDefaults? Defaults { get; init; }
/// <summary>
/// Per-plugin configuration entries keyed by plugin ID (short name).
/// </summary>
[JsonPropertyName("plugins")]
public IReadOnlyDictionary<string, PluginRegistryEntry> Plugins { get; init; } = new Dictionary<string, PluginRegistryEntry>();
}
/// <summary>
/// Default settings applied to all plugins unless overridden.
/// </summary>
public sealed record PluginRegistryDefaults
{
/// <summary>
/// Default enabled state for all plugins.
/// </summary>
[JsonPropertyName("enabled")]
public bool Enabled { get; init; } = false;
/// <summary>
/// Default timeout for plugin operations.
/// </summary>
[JsonPropertyName("timeout")]
public TimeSpan Timeout { get; init; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Default retry count for plugin operations.
/// </summary>
[JsonPropertyName("retryCount")]
public int RetryCount { get; init; } = 3;
/// <summary>
/// Default jurisdiction filter (null = no filter).
/// </summary>
[JsonPropertyName("jurisdiction")]
public string? Jurisdiction { get; init; }
/// <summary>
/// Required compliance standards filter.
/// </summary>
[JsonPropertyName("requiredCompliance")]
public IReadOnlyList<string> RequiredCompliance { get; init; } = [];
}
/// <summary>
/// Per-plugin entry in the registry.
/// </summary>
public sealed record PluginRegistryEntry
{
/// <summary>
/// Whether the plugin is enabled.
/// </summary>
[JsonPropertyName("enabled")]
public bool? Enabled { get; init; }
/// <summary>
/// Priority for loading order (higher = first).
/// </summary>
[JsonPropertyName("priority")]
public int? Priority { get; init; }
/// <summary>
/// Path to plugin-specific config.yaml file (relative to registry).
/// </summary>
[JsonPropertyName("config")]
public string? Config { get; init; }
/// <summary>
/// Timeout override for this plugin.
/// </summary>
[JsonPropertyName("timeout")]
public TimeSpan? Timeout { get; init; }
/// <summary>
/// Additional environment variables for this plugin.
/// </summary>
[JsonPropertyName("environment")]
public IReadOnlyDictionary<string, string> Environment { get; init; } = new Dictionary<string, string>();
}
/// <summary>
/// Result of loading a plugin registry.
/// </summary>
public sealed class PluginRegistryResult
{
/// <summary>
/// The loaded registry.
/// </summary>
public PluginRegistry? Registry { get; init; }
/// <summary>
/// Path to the registry file.
/// </summary>
public required string RegistryPath { get; init; }
/// <summary>
/// Whether the registry was successfully loaded.
/// </summary>
public bool IsLoaded => Registry is not null;
/// <summary>
/// Error message if loading failed.
/// </summary>
public string? ErrorMessage { get; init; }
/// <summary>
/// Discovered plugin directories.
/// </summary>
public IReadOnlyList<string> PluginDirectories { get; init; } = [];
/// <summary>
/// Loaded plugin manifests.
/// </summary>
public IReadOnlyList<LoadedPlugin> LoadedPlugins { get; init; } = [];
/// <summary>
/// Plugins that failed to load.
/// </summary>
public IReadOnlyList<PluginLoadError> Errors { get; init; } = [];
}
/// <summary>
/// Error that occurred during plugin loading.
/// </summary>
public sealed record PluginLoadError
{
/// <summary>
/// Plugin ID or path that failed.
/// </summary>
public required string PluginId { get; init; }
/// <summary>
/// Error category.
/// </summary>
public required PluginLoadErrorCategory Category { get; init; }
/// <summary>
/// Error message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Inner exception if available.
/// </summary>
public Exception? Exception { get; init; }
}
/// <summary>
/// Categories of plugin load errors.
/// </summary>
public enum PluginLoadErrorCategory
{
/// <summary>
/// Plugin manifest not found.
/// </summary>
ManifestNotFound,
/// <summary>
/// Plugin manifest is invalid.
/// </summary>
ManifestInvalid,
/// <summary>
/// Plugin config is invalid.
/// </summary>
ConfigInvalid,
/// <summary>
/// Plugin assembly not found.
/// </summary>
AssemblyNotFound,
/// <summary>
/// Plugin assembly failed to load.
/// </summary>
AssemblyLoadFailed,
/// <summary>
/// Plugin failed signature verification.
/// </summary>
SignatureInvalid,
/// <summary>
/// Plugin is incompatible with host version.
/// </summary>
IncompatibleVersion,
/// <summary>
/// Plugin dependencies are missing.
/// </summary>
MissingDependencies,
/// <summary>
/// Plugin is filtered out by platform.
/// </summary>
PlatformMismatch,
/// <summary>
/// Plugin is filtered out by jurisdiction.
/// </summary>
JurisdictionMismatch,
/// <summary>
/// Plugin is filtered out by compliance.
/// </summary>
ComplianceMismatch
}
/// <summary>
/// Options for loading a plugin registry.
/// </summary>
public sealed class PluginRegistryOptions
{
/// <summary>
/// Base directory for plugin discovery (e.g., etc/plugins/{module}).
/// </summary>
public required string BaseDirectory { get; init; }
/// <summary>
/// Module name (e.g., "router", "scanner", "excititor").
/// </summary>
public required string ModuleName { get; init; }
/// <summary>
/// Directory containing built plugin assemblies (e.g., plugins/{module}).
/// </summary>
public string? AssemblyDirectory { get; init; }
/// <summary>
/// Platform filter (e.g., "linux-x64", "win-x64").
/// </summary>
public string? Platform { get; init; }
/// <summary>
/// Jurisdiction filter (e.g., "russia", "china", "eu").
/// </summary>
public string? Jurisdiction { get; init; }
/// <summary>
/// Required compliance standards filter.
/// </summary>
public IReadOnlyList<string> RequiredCompliance { get; init; } = [];
/// <summary>
/// Whether to load plugin assemblies.
/// </summary>
public bool LoadAssemblies { get; init; } = true;
/// <summary>
/// Whether to enforce signature verification.
/// </summary>
public bool EnforceSignatures { get; init; } = false;
/// <summary>
/// Host version for compatibility checking.
/// </summary>
public Version? HostVersion { get; init; }
}

View File

@@ -14,9 +14,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<ItemGroup>