""" Plugin Auto-Discovery Service Automatically discovers and registers plugins from the /plugins directory on startup """ import asyncio import os import uuid import hashlib from pathlib import Path from typing import Dict, List, Optional, Any from datetime import datetime, timezone from sqlalchemy.orm import Session from app.core.config import settings from app.core.logging import get_logger from app.models.plugin import Plugin from app.db.database import SessionLocal from app.schemas.plugin_manifest import validate_manifest_file from app.services.plugin_database import plugin_db_manager from app.services.plugin_sandbox import plugin_loader from app.utils.exceptions import PluginError logger = get_logger("plugin.autodiscovery") class PluginAutoDiscovery: """Service for automatically discovering and registering plugins""" def __init__(self, plugins_dir: str = None): self.plugins_dir = Path(plugins_dir or settings.PLUGINS_DIR) self.discovered_plugins: Dict[str, Dict[str, Any]] = {} async def scan_plugins_directory(self) -> List[Dict[str, Any]]: """Scan plugins directory for valid plugin manifests""" logger.info(f"Scanning plugins directory: {self.plugins_dir}") if not self.plugins_dir.exists(): logger.warning(f"Plugins directory does not exist: {self.plugins_dir}") return [] discovered = [] try: for item in self.plugins_dir.iterdir(): if item.is_dir(): plugin_info = await self._discover_plugin(item) if plugin_info: discovered.append(plugin_info) self.discovered_plugins[plugin_info['slug']] = plugin_info logger.info(f"Discovered {len(discovered)} plugins in directory") return discovered except Exception as e: logger.error(f"Error scanning plugins directory: {e}") return [] async def _discover_plugin(self, plugin_path: Path) -> Optional[Dict[str, Any]]: """Discover and validate a single plugin""" try: manifest_path = plugin_path / "manifest.yaml" if not manifest_path.exists(): logger.debug(f"No manifest found in {plugin_path.name}") return None # Validate manifest validation_result = validate_manifest_file(manifest_path) if not validation_result["valid"]: logger.warning(f"Invalid manifest for plugin {plugin_path.name}: {validation_result['errors']}") return None manifest = validation_result["manifest"] # Check if main.py exists main_py_path = plugin_path / "main.py" if not main_py_path.exists(): logger.warning(f"No main.py found for plugin {plugin_path.name}") return None # Generate hashes for plugin integrity manifest_hash = hashlib.sha256(manifest_path.read_bytes()).hexdigest() package_hash = hashlib.sha256(str(plugin_path).encode()).hexdigest() # Convert manifest to JSON-serializable format import json manifest_dict = json.loads(manifest.json()) plugin_info = { "slug": manifest.metadata.name, # Use slug for string identifier "name": manifest.metadata.name, "display_name": manifest.metadata.description, "version": manifest.metadata.version, "description": manifest.metadata.description, "author": manifest.metadata.author, "manifest_data": manifest_dict, # Use JSON-serialized version "plugin_path": str(plugin_path), "manifest_path": str(manifest_path), "main_py_path": str(main_py_path), "manifest_hash": manifest_hash, "package_hash": package_hash, "discovered_at": datetime.now(timezone.utc) } logger.info(f"Discovered plugin: {manifest.metadata.name} v{manifest.metadata.version}") return plugin_info except Exception as e: logger.error(f"Error discovering plugin at {plugin_path}: {e}") return None async def register_discovered_plugins(self) -> Dict[str, bool]: """Register all discovered plugins in the database""" logger.info("Registering discovered plugins in database...") registration_results = {} db = SessionLocal() try: for plugin_slug, plugin_info in self.discovered_plugins.items(): try: success = await self._register_single_plugin(db, plugin_info) registration_results[plugin_slug] = success if success: logger.info(f"Plugin {plugin_slug} registered successfully") else: logger.warning(f"Failed to register plugin {plugin_slug}") except Exception as e: logger.error(f"Error registering plugin {plugin_slug}: {e}") registration_results[plugin_slug] = False finally: db.close() successful_registrations = sum(1 for success in registration_results.values() if success) logger.info(f"Plugin registration complete: {successful_registrations}/{len(registration_results)} successful") return registration_results async def _register_single_plugin(self, db: Session, plugin_info: Dict[str, Any]) -> bool: """Register a single plugin in the database""" try: plugin_slug = plugin_info["slug"] # Check if plugin already exists by slug existing_plugin = db.query(Plugin).filter(Plugin.slug == plugin_slug).first() if existing_plugin: # Update existing plugin if version is different if existing_plugin.version != plugin_info["version"]: logger.info(f"Updating plugin {plugin_slug}: {existing_plugin.version} -> {plugin_info['version']}") existing_plugin.version = plugin_info["version"] existing_plugin.description = plugin_info["description"] existing_plugin.author = plugin_info["author"] existing_plugin.manifest_data = plugin_info["manifest_data"] existing_plugin.package_path = plugin_info["plugin_path"] existing_plugin.manifest_hash = plugin_info["manifest_hash"] existing_plugin.package_hash = plugin_info["package_hash"] existing_plugin.last_updated_at = datetime.now(timezone.utc) db.commit() # Update plugin schema await self._setup_plugin_database(plugin_slug, plugin_info) return True else: logger.debug(f"Plugin {plugin_slug} already up to date") return True else: # Create new plugin record logger.info(f"Installing new plugin {plugin_slug}") plugin = Plugin( id=uuid.uuid4(), # Generate UUID for primary key name=plugin_info["name"], slug=plugin_info["slug"], display_name=plugin_info["display_name"], version=plugin_info["version"], description=plugin_info["description"], author=plugin_info["author"], manifest_data=plugin_info["manifest_data"], package_path=plugin_info["plugin_path"], manifest_hash=plugin_info["manifest_hash"], package_hash=plugin_info["package_hash"], status="installed", installed_by_user_id=1, # System installation auto_enable=True # Auto-enable discovered plugins ) db.add(plugin) db.commit() # Setup plugin database schema await self._setup_plugin_database(plugin_slug, plugin_info) logger.info(f"Plugin {plugin_slug} installed successfully") return True except Exception as e: db.rollback() logger.error(f"Database error registering plugin {plugin_info['slug']}: {e}") return False async def _setup_plugin_database(self, plugin_id: str, plugin_info: Dict[str, Any]) -> bool: """Setup database schema for plugin""" try: manifest_data = plugin_info["manifest_data"] # Create plugin database schema if specified if "database" in manifest_data.get("spec", {}): logger.info(f"Creating database schema for plugin {plugin_id}") await plugin_db_manager.create_plugin_schema(plugin_id, manifest_data) logger.info(f"Database schema created for plugin {plugin_id}") return True except Exception as e: logger.error(f"Failed to setup database for plugin {plugin_id}: {e}") return False async def load_discovered_plugins(self) -> Dict[str, bool]: """Load all discovered plugins into the plugin sandbox""" logger.info("Loading discovered plugins into sandbox...") loading_results = {} for plugin_slug, plugin_info in self.discovered_plugins.items(): try: # Load plugin into sandbox using the correct method plugin_dir = Path(plugin_info["plugin_path"]) plugin_token = f"plugin_{plugin_slug}_token" # Generate a token for the plugin plugin_instance = await plugin_loader.load_plugin_with_sandbox( plugin_dir, plugin_token ) if plugin_instance: loading_results[plugin_slug] = True logger.info(f"Plugin {plugin_slug} loaded successfully") else: loading_results[plugin_slug] = False logger.warning(f"Failed to load plugin {plugin_slug}") except Exception as e: logger.error(f"Error loading plugin {plugin_slug}: {e}") loading_results[plugin_slug] = False successful_loads = sum(1 for success in loading_results.values() if success) logger.info(f"Plugin loading complete: {successful_loads}/{len(loading_results)} successful") return loading_results async def auto_discover_and_register(self) -> Dict[str, Any]: """Complete auto-discovery workflow: scan, register, and load plugins""" logger.info("Starting plugin auto-discovery...") results = { "discovered": [], "registered": {}, "loaded": {}, "summary": { "total_discovered": 0, "successful_registrations": 0, "successful_loads": 0 } } try: # Step 1: Scan directory for plugins discovered_plugins = await self.scan_plugins_directory() results["discovered"] = [p["slug"] for p in discovered_plugins] results["summary"]["total_discovered"] = len(discovered_plugins) if not discovered_plugins: logger.info("No plugins discovered") return results # Step 2: Register plugins in database registration_results = await self.register_discovered_plugins() results["registered"] = registration_results results["summary"]["successful_registrations"] = sum(1 for success in registration_results.values() if success) # Step 3: Load plugins into sandbox loading_results = await self.load_discovered_plugins() results["loaded"] = loading_results results["summary"]["successful_loads"] = sum(1 for success in loading_results.values() if success) logger.info(f"Auto-discovery complete! Discovered: {results['summary']['total_discovered']}, " f"Registered: {results['summary']['successful_registrations']}, " f"Loaded: {results['summary']['successful_loads']}") return results except Exception as e: logger.error(f"Auto-discovery failed: {e}") results["error"] = str(e) return results def get_discovery_status(self) -> Dict[str, Any]: """Get current discovery status""" return { "plugins_dir": str(self.plugins_dir), "plugins_dir_exists": self.plugins_dir.exists(), "discovered_plugins": list(self.discovered_plugins.keys()), "discovery_count": len(self.discovered_plugins), "last_scan": datetime.now(timezone.utc).isoformat() } # Global auto-discovery service instance plugin_autodiscovery = PluginAutoDiscovery() async def initialize_plugin_autodiscovery() -> Dict[str, Any]: """Initialize plugin auto-discovery service (called from main.py)""" logger.info("Initializing plugin auto-discovery service...") try: results = await plugin_autodiscovery.auto_discover_and_register() logger.info("Plugin auto-discovery service initialized successfully") return results except Exception as e: logger.error(f"Plugin auto-discovery initialization failed: {e}") return {"error": str(e), "summary": {"total_discovered": 0, "successful_registrations": 0, "successful_loads": 0}} # Convenience function for manual plugin discovery async def discover_plugins_now() -> Dict[str, Any]: """Manually trigger plugin discovery (for testing/debugging)""" return await plugin_autodiscovery.auto_discover_and_register()