# T03: Plugin System Framework ## Context - Project: LeoCRM (greenfield rewrite, Option C) - Repo: /a0/usr/workdir/dev-projects/leocrm - T01 COMPLETE: auth, multi-tenant, RBAC, sessions, audit, notifications - T01 commit: 7a7daf8 (pushed to Forgejo) - Tech: FastAPI + SQLAlchemy 2.0 async + PostgreSQL + Redis + Pydantic v2 ## Existing T01 Code to Build On - `app/core/event_bus.py` — Event bus (0% coverage, needs integration) - `app/core/service_container.py` — DI container (0% coverage, needs integration) - `app/core/db/__init__.py` — Engine, Session, Base, TenantMixin, set_tenant_context - `app/deps.py` — get_current_user, require_admin, get_tenant_id - `app/core/audit.py` — log_audit function - `app/main.py` — create_app factory, mounts routers, middleware - `app/routes/__init__.py` — router aggregator - `tests/conftest.py` — Test fixtures with TRUNCATE CASCADE ## Requirements (7) F-PLUGIN-01: Plugin-System für Module — Module als Plugins, Daten austauschbar F-PLUGIN-02: Plugin-Schnittstellen-Definition — API-Contract, Lifecycle-Hooks, Manifest, Abhängigkeiten F-CORE-01: Multi-Tenant-Architektur F-CORE-03: RBAC F-CORE-04: Audit-Log F-CORE-05: Event-System F-TEST-01: Test-Coverage ## Acceptance Criteria (14) 1. GET /api/v1/plugins → 200 + list of plugins with status 2. POST /api/v1/plugins/{name}/install → 200, plugin status=installed, migrations run 3. POST /api/v1/plugins/{name}/activate → 200, plugin status=active, routes registered 4. POST /api/v1/plugins/{name}/deactivate → 200, plugin status=inactive, routes unregistered 5. DELETE /api/v1/plugins/{name} → 200, plugin removed 6. DELETE /api/v1/plugins/{name}?remove_data=true → 200, plugin tables dropped 7. GET /api/v1/plugins/manifest → 200 + manifest schema documentation 8. Plugin activation registers event listeners on event bus 9. Plugin deactivation unregisters event listeners 10. Plugin migration creates tables with tenant_id column 11. Plugin migration validator rejects tables without tenant_id 12. Plugin DB migrations tracked in plugin_migrations table 13. Activating already-active plugin → idempotent (200, no error) 14. Deactivating inactive plugin → idempotent (200) ## Files to Create/Modify ### New Files: - `app/models/plugin.py` — Plugin, PluginMigration (plugin_migrations tracking table) - `app/schemas/plugin.py` — Plugin schemas (manifest, status, install/activate response) - `app/services/plugin_service.py` — Plugin lifecycle: discover, install, activate, deactivate, uninstall - `app/routes/plugins.py` — Plugin endpoints (list/install/activate/deactivate/uninstall/manifest) - `app/plugins/__init__.py` — Plugin package init - `app/plugins/base.py` — BasePlugin abstract class with lifecycle hooks - `app/plugins/manifest.py` — PluginManifest Pydantic schema (name, version, dependencies, routes, events, migrations) - `app/plugins/registry.py` — Plugin registry (in-memory + DB-backed status) - `app/plugins/migration_runner.py` — Plugin DB migration runner + validator (tenant_id check) - `app/plugins/builtins/__init__.py` — Built-in plugins directory (empty for now, just structure) - `tests/test_plugins.py` — Plugin lifecycle tests (install/activate/deactivate/uninstall/idempotent) ### Modify: - `app/models/__init__.py` — Register Plugin, PluginMigration - `app/routes/__init__.py` — Register plugins router - `app/schemas/__init__.py` — Register plugin schemas - `app/services/__init__.py` — Register plugin_service - `app/main.py` — Initialize plugin registry on startup (discover builtins) - `app/core/event_bus.py` — Ensure register/unregister listener API works for plugins - `app/core/service_container.py` — Ensure plugins can receive db, cache, event_bus, storage, notifications - `tests/conftest.py` — Add plugins, plugin_migrations to TRUNCATE list - `alembic/versions/` — New migration for plugins + plugin_migrations tables ## Plugin Lifecycle Design ``` discovered → installed → active → inactive → uninstalled ↑ ↓ └──────────────────────┘ (can re-activate) ``` ## Plugin Manifest Schema (Pydantic v2) ```python class PluginManifest(BaseModel): name: str # unique identifier version: str # semver display_name: str description: str dependencies: list[str] = [] # other plugin names required routes: list[dict] = [] # route definitions events: list[str] = [] # event names to listen migrations: list[str] = [] # migration file names permissions: list[str] = [] # required permissions ``` ## BasePlugin Abstract Class ```python class BasePlugin(ABC): manifest: PluginManifest async def on_install(self, db, service_container): ... async def on_activate(self, db, service_container, event_bus): ... async def on_deactivate(self, db, service_container, event_bus): ... async def on_uninstall(self, db, service_container): ... def get_routes(self) -> list[APIRouter]: ... ``` ## Forbidden Patterns - NO JWT tokens (session-based auth from T01) - NO SQLite (PostgreSQL only) - NO wildcard CORS - NO raw SQL without tenant context - NO hardcoded secrets - NO .test TLD emails (use .com) - NO `SET LOCAL` with bound params (use `SELECT set_config()`) - NO raising HTTPException in middleware (return JSONResponse) - NO POST without status_code=201 (where applicable) - NO plugin tables without tenant_id column (validator enforces) ## Test Spec - Commands: `cd /a0/usr/workdir/dev-projects/leocrm && source venv/bin/activate && python -m pytest tests/test_plugins.py -v --tb=short` - Coverage: `python -m pytest tests/test_plugins.py --cov=app/plugins --cov-report=term-missing` - Target: 85% for plugin modules - All 14 ACs must pass ## Token Rule - Use `text_editor:read` for MODIFY, `text_editor:write` for NEW, `code_execution_tool:terminal` for test runs - Reference files by path, not inline - Multi-file output: separate files, not one big file ## JSON Tool Examples ```json {"tool_name":"text_editor","tool_args":{"action":"write","path":"/a0/usr/workdir/dev-projects/leocrm/app/plugins/base.py","content":"..."}} ``` ```json {"tool_name":"code_execution_tool","tool_args":{"runtime":"terminal","session":0,"code":"cd /a0/usr/workdir/dev-projects/leocrm && source venv/bin/activate && python -m pytest tests/test_plugins.py -v --tb=short"}} ```