# 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**: ```typescript 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**: ```typescript interface PluginLoader { // Discovery discover(): Promise; refresh(): Promise; // Lifecycle load(pluginId: string): Promise; start(pluginId: string): Promise; stop(pluginId: string): Promise; unload(pluginId: string): Promise; restart(pluginId: string): Promise; // Health checkHealth(pluginId: string): Promise; getStatus(pluginId: string): Promise; // Hot reload reload(pluginId: string): Promise; } ``` --- ### Module: `plugin-sandbox` | Aspect | Specification | |--------|---------------| | **Responsibility** | Isolation; resource limits; security | | **Enforcement** | Process isolation, capability-based security | **Sandbox Configuration**: ```typescript 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**: ```typescript 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**: ```typescript // Base plugin interface interface StellaPlugin { // Lifecycle initialize(config: PluginConfig): Promise; start(): Promise; stop(): Promise; dispose(): Promise; // Health getHealth(): Promise; // Metadata getManifest(): PluginManifest; } // Connector plugin interface interface ConnectorPlugin extends StellaPlugin { createConnector(config: ConnectorConfig): Promise; } // Step provider plugin interface interface StepProviderPlugin extends StellaPlugin { getStepTypes(): StepType[]; executeStep( stepType: string, config: StepConfig, inputs: StepInputs, context: StepContext ): AsyncGenerator; } // Gate provider plugin interface interface GateProviderPlugin extends StellaPlugin { getGateTypes(): GateType[]; evaluateGate( gateType: string, config: GateConfig, context: GateContext ): Promise; } ``` --- ## 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) ```yaml # plugin.stella.yaml plugin: id: "com.example.jenkins-connector" version: "1.0.0" vendor: "Example Corp" license: "BUSL-1.1" 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: ```typescript // Jenkins connector implementation class JenkinsConnector implements CIConnector { private client: JenkinsClient; async initialize(config: ConnectorConfig, secrets: SecretHandle[]): Promise { const apiToken = await this.getSecret(secrets, "api_token"); this.client = new JenkinsClient({ baseUrl: config.endpoint, username: config.username, apiToken: apiToken, }); } async testConnection(): Promise { 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 { 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 { const queueItem = await this.client.build(pipelineId, params); return { id: queueItem.id.toString(), pipelineId, status: "queued", startedAt: new Date(), }; } async getPipelineRun(runId: string): Promise { 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: ```typescript // Jenkins trigger step implementation class JenkinsTriggerStep implements StepExecutor { async *execute( config: StepConfig, inputs: StepInputs, context: StepContext ): AsyncGenerator { const connector = await context.getConnector(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 ```sql -- 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 ```yaml # 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: 1. **Network Access**: Plugins can only access declared hosts 2. **Secret Access**: Plugins receive secrets through controlled injection 3. **Database Access**: No direct database access; API only 4. **Filesystem Access**: Limited to declared paths ### Sandbox Enforcement ```typescript // Plugin execution is sandboxed class PluginSandbox { async execute( plugin: Plugin, operation: () => Promise ): Promise { // 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 ```csharp // Core orchestration is protected from plugin failures public sealed class PromotionDecisionEngine { public async Task EvaluateAsync( Promotion promotion, IReadOnlyList gates, CancellationToken ct) { var results = new List(); 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); } } ``` --- ## References - [Module Overview](overview.md) - [Integration Hub](integration-hub.md) - [Workflow Engine](workflow-engine.md) - [Connector Interface](../integrations/connectors.md)