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
32 KiB
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.pyunverändert — Plan passt - ✅
registry.pyunverändert — Plan passt - ✅
manifest.pyunverä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_pluginindeps.pyhinzugekommen — 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:
"""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:
# 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():
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 Teststest_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
"""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)
# 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.pymit 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
"""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 checkin 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 importierenapp/plugins/builtins/__init__.py— Plugin-Discoveryapp/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
"""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:
# 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:
# 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:
-- 0001_initial.sql
-- UP:
CREATE TABLE ...;
-- DOWN:
DROP TABLE ... CASCADE;
Oder separate Dateien:
0001_initial_up.sql0001_initial_down.sql
4.3 Version-Kompatibilitäts-Check (3 Std)
Manifest-Erweiterung:
class PluginManifest(BaseModel):
# ... existing fields ...
min_app_version: str = Field(
default="0.0.0",
description="Minimum LeoCRM version required"
)
Check bei Installation:
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 Casestest_versioning.py— Upgrade, Downgrade, Kompatibilitäts-Checktest_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:
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
"""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
# 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:
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
@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)
@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)
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 Flowtest_signature.py— Signatur-Validierungtest_allowlist.py— Allowlist-Managementtest_quarantine.py— Quarantine-Validierungtest_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):
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:
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/manifestliefert 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:
# 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:
@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 validierentest_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:
- ✅
PluginManifesthat alle neuen Felder (min_app_version, author, hooks, contract_version, etc.) - ✅
MANIFEST_SCHEMA_DOCist 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:
- ✅ 0 direkte Cross-Plugin-Imports (grep-verifiziert, linting-enforced)
- ✅ Alle 16 Plugins haben contracts.py mit klarer öffentlicher API
- ✅ Contracts werden bei Deaktivierung abgemeldet
- ✅ Hooks/Filters-System mit 15+ Hook-Punkten in Core-Services
- ✅ Plugin-Isolation durch Linting-Regeln erzwungen
- ✅ SemVer-Vergleich statt String-Vergleich
- ✅ Rollback-Migrationen für alle Plugins verfügbar
- ✅ min_app_version wird bei Installation geprüft
- ✅ Update-Benachrichtigung im Frontend
- ✅ Marketplace-Endpoint vorbereitet (deaktiviert)
- ✅ Signatur-Validierung für externe Plugins
- ✅ Allowlist schützt vor nicht autorisierten Plugins
- ✅ Externe Plugin-Discovery funktioniert
- ✅ Alle Tests bestanden
- ✅ Built-in Plugins laufen ohne Marketplace
- ✅ PluginManifest hat alle neuen Felder (min_app_version, author, hooks, contract_version, etc.)
- ✅ Alle 19 Plugin-Manifeste aktualisiert mit neuen Feldern
- ✅ Manifest-Validierung verschärft (SemVer, Hook-Names)
- ✅ Frontend-Typen aktualisiert für neue Manifest-Felder
Dokumentation
Nach Abschluss jeder Phase:
docs/plugin-system/phase-N.md— Was wurde gemacht, was geändertdocs/plugin-system/contracts-api.md— Contract-API Referenzdocs/plugin-system/hooks-api.md— Hooks/Filters Referenzdocs/plugin-system/marketplace-api.md— Marketplace-API Referenzdocs/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.