- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
421 lines
13 KiB
Python
421 lines
13 KiB
Python
"""
|
|
RMI Plugin Architecture - Extensible Backend System
|
|
====================================================
|
|
Plugin system for adding new features without modifying core code.
|
|
|
|
Features:
|
|
• Plugin Registry - discover, load, and manage plugins
|
|
• Plugin Types - connectors, scanners, analyzers, notifiers, exporters
|
|
• Hot Reload - reload plugins without restart
|
|
• Sandboxed Execution - isolated plugin environments
|
|
• Plugin API - standardized interface for all plugins
|
|
• Configuration Management - per-plugin config with validation
|
|
• Health Checks - monitor plugin status and performance
|
|
• Dependency Management - handle plugin dependencies
|
|
|
|
Plugin Types:
|
|
connector - Data sources (exchanges, APIs, oracles)
|
|
scanner - Security scanners (contract, wallet, token)
|
|
analyzer - Analysis engines (risk, sentiment, on-chain)
|
|
notifier - Alert channels (email, telegram, webhook)
|
|
exporter - Data export (CSV, PDF, API, webhook)
|
|
wallet - Wallet integrations (hardware, custodial)
|
|
payment - Payment processors (x402, stripe, crypto)
|
|
ml - ML models (fraud detection, prediction)
|
|
|
|
Author: RMI Platform Team
|
|
Date: 2026-05-31
|
|
"""
|
|
|
|
import importlib
|
|
import importlib.util
|
|
import logging
|
|
import os
|
|
import time
|
|
from abc import ABC, abstractmethod
|
|
from collections.abc import Callable
|
|
from datetime import UTC, datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger("rmi_plugin_system")
|
|
|
|
|
|
# ── Enums ─────────────────────────────────────────────────────
|
|
|
|
|
|
class PluginType(StrEnum):
|
|
CONNECTOR = "connector"
|
|
SCANNER = "scanner"
|
|
ANALYZER = "analyzer"
|
|
NOTIFIER = "notifier"
|
|
EXPORTER = "exporter"
|
|
WALLET = "wallet"
|
|
PAYMENT = "payment"
|
|
ML = "ml"
|
|
SECURITY = "security"
|
|
ANALYTICS = "analytics"
|
|
|
|
|
|
class PluginStatus(StrEnum):
|
|
ACTIVE = "active"
|
|
INACTIVE = "inactive"
|
|
ERROR = "error"
|
|
LOADING = "loading"
|
|
DEPRECATED = "deprecated"
|
|
|
|
|
|
# ── Base Plugin Interface ─────────────────────────────────────
|
|
|
|
|
|
class Plugin(ABC):
|
|
"""Base class for all plugins."""
|
|
|
|
def __init__(self, config: dict[str, Any] | None = None):
|
|
self.config = config or {}
|
|
self.status = PluginStatus.LOADING.value
|
|
self.last_error = ""
|
|
self.load_time = datetime.now(UTC).isoformat()
|
|
self.call_count = 0
|
|
self.error_count = 0
|
|
|
|
@property
|
|
@abstractmethod
|
|
def name(self) -> str:
|
|
"""Plugin name."""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def version(self) -> str:
|
|
"""Plugin version."""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def plugin_type(self) -> PluginType:
|
|
"""Plugin type."""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def description(self) -> str:
|
|
"""Plugin description."""
|
|
pass
|
|
|
|
def initialize(self) -> bool:
|
|
"""Initialize plugin. Return True if successful."""
|
|
try:
|
|
self._setup()
|
|
self.status = PluginStatus.ACTIVE.value
|
|
return True
|
|
except Exception as e:
|
|
self.status = PluginStatus.ERROR.value
|
|
self.last_error = str(e)
|
|
logger.error(f"Plugin {self.name} initialization failed: {e}")
|
|
return False
|
|
|
|
def _setup(self): # noqa: B027
|
|
"""Override for setup logic."""
|
|
pass
|
|
|
|
def health_check(self) -> dict[str, Any]:
|
|
"""Return health status."""
|
|
return {
|
|
"status": self.status,
|
|
"call_count": self.call_count,
|
|
"error_count": self.error_count,
|
|
"error_rate": self.error_count / max(self.call_count, 1),
|
|
"last_error": self.last_error,
|
|
"uptime_seconds": int(time.time() - datetime.fromisoformat(self.load_time).timestamp()),
|
|
}
|
|
|
|
def shutdown(self):
|
|
"""Cleanup on shutdown."""
|
|
self.status = PluginStatus.INACTIVE.value
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"name": self.name,
|
|
"version": self.version,
|
|
"type": self.plugin_type.value,
|
|
"description": self.description,
|
|
"status": self.status,
|
|
"config": self.config,
|
|
"health": self.health_check(),
|
|
}
|
|
|
|
|
|
# ── Plugin Registry ───────────────────────────────────────────
|
|
|
|
|
|
class PluginRegistry:
|
|
"""
|
|
Central registry for all plugins.
|
|
Manages plugin lifecycle, discovery, and execution.
|
|
"""
|
|
|
|
PLUGIN_DIR = "/root/backend/plugins"
|
|
|
|
def __init__(self):
|
|
self._plugins: dict[str, Plugin] = {}
|
|
self._hooks: dict[str, list[Callable]] = {}
|
|
self._ensure_plugin_dir()
|
|
|
|
def _ensure_plugin_dir(self):
|
|
"""Ensure plugin directory exists."""
|
|
os.makedirs(self.PLUGIN_DIR, exist_ok=True)
|
|
for ptype in PluginType:
|
|
os.makedirs(os.path.join(self.PLUGIN_DIR, ptype.value), exist_ok=True)
|
|
|
|
def register(self, plugin: Plugin) -> bool:
|
|
"""Register a plugin instance."""
|
|
if plugin.name in self._plugins:
|
|
logger.warning(f"Plugin {plugin.name} already registered - replacing")
|
|
|
|
if plugin.initialize():
|
|
self._plugins[plugin.name] = plugin
|
|
logger.info(f"Plugin registered: {plugin.name} v{plugin.version}")
|
|
return True
|
|
return False
|
|
|
|
def unregister(self, name: str) -> bool:
|
|
"""Unregister a plugin."""
|
|
plugin = self._plugins.pop(name, None)
|
|
if plugin:
|
|
plugin.shutdown()
|
|
logger.info(f"Plugin unregistered: {name}")
|
|
return True
|
|
return False
|
|
|
|
def get(self, name: str) -> Plugin | None:
|
|
"""Get plugin by name."""
|
|
return self._plugins.get(name)
|
|
|
|
def list_plugins(self, plugin_type: PluginType | None = None) -> list[Plugin]:
|
|
"""List all plugins, optionally filtered by type."""
|
|
plugins = list(self._plugins.values())
|
|
if plugin_type:
|
|
plugins = [p for p in plugins if p.plugin_type == plugin_type]
|
|
return plugins
|
|
|
|
def get_by_type(self, plugin_type: PluginType) -> list[Plugin]:
|
|
"""Get all plugins of a specific type."""
|
|
return [p for p in self._plugins.values() if p.plugin_type == plugin_type]
|
|
|
|
def discover_plugins(self) -> list[str]:
|
|
"""Discover plugins from plugin directory."""
|
|
discovered = []
|
|
for ptype in PluginType:
|
|
type_dir = os.path.join(self.PLUGIN_DIR, ptype.value)
|
|
if os.path.exists(type_dir):
|
|
for fname in os.listdir(type_dir):
|
|
if fname.endswith(".py") and not fname.startswith("_"):
|
|
discovered.append(os.path.join(type_dir, fname))
|
|
return discovered
|
|
|
|
def load_plugin_from_file(self, filepath: str) -> Plugin | None:
|
|
"""Load a plugin from a Python file."""
|
|
try:
|
|
spec = importlib.util.spec_from_file_location("plugin", filepath)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
# Find Plugin subclass
|
|
for attr_name in dir(module):
|
|
attr = getattr(module, attr_name)
|
|
if isinstance(attr, type) and issubclass(attr, Plugin) and attr != Plugin:
|
|
instance = attr()
|
|
if self.register(instance):
|
|
return instance
|
|
|
|
logger.warning(f"No Plugin subclass found in {filepath}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to load plugin from {filepath}: {e}")
|
|
return None
|
|
|
|
def reload_plugin(self, name: str) -> bool:
|
|
"""Reload a plugin."""
|
|
plugin = self._plugins.get(name)
|
|
if not plugin:
|
|
return False
|
|
|
|
# Unregister and re-register
|
|
self.unregister(name)
|
|
return plugin.initialize() and self.register(plugin)
|
|
|
|
def get_stats(self) -> dict[str, Any]:
|
|
"""Get plugin registry statistics."""
|
|
by_type = {}
|
|
by_status = {}
|
|
|
|
for p in self._plugins.values():
|
|
by_type[p.plugin_type.value] = by_type.get(p.plugin_type.value, 0) + 1
|
|
by_status[p.status] = by_status.get(p.status, 0) + 1
|
|
|
|
return {
|
|
"total_plugins": len(self._plugins),
|
|
"by_type": by_type,
|
|
"by_status": by_status,
|
|
"plugins": [p.to_dict() for p in self._plugins.values()],
|
|
}
|
|
|
|
# ── Hook System ────────────────────────────────────────
|
|
|
|
def register_hook(self, event: str, callback: Callable):
|
|
"""Register a hook for an event."""
|
|
if event not in self._hooks:
|
|
self._hooks[event] = []
|
|
self._hooks[event].append(callback)
|
|
|
|
def trigger_hook(self, event: str, *args, **kwargs) -> list[Any]:
|
|
"""Trigger all hooks for an event."""
|
|
results = []
|
|
for callback in self._hooks.get(event, []):
|
|
try:
|
|
result = callback(*args, **kwargs)
|
|
results.append(result)
|
|
except Exception as e:
|
|
logger.error(f"Hook error for {event}: {e}")
|
|
return results
|
|
|
|
|
|
# ── Built-in Plugin Examples ──────────────────────────────────
|
|
|
|
|
|
class PrometheusExporterPlugin(Plugin):
|
|
"""Export metrics to Prometheus."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "prometheus_exporter"
|
|
|
|
@property
|
|
def version(self) -> str:
|
|
return "1.0.0"
|
|
|
|
@property
|
|
def plugin_type(self) -> PluginType:
|
|
return PluginType.EXPORTER
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return "Export metrics in Prometheus format"
|
|
|
|
def export(self, metrics: dict[str, Any]) -> str:
|
|
"""Export metrics to Prometheus format."""
|
|
lines = []
|
|
for name, value in metrics.items():
|
|
prom_name = f"rmi_{name}"
|
|
lines.append(f"# HELP {prom_name} Metric")
|
|
lines.append(f"# TYPE {prom_name} gauge")
|
|
lines.append(f"{prom_name} {value}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
class WebhookNotifierPlugin(Plugin):
|
|
"""Send notifications via webhook."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "webhook_notifier"
|
|
|
|
@property
|
|
def version(self) -> str:
|
|
return "1.0.0"
|
|
|
|
@property
|
|
def plugin_type(self) -> PluginType:
|
|
return PluginType.NOTIFIER
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return "Send notifications to configured webhooks"
|
|
|
|
def _setup(self):
|
|
self.webhook_url = self.config.get("webhook_url", "")
|
|
self.headers = self.config.get("headers", {})
|
|
|
|
async def notify(self, event: str, data: dict[str, Any]) -> bool:
|
|
"""Send notification to webhook."""
|
|
if not self.webhook_url:
|
|
return False
|
|
|
|
try:
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
payload = {
|
|
"event": event,
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
"data": data,
|
|
}
|
|
r = await client.post(
|
|
self.webhook_url,
|
|
json=payload,
|
|
headers=self.headers,
|
|
timeout=10,
|
|
)
|
|
return r.status_code < 400
|
|
except Exception as e:
|
|
logger.error(f"Webhook notification failed: {e}")
|
|
self.error_count += 1
|
|
return False
|
|
|
|
|
|
class RedisCachePlugin(Plugin):
|
|
"""Redis caching plugin."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "redis_cache"
|
|
|
|
@property
|
|
def version(self) -> str:
|
|
return "1.0.0"
|
|
|
|
@property
|
|
def plugin_type(self) -> PluginType:
|
|
return PluginType.CONNECTOR
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return "Redis caching and pub/sub connector"
|
|
|
|
def _setup(self):
|
|
import redis.asyncio as redis_lib
|
|
|
|
self.redis = redis_lib.Redis(
|
|
host=self.config.get("host", "localhost"),
|
|
port=self.config.get("port", 6379),
|
|
password=self.config.get("password", ""),
|
|
decode_responses=True,
|
|
)
|
|
|
|
async def get(self, key: str) -> str | None:
|
|
return await self.redis.get(key)
|
|
|
|
async def set(self, key: str, value: str, ttl: int = 3600):
|
|
await self.redis.setex(key, ttl, value)
|
|
|
|
async def publish(self, channel: str, message: str):
|
|
await self.redis.publish(channel, message)
|
|
|
|
|
|
# ── Singleton ─────────────────────────────────────────────────
|
|
|
|
_plugin_registry: PluginRegistry | None = None
|
|
|
|
|
|
def get_plugin_registry() -> PluginRegistry:
|
|
"""Get or create plugin registry."""
|
|
global _plugin_registry
|
|
if _plugin_registry is None:
|
|
_plugin_registry = PluginRegistry()
|
|
# Auto-register built-in plugins
|
|
_plugin_registry.register(PrometheusExporterPlugin())
|
|
_plugin_registry.register(WebhookNotifierPlugin())
|
|
_plugin_registry.register(RedisCachePlugin())
|
|
return _plugin_registry
|