20 KiB
20 KiB
PLUGIN: Plugin Infrastructure
Purpose: Extensible plugin system for integrations, steps, and custom functionality.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ PLUGIN ARCHITECTURE │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PLUGIN REGISTRY │ │
│ │ │ │
│ │ - Plugin discovery and versioning │ │
│ │ - Manifest validation │ │
│ │ - Dependency resolution │ │
│ └──────────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PLUGIN LOADER │ │
│ │ │ │
│ │ - Lifecycle management (load, start, stop, unload) │ │
│ │ - Health monitoring │ │
│ │ - Hot reload support │ │
│ └──────────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PLUGIN SANDBOX │ │
│ │ │ │
│ │ - Process isolation │ │
│ │ - Resource limits (CPU, memory, network) │ │
│ │ - Capability enforcement │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Plugin Types: │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Connector │ │ Step │ │ Gate │ │ Agent │ │
│ │ Plugins │ │ Providers │ │ Providers │ │ Plugins │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Modules
Module: plugin-registry
| Aspect | Specification |
|---|---|
| Responsibility | Plugin discovery; versioning; manifest management |
| Data Entities | Plugin, PluginManifest, PluginVersion |
| Events Produced | plugin.discovered, plugin.registered, plugin.unregistered |
Plugin Entity:
interface Plugin {
id: UUID;
pluginId: string; // "com.example.my-connector"
version: string; // "1.2.3"
vendor: string;
license: string;
manifest: PluginManifest;
status: PluginStatus;
entrypoint: string; // Path to plugin executable/module
lastHealthCheck: DateTime;
healthMessage: string | null;
installedAt: DateTime;
updatedAt: DateTime;
}
type PluginStatus =
| "discovered" // Found but not loaded
| "loaded" // Loaded but not active
| "active" // Running and healthy
| "stopped" // Manually stopped
| "failed" // Failed to load or crashed
| "degraded"; // Running but with issues
Module: plugin-loader
| Aspect | Specification |
|---|---|
| Responsibility | Plugin lifecycle management |
| Dependencies | plugin-registry, plugin-sandbox |
| Events Produced | plugin.loaded, plugin.started, plugin.stopped, plugin.failed |
Plugin Lifecycle:
┌──────────────┐
│ DISCOVERED │ ──── Plugin found in registry
└──────┬───────┘
│ load()
▼
┌──────────────┐
│ LOADED │ ──── Plugin validated and prepared
└──────┬───────┘
│ start()
▼
┌──────────────┐ ┌──────────────┐
│ ACTIVE │ ──── │ DEGRADED │ ◄── Health issues
└──────┬───────┘ └──────────────┘
│ stop() │
▼ │
┌──────────────┐ │
│ STOPPED │ ◄───────────┘ manual stop
└──────────────┘
│ unload()
▼
┌──────────────┐
│ UNLOADED │
└──────────────┘
Lifecycle Operations:
interface PluginLoader {
// Discovery
discover(): Promise<Plugin[]>;
refresh(): Promise<void>;
// Lifecycle
load(pluginId: string): Promise<Plugin>;
start(pluginId: string): Promise<void>;
stop(pluginId: string): Promise<void>;
unload(pluginId: string): Promise<void>;
restart(pluginId: string): Promise<void>;
// Health
checkHealth(pluginId: string): Promise<HealthStatus>;
getStatus(pluginId: string): Promise<PluginStatus>;
// Hot reload
reload(pluginId: string): Promise<void>;
}
Module: plugin-sandbox
| Aspect | Specification |
|---|---|
| Responsibility | Isolation; resource limits; security |
| Enforcement | Process isolation, capability-based security |
Sandbox Configuration:
interface SandboxConfig {
// Process isolation
processIsolation: boolean; // Run in separate process
containerIsolation: boolean; // Run in container
// Resource limits
resourceLimits: {
maxMemoryMb: number; // Memory limit
maxCpuPercent: number; // CPU limit
maxDiskMb: number; // Disk quota
maxNetworkBandwidth: number; // Network bandwidth limit
};
// Network restrictions
networkPolicy: {
allowedHosts: string[]; // Allowed outbound hosts
blockedHosts: string[]; // Blocked hosts
allowOutbound: boolean; // Allow any outbound
};
// Filesystem restrictions
filesystemPolicy: {
readOnlyPaths: string[];
writablePaths: string[];
blockedPaths: string[];
};
// Timeouts
timeouts: {
initializationMs: number;
operationMs: number;
shutdownMs: number;
};
}
Capability Enforcement:
interface PluginCapabilities {
// Integration capabilities
integrations: {
scm: boolean;
ci: boolean;
registry: boolean;
vault: boolean;
router: boolean;
};
// Step capabilities
steps: {
deploy: boolean;
gate: boolean;
notify: boolean;
custom: boolean;
};
// System capabilities
system: {
network: boolean;
filesystem: boolean;
secrets: boolean;
database: boolean;
};
}
Module: plugin-sdk
| Aspect | Specification |
|---|---|
| Responsibility | SDK for plugin development |
| Languages | C#, TypeScript, Go |
Plugin SDK Interface:
// Base plugin interface
interface StellaPlugin {
// Lifecycle
initialize(config: PluginConfig): Promise<void>;
start(): Promise<void>;
stop(): Promise<void>;
dispose(): Promise<void>;
// Health
getHealth(): Promise<HealthStatus>;
// Metadata
getManifest(): PluginManifest;
}
// Connector plugin interface
interface ConnectorPlugin extends StellaPlugin {
createConnector(config: ConnectorConfig): Promise<Connector>;
}
// Step provider plugin interface
interface StepProviderPlugin extends StellaPlugin {
getStepTypes(): StepType[];
executeStep(
stepType: string,
config: StepConfig,
inputs: StepInputs,
context: StepContext
): AsyncGenerator<StepEvent>;
}
// Gate provider plugin interface
interface GateProviderPlugin extends StellaPlugin {
getGateTypes(): GateType[];
evaluateGate(
gateType: string,
config: GateConfig,
context: GateContext
): Promise<GateResult>;
}
Three-Surface Plugin Model
Plugins contribute to the system through three distinct surfaces:
1. Manifest Surface (Static)
The plugin manifest declares:
- Plugin identity and version
- Required capabilities
- Provided integrations/steps/gates
- Configuration schema
- UI components (optional)
# plugin.stella.yaml
plugin:
id: "com.example.jenkins-connector"
version: "1.0.0"
vendor: "Example Corp"
license: "Apache-2.0"
description: "Jenkins CI integration for Stella Ops"
capabilities:
required:
- network
optional:
- secrets
provides:
integrations:
- type: "ci.jenkins"
displayName: "Jenkins"
configSchema: "./schemas/jenkins-config.json"
capabilities:
- "pipelines"
- "builds"
- "artifacts"
steps:
- type: "jenkins-trigger"
displayName: "Trigger Jenkins Build"
category: "integration"
configSchema: "./schemas/jenkins-trigger-config.json"
inputSchema: "./schemas/jenkins-trigger-input.json"
outputSchema: "./schemas/jenkins-trigger-output.json"
ui:
configScreen: "./ui/config.html"
icon: "./assets/jenkins-icon.svg"
dependencies:
stellaCore: ">=1.0.0"
2. Connector Runtime Surface (Dynamic)
Plugins implement connector interfaces for runtime operations:
// Jenkins connector implementation
class JenkinsConnector implements CIConnector {
private client: JenkinsClient;
async initialize(config: ConnectorConfig, secrets: SecretHandle[]): Promise<void> {
const apiToken = await this.getSecret(secrets, "api_token");
this.client = new JenkinsClient({
baseUrl: config.endpoint,
username: config.username,
apiToken: apiToken,
});
}
async testConnection(): Promise<ConnectionTestResult> {
try {
const crumb = await this.client.getCrumb();
return { success: true, message: "Connected to Jenkins" };
} catch (error) {
return { success: false, message: error.message };
}
}
async listPipelines(): Promise<PipelineInfo[]> {
const jobs = await this.client.getJobs();
return jobs.map(job => ({
id: job.name,
name: job.displayName,
url: job.url,
lastBuild: job.lastBuild?.number,
}));
}
async triggerPipeline(pipelineId: string, params: object): Promise<PipelineRun> {
const queueItem = await this.client.build(pipelineId, params);
return {
id: queueItem.id.toString(),
pipelineId,
status: "queued",
startedAt: new Date(),
};
}
async getPipelineRun(runId: string): Promise<PipelineRun> {
const build = await this.client.getBuild(runId);
return {
id: build.number.toString(),
pipelineId: build.job,
status: this.mapStatus(build.result),
startedAt: new Date(build.timestamp),
completedAt: build.result ? new Date(build.timestamp + build.duration) : null,
};
}
}
3. Step Provider Surface (Execution)
Plugins implement step execution logic:
// Jenkins trigger step implementation
class JenkinsTriggerStep implements StepExecutor {
async *execute(
config: StepConfig,
inputs: StepInputs,
context: StepContext
): AsyncGenerator<StepEvent> {
const connector = await context.getConnector<JenkinsConnector>(config.integrationId);
yield { type: "log", line: `Triggering Jenkins job: ${config.jobName}` };
// Trigger build
const run = await connector.triggerPipeline(config.jobName, inputs.parameters);
yield { type: "output", name: "buildId", value: run.id };
yield { type: "log", line: `Build queued: ${run.id}` };
// Wait for completion if configured
if (config.waitForCompletion) {
yield { type: "log", line: "Waiting for build to complete..." };
while (true) {
const status = await connector.getPipelineRun(run.id);
if (status.status === "succeeded") {
yield { type: "output", name: "status", value: "succeeded" };
yield { type: "result", success: true };
return;
}
if (status.status === "failed") {
yield { type: "output", name: "status", value: "failed" };
yield { type: "result", success: false, message: "Build failed" };
return;
}
yield { type: "progress", progress: 50, message: `Build running: ${status.status}` };
await sleep(config.pollIntervalSeconds * 1000);
}
}
yield { type: "result", success: true };
}
}
Database Schema
-- Plugins
CREATE TABLE release.plugins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plugin_id VARCHAR(255) NOT NULL UNIQUE,
version VARCHAR(50) NOT NULL,
vendor VARCHAR(255) NOT NULL,
license VARCHAR(100),
manifest JSONB NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'discovered' CHECK (status IN (
'discovered', 'loaded', 'active', 'stopped', 'failed', 'degraded'
)),
entrypoint VARCHAR(500) NOT NULL,
last_health_check TIMESTAMPTZ,
health_message TEXT,
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_plugins_status ON release.plugins(status);
-- Plugin Instances (per-tenant configuration)
CREATE TABLE release.plugin_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plugin_id UUID NOT NULL REFERENCES release.plugins(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
config JSONB NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_plugin_instances_tenant ON release.plugin_instances(tenant_id);
-- Integration types (populated by plugins)
CREATE TABLE release.integration_types (
id TEXT PRIMARY KEY, -- "scm.github", "ci.jenkins"
plugin_id UUID REFERENCES release.plugins(id),
display_name TEXT NOT NULL,
description TEXT,
icon_url TEXT,
config_schema JSONB NOT NULL, -- JSON Schema for config
capabilities TEXT[] NOT NULL, -- ["repos", "webhooks", "status"]
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
API Endpoints
# Plugin Registry
GET /api/v1/plugins
Query: ?status={status}&capability={type}
Response: Plugin[]
GET /api/v1/plugins/{id}
Response: Plugin (with manifest)
POST /api/v1/plugins/{id}/enable
Response: Plugin
POST /api/v1/plugins/{id}/disable
Response: Plugin
GET /api/v1/plugins/{id}/health
Response: { status, message, diagnostics[] }
# Plugin Instances (per-tenant config)
POST /api/v1/plugin-instances
Body: { pluginId: UUID, config: object }
Response: PluginInstance
GET /api/v1/plugin-instances
Response: PluginInstance[]
PUT /api/v1/plugin-instances/{id}
Body: { config: object, enabled: boolean }
Response: PluginInstance
DELETE /api/v1/plugin-instances/{id}
Response: { deleted: true }
Plugin Security
Capability Declaration
Plugins must declare all required capabilities in their manifest. The system enforces:
- Network Access: Plugins can only access declared hosts
- Secret Access: Plugins receive secrets through controlled injection
- Database Access: No direct database access; API only
- Filesystem Access: Limited to declared paths
Sandbox Enforcement
// Plugin execution is sandboxed
class PluginSandbox {
async execute<T>(
plugin: Plugin,
operation: () => Promise<T>
): Promise<T> {
// 1. Verify capabilities
this.verifyCapabilities(plugin);
// 2. Set resource limits
const limits = this.getResourceLimits(plugin);
await this.applyLimits(limits);
// 3. Create isolated context
const context = await this.createIsolatedContext(plugin);
try {
// 4. Execute with timeout
return await this.withTimeout(
operation(),
plugin.manifest.timeouts.operationMs
);
} catch (error) {
// 5. Log and handle errors
await this.handlePluginError(plugin, error);
throw error;
} finally {
// 6. Cleanup
await context.dispose();
}
}
}
Plugin Failures Cannot Crash Core
// Core orchestration is protected from plugin failures
public sealed class PromotionDecisionEngine
{
public async Task<DecisionResult> EvaluateAsync(
Promotion promotion,
IReadOnlyList<IGateProvider> gates,
CancellationToken ct)
{
var results = new List<GateResult>();
foreach (var gate in gates)
{
try
{
// Plugin provides evaluation logic
var result = await gate.EvaluateAsync(promotion, ct);
results.Add(result);
}
catch (Exception ex)
{
// Plugin failure is logged but doesn't crash core
_logger.LogError(ex, "Gate {GateType} failed", gate.Type);
results.Add(new GateResult
{
GateType = gate.Type,
Status = GateStatus.Failed,
Message = $"Gate evaluation failed: {ex.Message}",
IsBlocking = gate.IsBlocking,
});
}
// Core decides how to aggregate (plugins cannot override)
if (results.Last().IsBlocking && _policy.FailFast)
break;
}
// Core makes final decision
return _decisionAggregator.Aggregate(results);
}
}