# Plugin Development SDK This guide covers the StellaOps Plugin SDK for developing custom plugins. ## Overview The Plugin SDK provides: - Base interfaces for all plugin types - DI registration utilities - Configuration binding helpers - Version compatibility attributes - Testing infrastructure ## Prerequisites - .NET 10 SDK - Understanding of dependency injection - Familiarity with async/await patterns ## Core Interfaces ### IAvailabilityPlugin Base interface for plugins with availability checks: ```csharp namespace StellaOps.Plugin; public interface IAvailabilityPlugin { /// /// Checks whether the plugin is available in the current environment. /// bool IsAvailable(IServiceProvider services); } ``` ### IRouterTransportPlugin Interface for router transport plugins: ```csharp namespace StellaOps.Router.Common.Plugins; public interface IRouterTransportPlugin { string TransportName { get; } string DisplayName { get; } bool IsAvailable(IServiceProvider services); void Register(RouterTransportRegistrationContext context); } ``` ### IConcielierConnector Interface for vulnerability data connectors: ```csharp namespace StellaOps.Concelier.Core.Plugins; public interface IConcielierConnector : IAvailabilityPlugin { string ConnectorId { get; } string DisplayName { get; } Task> FetchAsync( FetchOptions options, CancellationToken cancellationToken); } ``` ### IScannerAnalyzerPlugin Interface for language-specific scanners: ```csharp namespace StellaOps.Scanner.Core.Plugins; public interface IScannerAnalyzerPlugin : IAvailabilityPlugin { string AnalyzerId { get; } string Language { get; } IEnumerable SupportedFilePatterns { get; } Task AnalyzeAsync( AnalysisContext context, CancellationToken cancellationToken); } ``` ## Creating a Plugin ### 1. Create Project ```bash mkdir MyCompany.StellaOps.Plugin.MyPlugin cd MyCompany.StellaOps.Plugin.MyPlugin dotnet new classlib -f net10.0 ``` ### 2. Add Package References ```xml ``` ### 3. Create Options Class ```csharp namespace MyCompany.StellaOps.Plugin.MyPlugin; public sealed class MyPluginOptions { public string? ApiEndpoint { get; set; } public string? ApiKey { get; set; } public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1); public int MaxRetries { get; set; } = 3; } ``` ### 4. Create Plugin Implementation ```csharp using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Plugin; namespace MyCompany.StellaOps.Plugin.MyPlugin; public sealed class MyPlugin : IAvailabilityPlugin { public string PluginId => "mycompany.stellaops.myplugin"; public string DisplayName => "My Custom Plugin"; public bool IsAvailable(IServiceProvider services) { // Check if required dependencies are available var config = services.GetService(); var apiKey = config?["MyPlugin:ApiKey"]; return !string.IsNullOrEmpty(apiKey); } public void Register(PluginRegistrationContext context) { var services = context.Services; var configuration = context.Configuration; // Bind configuration var section = configuration.GetSection("MyPlugin"); services.Configure(options => { section.Bind(options); }); // Register services services.AddSingleton(); } } ``` ### 5. Add Version Attribute ```csharp using StellaOps.Plugin.Versioning; [assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "1.0.0")] ``` ## Registration Context ### Generic Context ```csharp public sealed class PluginRegistrationContext { public IServiceCollection Services { get; } public IConfiguration Configuration { get; } } ``` ### Router Transport Context ```csharp public sealed class RouterTransportRegistrationContext { public IServiceCollection Services { get; } public IConfiguration Configuration { get; } public RouterTransportMode Mode { get; } public string? ConfigurationSection { get; init; } } [Flags] public enum RouterTransportMode { None = 0, Server = 1, Client = 2, Both = Server | Client } ``` ## Configuration Binding ### Basic Binding ```csharp public void Register(PluginRegistrationContext context) { context.Services.Configure(options => { context.Configuration.GetSection("MyPlugin").Bind(options); }); } ``` ### With Validation ```csharp public void Register(PluginRegistrationContext context) { context.Services.AddOptions() .Bind(context.Configuration.GetSection("MyPlugin")) .ValidateDataAnnotations() .ValidateOnStart(); } ``` ### Options Class with Validation ```csharp using System.ComponentModel.DataAnnotations; public sealed class MyOptions { [Required] [Url] public string? ApiEndpoint { get; set; } [Required] public string? ApiKey { get; set; } [Range(1, 100)] public int MaxRetries { get; set; } = 3; } ``` ## DI Registration Patterns ### Singleton Services ```csharp public void Register(PluginRegistrationContext context) { context.Services.AddSingleton(); } ``` ### Factory Registration ```csharp public void Register(PluginRegistrationContext context) { context.Services.AddSingleton(sp => { var options = sp.GetRequiredService>(); var logger = sp.GetRequiredService>(); return new MyService(options.Value, logger); }); } ``` ### Keyed Services ```csharp public void Register(PluginRegistrationContext context) { context.Services.AddKeyedSingleton("myplugin", (sp, key) => { return new MyService(); }); } ``` ## Logging ### Structured Logging ```csharp public sealed class MyPluginService { private readonly ILogger _logger; public MyPluginService(ILogger logger) { _logger = logger; } public async Task ProcessAsync(string itemId) { using var scope = _logger.BeginScope( new Dictionary { ["PluginId"] = "myplugin", ["ItemId"] = itemId }); _logger.LogInformation("Processing item {ItemId}", itemId); try { // Process... _logger.LogDebug("Item {ItemId} processed successfully", itemId); } catch (Exception ex) { _logger.LogError(ex, "Failed to process item {ItemId}", itemId); throw; } } } ``` ## Error Handling ### Availability Check ```csharp public bool IsAvailable(IServiceProvider services) { try { var config = services.GetRequiredService(); var options = config.GetSection("MyPlugin").Get(); if (string.IsNullOrEmpty(options?.ApiEndpoint)) { return false; } // Check connectivity if needed return true; } catch (Exception ex) { _logger?.LogWarning(ex, "Availability check failed"); return false; } } ``` ### Graceful Degradation ```csharp public async Task ProcessAsync(Request request) { try { return await ProcessInternalAsync(request); } catch (ApiException ex) when (ex.StatusCode == 429) { _logger.LogWarning("Rate limited, using cached data"); return await GetCachedResultAsync(request); } catch (TimeoutException) { _logger.LogWarning("Request timeout, using fallback"); return Result.Fallback(); } } ``` ## Testing ### Unit Testing ```csharp public class MyPluginTests { [Fact] public void IsAvailable_WithValidConfig_ReturnsTrue() { // Arrange var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["MyPlugin:ApiEndpoint"] = "https://api.example.com", ["MyPlugin:ApiKey"] = "test-key" }) .Build(); var services = new ServiceCollection(); services.AddSingleton(config); var provider = services.BuildServiceProvider(); var plugin = new MyPlugin(); // Act var available = plugin.IsAvailable(provider); // Assert Assert.True(available); } [Fact] public void Register_AddsServices() { // Arrange var config = new ConfigurationBuilder().Build(); var services = new ServiceCollection(); var context = new PluginRegistrationContext(services, config); var plugin = new MyPlugin(); // Act plugin.Register(context); // Assert var provider = services.BuildServiceProvider(); Assert.NotNull(provider.GetService()); } } ``` ### Integration Testing ```csharp public class MyPluginIntegrationTests : IClassFixture { private readonly PluginTestFixture _fixture; public MyPluginIntegrationTests(PluginTestFixture fixture) { _fixture = fixture; } [Fact] public async Task ProcessAsync_WithValidInput_ReturnsResult() { // Arrange var service = _fixture.GetService(); var request = new Request { Id = "test-001" }; // Act var result = await service.ProcessAsync(request); // Assert Assert.NotNull(result); Assert.Equal("test-001", result.RequestId); } } ``` ## Packaging ### Project Configuration ```xml net10.0 enable enable MyCompany.StellaOps.Plugin.MyPlugin 1.0.0 My Company My custom StellaOps plugin true true ``` ### Plugin Manifest Create `plugin.json`: ```json { "schemaVersion": "2.0", "id": "mycompany.stellaops.myplugin", "name": "My Custom Plugin", "version": "1.0.0", "assembly": { "path": "MyCompany.StellaOps.Plugin.MyPlugin.dll", "entryType": "MyCompany.StellaOps.Plugin.MyPlugin.MyPlugin" }, "capabilities": ["custom-feature"], "platforms": ["linux-x64", "win-x64", "osx-arm64"], "enabled": true, "priority": 100 } ``` ### Build and Package ```bash # Build release dotnet build -c Release # Create NuGet package dotnet pack -c Release # Publish to plugins directory dotnet publish -c Release -o ./plugins ``` ## Signing ### Sign with Cosign ```bash # Generate key pair cosign generate-key-pair # Sign plugin assembly cosign sign-blob --key cosign.key \ plugins/MyPlugin.dll \ --output-file plugins/MyPlugin.dll.sig ``` ### Verify Signature ```bash cosign verify-blob --key cosign.pub \ --signature plugins/MyPlugin.dll.sig \ plugins/MyPlugin.dll ``` ## Best Practices ### Determinism - Use stable ordering for collections - Use UTC timestamps in ISO-8601 format - Avoid non-deterministic random values - Cache results consistently ### Offline Support - Don't hardcode external URLs - Provide fallback mechanisms - Cache remote data when possible - Log warnings, not errors, for optional network features ### Performance - Use async/await properly - Implement cancellation tokens - Pool expensive resources - Limit memory allocations ### Security - Validate all inputs - Sanitize output data - Use secure defaults - Never log secrets ## Examples See the [plugin templates](./plugin-templates/README.md) for complete working examples. ## See Also - [Plugin Overview](../plugins/README.md) - [Plugin Architecture](../plugins/ARCHITECTURE.md) - [Plugin Configuration](../plugins/CONFIGURATION.md) - [Router Transport Development](../router/transports/development.md)