Files
leocrm/PLUGIN-SYSTEM-UMBAUPLAN.md
T
Agent Zero 5ec1fc9b05 Phase 1: Fix all critical release blockers (B1-B10)
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.
2026-07-26 20:45:42 +02:00

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 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

"""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.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

"""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

"""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.sql
  • 0001_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 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:

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 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

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:

  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

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.