LifeRPG_v2.0/modern/backend/plugins.py
TLimoges33 2b961611fd
🚀 Major Enhancement: Complete AI-Powered LifeRPG Platform with Git LFS
 New Features:
- AI-powered habit creation with natural language processing
- HuggingFace transformers integration for sentiment analysis (tracked via Git LFS)
- Advanced predictive analytics and behavioral insights
- Voice & image input capabilities for hands-free habit tracking
- Real-time notifications and community features
- Plugin system with extensible architecture

🔧 Technical Improvements:
- Comprehensive FastAPI backend with 30+ endpoints
- React frontend with PWA capabilities
- Advanced authentication with 2FA support
- RBAC authorization system
- Comprehensive security features (CSRF, rate limiting, audit logging)
- Database migrations and health monitoring
- Docker containerization support
- Git LFS configured for large AI model files (2+ GB)

📚 Documentation & DevOps:
- Complete deployment guides for multiple platforms
- Professional README with feature highlights
- GitHub Actions CI/CD workflows
- Comprehensive API documentation
- Security audit roadmap and compliance framework
- Setup scripts for development environment

🧪 Testing & Quality:
- Comprehensive test suite with 20+ test modules
- Setup verification scripts
- Working development environment with both backend and frontend
- Health checks and monitoring systems

🌟 Ready for:
- Portfolio showcasing
- Community contributions
- Production deployment
- Professional presentation
2025-09-28 21:29:19 +00:00

446 lines
16 KiB
Python

"""
LifeRPG Plugin System - Backend Implementation
This module implements the server-side components of the LifeRPG plugin system:
- Plugin registry and metadata storage
- Plugin sandboxing and execution
- Plugin API endpoints
- Plugin permissions and security
The plugin system uses WebAssembly (WASM) for secure sandboxing of plugin code.
"""
import asyncio
import json
import logging
import os
import uuid
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Union
from fastapi import APIRouter, Depends, FastAPI, File, HTTPException, Request, UploadFile
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, validator
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Table, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, relationship
from db import get_db
import models
# Configure logging
logger = logging.getLogger("liferpg.plugins")
# Define plugin models
class PluginPermission(str, Enum):
"""Permissions that can be granted to plugins."""
# Data access permissions
HABITS_READ = "habits:read"
HABITS_WRITE = "habits:write"
PROJECTS_READ = "projects:read"
PROJECTS_WRITE = "projects:write"
USERS_READ = "users:read"
# UI permissions
UI_DASHBOARD = "ui:dashboard"
UI_SETTINGS = "ui:settings"
UI_REPORTS = "ui:reports"
# System permissions
STORAGE_PLUGIN = "storage:plugin"
NETWORK_SAME_ORIGIN = "network:same-origin"
NETWORK_EXTERNAL = "network:external"
class PluginStatus(str, Enum):
"""Status of a plugin in the system."""
ACTIVE = "active"
DISABLED = "disabled"
PENDING_REVIEW = "pending_review"
REJECTED = "rejected"
class ResourceLimits(BaseModel):
"""Resource limits for plugin execution."""
memory_mb: int = Field(16, description="Memory limit in MB")
storage_mb: int = Field(5, description="Storage limit in MB")
cpu_limit: str = Field("moderate", description="CPU limit (low, moderate, high)")
@validator("cpu_limit")
def validate_cpu_limit(cls, v):
allowed = ["low", "moderate", "high"]
if v not in allowed:
raise ValueError(f"CPU limit must be one of {allowed}")
return v
class PluginMetadata(BaseModel):
"""Metadata for a plugin."""
id: str = Field(..., description="Unique plugin identifier")
name: str = Field(..., description="Display name of the plugin")
version: str = Field(..., description="Plugin version (semver)")
author: str = Field(..., description="Plugin author")
description: str = Field(..., description="Plugin description")
homepage: Optional[str] = Field(None, description="Plugin homepage URL")
target_api_version: str = Field(..., description="Target API version")
min_app_version: str = Field(..., description="Minimum app version required")
permissions: List[PluginPermission] = Field([], description="Required permissions")
extension_points: List[str] = Field([], description="Extension points used")
entry_point: str = Field("initialize", description="Main entry point function")
resource_limits: ResourceLimits = Field(default_factory=ResourceLimits)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
status: PluginStatus = Field(PluginStatus.PENDING_REVIEW)
# Database models
class DBPlugin(models.Base):
"""Database model for plugin metadata."""
__tablename__ = "plugins"
id = Column(String, primary_key=True)
name = Column(String, nullable=False)
version = Column(String, nullable=False)
author = Column(String, nullable=False)
description = Column(Text, nullable=False)
homepage = Column(String, nullable=True)
target_api_version = Column(String, nullable=False)
min_app_version = Column(String, nullable=False)
permissions = Column(Text, nullable=False) # JSON
extension_points = Column(Text, nullable=False) # JSON
entry_point = Column(String, nullable=False)
resource_limits = Column(Text, nullable=False) # JSON
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow)
status = Column(String, nullable=False, default=PluginStatus.PENDING_REVIEW.value)
def to_metadata(self) -> PluginMetadata:
"""Convert database model to PluginMetadata."""
return PluginMetadata(
id=self.id,
name=self.name,
version=self.version,
author=self.author,
description=self.description,
homepage=self.homepage,
target_api_version=self.target_api_version,
min_app_version=self.min_app_version,
permissions=json.loads(self.permissions),
extension_points=json.loads(self.extension_points),
entry_point=self.entry_point,
resource_limits=ResourceLimits(**json.loads(self.resource_limits)),
created_at=self.created_at,
updated_at=self.updated_at,
status=PluginStatus(self.status),
)
@classmethod
def from_metadata(cls, metadata: PluginMetadata) -> "DBPlugin":
"""Create database model from PluginMetadata."""
return cls(
id=metadata.id,
name=metadata.name,
version=metadata.version,
author=metadata.author,
description=metadata.description,
homepage=metadata.homepage,
target_api_version=metadata.target_api_version,
min_app_version=metadata.min_app_version,
permissions=json.dumps([p.value for p in metadata.permissions]),
extension_points=json.dumps(metadata.extension_points),
entry_point=metadata.entry_point,
resource_limits=json.dumps(metadata.resource_limits.dict()),
created_at=metadata.created_at,
updated_at=metadata.updated_at,
status=metadata.status.value,
)
class PluginManager:
"""Manages plugin lifecycle and execution."""
def __init__(self, db: Session, plugins_dir: Path):
self.db = db
self.plugins_dir = plugins_dir
self.plugins_dir.mkdir(exist_ok=True, parents=True)
logger.info(f"Plugin manager initialized with plugins directory: {plugins_dir}")
async def register_plugin(self, metadata: PluginMetadata, wasm_binary: bytes) -> str:
"""Register a new plugin."""
# Check for existing plugin
existing = self.db.query(DBPlugin).filter(DBPlugin.id == metadata.id).first()
if existing:
raise HTTPException(status_code=400, detail=f"Plugin {metadata.id} already exists")
# Save plugin binary
plugin_dir = self.plugins_dir / metadata.id
plugin_dir.mkdir(exist_ok=True)
with open(plugin_dir / "plugin.wasm", "wb") as f:
f.write(wasm_binary)
with open(plugin_dir / "metadata.json", "w") as f:
f.write(metadata.json())
# Save to database
db_plugin = DBPlugin.from_metadata(metadata)
self.db.add(db_plugin)
self.db.commit()
# Load plugin if it's active
if metadata.status == PluginStatus.ACTIVE:
runtime = await get_plugin_runtime()
success = await runtime.load_plugin(metadata.id, metadata, wasm_binary, self.db)
if not success:
logger.warning(f"Failed to load plugin {metadata.id} at registration")
logger.info(f"Registered plugin: {metadata.id} v{metadata.version}")
return metadata.id
async def update_plugin(self, plugin_id: str, metadata: PluginMetadata, wasm_binary: Optional[bytes] = None) -> None:
"""Update an existing plugin."""
# Check for existing plugin
existing = self.db.query(DBPlugin).filter(DBPlugin.id == plugin_id).first()
if not existing:
raise HTTPException(status_code=404, detail=f"Plugin {plugin_id} not found")
# Update metadata
metadata.updated_at = datetime.utcnow()
plugin_dir = self.plugins_dir / plugin_id
with open(plugin_dir / "metadata.json", "w") as f:
f.write(metadata.json())
# Update binary if provided
if wasm_binary:
with open(plugin_dir / "plugin.wasm", "wb") as f:
f.write(wasm_binary)
# Update database
db_plugin = DBPlugin.from_metadata(metadata)
db_plugin.id = plugin_id # Ensure ID remains the same
self.db.query(DBPlugin).filter(DBPlugin.id == plugin_id).update({
"name": db_plugin.name,
"version": db_plugin.version,
"author": db_plugin.author,
"description": db_plugin.description,
"homepage": db_plugin.homepage,
"target_api_version": db_plugin.target_api_version,
"min_app_version": db_plugin.min_app_version,
"permissions": db_plugin.permissions,
"extension_points": db_plugin.extension_points,
"entry_point": db_plugin.entry_point,
"resource_limits": db_plugin.resource_limits,
"updated_at": db_plugin.updated_at,
"status": db_plugin.status,
})
self.db.commit()
logger.info(f"Updated plugin: {plugin_id} to v{metadata.version}")
async def get_plugin(self, plugin_id: str) -> Optional[PluginMetadata]:
"""Get plugin metadata."""
plugin = self.db.query(DBPlugin).filter(DBPlugin.id == plugin_id).first()
if not plugin:
return None
return plugin.to_metadata()
async def list_plugins(self, status: Optional[PluginStatus] = None) -> List[PluginMetadata]:
"""List all plugins."""
query = self.db.query(DBPlugin)
if status:
query = query.filter(DBPlugin.status == status.value)
plugins = query.all()
return [p.to_metadata() for p in plugins]
async def set_plugin_status(self, plugin_id: str, status: PluginStatus) -> None:
"""Set plugin status."""
plugin = self.db.query(DBPlugin).filter(DBPlugin.id == plugin_id).first()
if not plugin:
raise HTTPException(status_code=404, detail=f"Plugin {plugin_id} not found")
old_status = PluginStatus(plugin.status)
plugin.status = status.value
plugin.updated_at = datetime.utcnow()
self.db.commit()
# Handle runtime loading/unloading
runtime = await get_plugin_runtime()
if status == PluginStatus.ACTIVE and old_status != PluginStatus.ACTIVE:
# Load the plugin
plugin_dir = self.plugins_dir / plugin_id
wasm_file = plugin_dir / "plugin.wasm"
if wasm_file.exists():
with open(wasm_file, "rb") as f:
wasm_binary = f.read()
metadata = plugin.to_metadata()
success = await runtime.load_plugin(plugin_id, metadata, wasm_binary, self.db)
if not success:
logger.error(f"Failed to load plugin {plugin_id}")
elif status != PluginStatus.ACTIVE and old_status == PluginStatus.ACTIVE:
# Unload the plugin
await runtime.unload_plugin(plugin_id)
logger.info(f"Set plugin {plugin_id} status to {status.value}")
async def delete_plugin(self, plugin_id: str) -> None:
"""Delete a plugin."""
plugin = self.db.query(DBPlugin).filter(DBPlugin.id == plugin_id).first()
if not plugin:
raise HTTPException(status_code=404, detail=f"Plugin {plugin_id} not found")
# Unload from runtime if active
runtime = await get_plugin_runtime()
await runtime.unload_plugin(plugin_id)
# Remove files
plugin_dir = self.plugins_dir / plugin_id
if plugin_dir.exists():
import shutil
shutil.rmtree(plugin_dir)
# Remove from database
self.db.delete(plugin)
self.db.commit()
logger.info(f"Deleted plugin: {plugin_id}")
async def get_extension_points(self) -> Dict[str, List[Any]]:
"""Get all extension points from loaded plugins."""
runtime = await get_plugin_runtime()
return runtime.get_all_extension_points()
# API Router
router = APIRouter(prefix="/api/v1/plugins", tags=["plugins"])
# Dependency to get plugin manager
async def get_plugin_manager(db: Session = Depends(get_db)):
plugins_dir = Path(os.getenv("PLUGINS_DIR", "plugins"))
return PluginManager(db, plugins_dir)
# API Endpoints
@router.get("/", response_model=List[PluginMetadata])
async def list_plugins(
status: Optional[PluginStatus] = None,
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""List all plugins."""
return await plugin_manager.list_plugins(status)
@router.get("/{plugin_id}", response_model=PluginMetadata)
async def get_plugin(
plugin_id: str,
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Get plugin metadata."""
plugin = await plugin_manager.get_plugin(plugin_id)
if not plugin:
raise HTTPException(status_code=404, detail=f"Plugin {plugin_id} not found")
return plugin
@router.post("/", response_model=dict)
async def register_plugin(
metadata: PluginMetadata,
wasm_file: UploadFile = File(...),
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Register a new plugin."""
wasm_binary = await wasm_file.read()
plugin_id = await plugin_manager.register_plugin(metadata, wasm_binary)
return {"id": plugin_id, "status": "registered"}
@router.put("/{plugin_id}", response_model=dict)
async def update_plugin(
plugin_id: str,
metadata: PluginMetadata,
wasm_file: Optional[UploadFile] = None,
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Update an existing plugin."""
wasm_binary = await wasm_file.read() if wasm_file else None
await plugin_manager.update_plugin(plugin_id, metadata, wasm_binary)
return {"id": plugin_id, "status": "updated"}
@router.patch("/{plugin_id}/status", response_model=dict)
async def set_plugin_status(
plugin_id: str,
status: PluginStatus,
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Set plugin status."""
await plugin_manager.set_plugin_status(plugin_id, status)
return {"id": plugin_id, "status": status}
@router.delete("/{plugin_id}", response_model=dict)
async def delete_plugin(
plugin_id: str,
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Delete a plugin."""
await plugin_manager.delete_plugin(plugin_id)
return {"id": plugin_id, "status": "deleted"}
@router.get("/extension-points", response_model=dict)
async def get_extension_points(
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Get all extension points from loaded plugins."""
extension_points = await plugin_manager.get_extension_points()
return {"extension_points": extension_points}
@router.get("/{plugin_id}/wasm")
async def get_plugin_wasm(
plugin_id: str,
plugin_manager: PluginManager = Depends(get_plugin_manager),
):
"""Get the WASM binary for a plugin."""
plugin = await plugin_manager.get_plugin(plugin_id)
if not plugin:
raise HTTPException(status_code=404, detail=f"Plugin {plugin_id} not found")
plugin_dir = plugin_manager.plugins_dir / plugin_id
wasm_file = plugin_dir / "plugin.wasm"
if not wasm_file.exists():
raise HTTPException(status_code=404, detail=f"WASM binary not found for plugin {plugin_id}")
from fastapi.responses import FileResponse
return FileResponse(wasm_file, media_type="application/wasm")
# Function to add plugin system to FastAPI app
def setup_plugin_system(app: FastAPI):
"""Set up the plugin system in a FastAPI application."""
app.include_router(router)
# Make sure plugins directory exists
plugins_dir = Path(os.getenv("PLUGINS_DIR", "plugins"))
plugins_dir.mkdir(exist_ok=True, parents=True)
logger.info("Plugin system initialized")
return app