- Implemented comprehensive unit tests for VexCandidateEmitter to validate candidate emission logic based on various scenarios including absent and present APIs, confidence thresholds, and rate limiting. - Added integration tests for SmartDiff PostgreSQL repositories, covering snapshot storage and retrieval, candidate storage, and material risk change handling. - Ensured tests validate correct behavior for storing, retrieving, and querying snapshots and candidates, including edge cases and expected outcomes.
429 lines
15 KiB
Markdown
Executable File
429 lines
15 KiB
Markdown
Executable File
# 10 · Plug‑in SDK Guide — **Stella Ops**
|
||
*(v 1.5 — 11 Jul 2025 · template install, no reload, IoC)*
|
||
|
||
---
|
||
|
||
## 0 Audience & Scope
|
||
Guidance for developers who extend Stella Ops with schedule jobs, scanner adapters, TLS providers, notification channels, etc. Everything here is OSS; commercial variants simply ship additional signed plug‑ins.
|
||
|
||
---
|
||
|
||
## 1 Prerequisites
|
||
|
||
| Tool | Min Version |
|
||
| ----------------------- | ----------------------------------------------------------------- |
|
||
| .NET SDK | {{ dotnet }} |
|
||
| **StellaOps templates** | install once via `bash dotnet new install StellaOps.Templates::*` |
|
||
| **Cosign** | 2.3 + — used to sign DLLs |
|
||
| xUnit | 2.6 |
|
||
| Docker CLI | only if your plug‑in shells out to containers |
|
||
|
||
---
|
||
|
||
## 2 Repository & Build Output
|
||
|
||
Every plug‑in is hosted in **`git.stella‑ops.org`**.
|
||
At publish time it must copy its signed artefacts to:
|
||
|
||
~~~text
|
||
src/backend/Stella.Ops.Plugin.Binaries/<MyPlugin>/
|
||
├── MyPlugin.dll
|
||
└── MyPlugin.dll.sig
|
||
~~~
|
||
|
||
The back‑end scans this folder on start‑up, verifies the **Cosign** signature, confirms the `[StellaPluginVersion]` gate, then loads the DLL inside an **isolated AssemblyLoadContext** to avoid dependency clashes
|
||
|
||
---
|
||
|
||
## 3 Project Scaffold
|
||
|
||
Generate with the installed template:
|
||
|
||
~~~bash
|
||
dotnet new stellaops-plugin-schedule \
|
||
-n MyPlugin.Schedule \
|
||
--output src
|
||
~~~
|
||
|
||
Result:
|
||
|
||
~~~text
|
||
src/
|
||
├─ MyPlugin.Schedule/
|
||
│ ├─ MyJob.cs
|
||
│ └─ MyPlugin.Schedule.csproj
|
||
└─ tests/
|
||
└─ MyPlugin.Schedule.Tests/
|
||
~~~
|
||
|
||
---
|
||
|
||
## 4 MSBuild Wiring
|
||
|
||
Add this to **`MyPlugin.Schedule.csproj`** so the signed DLL + `.sig` land in the canonical plug‑in folder:
|
||
|
||
~~~xml
|
||
<PropertyGroup>
|
||
<StellaPluginOut>$(SolutionDir)src/backend/Stella.Ops.Plugin.Binaries/$(MSBuildProjectName)</StellaPluginOut>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
|
||
<ProjectReference Include="..\..\StellaOps.Common\StellaOps.Common.csproj"
|
||
PrivateAssets="all" />
|
||
</ItemGroup>
|
||
|
||
<Target Name="CopyStellaPlugin" AfterTargets="Publish">
|
||
<MakeDir Directories="$(StellaPluginOut)" />
|
||
<Copy SourceFiles="$(PublishDir)$(AssemblyName).dll;$(PublishDir)$(AssemblyName).dll.sig"
|
||
DestinationFolder="$(StellaPluginOut)" />
|
||
</Target>
|
||
~~~
|
||
|
||
---
|
||
|
||
## 5 Dependency‑Injection Entry‑point
|
||
|
||
Back‑end auto‑discovers restart‑time bindings through two mechanisms:
|
||
|
||
1. **Service binding metadata** for simple contracts.
|
||
2. **`IDependencyInjectionRoutine`** implementations when you need full control.
|
||
|
||
### 5.1 Service binding metadata
|
||
|
||
Annotate implementations with `[ServiceBinding]` to declare their lifetime and service contract.
|
||
The loader honours scoped lifetimes and will register the service before executing any custom DI routines.
|
||
|
||
~~~csharp
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using StellaOps.DependencyInjection;
|
||
|
||
[ServiceBinding(typeof(IJob), ServiceLifetime.Scoped, RegisterAsSelf = true)]
|
||
public sealed class MyJob : IJob
|
||
{
|
||
// IJob dependencies can now use scoped services (PostgreSQL connections, etc.)
|
||
}
|
||
~~~
|
||
|
||
Use `RegisterAsSelf = true` when you also want to resolve the concrete type.
|
||
Set `ReplaceExisting = true` to override default descriptors if the host already provides one.
|
||
|
||
### 5.2 Dependency injection routines
|
||
|
||
For advanced scenarios continue to expose a routine:
|
||
|
||
~~~csharp
|
||
namespace StellaOps.DependencyInjection;
|
||
|
||
public sealed class IoCConfigurator : IDependencyInjectionRoutine
|
||
{
|
||
public IServiceCollection Register(IServiceCollection services, IConfiguration cfg)
|
||
{
|
||
services.AddSingleton<IJob, MyJob>(); // schedule job
|
||
services.Configure<MyPluginOptions>(cfg.GetSection("Plugins:MyPlugin"));
|
||
return services;
|
||
}
|
||
}
|
||
~~~
|
||
|
||
---
|
||
|
||
## 6 Schedule Plug‑ins
|
||
|
||
### 6.1 Minimal Job
|
||
|
||
~~~csharp
|
||
using StellaOps.Scheduling; // contract
|
||
|
||
[StellaPluginVersion("2.0.0")]
|
||
public sealed class MyJob : IJob
|
||
{
|
||
public async Task ExecuteAsync(CancellationToken ct)
|
||
{
|
||
Console.WriteLine("Hello from plug‑in!");
|
||
await Task.Delay(500, ct);
|
||
}
|
||
}
|
||
~~~
|
||
|
||
### 6.2 Cron Registration
|
||
|
||
```csharp
|
||
services.AddCronJob<MyJob>("0 15 * * *"); // everyday
|
||
```
|
||
|
||
15:00
|
||
Cron syntax follows Hangfire rules
|
||
|
||
## 7 Scanner Adapters
|
||
|
||
Implement IScannerRunner.
|
||
Register inside Configure:
|
||
```csharp
|
||
services.AddScanner<MyAltScanner>("alt"); // backend
|
||
```
|
||
|
||
selects by --engine alt
|
||
If the engine needs a side‑car container, include a Dockerfile in your repo and document resource expectations.
|
||
## 8 Packaging & Signing
|
||
|
||
```bash
|
||
dotnet publish -c Release -p:PublishSingleFile=true -o out
|
||
cosign sign --key $COSIGN_KEY out/MyPlugin.Schedule.dll # sign binary only
|
||
sha256sum out/MyPlugin.Schedule.dll > out/.sha256 # optional checksum
|
||
zip MyPlugin.zip out/* README.md
|
||
```
|
||
|
||
Unsigned DLLs are refused when StellaOps:Security:DisableUnsigned=false.
|
||
|
||
## 9 Deployment
|
||
|
||
```bash
|
||
docker cp MyPlugin.zip <backend>:/opt/plugins/ && docker restart <backend>
|
||
```
|
||
|
||
Check /health – "plugins":["MyPlugin.Schedule@2.0.0"].
|
||
(Hot‑reload was removed to keep the core process simple and memory‑safe.)
|
||
|
||
## 10 Configuration Patterns
|
||
|
||
| Need | Pattern |
|
||
| ------------ | --------------------------------------------------------- |
|
||
| Settings | Plugins:MyPlugin:* in appsettings.json. |
|
||
| Secrets | Redis secure:<plugin>:<key> (encrypted per TLS provider). |
|
||
| Dynamic cron | Implement ICronConfigurable; UI exposes editor. |
|
||
|
||
## 11 Testing & CI
|
||
|
||
| Layer | Tool | Gate |
|
||
| ----------- | -------------------------- | ------------------- |
|
||
| Unit | xUnit + Moq | ≥ 50 % lines |
|
||
| Integration | Testcontainers ‑ run in CI | Job completes < 5 s |
|
||
| Style | dotnet | format 0 warnings |
|
||
|
||
Use the pre‑baked workflow in StellaOps.Templates as starting point.
|
||
|
||
## 12 Publishing to the Community Marketplace
|
||
|
||
Tag Git release plugin‑vX.Y.Z and attach the signed ZIP.
|
||
Submit a PR to stellaops/community-plugins.json with metadata & git URL.
|
||
On merge, the plug‑in shows up in the UI Marketplace.
|
||
|
||
## 13 Common Pitfalls
|
||
|
||
| Symptom | Root cause | Fix |
|
||
| ------------------- | -------------------------- | ------------------------------------------- |
|
||
| NotDetected | .sig missing | cosign sign … |
|
||
| VersionGateMismatch | Backend 2.1 vs plug‑in 2.0 | Re‑compile / bump attribute |
|
||
| FileLoadException | Duplicate | StellaOps.Common Ensure PrivateAssets="all" |
|
||
| Redis | timeouts Large writes | Batch or use PostgreSQL |
|
||
|
||
---
|
||
|
||
## 14 Plugin Version Compatibility (v2.0)
|
||
|
||
**IMPORTANT:** All plugins **must** declare a `[StellaPluginVersion]` attribute. Plugins without this attribute will be rejected by the host loader.
|
||
|
||
Declare your plugin's version and host compatibility requirements:
|
||
|
||
```csharp
|
||
using StellaOps.Plugin.Versioning;
|
||
|
||
// In AssemblyInfo.cs or any file at assembly level
|
||
[assembly: StellaPluginVersion("1.2.0", MinimumHostVersion = "1.0.0", MaximumHostVersion = "2.0.0")]
|
||
```
|
||
|
||
| Property | Purpose | Required |
|
||
|----------|---------|----------|
|
||
| `pluginVersion` (constructor) | Your plugin's semantic version | **Yes** |
|
||
| `MinimumHostVersion` | Lowest host version that can load this plugin | Recommended |
|
||
| `MaximumHostVersion` | Highest host version supported | Recommended for cross-major compatibility |
|
||
| `RequiresSignature` | Whether signature verification is mandatory (default: true) | No |
|
||
|
||
### Version Compatibility Rules
|
||
|
||
1. **Attribute Required:** Plugins without `[StellaPluginVersion]` are rejected
|
||
2. **Minimum Version:** Host version must be ≥ `MinimumHostVersion`
|
||
3. **Maximum Version:** Host version must be ≤ `MaximumHostVersion` (if specified)
|
||
4. **Strict Major Version:** If `MaximumHostVersion` is not specified, the plugin is assumed to only support the same major version as `MinimumHostVersion`
|
||
|
||
### Examples
|
||
|
||
```csharp
|
||
// Plugin works with host 1.0.0 through 2.x (explicit range)
|
||
[assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "1.0.0", MaximumHostVersion = "2.99.99")]
|
||
|
||
// Plugin works with host 2.x only (strict - no MaximumHostVersion means same major version)
|
||
[assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "2.0.0")]
|
||
|
||
// Plugin version 3.0.0 with no host constraints (uses plugin major version as reference)
|
||
[assembly: StellaPluginVersion("3.0.0")]
|
||
```
|
||
|
||
---
|
||
|
||
## 15 Plugin Host Configuration (v2.0)
|
||
|
||
Configure the plugin loader with security-first defaults in `PluginHostOptions`:
|
||
|
||
```csharp
|
||
var options = new PluginHostOptions
|
||
{
|
||
// Version enforcement (all default to true for security)
|
||
HostVersion = new Version(2, 0, 0),
|
||
EnforceVersionCompatibility = true, // Reject incompatible plugins
|
||
RequireVersionAttribute = true, // Reject plugins without [StellaPluginVersion]
|
||
StrictMajorVersionCheck = true, // Reject plugins crossing major version boundaries
|
||
|
||
// Signature verification (opt-in, requires infrastructure)
|
||
EnforceSignatureVerification = true,
|
||
SignatureVerifier = new CosignPluginVerifier(new CosignVerifierOptions
|
||
{
|
||
PublicKeyPath = "/keys/cosign.pub",
|
||
UseRekorTransparencyLog = true,
|
||
AllowUnsigned = false
|
||
})
|
||
};
|
||
|
||
var result = await PluginHost.LoadPluginsAsync(options, logger);
|
||
|
||
// Check for failures
|
||
if (result.HasFailures)
|
||
{
|
||
foreach (var failure in result.Failures)
|
||
{
|
||
logger.LogError("Plugin {Path} failed: {Reason} - {Message}",
|
||
failure.AssemblyPath, failure.Reason, failure.Message);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Host Options Reference
|
||
|
||
| Option | Default | Purpose |
|
||
|--------|---------|---------|
|
||
| `HostVersion` | null | The host application version for compatibility checking |
|
||
| `EnforceVersionCompatibility` | **true** | Reject plugins that fail version checks |
|
||
| `RequireVersionAttribute` | **true** | Reject plugins without `[StellaPluginVersion]` |
|
||
| `StrictMajorVersionCheck` | **true** | Reject plugins that don't explicitly support the host's major version |
|
||
| `EnforceSignatureVerification` | false | Reject plugins without valid signatures |
|
||
| `SignatureVerifier` | null | The verifier implementation (e.g., `CosignPluginVerifier`) |
|
||
|
||
### Failure Reasons
|
||
|
||
| Reason | Description |
|
||
|--------|-------------|
|
||
| `LoadError` | Assembly could not be loaded (missing dependencies, corrupt file) |
|
||
| `SignatureInvalid` | Signature verification failed |
|
||
| `IncompatibleVersion` | Plugin version constraints not satisfied |
|
||
| `MissingVersionAttribute` | Plugin lacks required `[StellaPluginVersion]` attribute |
|
||
|
||
---
|
||
|
||
## 16 Fail-Fast Options Validation (v2.0)
|
||
|
||
Use the fail-fast validation pattern to catch configuration errors at startup:
|
||
|
||
```csharp
|
||
using StellaOps.DependencyInjection.Validation;
|
||
|
||
// Register options with automatic startup validation
|
||
services.AddOptionsWithValidation<MyPluginOptions, MyPluginOptionsValidator>(
|
||
MyPluginOptions.SectionName);
|
||
|
||
// Or with data annotations
|
||
services.AddOptionsWithDataAnnotations<MyPluginOptions>(
|
||
MyPluginOptions.SectionName);
|
||
```
|
||
|
||
Create validators using the base class:
|
||
|
||
```csharp
|
||
public sealed class MyPluginOptionsValidator : OptionsValidatorBase<MyPluginOptions>
|
||
{
|
||
protected override string SectionPrefix => "Plugins:MyPlugin";
|
||
|
||
protected override void ValidateOptions(MyPluginOptions options, ValidationContext context)
|
||
{
|
||
context
|
||
.RequireNotEmpty(options.BaseUrl, nameof(options.BaseUrl))
|
||
.RequirePositive(options.TimeoutSeconds, nameof(options.TimeoutSeconds))
|
||
.RequireInRange(options.MaxRetries, nameof(options.MaxRetries), 0, 10);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 17 Available Templates (v2.0)
|
||
|
||
Install and use the official plugin templates:
|
||
|
||
```bash
|
||
# Install from local templates directory
|
||
dotnet new install ./templates
|
||
|
||
# Or install from NuGet
|
||
dotnet new install StellaOps.Templates
|
||
|
||
# Create a connector plugin
|
||
dotnet new stellaops-plugin-connector -n MyCompany.AcmeConnector
|
||
|
||
# Create a scheduled job plugin
|
||
dotnet new stellaops-plugin-scheduler -n MyCompany.CleanupJob
|
||
```
|
||
|
||
Templates include:
|
||
- Plugin entry point with version attribute
|
||
- Options class with data annotations
|
||
- Options validator with fail-fast pattern
|
||
- DI routine registration
|
||
- README with build/sign instructions
|
||
|
||
---
|
||
|
||
## 18 Migration Guide: v2.0 to v2.1
|
||
|
||
### Breaking Change: Version Attribute Required
|
||
|
||
As of v2.1, all plugins **must** include a `[StellaPluginVersion]` attribute. Plugins without this attribute will be rejected with `MissingVersionAttribute` failure.
|
||
|
||
**Before (v2.0):** Optional, plugins without attribute loaded with warning.
|
||
**After (v2.1):** Required, plugins without attribute are rejected.
|
||
|
||
### Migration Steps
|
||
|
||
1. Add the version attribute to your plugin's AssemblyInfo.cs:
|
||
```csharp
|
||
[assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "2.0.0", MaximumHostVersion = "2.99.99")]
|
||
```
|
||
|
||
2. If your plugin must support multiple major host versions, explicitly set `MaximumHostVersion`:
|
||
```csharp
|
||
// Supports host 1.x through 3.x
|
||
[assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "1.0.0", MaximumHostVersion = "3.99.99")]
|
||
```
|
||
|
||
3. Rebuild and re-sign your plugin.
|
||
|
||
### Opt-out (Not Recommended)
|
||
|
||
If you must load legacy plugins without version attributes:
|
||
```csharp
|
||
var options = new PluginHostOptions
|
||
{
|
||
RequireVersionAttribute = false, // Allow unversioned plugins (NOT recommended)
|
||
StrictMajorVersionCheck = false // Allow cross-major version loading
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Change Log
|
||
|
||
| Version | Date | Changes |
|
||
|---------|------|---------|
|
||
| v2.1 | 2025-12-14 | **Breaking:** `[StellaPluginVersion]` attribute now required by default. Added `RequireVersionAttribute`, `StrictMajorVersionCheck` options. Added `MissingVersionAttribute` failure reason. |
|
||
| v2.0 | 2025-12-14 | Added StellaPluginVersion attribute, Cosign verification options, fail-fast validation, new templates |
|
||
| v1.5 | 2025-07-11 | Template install, no hot-reload, IoC conventions |
|