Files
git.stella-ops.org/docs/modules/release-orchestrator/modules/plugin-system.md

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:

  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

// 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);
    }
}

References