B1: Remove duplicate get_redis() — singleton no longer overwritten B2: Plugin routes now enforce activation status via require_active_plugin() B3: Fix UploadFile ForwardRef error — remove functools.wraps from wrap_plugin_route B4: DMS upload uses true streaming via save_stream() instead of RAM accumulation B5: Worker on_startup registers plugin event handlers + webhook dispatcher B6: Implement send_password_reset_email job, remove raw token logging B7: Webhook SSRF protection (IP validation, no redirects), secret removed from response B8: RLS repair migration 0044 + separate crm_runtime DB user (NOSUPERUSER, NOBYPASSRLS) B9: Fix .env.docker.example AUTH_SECRET → SECRET_KEY B10: Remove Redis default password, remove exposed DB/Redis ports Also: add frontend_url to config, add SMTP settings to .env.docker.example, update prestart.sh to use MIGRATION_DATABASE_URL for alembic.
24 KiB
LeoCRM Plugin-System — Kompletter Umbauplan
Erstellt: 2026-07-26 Geschätzter Gesamtaufwand: ~129 Stunden (~16 Arbeitstage) Status: Geplant — noch nicht gestartet
Ü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 |
| Gesamt | 129 | 16 |
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
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
(Tag 20): 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)
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
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
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.