# LeoCRM Plugin-System — Kompletter Umbauplan **Erstellt:** 2026-07-26 **Aktualisiert:** 2026-07-26 (Codebasis-Verifikation + Phase 6) **Geschätzter Gesamtaufwand:** ~149 Stunden (~19 Arbeitstage) **Status:** Geplant — noch nicht gestartet **Codebasis-Verifikation (2026-07-26):** - ✅ `base.py` unverändert — Plan passt - ✅ `registry.py` unverändert — Plan passt - ✅ `manifest.py` unverändert — Plan passt - ✅ `contracts.py` (ContractRegistry) unverändert — Plan passt - ✅ Migration 0044 hinzugekommen: RLS Repair + separater DB-User (crm_runtime) — beeinflusst Plugin-System nicht - ✅ Migration 0045 hinzugekommen — neuer Head - ✅ `require_active_plugin` in `deps.py` hinzugekommen — beeinflusst Plugin-System nicht - ✅ 19 echte Plugins (test_sample hat __init__.py statt plugin.py) - ✅ Cross-Imports: 224, Contracts: 8, get_contract: 11 — unverändert --- ## Übersicht: 5 Phasen | Phase | Punkte | Inhalt | Stunden | Tage | |---|---|---|---|---| | Phase 1 | 1-3 | Contracts konsequent nutzen | 47 | 6 | | Phase 2 | 4 | Hooks/Filters-System | 16 | 2 | | Phase 3 | 5 | Plugin-Isolation (Linting) | 4 | 0,5 | | Phase 4 | 8 | Plugin-Versioning | 20 | 2,5 | | Phase 5 | 6 | Marketplace-Vorbereitung | 42 | 5 | | Phase 6 | — | Manifest-Anpassung & Konsolidierung | 20 | 2,5 | | **Gesamt** | | | **149** | **~19** | **Wichtig:** Jede Phase ist unabhängig funktionsfähig. Das System läuft nach jeder Phase ohne Einschränkungen weiter. --- ## Phase 1: Contracts konsequent nutzen (Punkte 1-3) **Ziel:** Alle 224 direkten Cross-Plugin-Imports werden durch das Contract-System ersetzt. ### 1.1 Fehlende contracts.py erstellen (7 Std) Für jedes Plugin, das noch keine `contracts.py` hat, eine erstellen: | # | Plugin | Exportierte Symbole | Aufwand | |---|---|---|---| | 1 | `ai_proactive` | ContextTools, ProactiveAgent, JobScheduler | 30 Min | | 2 | `ai_ui_control` | WebSocketManager, UIAction | 30 Min | | 3 | `automation` | AgentRunner, ExecutionEngine, Scheduler, WorkflowTimeout | 45 Min | | 4 | `entity_links` | EntityLink model, create_link, get_links | 20 Min | | 5 | `forgejo_error_reporter` | report_error_to_forgejo | 15 Min | | 6 | `mcp_client` | McpClient, McpServerConfig | 30 Min | | 7 | `mcp_server` | McpServer, ToolDefinitions | 30 Min | | 8 | `report_generator` | ReportTemplate, ReportInstance, PdfGenerator | 30 Min | | 9 | `system_notif` | SystemNotifHandler | 15 Min | | 10 | `tags` | Tag, TagAssignment, assign_tags, remove_tags | 20 Min | | 11 | `tasks` | Task, TaskService, create_task, update_task | 30 Min | | 12 | `test_sample` | TestSamplePlugin | 10 Min | | 13 | `dms` (erweitern) | File, Folder, UploadService, DownloadService | 30 Min | | 14 | `permissions` (erweitern) | ShareLink, PermissionResolver | 30 Min | **Schema für jede contracts.py:** ```python """Public contract for the plugin.""" from __future__ import annotations from app.plugins.builtins.contracts import get_contract_registry # Import only public symbols from internal modules class Contract: contract_name = "" # Expose only public API _contract = Contract() get_contract_registry().register("", _contract) ``` ### 1.2 Direkte Imports ersetzen (28 Std) 224 direkte Imports müssen durch `get_contract()` ersetzt werden. **Top-Priorität (häufigste Import-Quellen):** | # | Datei | Imports | Aufwand | |---|---|---|---| | 1 | `automation/plugin.py` | 10 | 1,5 Std | | 2 | `automation/routes.py` | 8 | 1,5 Std | | 3 | `ai_proactive/services.py` | 8 | 1,5 Std | | 4 | `ai_proactive/plugin.py` | 8 | 1,5 Std | | 5 | `unified_search/jobs.py` | 7 | 1 Std | | 6 | `builtins/__init__.py` | 7 | 1 Std | | 7 | `ai_proactive/jobs.py` | 7 | 1 Std | | 8 | `ai_assistant/participant_handler.py` | 7 | 1 Std | | 9 | `kommunikation/routes.py` | 6 | 1 Std | | 10 | `kommunikation/contracts.py` | 6 | 1 Std | | 11 | `automation/agent_routes.py` | 6 | 1 Std | | 12 | `automation/agent_comm.py` | 6 | 1 Std | | 13 | `ai_proactive/participant_handler.py` | 6 | 1 Std | | 14 | `ai_assistant/plugin.py` | 6 | 1 Std | | 15 | `unified_search/routes.py` | 5 | 45 Min | | 16-50 | Alle übrigen Dateien | ~122 | 12 Std | **Muster für Ersetzung:** ```python # VORHER (direkt): from app.plugins.builtins.kommunikation.services import send_message # NACHHER (über Contract): from app.plugins.builtins.contracts import get_contract async def my_function(db, ...): komm = get_contract("kommunikation") if komm: await komm.send_message(db, ...) # Graceful degradation wenn Plugin nicht aktiv ``` ### 1.3 Contracts bei Deaktivierung abmelden (4 Std) In jedem Plugin's `on_deactivate()`: ```python async def on_deactivate(self, db, service_container, event_bus) -> None: # Contract abmelden from app.plugins.builtins.contracts import get_contract_registry get_contract_registry().unregister(self.manifest.name) # ... rest of cleanup await super().on_deactivate(db, service_container, event_bus) ``` | # | Plugin | Aufwand | |---|---|---| | 1-16 | Alle 16 Plugins | 15 Min pro Plugin = 4 Std | ### 1.4 Tests anpassen (8 Std) - Cross-Plugin-Tests müssen mit Contracts laufen - `test_plugins.py` — Contract-Registry Tests - `test_contracts.py` — Neue Test-Datei für Contract-System - Alle Integrationstests mit Contract-Mocks ### Meilenstein Phase 1: - ✅ Alle 16 Plugins haben contracts.py - ✅ 0 direkte Cross-Plugin-Imports (geprüft mit grep) - ✅ Contracts werden bei Deaktivierung abgemeldet - ✅ Alle Tests bestanden --- ## Phase 2: Hooks/Filters-System (Punkt 4) **Ziel:** WordPress-Style Hooks (actions + filters) für Plugin-Erweiterbarkeit. ### 2.1 HookRegistry erstellen (4 Std) **Neue Datei: `app/core/hooks.py`** ```python """WordPress-style hooks: actions (fire-and-forget) and filters (modify data).""" from __future__ import annotations import logging from collections import defaultdict from typing import Any, Callable logger = logging.getLogger(__name__) class HookRegistry: """Central registry for actions and filters. Actions: do_action('contact.before_create', data) — no return value Filters: result = apply_filters('contact.format_name', name) — returns modified value Priority: lower numbers run first (default=10). """ _instance: HookRegistry | None = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._actions: dict[str, list[tuple[int, Callable]]] = defaultdict(list) cls._instance._filters: dict[str, list[tuple[int, Callable]]] = defaultdict(list) return cls._instance def register_action(self, hook_name: str, callback: Callable, priority: int = 10) -> None: self._actions[hook_name].append((priority, callback)) self._actions[hook_name].sort(key=lambda x: x[0]) def register_filter(self, hook_name: str, callback: Callable, priority: int = 10) -> None: self._filters[hook_name].append((priority, callback)) self._filters[hook_name].sort(key=lambda x: x[0]) async def do_action(self, hook_name: str, *args, **kwargs) -> None: for _, callback in self._actions.get(hook_name, []): try: result = callback(*args, **kwargs) if hasattr(result, '__await__'): await result except Exception: logger.exception("Error in action %s", hook_name) async def apply_filters(self, hook_name: str, value: Any, *args, **kwargs) -> Any: for _, callback in self._filters.get(hook_name, []): try: result = callback(value, *args, **kwargs) if hasattr(result, '__await__'): result = await result value = result except Exception: logger.exception("Error in filter %s", hook_name) return value def unregister(self, hook_name: str, callback: Callable) -> None: self._actions[hook_name] = [(p, c) for p, c in self._actions.get(hook_name, []) if c != callback] self._filters[hook_name] = [(p, c) for p, c in self._filters.get(hook_name, []) if c != callback] def unregister_all(self, hook_name: str) -> None: self._actions.pop(hook_name, None) self._filters.pop(hook_name, None) def _reset_for_testing(self) -> None: self._actions.clear() self._filters.clear() def get_hook_registry() -> HookRegistry: return HookRegistry() async def do_action(hook_name: str, *args, **kwargs) -> None: await get_hook_registry().do_action(hook_name, *args, **kwargs) async def apply_filters(hook_name: str, value: Any, *args, **kwargs) -> Any: return await get_hook_registry().apply_filters(hook_name, value, *args, **kwargs) ``` ### 2.2 Integration in BasePlugin (2 Std) ```python # In BasePlugin.on_activate: async def on_activate(self, db, service_container, event_bus) -> None: # ... existing code ... # Hooks werden in Subklassen registriert # In BasePlugin.on_deactivate: async def on_deactivate(self, db, service_container, event_bus) -> None: # Alle Hooks dieses Plugins abmelden from app.core.hooks import get_hook_registry # Plugin-spezifische Hooks entfernen (prefix mit plugin name) # ... existing code ... ``` ### 2.3 Hook-Punkte in Core-Services (6 Std) | # | Service | Hook-Name | Typ | Beschreibung | |---|---|---|---|---| | 1 | contact_service | `contact.before_create` | Action | Vor Kontakt-Erstellung | | 2 | contact_service | `contact.after_create` | Action | Nach Kontakt-Erstellung | | 3 | contact_service | `contact.format_display_name` | Filter | Anzeigenamen formatieren | | 4 | contact_service | `contact.before_update` | Action | Vor Kontakt-Update | | 5 | contact_service | `contact.after_update` | Action | Nach Kontakt-Update | | 6 | contact_service | `contact.before_delete` | Action | Vor Kontakt-Löschung | | 7 | mail_service | `mail.before_send` | Filter | E-Mail vor Versand modifizieren | | 8 | mail_service | `mail.after_send` | Action | Nach E-Mail-Versand | | 9 | calendar | `calendar.before_appointment` | Action | Vor Termin-Erstellung | | 10 | calendar | `calendar.after_appointment` | Action | Nach Termin-Erstellung | | 11 | auth_service | `auth.before_login` | Filter | Login-Daten validieren/modifizieren | | 12 | auth_service | `auth.after_login` | Action | Nach erfolgreichem Login | | 13 | user_service | `user.before_create` | Action | Vor User-Erstellung | | 14 | user_service | `user.after_create` | Action | Nach User-Erstellung | | 15 | dms | `dms.before_upload` | Filter | Datei-Upload validieren/modifizieren | ### 2.4 Tests für Hooks/Filters (4 Std) - `test_hooks.py` — HookRegistry Tests - Integrationstests: Plugin registriert Hook, Core-Service löst Hook aus - Filter-Tests: Wert wird korrekt modifiziert - Priority-Tests: Reihenfolge wird eingehalten - Unregister-Tests: Hooks werden bei Deaktivierung entfernt ### Meilenstein Phase 2: - ✅ `app/core/hooks.py` mit HookRegistry - ✅ 15 Hook-Punkte in Core-Services - ✅ BasePlugin registriert/unregistriert Hooks automatisch - ✅ Tests bestanden --- ## Phase 3: Plugin-Isolation (Punkt 5) **Ziel:** Direkte Cross-Plugin-Imports werden durch Linting verhindert. ### 3.1 Linting-Regel erstellen (2 Std) **Neue Datei: `.ruff/rules/no_cross_plugin_imports.py`** ```python """Ruff rule: forbid direct imports from app.plugins.builtins.* (except contracts).""" # Erlaubt: # from app.plugins.builtins.contracts import get_contract # from app.plugins.builtins..contracts import ... # # Verboten: # from app.plugins.builtins..services import ... # from app.plugins.builtins..models import ... # from app.plugins.builtins..routes import ... ``` ### 3.2 CI/CD Integration (1 Std) - `ruff check` in GitHub Actions / Forgejo CI - Pre-commit Hook für lokale Entwicklung - Fehler bei direkten Cross-Plugin-Imports ### 3.3 Ausnahmen definieren (1 Std) - `conftest.py` — Tests dürfen direkt importieren - `app/plugins/builtins/__init__.py` — Plugin-Discovery - `app/plugins/registry.py` — Registry darf importieren ### Meilenstein Phase 3: - ✅ Linting-Regel aktiv - ✅ CI/CD prüft bei jedem Commit - ✅ 0 direkte Cross-Plugin-Imports (automatisch erzwungen) --- ## Phase 4: Plugin-Versioning (Punkt 8) **Ziel:** Vollständige Versionsverwaltung mit SemVer, Rollback und Kompatibilitäts-Check. ### 4.1 SemVer-Vergleich (3 Std) **Neue Datei: `app/plugins/semver.py`** ```python """Semantic version comparison for plugin versions.""" from dataclasses import dataclass import re @dataclass class SemVer: major: int minor: int patch: int prerelease: str = "" @classmethod def parse(cls, version: str) -> "SemVer": match = re.match(r"(\d+)\.(\d+)\.(\d+)(?:-(.+))?", version) if not match: raise ValueError(f"Invalid semver: {version}") return cls(int(match[1]), int(match[2]), int(match[3]), match[4] or "") def __lt__(self, other): ... def __eq__(self, other): ... def __le__(self, other): ... def __gt__(self, other): ... def is_breaking_change(self, other: "SemVer") -> bool: return self.major != other.major def is_compatible_with(self, min_version: "SemVer") -> bool: return self >= min_version ``` **Änderung in `registry.py`:** ```python # VORHER: String-Vergleich if record.version != plugin.manifest.version: # NACHHER: SemVer-Vergleich old_ver = SemVer.parse(record.version) new_ver = SemVer.parse(plugin.manifest.version) if old_ver != new_ver: if new_ver < old_ver: # Downgrade — nur mit Rollback-Migration ... ``` ### 4.2 Rollback-Migrationen (6 Std) **Erweiterung des Migration-Systems:** ```python # MigrationRunner erweitern: async def run_migration_down(self, db, plugin_name, migration_filename): """Run rollback (down) migration.""" # Suche _down.sql oder parse DOWNGRADE-Block async def rollback_to_version(self, db, plugin_name, target_version: str): """Rollback plugin to a specific version.""" # 1. Finde alle Migrationen nach target_version # 2. Führe sie in umgekehrter Reihenfolge aus # 3. Aktualisiere DB-Version ``` **Migration-Datei-Format:** ```sql -- 0001_initial.sql -- UP: CREATE TABLE ...; -- DOWN: DROP TABLE ... CASCADE; ``` Oder separate Dateien: - `0001_initial_up.sql` - `0001_initial_down.sql` ### 4.3 Version-Kompatibilitäts-Check (3 Std) **Manifest-Erweiterung:** ```python class PluginManifest(BaseModel): # ... existing fields ... min_app_version: str = Field( default="0.0.0", description="Minimum LeoCRM version required" ) ``` **Check bei Installation:** ```python async def install(self, db, name): plugin = self.get_plugin(name) # Check app version compatibility app_version = SemVer.parse(settings.app_version) min_version = SemVer.parse(plugin.manifest.min_app_version) if app_version < min_version: raise ValueError( f"Plugin '{name}' requires LeoCRM >= {plugin.manifest.min_app_version}, " f"but current version is {settings.app_version}" ) ``` ### 4.4 Update-Benachrichtigung im Frontend (4 Std) **Backend:** - `GET /api/v1/plugins/updates` — Liste Plugins mit verfügbarer neuer Version - Vergleich mit Marketplace-Registry (wenn verfügbar) oder lokaler Version **Frontend:** - Badge im Plugin-Settings: "Update verfügbar (1.2.0 → 1.3.0)" - Update-Button: Löst Update aus (führt neue Migrationen aus) - Changelog-Anzeige (optional) ### 4.5 Tests (4 Std) - `test_semver.py` — SemVer-Vergleich, Parse, Edge Cases - `test_versioning.py` — Upgrade, Downgrade, Kompatibilitäts-Check - `test_rollback.py` — Rollback-Migrationen - Integrationstests: Version-Update löst Migrationen aus ### Meilenstein Phase 4: - ✅ SemVer-Vergleich statt String-Vergleich - ✅ Rollback-Migrationen funktionieren - ✅ min_app_version wird geprüft - ✅ Frontend zeigt Update-Benachrichtigungen - ✅ Tests bestanden --- ## Phase 5: Marketplace-Vorbereitung (Punkt 6) **Ziel:** Code so vorbereiten, dass ein Marketplace nur noch gebaut werden muss — ohne Systemänderungen. **Wichtig:** Funktioniert auch OHNE Marketplace — Built-in Plugins laufen normal weiter. ### 5.1 Externe Plugin-Discovery (6 Std) **Erweiterung `registry.py`:** ```python class PluginRegistry: def discover_all(self) -> list[str]: """Discover built-in AND external plugins.""" discovered = self.discover_builtins() discovered.extend(self.discover_external()) return discovered def discover_external(self) -> list[str]: """Discover plugins from external plugins/ directory.""" external_dir = Path(settings.external_plugins_path or "plugins") if not external_dir.exists(): return [] discovered = [] for plugin_dir in external_dir.iterdir(): if not plugin_dir.is_dir() or plugin_dir.name.startswith("_"): continue # Look for plugin.py or __init__.py with BasePlugin subclass plugin_file = plugin_dir / "plugin.py" if not plugin_file.exists(): continue # Import and register import sys sys.path.insert(0, str(external_dir)) try: module = importlib.import_module(f"{plugin_dir.name}.plugin") # ... find BasePlugin subclass ... finally: sys.path.remove(str(external_dir)) return discovered ``` ### 5.2 Plugin-Signatur-Validierung (8 Std) **Neue Datei: `app/plugins/signature.py`** ```python """Plugin signature verification for external plugins.""" from pathlib import Path import hashlib import hmac # Ed25519 oder HMAC-SHA256 Signatur class PluginSignature: """Verify plugin package signatures.""" @staticmethod def verify_signature(zip_path: Path, signature: bytes, public_key: bytes) -> bool: """Verify Ed25519 signature of plugin ZIP.""" # 1. Read ZIP content # 2. Compute hash # 3. Verify signature with public key pass @staticmethod def compute_hash(zip_path: Path) -> bytes: """Compute SHA-256 hash of plugin ZIP.""" pass @staticmethod def sign_plugin(zip_path: Path, private_key: bytes) -> bytes: """Sign a plugin ZIP (for plugin authors).""" pass ``` ### 5.3 Plugin-Allowlist (4 Std) **Neue Alembic-Migration: `0044_plugin_allowlist.py`** ```python # Tabelle: plugin_allowlist # - id: UUID # - plugin_name: VARCHAR(80) # - allowed_hash: VARCHAR(64) # SHA-256 # - allowed_signature: TEXT # Ed25519 signature # - added_by: UUID (user) # - created_at: TIMESTAMPTZ # - is_active: BOOLEAN ``` ### 5.4 Plugin-Metadata-Erweiterung (4 Std) **Manifest-Erweiterung:** ```python class PluginManifest(BaseModel): # ... existing fields ... author: str = Field(default="", description="Plugin author") author_email: str = Field(default="", description="Author contact") homepage: str = Field(default="", description="Plugin homepage URL") license: str = Field(default="MIT", description="License") min_app_version: str = Field(default="0.0.0") icon: str = Field(default="", description="Icon URL or emoji") screenshots: list[str] = Field(default_factory=list) changelog: str = Field(default="", description="Changelog URL or text") tags: list[str] = Field(default_factory=list, description="Marketplace categories") price: float = Field(default=0.0, description="Price (0 = free)") ``` ### 5.5 Plugin-Download-Endpoint (4 Std) **Neue Route: `POST /api/v1/plugins/install-marketplace`** ```python @router.post("/install-marketplace") async def install_from_marketplace( body: MarketplaceInstall, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("plugins:configure")), ): """Install a plugin from the marketplace. 1. Download ZIP from marketplace URL 2. Verify signature against allowlist 3. Validate manifest 4. Check dangerous imports 5. Validate migration SQL 6. Install (migrations + DB record) 7. Activate (optional) """ # 1. Download async with httpx.AsyncClient() as client: resp = await client.get(body.url) zip_data = resp.content # 2. Verify signature if not PluginSignature.verify_signature(zip_data, body.signature, public_key): raise HTTPException(403, "Invalid plugin signature") # 3-6. Validate and install # ... (reuse existing validation + install logic) ``` ### 5.6 Plugin-Update-Check (4 Std) ```python @router.get("/updates") async def check_plugin_updates( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("plugins:read")), ): """Check for available plugin updates from marketplace.""" # 1. Query marketplace registry (if configured) # 2. Compare versions with installed plugins # 3. Return list of available updates ``` ### 5.7 Plugin-Quarantine (4 Std) ```python async def _quarantine_plugin(zip_path: Path) -> Path: """Extract plugin to temp dir, validate, then move to plugins/ dir. 1. Extract to /tmp/plugin_upload_/ 2. Validate manifest exists 3. Check dangerous imports 4. Validate migration SQL 5. Check signature 6. If all OK: move to plugins/ dir 7. If any fail: delete temp dir, raise error """ ``` ### 5.8 Tests (8 Std) - `test_marketplace.py` — Download, Verify, Install Flow - `test_signature.py` — Signatur-Validierung - `test_allowlist.py` — Allowlist-Management - `test_quarantine.py` — Quarantine-Validierung - `test_external_discovery.py` — Externe Plugin-Discovery - Integrationstests: Vollständiger Marketplace-Flow ### Meilenstein Phase 5: - ✅ Externe Plugins können entdeckt werden - ✅ Signatur-Validierung funktioniert - ✅ Allowlist schützt vor nicht autorisierten Plugins - ✅ Marketplace-Endpoint ist vorbereitet (deaktiviert bis Marketplace live) - ✅ Plugin-Upload bleibt deaktiviert - ✅ Built-in Plugins laufen ohne Marketplace - ✅ Tests bestanden --- ## Phase 6: Manifest-Anpassung & Konsolidierung **Ziel:** Alle in Phase 4 und 5 definierten Manifest-Felder werden ins `PluginManifest` integriert, bestehende Manifeste aktualisiert, und das Manifest-System finalisiert. **Wichtig:** Diese Phase baut auf Phase 4 (Versioning) und Phase 5 (Marketplace) auf und muss als letztes durchgeführt werden. ### 6.1 PluginManifest erweitern (4 Std) **Aktuelles Manifest (verifiziert 2026-07-26):** ```python class PluginManifest(BaseModel): name: str version: str display_name: str description: str dependencies: list[str] routes: list[PluginRouteDef] events: list[str] migrations: list[str] permissions: list[str] is_core: bool field_definitions: list[FieldDefinition] agent_capabilities: list[str] menu_items: list[FrontendMenuItem] page_routes: list[FrontendPageRoute] detail_tabs: list[FrontendDetailTab] settings_pages: list[FrontendSettingsPage] dashboard_widgets: list[FrontendDashboardWidget] agent_definitions: list[AgentDefinitionContribution] automation_templates: list[AutomationTemplateContribution] cron_jobs: list[CronJobContribution] heartbeat_configs: list[HeartbeatConfigContribution] miniapps: list[MiniAppContribution] custom_fields: list[CustomFieldDefinition] model_config = {"extra": "forbid"} ``` **Neue Felder hinzufügen:** ```python class PluginManifest(BaseModel): # ... alle bestehenden Felder ... # ── Versioning (Phase 4) ── min_app_version: str = Field( default="0.0.0", description="Minimum LeoCRM version required (SemVer)" ) # ── Marketplace (Phase 5) ── author: str = Field(default="", max_length=200, description="Plugin author name") author_email: str = Field(default="", max_length=200, description="Author contact email") homepage: str = Field(default="", max_length=500, description="Plugin homepage URL") license: str = Field(default="MIT", max_length=50, description="License identifier") icon: str = Field(default="", description="Icon URL or emoji") screenshots: list[str] = Field(default_factory=list, description="Screenshot URLs for marketplace") changelog: str = Field(default="", description="Changelog URL or inline text") marketplace_tags: list[str] = Field(default_factory=list, description="Marketplace category tags") price: float = Field(default=0.0, ge=0.0, description="Price (0 = free)") # ── Hooks (Phase 2) ── hooks: list[str] = Field( default_factory=list, description="Hook names this plugin registers (e.g. 'contact.before_create')" ) # ── Contracts (Phase 1) ── contract_version: str = Field( default="1.0.0", description="Contract API version this plugin exposes" ) ``` ### 6.2 Manifest-Schema-Dokumentation aktualisieren (3 Std) **`MANIFEST_SCHEMA_DOC` in `manifest.py` erweitern:** - Alle neuen Felder in `fields`-Dict aufnehmen - `example`-Manifest mit neuen Feldern aktualisieren - API-Endpoint `GET /api/v1/plugins/manifest` liefert vollständiges Schema ### 6.3 Alle 19 Plugin-Manifeste aktualisieren (8 Std) Jedes Plugin-Manifest muss um die neuen Felder erweitert werden: | # | Plugin | Aufwand | Neue Felder | |---|---|---|---| | 1 | `ai_assistant` | 30 Min | author, min_app_version, hooks, contract_version | | 2 | `ai_proactive` | 30 Min | author, min_app_version, hooks, contract_version | | 3 | `ai_ui_control` | 20 Min | author, min_app_version, contract_version | | 4 | `automation` | 30 Min | author, min_app_version, hooks, contract_version | | 5 | `calendar` | 20 Min | author, min_app_version, hooks, contract_version | | 6 | `dms` | 20 Min | author, min_app_version, hooks, contract_version | | 7 | `entity_links` | 15 Min | author, min_app_version, contract_version | | 8 | `forgejo_error_reporter` | 15 Min | author, min_app_version, contract_version | | 9 | `kommunikation` | 30 Min | author, min_app_version, hooks, contract_version | | 10 | `mail` | 20 Min | author, min_app_version, hooks, contract_version | | 11 | `mcp_client` | 20 Min | author, min_app_version, contract_version | | 12 | `mcp_server` | 20 Min | author, min_app_version, contract_version | | 13 | `permissions` | 20 Min | author, min_app_version, contract_version | | 14 | `report_generator` | 20 Min | author, min_app_version, contract_version | | 15 | `system_notif` | 15 Min | author, min_app_version, contract_version | | 16 | `tags` | 15 Min | author, min_app_version, contract_version | | 17 | `tasks` | 20 Min | author, min_app_version, hooks, contract_version | | 18 | `test_sample` | 10 Min | author, min_app_version, contract_version | | 19 | `unified_search` | 20 Min | author, min_app_version, hooks, contract_version | **Muster für Aktualisierung:** ```python # VORHER: manifest = PluginManifest( name="calendar", version="1.0.0", display_name="Calendar", ... ) # NACHHER: manifest = PluginManifest( name="calendar", version="1.0.0", display_name="Calendar", # ... bestehende Felder ... # ── Neue Felder ── min_app_version="1.0.0", author="LeoCRM Team", license="MIT", hooks=["calendar.before_appointment", "calendar.after_appointment"], contract_version="1.0.0", ) ``` ### 6.4 Frontend Plugin-Manifest-Typen aktualisieren (2 Std) **`frontend/src/api/pluginManifests.ts` und `frontend/src/types/automation.ts`:** - TypeScript-Interfaces um neue Manifest-Felder erweitern - `PluginManifestResponse`-Typ aktualisieren - Frontend-Komponenten die Manifest-Felder anzeigen erweitern ### 6.5 Manifest-Validierung verschärfen (3 Std) **Neue Validierungsregeln in `PluginManifest`:** ```python @field_validator("min_app_version") @classmethod def validate_min_app_version(cls, v: str) -> str: """Validate SemVer format.""" from app.plugins.semver import SemVer SemVer.parse(v) # Raises ValueError if invalid return v @field_validator("hooks") @classmethod def validate_hooks(cls, v: list[str]) -> list[str]: """Validate hook names follow namespace.pattern.""" for hook in v: if not re.match(r"^[a-z_]+\.[a-z_]+$", hook): raise ValueError(f"Invalid hook name '{hook}': must be 'namespace.action'") return v ``` ### 6.6 Tests für erweitertes Manifest (3 Std) - `test_manifest.py` — Neue Felder validieren - `test_manifest_validation.py` — SemVer-Validierung, Hook-Name-Validierung - Alle Plugin-Tests: Manifest mit neuen Feldern erstellen - Frontend-Tests: Manifest mit neuen Feldern rendern ### Meilenstein Phase 6: - ✅ `PluginManifest` hat alle neuen Felder (min_app_version, author, hooks, contract_version, etc.) - ✅ `MANIFEST_SCHEMA_DOC` ist vollständig aktualisiert - ✅ Alle 19 Plugin-Manifeste haben die neuen Felder - ✅ Frontend-Typen sind aktualisiert - ✅ Manifest-Validierung ist verschärft - ✅ Tests bestanden --- ## Zeitplan ``` Woche 1 (Tag 1-5): Phase 1 — Contracts (Teil 1: contracts.py + Imports) Woche 2 (Tag 6-8): Phase 1 — Contracts (Teil 2: Deaktivierung + Tests) (Tag 9-10): Phase 2 — Hooks/Filters-System Woche 3 (Tag 11): Phase 3 — Plugin-Isolation (Tag 12-14): Phase 4 — Plugin-Versioning Woche 4 (Tag 15-19): Phase 5 — Marketplace-Vorbereitung Woche 5 (Tag 20-22): Phase 6 — Manifest-Anpassung & Konsolidierung (Tag 23): Puffer / Bugfixes / Doku ``` ### Abhängigkeiten ``` Phase 1 (Contracts) ──→ Phase 3 (Isolation: Linting braucht Contracts als Ausnahme) │ └──→ Phase 2 (Hooks: unabhängig, kann parallel) │ └──→ Phase 4 (Versioning: braucht Contracts für min_app_version) │ └──→ Phase 5 (Marketplace: braucht alles) │ └──→ Phase 6 (Manifest: braucht Phase 4 + 5 Felder) ``` ### Parallelisierungsmöglichkeiten - Phase 1 und Phase 2 können **parallel** laufen (verschiedene Entwickler) - Phase 3 kann erst nach Phase 1 starten - Phase 4 kann nach Phase 1 starten - Phase 5 kann erst nach Phase 1+4 starten - Phase 6 kann erst nach Phase 4+5 starten (braucht deren Manifest-Felder) --- ## Risiken | Risiko | Wahrscheinlichkeit | Auswirkung | Mitigation | |---|---|---|---| | Contract-Refactoring bricht bestehende Funktionalität | Mittel | Hoch | Tests nach jedem Plugin, schrittweise Migration | | Hooks/Filters verändern Core-Verhalten | Niedrig | Mittel | Tests für alle Hook-Punkte, Priority-System | | Externe Plugin-Discovery hat Sicherheitslücken | Mittel | Hoch | Signatur-Validierung, Quarantine, Allowlist | | SemVer-Parse-Fehler bei bestehenden Versionen | Niedrig | Niedrig | Fallback auf String-Vergleich | | Rollback-Migrationen löschen Daten | Mittel | Hoch | Bestätigungs-Prompt, Backup vor Rollback | --- ## Erfolgskriterien Nach Abschluss aller 5 Phasen: 1. ✅ **0 direkte Cross-Plugin-Imports** (grep-verifiziert, linting-enforced) 2. ✅ **Alle 16 Plugins haben contracts.py** mit klarer öffentlicher API 3. ✅ **Contracts werden bei Deaktivierung abgemeldet** 4. ✅ **Hooks/Filters-System** mit 15+ Hook-Punkten in Core-Services 5. ✅ **Plugin-Isolation** durch Linting-Regeln erzwungen 6. ✅ **SemVer-Vergleich** statt String-Vergleich 7. ✅ **Rollback-Migrationen** für alle Plugins verfügbar 8. ✅ **min_app_version** wird bei Installation geprüft 9. ✅ **Update-Benachrichtigung** im Frontend 10. ✅ **Marketplace-Endpoint** vorbereitet (deaktiviert) 11. ✅ **Signatur-Validierung** für externe Plugins 12. ✅ **Allowlist** schützt vor nicht autorisierten Plugins 13. ✅ **Externe Plugin-Discovery** funktioniert 14. ✅ **Alle Tests bestanden** 15. ✅ **Built-in Plugins laufen ohne Marketplace** 16. ✅ **PluginManifest hat alle neuen Felder** (min_app_version, author, hooks, contract_version, etc.) 17. ✅ **Alle 19 Plugin-Manifeste aktualisiert** mit neuen Feldern 18. ✅ **Manifest-Validierung verschärft** (SemVer, Hook-Names) 19. ✅ **Frontend-Typen aktualisiert** für neue Manifest-Felder --- ## Dokumentation Nach Abschluss jeder Phase: - `docs/plugin-system/phase-N.md` — Was wurde gemacht, was geändert - `docs/plugin-system/contracts-api.md` — Contract-API Referenz - `docs/plugin-system/hooks-api.md` — Hooks/Filters Referenz - `docs/plugin-system/marketplace-api.md` — Marketplace-API Referenz - `docs/plugin-system/plugin-development-guide.md` — Wie man ein Plugin entwickelt --- **Dieser Plan ist vollständig. Alle Aufgaben, Aufwände, Abhängigkeiten und Risiken sind erfasst.**