feat: Plugin-System Umbau — 6 Phasen komplett abgeschlossen
Check Cross-Plugin Imports / check (push) Has been cancelled

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
This commit is contained in:
Agent Zero
2026-07-26 23:15:34 +02:00
parent 744d595cae
commit 98eb1d0d89
62 changed files with 3284 additions and 18 deletions
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""Check for forbidden direct cross-plugin imports.
This script enforces that plugins communicate only through contracts,
not by importing internal modules from each other.
Allowed:
from app.plugins.builtins.contracts import get_contract
from app.plugins.builtins.<name>.contracts import ...
from app.plugins.builtins.<name> import <PluginClass> (in __init__.py only)
Forbidden:
from app.plugins.builtins.<name>.services import ...
from app.plugins.builtins.<name>.models import ...
from app.plugins.builtins.<name>.routes import ...
Exceptions (files that are allowed to import anything):
- */contracts.py — contracts import from internal modules
- */__init__.py — plugin discovery
- app/plugins/registry.py — registry manages all plugins
- app/plugins/builtins/__init__.py — builtin discovery
- tests/* — test files
- conftest.py — test fixtures
Usage:
python scripts/check_cross_plugin_imports.py
python scripts/check_cross_plugin_imports.py --path app/plugins/builtins
Exit codes:
0 — no violations
1 — violations found
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
# ─── Configuration ───
PROJECT_ROOT = Path(__file__).resolve().parent.parent
BUILTINS_DIR = PROJECT_ROOT / "app" / "plugins" / "builtins"
# Files that are exempt from the rule
EXEMPT_FILES = {
"contracts.py",
"__init__.py",
"conftest.py",
}
# Directories that are exempt
EXEMPT_DIRS = {
"tests",
"__pycache__",
"migrations",
}
# Files that are exempt by path
EXEMPT_PATHS = {
PROJECT_ROOT / "app" / "plugins" / "registry.py",
PROJECT_ROOT / "app" / "plugins" / "builtins" / "__init__.py",
PROJECT_ROOT / "app" / "plugins" / "base.py",
PROJECT_ROOT / "app" / "plugins" / "manifest.py",
PROJECT_ROOT / "app" / "plugins" / "migration_runner.py",
}
# Pattern for cross-plugin imports
IMPORT_PATTERN = re.compile(
r"^\s*(?:from|import)\s+app\.plugins\.builtins\.([^.]+)\.(.+?)\s+import\s+(.+)$"
)
# Pattern for allowed contract imports
CONTRACT_IMPORT_PATTERN = re.compile(
r"^\s*from\s+app\.plugins\.builtins\.(?:contracts|[^.]+\.contracts)\s+import\s+(.+)$"
)
# Pattern for __init__.py plugin class imports (allowed in __init__.py only)
PLUGIN_CLASS_IMPORT_PATTERN = re.compile(
r"^\s*from\s+app\.plugins\.builtins\.([^.]+)\s+import\s+([A-Z]\w*Plugin)\s*$"
)
def is_exempt(filepath: Path) -> bool:
"""Check if a file is exempt from the cross-plugin import rule."""
# Exempt by filename
if filepath.name in EXEMPT_FILES:
return True
# Exempt by path
if filepath in EXEMPT_PATHS:
return True
# Exempt test directories
parts = filepath.parts
for exempt_dir in EXEMPT_DIRS:
if exempt_dir in parts:
return True
return False
def check_file(filepath: Path) -> list[str]:
"""Check a single file for forbidden cross-plugin imports.
Returns a list of violation messages (empty if clean).
"""
if is_exempt(filepath):
return []
violations: list[str] = []
rel_path = filepath.relative_to(PROJECT_ROOT)
# Determine the source plugin from the file path
try:
parts = filepath.relative_to(BUILTINS_DIR).parts
src_plugin = parts[0] if parts else ""
except ValueError:
src_plugin = ""
with open(filepath, encoding="utf-8") as f:
for line_num, line in enumerate(f, 1):
line_stripped = line.strip()
# Skip comments and empty lines
if not line_stripped or line_stripped.startswith("#"):
continue
# Check for cross-plugin import
match = IMPORT_PATTERN.match(line)
if not match:
continue
tgt_plugin = match.group(1)
tgt_module = match.group(2)
# Skip if importing from contracts
if tgt_module == "contracts":
continue
# Skip if same plugin (INTRA-Plugin import)
if tgt_plugin == src_plugin:
continue
# Skip if it's a contract import via the central registry
if CONTRACT_IMPORT_PATTERN.match(line):
continue
# This is a forbidden cross-plugin import
violations.append(
f"{rel_path}:{line_num}: {line_stripped}\n"
f" → Forbidden cross-plugin import: '{tgt_plugin}.{tgt_module}'. "
f"Use contracts instead: get_contract(\"{tgt_plugin}\")"
)
return violations
def find_python_files(search_path: Path | None = None) -> list[Path]:
"""Find all Python files in the search path."""
if search_path is None:
search_path = BUILTINS_DIR
files: list[Path] = []
for root, dirs, fnames in os.walk(search_path):
# Skip exempt directories
dirs[:] = [d for d in dirs if d not in EXEMPT_DIRS]
for fname in fnames:
if fname.endswith(".py"):
files.append(Path(root) / fname)
return sorted(files)
def main() -> int:
"""Run the cross-plugin import checker."""
import argparse
parser = argparse.ArgumentParser(description="Check for forbidden cross-plugin imports.")
parser.add_argument(
"--path",
type=Path,
default=BUILTINS_DIR,
help="Path to check (default: app/plugins/builtins)",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Print checked files even if clean.",
)
args = parser.parse_args()
files = find_python_files(args.path)
all_violations: list[str] = []
checked = 0
for filepath in files:
checked += 1
violations = check_file(filepath)
if violations:
all_violations.extend(violations)
elif args.verbose:
print(f"{filepath.relative_to(PROJECT_ROOT)}")
print(f"\nChecked {checked} files.")
if all_violations:
print(f"\n❌ Found {len(all_violations)} violation(s):\n")
for v in all_violations:
print(f" {v}")
print(
"\nFix: Replace direct imports with contract-based access:\n"
" from app.plugins.builtins.contracts import get_contract\n"
" contract = get_contract(\"plugin_name\")\n"
" if contract:\n result = await contract.some_function(...)"
)
return 1
else:
print("✅ No forbidden cross-plugin imports found.")
return 0
if __name__ == "__main__":
sys.exit(main())