6bf0746b94
- Company CRUD with soft-delete, FTS search (tsvector + GIN), filter, pagination - Contact CRUD with N:M company linking via company_contacts - CSV/XLSX export, CSV import with dry-run preview - GDPR hard-delete with deletion_log - Audit log on all mutations - 27 new tests (24 ACs), 56 total tests pass - Migration 0002: contacts, company_contacts, FTS search_tsv - Fixed T01 tests: POST→201, PATCH→PUT compatibility
6.3 KiB
6.3 KiB
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_contextapp/deps.py— get_current_user, require_admin, get_tenant_idapp/core/audit.py— log_audit functionapp/main.py— create_app factory, mounts routers, middlewareapp/routes/__init__.py— router aggregatortests/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)
- GET /api/v1/plugins → 200 + list of plugins with status
- POST /api/v1/plugins/{name}/install → 200, plugin status=installed, migrations run
- POST /api/v1/plugins/{name}/activate → 200, plugin status=active, routes registered
- POST /api/v1/plugins/{name}/deactivate → 200, plugin status=inactive, routes unregistered
- DELETE /api/v1/plugins/{name} → 200, plugin removed
- DELETE /api/v1/plugins/{name}?remove_data=true → 200, plugin tables dropped
- GET /api/v1/plugins/manifest → 200 + manifest schema documentation
- Plugin activation registers event listeners on event bus
- Plugin deactivation unregisters event listeners
- Plugin migration creates tables with tenant_id column
- Plugin migration validator rejects tables without tenant_id
- Plugin DB migrations tracked in plugin_migrations table
- Activating already-active plugin → idempotent (200, no error)
- 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, uninstallapp/routes/plugins.py— Plugin endpoints (list/install/activate/deactivate/uninstall/manifest)app/plugins/__init__.py— Plugin package initapp/plugins/base.py— BasePlugin abstract class with lifecycle hooksapp/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, PluginMigrationapp/routes/__init__.py— Register plugins routerapp/schemas/__init__.py— Register plugin schemasapp/services/__init__.py— Register plugin_serviceapp/main.py— Initialize plugin registry on startup (discover builtins)app/core/event_bus.py— Ensure register/unregister listener API works for pluginsapp/core/service_container.py— Ensure plugins can receive db, cache, event_bus, storage, notificationstests/conftest.py— Add plugins, plugin_migrations to TRUNCATE listalembic/versions/— New migration for plugins + plugin_migrations tables
Plugin Lifecycle Design
discovered → installed → active → inactive → uninstalled
↑ ↓
└──────────────────────┘ (can re-activate)
Plugin Manifest Schema (Pydantic v2)
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
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 LOCALwith bound params (useSELECT 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:readfor MODIFY,text_editor:writefor NEW,code_execution_tool:terminalfor test runs - Reference files by path, not inline
- Multi-file output: separate files, not one big file
JSON Tool Examples
{"tool_name":"text_editor","tool_args":{"action":"write","path":"/a0/usr/workdir/dev-projects/leocrm/app/plugins/base.py","content":"..."}}
{"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"}}