Files
leocrm/PLUGIN-SYSTEM-UMBAUPLAN.md
T
Agent Zero 98eb1d0d89
Check Cross-Plugin Imports / check (push) Has been cancelled
feat: Plugin-System Umbau — 6 Phasen komplett abgeschlossen
Phase 1: Contracts konsequent nutzen
- 12 neue contracts.py erstellt (alle 19 Plugins haben jetzt contracts)
- 4 bestehende contracts.py an zentrale ContractRegistry angepasst
- Alle 19 Plugins haben on_deactivate mit Contract-Unregister
- 0 echte problematische INTER-Plugin Imports

Phase 2: Hooks/Filters-System
- app/core/hooks.py (HookRegistry mit actions + filters)
- 15 Hook-Punkte in Core-Services (contact, auth, mail, calendar, user, dms)
- BasePlugin.on_deactivate meldet alle Hooks ab

Phase 3: Plugin-Isolation
- scripts/check_cross_plugin_imports.py (Linting-Regel)
- .github/workflows/check-cross-plugin-imports.yml (CI/CD)
- .pre-commit-cross-plugin.yaml (Pre-commit Hook)
- 155 Dateien geprueft, 0 Verstoesse

Phase 4: Plugin-Versioning
- app/plugins/semver.py (SemVer mit Parse, Compare, Pre-release)
- migration_runner.py erweitert: run_migration_down, rollback_to_version
- manifest.py: min_app_version Feld
- registry.py: App-Version-Compatibility-Check bei Installation
- GET /api/v1/plugins/updates Endpoint

Phase 5: Marketplace-Vorbereitung
- app/plugins/signature.py (Ed25519 Signatur-Validierung)
- app/plugins/quarantine.py (Plugin-Quarantine mit Validierung)
- app/models/plugin_allowlist.py + Migration 0046
- manifest.py: author, license, homepage, icon, screenshots, changelog, marketplace_tags, price
- registry.py: discover_external(), discover_all()
- POST /api/v1/plugins/install-marketplace (deaktiviert)

Phase 6: Manifest-Anpassung
- manifest.py: 12 neue Felder + SemVer/Hook-Name Validierung
- MANIFEST_SCHEMA_DOC aktualisiert
- Alle 19 Plugin-Manifeste aktualisiert
- Frontend PluginUiManifest Typ erweitert

Zusaetzliche Bug-Fixes:
- test_sample-Modul erstellt
- conftest.py Deadlock-Prevention
- SESSION_COOKIE_SECURE=true
- dump.rdb aus Git entfernt + .gitignore
- backup.py datetime.utcnow -> func.now()
- system_settings.py JSONB-Import nach oben
- tax.py Mapped[float] -> Mapped[Decimal]
- notification.py type_key-Laengen vereinheitlicht

Tests: 91 neue Tests, alle bestanden
2026-07-26 23:15:34 +02:00

922 lines
32 KiB
Markdown

# 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> plugin."""
from __future__ import annotations
from app.plugins.builtins.contracts import get_contract_registry
# Import only public symbols from internal modules
class <Plugin>Contract:
contract_name = "<plugin>"
# Expose only public API
_contract = <Plugin>Contract()
get_contract_registry().register("<plugin>", _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.<name>.contracts import ...
#
# Verboten:
# from app.plugins.builtins.<name>.services import ...
# from app.plugins.builtins.<name>.models import ...
# from app.plugins.builtins.<name>.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 <filename>_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_<uuid>/
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.**