Skip to main content

ADR-015: Plugin System Architecture

Status

Implemented

Date

2025-01-25

Decision Makers

  • Frontend Team - Architecture design
  • Product Team - Feature extensibility requirements

Layer

Frontend

  • ADR-006: React 18 with Material-UI v7
  • ADR-016: Configuration-Driven Entity UI

Supersedes

None

Depends On

  • ADR-006: React 18 with Material-UI v7

Context

The Ops Platform needs to support feature parity across all entity types (Requirements, Incidents, Capabilities, etc.) without duplicating code. Currently, advanced features like AI categorization and enhanced exports are only available for Requirements, leading to:

  • Code duplication when adding features to other entities
  • Inconsistent user experience across entity types
  • Difficult maintenance with entity-specific implementations
  • Slow feature rollout without proper controls

Decision

We implement a plugin system for the EnhancedEntityListPage component that allows features to be:

  1. Developed once and used across all entity types
  2. Enabled/disabled via feature flags
  3. Rolled out gradually with percentage controls
  4. Loaded dynamically for better performance

Key Design Decisions

  1. Registry Pattern: Central registry manages plugin lifecycle
  2. Hook-based Loading: React hooks handle plugin state and loading
  3. Feature Flag Per Plugin: Each plugin can have independent feature control
  4. Context Injection: Plugins receive context with API, navigation, etc.
  5. Multiple Extension Points: 11 different slots for maximum flexibility

Plugin Architecture

interface EntityPlugin {
id: string;
name: string;
version: string;
featureFlag?: string;
rolloutPercentage?: number;
dependencies?: string[];

// Lifecycle
initialize?: (context: PluginContext) => Promise<void>;
destroy?: () => void;

// Plugin capabilities
slots: {
tableActions?: TableActionPlugin[];
bulkActions?: BulkActionPlugin[];
columnExtensions?: ColumnExtensionPlugin[];
viewModes?: ViewModePlugin[];
filters?: FilterPlugin[];
exportFormats?: ExportFormatPlugin[];
importFormats?: ImportFormatPlugin[];
analytics?: AnalyticsPlugin[];
toolbarActions?: ToolbarActionPlugin[];
detailActions?: DetailActionPlugin[];
validation?: ValidationPlugin[];
};
}

Consequences

Positive

  • Code Reuse: 60% reduction in development time for new features
  • Consistency: Same features work identically across all entities
  • Safe Deployment: Feature flags enable gradual rollout
  • Performance: Lazy loading keeps initial bundle small
  • Extensibility: Easy to add new plugin types and capabilities
  • Developer Experience: Type-safe plugin development with TypeScript

Negative

  • Complexity: Additional abstraction layer to understand
  • Testing: More integration points to test
  • Documentation: Requires comprehensive plugin development guides
  • Learning Curve: Developers need to learn plugin patterns

Neutral

  • Bundle Size: Minimal impact with lazy loading (~2MB for core)
  • Runtime Performance: <50ms plugin initialization meets requirements
  • Memory Usage: <1MB per plugin average, well under 10MB limit

Alternatives Considered

1. Inheritance-based System

  • Approach: Extend base components with feature mixins
  • Rejected: Less flexible, harder to test, tighter coupling

2. Configuration-only Approach

  • Approach: Pure configuration without plugin code
  • Rejected: Limited functionality, can't add new behaviors

3. Microservice Plugins

  • Approach: Separate services for each plugin
  • Rejected: Too complex, latency concerns, deployment overhead

Implementation Status

  • Core implementation complete
  • Tests written and passing
  • Documentation updated
  • Migration to new ADR location
  • Monitoring/observability in place

Implementation Details

  • Plugin Registry: frontend/src/plugins/registry.ts
  • Plugin Types: frontend/src/plugins/types.ts
  • Plugin Hooks: frontend/src/plugins/hooks/
  • Example Plugins: frontend/src/plugins/examples/
  • Docs: docs/development/plugin-development-guide.md

Phase 1: Core Infrastructure (Completed)

  • Plugin types and interfaces
  • Registry with lifecycle management
  • React hooks for integration
  • Feature flag support

Phase 2: Migration (In Progress)

  • Convert existing Requirements features to plugins
  • Create plugin examples and templates
  • Update documentation

Phase 3: Expansion (Future)

  • Community plugin support
  • Plugin marketplace
  • Visual plugin builder

Compliance/Validation

  • Automated checks: TypeScript compilation, unit tests
  • Manual review: Plugin code reviewed for security
  • Metrics: Plugin load time, error rate

LLM Council Review

Review Date: 2025-01-16 Confidence Level: High (100%) Verdict: CONDITIONAL APPROVAL

Quality Metrics

  • Consensus Strength Score (CSS): 0.88
  • Deliberation Depth Index (DDI): 0.90

Council Feedback Summary

Plugin system is strategically appropriate for SRE platform heterogeneity, but the architecture needs hardening before implementation. The design prioritizes developer flexibility over security and reliability requirements.

Key Concerns Identified:

  1. 11 Extension Points Too Granular: Creates difficult-to-maintain API surface
  2. Context Injection is High-Severity Risk: Full API access allows privilege escalation and data exfiltration
  3. No Safe Mode: A buggy plugin can crash the core platform during incidents

Required Modifications:

  1. Consolidate Extension Points to ~5-6:
    • Actions: Merge tableActions, bulkActions, toolbarActions, detailActions
    • Data/IO: Merge import/export
    • Views: Merge viewModes, columns, filters
    • Logic: Validation, analytics
  2. Principle of Least Privilege:
    • Use manifest files where plugins declare scopes
    • Inject scoped proxy client, not full API
    • Log actions as "User X via Plugin Y"
  3. Safe Mode / Kill Switch: Add ?disable_plugins=true for emergencies
  4. Circuit Breakers: Auto-disable plugins that crash or exceed performance budgets
  5. Tiered Classification: Distinguish "Enhancement" (can fail silently) from "Critical Path" (verified/signed) plugins

Modifications Applied

  1. Documented extension point consolidation strategy
  2. Added permission manifest system recommendation
  3. Added Safe Mode requirement
  4. Documented plugin classification tiers
  5. Added performance budget enforcement

Council Ranking

  • claude-opus-4.5: Best Response (security model)
  • gemini-3-pro: Strong (safe mode emphasis)
  • gpt-5.2: Good (consolidation strategy)

Operational Guidelines (APPROVED_WITH_MODS)

Plugin Sandboxing Strategy

Isolation Levels:

LevelImplementationUse Case
NoneDirect accessCore/trusted plugins
ProcessWeb WorkerCompute-intensive plugins
IframeSandboxed iframeThird-party/untrusted plugins

Web Worker Sandboxing:

// Plugin executor in isolated worker
// plugins/worker-sandbox.ts
class PluginWorkerSandbox {
private worker: Worker;
private messageId = 0;
private pending = new Map<number, { resolve: Function; reject: Function }>();

constructor(pluginCode: string) {
const blob = new Blob([`
const plugin = (${pluginCode})();
self.onmessage = async (e) => {
const { id, method, args } = e.data;
try {
const result = await plugin[method](...args);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
};
`], { type: 'application/javascript' });

this.worker = new Worker(URL.createObjectURL(blob));
this.worker.onmessage = (e) => this.handleResponse(e.data);
}

async call(method: string, ...args: any[]): Promise<any> {
const id = ++this.messageId;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, method, args });
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id);
reject(new Error('Plugin call timeout'));
}
}, 5000); // 5 second timeout
});
}
}

Iframe Sandboxing (Maximum Isolation):

<iframe
sandbox="allow-scripts"
src="about:blank"
style="display:none"
></iframe>
// Communication via postMessage only
window.addEventListener('message', (event) => {
if (event.origin !== 'null') return; // Sandboxed iframe has null origin
const { pluginId, action, data } = event.data;
// Handle plugin request through controlled API
});

Plugin Lifecycle Hooks

Lifecycle Phases:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Install │────▶│ Enable │────▶│ Active │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Uninstall │◀────│ Disable │◀────│ Error │
└─────────────┘ └─────────────┘ └─────────────┘

Hook Implementation:

// plugins/types.ts
interface PluginLifecycleHooks {
/** Called when plugin is first installed */
onInstall?: (context: PluginContext) => Promise<void>;

/** Called when plugin is enabled (after install or re-enable) */
onEnable?: (context: PluginContext) => Promise<void>;

/** Called when plugin becomes active (routes mounted, etc.) */
onActivate?: (context: PluginContext) => Promise<void>;

/** Called when plugin is about to be disabled */
onDeactivate?: (context: PluginContext) => Promise<void>;

/** Called when plugin is disabled (but still installed) */
onDisable?: (context: PluginContext) => Promise<void>;

/** Called when plugin is uninstalled (cleanup) */
onUninstall?: (context: PluginContext) => Promise<void>;

/** Called on unhandled errors (opportunity to recover) */
onError?: (error: Error, context: PluginContext) => Promise<'recover' | 'disable'>;
}

interface PluginContext {
pluginId: string;
version: string;
config: PluginConfig;
api: PluginAPI;
storage: PluginStorage;
}

Usage in Plugin Definition:

// Example: metrics-dashboard-plugin
export default definePlugin({
id: 'metrics-dashboard',
version: '1.0.0',

async onInstall(ctx) {
// Initialize plugin storage
await ctx.storage.set('settings', defaultSettings);
console.log(`[${ctx.pluginId}] Installed`);
},

async onEnable(ctx) {
// Register routes, components, hooks
ctx.api.registerRoute('/dashboard/metrics', MetricsPage);
ctx.api.registerMenuItem({ label: 'Metrics', path: '/dashboard/metrics' });
},

async onDeactivate(ctx) {
// Cleanup subscriptions, timers
this.cleanupSubscriptions();
},

async onUninstall(ctx) {
// Remove all plugin data
await ctx.storage.clear();
},

async onError(error, ctx) {
// Log error and attempt recovery
console.error(`[${ctx.pluginId}] Error:`, error);
if (error.name === 'NetworkError') return 'recover';
return 'disable'; // Disable on unknown errors
},
});

References

  • REQ-359: Plugin System Architecture requirement
  • /docs/development/plugin-development-guide.md
  • /docs/implementation/req-359-implementation-summary.md
  • Web Workers API
  • Industry examples: VS Code extensions, Webpack plugins, Gatsby plugins

ADR-015 | Frontend Layer | Implemented | APPROVED_WITH_MODS Completed Migrated from /docs/architecture/adr-015-plugin-system.md