Back to Ruflo

ADR-015-v2: Unified Plugin System (Enhanced)

v3/implementation/adrs/ADR-015-v2-unified-plugin-system.md

3.6.3019.7 KB
Original Source

ADR-015-v2: Unified Plugin System (Enhanced)

Status

Accepted - Supersedes ADR-015

Date

2026-01-16

Context

ADR-015 established the unified plugin system with the microkernel pattern. This revision addresses identified gaps and adds production-ready features:

Issues Addressed from ADR-015 Review

IssueSeverityResolution
No version constraint enforcementMediumSemver range checking for dependencies
Missing dependent check on unloadMediumDependency graph prevents unsafe removal
No parallel initializationLowConfigurable parallel/sequential init
No plugin sandboxingMediumWorker thread isolation option
Service discoverabilityLowServiceContainer.list() method
No rollback on partial failureMediumTransaction-based initialization

New Requirements

  1. Plugin Collection/Marketplace - Shareable plugin packages with activation/deactivation
  2. Hot Reload - Update plugins without full restart
  3. Plugin Categories - Organized discovery and filtering
  4. Conflict Resolution - Handle duplicate tool/command names
  5. Permission Model - Fine-grained capability restrictions

Decision

Enhance the plugin system with the following additions:

1. Version Constraint System

typescript
interface PluginDependency {
  name: string;
  version: string;           // Semver range: "^3.0.0", ">=2.1.0 <3.0.0"
  optional?: boolean;        // Don't fail if missing
  peerDependency?: boolean;  // Expect host to provide
}

// In PluginMetadata
dependencies?: PluginDependency[];

Validation Algorithm:

typescript
function satisfiesVersion(required: string, actual: string): boolean {
  // Uses semver range matching
  // "^3.0.0" matches 3.x.x
  // "~3.1.0" matches 3.1.x
  // ">=2.0.0 <3.0.0" matches 2.x.x
}

2. Dependency Graph with Safe Unload

typescript
class DependencyGraph {
  private adjacencyList = new Map<string, Set<string>>();
  private reverseAdjacency = new Map<string, Set<string>>();

  addPlugin(name: string, dependencies: string[]): void;
  removePlugin(name: string): void;
  getDependents(name: string): string[];      // Who depends on this?
  getDependencies(name: string): string[];    // What does this depend on?
  getLoadOrder(): string[];                   // Topological sort
  canSafelyRemove(name: string): boolean;     // No dependents?
  getRemovalOrder(name: string): string[];    // Cascade unload order
}

Safe Unload Protocol:

typescript
async unregister(name: string, options?: UnregisterOptions): Promise<void> {
  const dependents = this.dependencyGraph.getDependents(name);

  if (dependents.length > 0) {
    if (options?.cascade) {
      // Unload dependents first (in reverse order)
      const order = this.dependencyGraph.getRemovalOrder(name);
      for (const dep of order) {
        await this.shutdownPlugin(dep);
      }
    } else if (options?.force) {
      this.logger.warn(`Force removing ${name}, breaking: ${dependents.join(', ')}`);
    } else {
      throw new Error(`Cannot remove ${name}: required by ${dependents.join(', ')}`);
    }
  }

  await this.shutdownPlugin(name);
}

3. Parallel Initialization

typescript
interface PluginRegistryConfig {
  // ... existing config
  initializationStrategy: 'sequential' | 'parallel' | 'parallel-safe';
  maxParallelInit?: number;  // Limit concurrent initializations
}

// Parallel-safe strategy:
// 1. Group plugins by dependency depth
// 2. Initialize each depth level in parallel
// 3. Wait for level N before starting level N+1

Implementation:

typescript
async initializeParallel(): Promise<void> {
  const levels = this.dependencyGraph.getDepthLevels();

  for (const level of levels) {
    const promises = level.map(name =>
      this.initializeWithTimeout(this.plugins.get(name)!)
    );

    const results = await Promise.allSettled(promises);

    // Handle failures - mark as error, continue with others
    for (let i = 0; i < results.length; i++) {
      if (results[i].status === 'rejected') {
        this.plugins.get(level[i])!.error = results[i].reason;
      }
    }
  }
}

4. Enhanced Service Container

typescript
interface ServiceContainer {
  get<T>(key: string): T | undefined;
  set<T>(key: string, value: T): void;
  has(key: string): boolean;
  delete(key: string): boolean;

  // New methods
  list(): string[];                           // All registered keys
  listByPrefix(prefix: string): string[];     // Filter by prefix
  getMetadata(key: string): ServiceMetadata | undefined;
  setWithMetadata<T>(key: string, value: T, metadata: ServiceMetadata): void;
}

interface ServiceMetadata {
  description?: string;
  provider: string;      // Plugin that registered it
  version?: string;
  deprecated?: boolean;
  replacement?: string;
}

5. Plugin Collection System

typescript
interface PluginCollection {
  readonly id: string;
  readonly name: string;
  readonly version: string;
  readonly description?: string;
  readonly author?: string;
  readonly plugins: PluginCollectionEntry[];
  readonly categories?: string[];
  readonly license?: string;
  readonly repository?: string;
}

interface PluginCollectionEntry {
  readonly plugin: IPlugin | PluginFactory;
  readonly defaultEnabled: boolean;
  readonly category: PluginCategory;
  readonly tags?: string[];
  readonly requiredCapabilities?: PluginCapability[];
}

type PluginCategory =
  | 'agent'        // Agent types and definitions
  | 'task'         // Task types and handlers
  | 'tool'         // MCP tools
  | 'memory'       // Memory backends
  | 'provider'     // LLM providers
  | 'hook'         // Lifecycle hooks
  | 'worker'       // Background workers
  | 'integration'  // External integrations
  | 'utility';     // General utilities

type PluginCapability =
  | 'network'      // Can make network requests
  | 'filesystem'   // Can access filesystem
  | 'subprocess'   // Can spawn processes
  | 'memory'       // Can store persistent data
  | 'llm'          // Can call LLM APIs
  | 'mcp';         // Can register MCP tools

6. Collection Manager

typescript
class PluginCollectionManager {
  private collections = new Map<string, PluginCollection>();
  private enabledPlugins = new Set<string>();
  private registry: PluginRegistry;

  // Collection management
  async loadCollection(collection: PluginCollection): Promise<void>;
  async unloadCollection(collectionId: string): Promise<void>;
  listCollections(): PluginCollection[];
  getCollection(id: string): PluginCollection | undefined;

  // Plugin activation
  async enablePlugin(collectionId: string, pluginName: string): Promise<void>;
  async disablePlugin(collectionId: string, pluginName: string): Promise<void>;
  isEnabled(collectionId: string, pluginName: string): boolean;

  // Bulk operations
  async enableCategory(category: PluginCategory): Promise<void>;
  async disableCategory(category: PluginCategory): Promise<void>;
  async enableAll(collectionId: string): Promise<void>;
  async disableAll(collectionId: string): Promise<void>;

  // Filtering
  getPluginsByCategory(category: PluginCategory): PluginCollectionEntry[];
  getPluginsByTag(tag: string): PluginCollectionEntry[];
  searchPlugins(query: string): PluginCollectionEntry[];

  // State persistence
  async saveState(path: string): Promise<void>;
  async loadState(path: string): Promise<void>;
  exportState(): CollectionManagerState;
  importState(state: CollectionManagerState): Promise<void>;
}

interface CollectionManagerState {
  version: string;
  collections: string[];
  enabledPlugins: Record<string, string[]>; // collectionId -> pluginNames
  settings: Record<string, Record<string, unknown>>; // plugin settings
}

7. Official Plugin Collections

typescript
// Core collection - essential plugins
const coreCollection: PluginCollection = {
  id: 'claude-flow-core',
  name: 'Claude Flow Core Plugins',
  version: '3.0.0',
  plugins: [
    { plugin: memoryPlugin, defaultEnabled: true, category: 'memory' },
    { plugin: agentDBPlugin, defaultEnabled: true, category: 'memory' },
    { plugin: sessionPlugin, defaultEnabled: true, category: 'hook' },
  ]
};

// Development collection - coding assistance
const developmentCollection: PluginCollection = {
  id: 'claude-flow-development',
  name: 'Development Tools',
  version: '3.0.0',
  plugins: [
    { plugin: coderAgentPlugin, defaultEnabled: true, category: 'agent' },
    { plugin: testerAgentPlugin, defaultEnabled: true, category: 'agent' },
    { plugin: reviewerAgentPlugin, defaultEnabled: false, category: 'agent' },
    { plugin: gitIntegrationPlugin, defaultEnabled: true, category: 'integration' },
    { plugin: linterPlugin, defaultEnabled: false, category: 'tool' },
  ]
};

// Intelligence collection - AI/ML features
const intelligenceCollection: PluginCollection = {
  id: 'claude-flow-intelligence',
  name: 'Intelligence & Learning',
  version: '3.0.0',
  plugins: [
    { plugin: sonaPlugin, defaultEnabled: false, category: 'integration' },
    { plugin: reasoningBankPlugin, defaultEnabled: false, category: 'memory' },
    { plugin: patternLearningPlugin, defaultEnabled: false, category: 'hook' },
  ]
};

// Swarm collection - multi-agent coordination
const swarmCollection: PluginCollection = {
  id: 'claude-flow-swarm',
  name: 'Swarm Coordination',
  version: '3.0.0',
  plugins: [
    { plugin: hiveMindPlugin, defaultEnabled: true, category: 'integration' },
    { plugin: maestroPlugin, defaultEnabled: true, category: 'integration' },
    { plugin: consensusPlugin, defaultEnabled: false, category: 'integration' },
    { plugin: coordinatorAgentPlugin, defaultEnabled: true, category: 'agent' },
  ]
};

// Security collection - security features
const securityCollection: PluginCollection = {
  id: 'claude-flow-security',
  name: 'Security & Audit',
  version: '3.0.0',
  plugins: [
    { plugin: inputValidationPlugin, defaultEnabled: true, category: 'hook' },
    { plugin: pathSecurityPlugin, defaultEnabled: true, category: 'hook' },
    { plugin: auditLogPlugin, defaultEnabled: false, category: 'hook' },
    { plugin: securityScanPlugin, defaultEnabled: false, category: 'tool' },
  ]
};

8. Hot Reload Support

typescript
interface HotReloadOptions {
  preserveState?: boolean;     // Keep plugin state across reload
  migrateState?: (oldState: unknown, newVersion: string) => unknown;
  timeout?: number;
}

class PluginRegistry {
  async reload(
    name: string,
    newPlugin: IPlugin | PluginFactory,
    options?: HotReloadOptions
  ): Promise<void> {
    const entry = this.plugins.get(name);
    if (!entry) throw new Error(`Plugin ${name} not found`);

    // Capture state if preserving
    let state: unknown;
    if (options?.preserveState && entry.plugin.getState) {
      state = await entry.plugin.getState();
    }

    // Shutdown old plugin
    await entry.plugin.shutdown();

    // Resolve and validate new plugin
    const resolved = typeof newPlugin === 'function' ? await newPlugin() : newPlugin;
    if (!validatePlugin(resolved)) {
      throw new Error('Invalid plugin replacement');
    }

    // Verify same name
    if (resolved.metadata.name !== name) {
      throw new Error(`Plugin name mismatch: expected ${name}, got ${resolved.metadata.name}`);
    }

    // Initialize new plugin
    const context = this.createPluginContext(entry);
    await this.initializeWithTimeout(resolved, context);

    // Restore state if applicable
    if (state && options?.migrateState) {
      state = options.migrateState(state, resolved.metadata.version);
    }
    if (state && resolved.setState) {
      await resolved.setState(state);
    }

    // Update entry
    entry.plugin = resolved;
    entry.initTime = new Date();
    entry.error = undefined;

    // Recollect extension points
    this.invalidateCaches();

    this.logger.info(`Plugin reloaded: ${name} -> v${resolved.metadata.version}`);
  }
}

9. Conflict Resolution

typescript
interface ConflictResolution {
  strategy: 'first' | 'last' | 'error' | 'namespace';
  namespaceTemplate?: string;  // e.g., "{plugin}:{name}"
}

interface PluginRegistryConfig {
  // ... existing config
  conflictResolution?: {
    mcpTools?: ConflictResolution;
    cliCommands?: ConflictResolution;
    agentTypes?: ConflictResolution;
    taskTypes?: ConflictResolution;
  };
}

// With namespace strategy, tools become:
// plugin-a:search, plugin-b:search
// Instead of conflicting "search" names

10. Permission Model

typescript
interface PluginPermissions {
  network?: boolean | { allowedHosts: string[] };
  filesystem?: boolean | { allowedPaths: string[], readOnly?: boolean };
  subprocess?: boolean | { allowedCommands: string[] };
  memory?: boolean | { maxSizeMb: number };
  llm?: boolean | { allowedModels: string[], maxTokensPerDay?: number };
  mcp?: boolean | { maxTools: number };
}

interface PluginConfig {
  // ... existing config
  permissions?: PluginPermissions;
}

// Permission enforcement in context
class SecurePluginContext implements PluginContext {
  private permissions: PluginPermissions;

  async fetch(url: string): Promise<Response> {
    if (!this.permissions.network) {
      throw new PermissionDeniedError('network access not permitted');
    }
    if (typeof this.permissions.network === 'object') {
      const host = new URL(url).hostname;
      if (!this.permissions.network.allowedHosts.includes(host)) {
        throw new PermissionDeniedError(`host ${host} not in allowlist`);
      }
    }
    return fetch(url);
  }
}

Implementation Plan

Phase 1: Core Fixes (Week 1)

  • Version constraint enforcement
  • Dependency graph with safe unload
  • Enhanced ServiceContainer
  • Parallel initialization

Phase 2: Collection System (Week 2)

  • PluginCollection interface
  • PluginCollectionManager
  • Official plugin collections
  • State persistence

Phase 3: Advanced Features (Week 3)

  • Hot reload support (in EnhancedPluginRegistry)
  • Conflict resolution (namespace strategy)
  • Permission model (types defined, enforcement TBD)
  • Worker thread isolation

Phase 4: Testing & Documentation (Week 4)

  • Unit tests (150+ test cases)
  • Integration tests
  • Performance benchmarks
  • Migration guide

Phase 5: CLI Integration (2026-01-24)

  • PluginManager for CLI (real npm installation)
  • Persist to .claude-flow/plugins/installed.json
  • Install, uninstall, upgrade, toggle commands
  • Local plugin installation support
  • Discovery service with npm fallback (IPFS demo mode)

Consequences

Positive

  1. Safe Plugin Management - Version constraints and dependency checks prevent broken states
  2. Scalable Initialization - Parallel init reduces startup time for many plugins
  3. Organized Discovery - Collection system enables marketplace-like experience
  4. Production Ready - Hot reload, permissions, and conflict resolution for real deployments
  5. Backward Compatible - Existing plugins work without changes

Negative

  1. Increased Complexity - More code to maintain
  2. Memory Overhead - Dependency graph and collection manager add ~2MB
  3. Migration Effort - Existing plugins should add version constraints

Neutral

  1. Optional Features - Advanced features can be disabled for simpler setups
  2. Configuration - More options to configure

Performance Targets

MetricADR-015ADR-015-v2
Plugin load time~20ms~20ms
Hook execution~0.5ms~0.5ms
Parallel init (10 plugins)N/A~100ms total
Dependency resolution~1ms~2ms (with graph)
Collection loadN/A~50ms
Hot reloadN/A~100ms

Test Coverage

Plugin Registry Tests       45 tests  (was 23)
Dependency Graph Tests      20 tests  (new)
Version Constraint Tests    15 tests  (new)
Collection Manager Tests    25 tests  (new)
Permission Model Tests      20 tests  (new)
Hot Reload Tests           15 tests  (new)
SDK Builder Tests          17 tests  (unchanged)
Security Module Tests      40 tests  (unchanged)
─────────────────────────────────────────────
Total                     197 tests

Migration from ADR-015

Minimal Migration (No Breaking Changes)

typescript
// Existing plugins work as-is
const myPlugin = new PluginBuilder('my-plugin', '1.0.0')
  .withMCPTools([...])
  .build();
typescript
// Add version constraints to dependencies
const myPlugin = new PluginBuilder('my-plugin', '1.0.0')
  .withDependencies([
    { name: 'core-plugin', version: '^3.0.0' },
    { name: 'optional-feature', version: '>=1.0.0', optional: true }
  ])
  .withMCPTools([...])
  .build();

Collection Migration

typescript
// Organize plugins into collections
const myCollection: PluginCollection = {
  id: 'my-collection',
  name: 'My Plugin Collection',
  version: '1.0.0',
  plugins: [
    { plugin: pluginA, defaultEnabled: true, category: 'tool' },
    { plugin: pluginB, defaultEnabled: false, category: 'agent' },
  ]
};

await collectionManager.loadCollection(myCollection);
  • ADR-015: Original unified plugin system (superseded)
  • ADR-001: agentic-flow as core foundation
  • ADR-004: Plugin-based architecture (microkernel pattern)
  • ADR-005: MCP-first API design
  • ADR-006: Unified memory service

Implementation Notes (2026-01-24)

What Works

FeatureStatusNotes
plugins install --name <pkg>WorkingInstalls from npm, persists to manifest
plugins list --installedWorkingReads from persisted manifest
plugins uninstall --name <pkg>WorkingRemoves from npm and manifest
plugins toggle --name <pkg>WorkingEnable/disable persists to manifest
plugins upgrade --name <pkg>WorkingUpgrades via npm
plugins list (registry)WorkingShows available plugins (demo + real npm stats)
plugins searchWorkingSearches plugin registry
plugins infoWorkingShows detailed plugin info
Local plugin installWorkingplugins install --name ./path/to/plugin

Demo Mode (Not Yet Production)

FeatureStatusNotes
IPFS RegistryDemoCIDs are placeholders, falls back to hardcoded list
IPNS ResolutionDemoReturns demo registry, npm stats are real
Plugin Signature VerificationDemoChecks format only, no real crypto verification
Dynamic CLI Command RegistrationTBDPlugins can't yet add new CLI commands at runtime
Hook IntegrationTBDPlugin hooks not yet loaded by CLI

Architecture

CLI Commands (plugins.ts)
    │
    └── PluginManager (manager.ts)
            │
            ├── InstalledPlugins manifest (.claude-flow/plugins/installed.json)
            ├── npm install/uninstall
            └── PluginDiscoveryService (discovery.ts)
                    │
                    ├── Demo Registry (hardcoded, with real npm stats)
                    └── IPFS/IPNS (demo mode, returns demo registry)

Migration Path

  1. Current: Plugins install via npm, persist state locally
  2. Next: Dynamic command/hook registration from installed plugins
  3. Future: Real IPFS registry with signature verification

References