2026-07-26 20:45:42 +02:00
# LeoCRM Plugin-System — Kompletter Umbauplan
**Erstellt: ** 2026-07-26
2026-07-26 23:15:34 +02:00
**Aktualisiert: ** 2026-07-26 (Codebasis-Verifikation + Phase 6)
**Geschätzter Gesamtaufwand: ** ~149 Stunden (~19 Arbeitstage)
2026-07-26 20:45:42 +02:00
**Status: ** Geplant — noch nicht gestartet
2026-07-26 23:15:34 +02:00
**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
2026-07-26 20:45:42 +02:00
---
## Ü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 |
2026-07-26 23:15:34 +02:00
| Phase 6 | — | Manifest-Anpassung & Konsolidierung | 20 | 2,5 |
| **Gesamt ** | | | **149 ** | * * ~19** |
2026-07-26 20:45:42 +02:00
**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
---
2026-07-26 23:15:34 +02:00
## 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
---
2026-07-26 20:45:42 +02:00
## 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
2026-07-26 23:15:34 +02:00
Woche 5 (Tag 20-22): Phase 6 — Manifest-Anpassung & Konsolidierung
(Tag 23): Puffer / Bugfixes / Doku
2026-07-26 20:45:42 +02:00
```
### 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)
2026-07-26 23:15:34 +02:00
│
└──→ Phase 6 (Manifest: braucht Phase 4 + 5 Felder)
2026-07-26 20:45:42 +02:00
```
### 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
2026-07-26 23:15:34 +02:00
- Phase 6 kann erst nach Phase 4+5 starten (braucht deren Manifest-Felder)
2026-07-26 20:45:42 +02:00
---
## 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 **
2026-07-26 23:15:34 +02:00
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
2026-07-26 20:45:42 +02:00
---
## 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. **