Spaces:
Running
Running
| import os | |
| import json | |
| import time | |
| import uuid | |
| import logging | |
| import hashlib | |
| from typing import List, Dict, Optional, Any | |
| from pydantic import BaseModel, Field | |
| from pathlib import Path | |
| from fastapi import APIRouter, Depends | |
| from .auth_guard import require_role, AuthRole | |
| _logger = logging.getLogger("api.plugins") | |
| router = APIRouter(prefix="/api/plugins", tags=["plugins"], dependencies=[Depends(require_role(AuthRole.MACHINE))]) | |
| class PluginPermission(str): | |
| FS_READ = "fs:read" | |
| FS_WRITE = "fs:write" | |
| NET_API = "net:api" | |
| SHELL_LIMITED = "shell:limited" | |
| class PluginManifest(BaseModel): | |
| id: str | |
| name: str | |
| version: str | |
| description: Optional[str] = None | |
| author: Optional[str] = None | |
| permissions: List[str] = [] | |
| dependencies: Dict[str, str] = {} | |
| entry_point: str = "main.py" | |
| signature: Optional[str] = None | |
| class Plugin(BaseModel): | |
| manifest: PluginManifest | |
| code: str | |
| registered_at: int = Field(default_factory=lambda: int(time.time())) | |
| status: str = "active" # active, disabled, error | |
| PLUGINS_REGISTRY: Dict[str, Plugin] = {} | |
| class PluginManager: | |
| """ | |
| ARCH-E3.3: Plugin System sandboxato | |
| Gestisce il ciclo di vita dei plugin e la loro esecuzione sicura. | |
| """ | |
| def _verify_signature(manifest: PluginManifest, code: str) -> bool: | |
| """Verifica l'integritΓ del plugin tramite hash del codice.""" | |
| if not manifest.signature: | |
| return True # In dev mode accettiamo senza firma | |
| actual_hash = hashlib.sha256(code.encode()).hexdigest() | |
| return actual_hash == manifest.signature | |
| async def register(manifest_dict: dict, code: str) -> Dict[str, Any]: | |
| """Registra un nuovo plugin nel sistema.""" | |
| try: | |
| manifest = PluginManifest(**manifest_dict) | |
| if not PluginManager._verify_signature(manifest, code): | |
| return {"status": "error", "message": "Firma del plugin non valida o codice corrotto"} | |
| plugin = Plugin(manifest=manifest, code=code) | |
| PLUGINS_REGISTRY[manifest.id] = plugin | |
| _logger.info(f"Plugin registrato: {manifest.id} v{manifest.version}") | |
| return {"status": "registered", "id": manifest.id, "version": manifest.version} | |
| except Exception as e: | |
| _logger.error(f"Errore registrazione plugin: {e}") | |
| return {"status": "error", "message": str(e)} | |
| async def list_plugins() -> List[Dict[str, Any]]: | |
| """Elenca tutti i plugin registrati e il loro stato.""" | |
| return [ | |
| { | |
| "id": p.manifest.id, | |
| "name": p.manifest.name, | |
| "version": p.manifest.version, | |
| "status": p.status, | |
| "permissions": p.manifest.permissions | |
| } for p in PLUGINS_REGISTRY.values() | |
| ] | |
| async def execute(plugin_id: str, input_data: Any, session_id: str = "default") -> Dict[str, Any]: | |
| """ | |
| Esegue un plugin in una sandbox sicura. | |
| Applica restrizioni basate sui permessi del manifest. | |
| """ | |
| if plugin_id not in PLUGINS_REGISTRY: | |
| return {"status": "error", "message": f"Plugin {plugin_id} non trovato"} | |
| plugin = PLUGINS_REGISTRY[plugin_id] | |
| if plugin.status != "active": | |
| return {"status": "error", "message": f"Plugin {plugin_id} Γ¨ in stato: {plugin.status}"} | |
| # Preparazione dell'ambiente di esecuzione (Sandbox) | |
| # Sfrutta backend/api/exec_sandbox.py | |
| try: | |
| from .exec_sandbox import run_in_sandbox_session | |
| # Wrapper del codice per iniettare input e catturare output | |
| # Il plugin deve definire una funzione 'main(input_data)' | |
| execution_wrapper = f""" | |
| import json | |
| import sys | |
| # Input data iniettato | |
| input_data = {json.dumps(input_data)} | |
| # Codice del plugin | |
| {plugin.code} | |
| # Esecuzione | |
| try: | |
| if 'main' in globals(): | |
| result = main(input_data) | |
| print("---PLUGIN_RESULT_START---") | |
| print(json.dumps(result)) | |
| print("---PLUGIN_RESULT_END---") | |
| else: | |
| print("Error: La funzione 'main(input_data)' non Γ¨ definita nel plugin.", file=sys.stderr) | |
| except Exception as e: | |
| print(f"Plugin Execution Error: {{e}}", file=sys.stderr) | |
| sys.exit(1) | |
| """ | |
| # TODO: In futuro, iniettare proxy limitati per FS/NET in base ai permessi | |
| # Per ora usiamo la sandbox standard che Γ¨ giΓ isolata | |
| res = await run_in_sandbox_session( | |
| code=execution_wrapper, | |
| lang="python", | |
| session_id=f"plugin_{plugin_id}_{session_id}", | |
| timeout=60.0 | |
| ) | |
| # Parsing del risultato dall'output standard | |
| stdout = res.get("stdout", "") | |
| if "---PLUGIN_RESULT_START---" in stdout: | |
| try: | |
| parts = stdout.split("---PLUGIN_RESULT_START---")[1].split("---PLUGIN_RESULT_END---") | |
| plugin_output = json.loads(parts[0].strip()) | |
| return { | |
| "status": "success", | |
| "plugin_id": plugin_id, | |
| "output": plugin_output, | |
| "logs": stdout.split("---PLUGIN_RESULT_START---")[0] | |
| } | |
| except Exception as e: | |
| return {"status": "error", "message": f"Errore parsing output plugin: {e}", "raw_stdout": stdout} | |
| return { | |
| "status": "error" if res.get("returncode") != 0 else "completed_no_output", | |
| "plugin_id": plugin_id, | |
| "returncode": res.get("returncode"), | |
| "stderr": res.get("stderr"), | |
| "stdout": stdout | |
| } | |
| except Exception as e: | |
| _logger.error(f"Errore esecuzione plugin {plugin_id}: {e}") | |
| return {"status": "error", "message": str(e)} | |
| # Singleton | |
| plugin_manager = PluginManager() | |
| # ββ HTTP Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class RegisterPluginRequest(BaseModel): | |
| manifest: dict | |
| code: str | |
| class ExecutePluginRequest(BaseModel): | |
| plugin_id: str | |
| input_data: Any | |
| session_id: Optional[str] = "default" | |
| async def http_register_plugin(req: RegisterPluginRequest): | |
| return await plugin_manager.register(req.manifest, req.code) | |
| async def http_list_plugins(): | |
| return await plugin_manager.list_plugins() | |
| async def http_execute_plugin(req: ExecutePluginRequest): | |
| return await plugin_manager.execute(req.plugin_id, req.input_data, req.session_id) | |
| async def http_plugin_health(plugin_id: str): | |
| if plugin_id not in PLUGINS_REGISTRY: | |
| return {"status": "not_found"} | |
| p = PLUGINS_REGISTRY[plugin_id] | |
| return { | |
| "status": p.status, | |
| "id": p.manifest.id, | |
| "version": p.manifest.version, | |
| "uptime": int(time.time()) - p.registered_at | |
| } | |