From 8a26737680a10bf7a5447f29b51bb08114e39587 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 17 Sep 2026 22:27:19 +0200 Subject: [PATCH] =?UTF-8?q?docs(audit):=20Astra-Externaudit=20aufgenommen?= =?UTF-8?q?=20=E2=80=94=2041=20Findings=20verifiziert,=20PHASE=20S=20(4=20?= =?UTF-8?q?Wellen)=20in=20Roadmap,=20Milestone=2016,=20Issues=20#396-399?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/audits/astra-audit-2026-09-17.md: vollstaendiger Pruefbericht (2 P0, 29 P1, 10 P2), 10 Findings intern stichprobenartig verifiziert (alle korrekt) - PLATFORM_ROADMAP.md: PHASE S (S1 Sicherheitsgrenzen, S2 Ausfuehrung verbinden, S3 Fachliche Integritaet, S4 Betriebsfreigabe) mit je Finding Korrektur+Abnahme; Abnahmeszenarien quer (Kontakt->Outbox->Worker->Suchindex->KI; Mail->Freigabe->Versand) - Phase R: 8 Astra-Kritikpunkte eingearbeitet (externe Ueberwachung, Sollzustand-Vergleich, Heartbeat statt Queue, echte Prozesse, Modelldiscovery, E2E-Szenarien, Restore-Nachweis, 95%-Formulierung als Freigabekriterien) - PROGRESS.md: Phase S als NÄCHSTE PHASE, Wellen-Issues verlinkt --- PLATFORM_ROADMAP.md | 85 ++- PROGRESS.md | 3 +- docs/audits/astra-audit-2026-09-17.md | 799 ++++++++++++++++++++++++++ 3 files changed, 878 insertions(+), 9 deletions(-) create mode 100644 docs/audits/astra-audit-2026-09-17.md diff --git a/PLATFORM_ROADMAP.md b/PLATFORM_ROADMAP.md index f8022c5..d192b7b 100644 --- a/PLATFORM_ROADMAP.md +++ b/PLATFORM_ROADMAP.md @@ -1814,7 +1814,71 @@ Der AI Assistent ist ein paralleles System das die Kommunikation-Plattform dupli **Reihenfolge (wie umgesetzt):** Q3 → Q1/Q2 → Q4 (Q4 fiel mit Q3 mit, da MiniAppHost dieselbe generierte Map nutzt). Jeder Schritt mit Vitest-Sicherung der betroffenen Seiten und Production-Build-Verifikation (Chunk-Existenz prüfen). -## Phase R — Betriebssicherheit & 95%-Produktionsreife (geplant, user-abgestimmt 2026-09-16 — NÄCHSTE PHASE, vor O/P) +## Externaudit Astra 2026-09-17 (41 Findings) — Sanierung PHASE S (bestätigt, NÄCHSTE PHASE, vor/neben R) + +**Auditergebnis:** 2 P0 (KI führt nicht freigegebene Tools aus; Mandantenverwaltung kann globale Anmeldeidentitäten ändern), 29 P1, 10 P2. Geprüft am vollständigen Stand ee5545d (ZIP). Interne Verifikation am 2026-09-17: 10 Findings stichprobenartig am Code nachgelesen (F01, F02, F05, F08, F10, F12, F17, F24, F37, F41) — **alle 10 korrekt**. Übrige Findings: detailliert mit Zeilennummern belegt, Detail-Verifikation erfolgt jeweils bei Umsetzung. Volltext des Audits: [docs/audits/astra-audit-2026-09-17.md](docs/audits/astra-audit-2026-09-17.md); Kernpunkte je Finding in den Wellen-Issues. + +**Strukturdiagnose (Astra):** Mehrere Stellen verwalten denselben Zustand (Plugin-Aktivität, Schema); Contracts garantieren zu wenig Verhalten; API- und Worker-Ausführung nicht gleichwertig; Berechtigungsprüfungen liegen zu weit vom Seiteneffekt entfernt; Statusanzeigen teils von tatsächlicher Funktion entkoppelt. — Bestätigt und deckt sich mit den realen Incidents (#389 Plugin down 4 Wochen, #380 158 Events failed). + +**Sanierungswellen (Reihenfolge nach Risiko, an Astra-Empfehlung angelehnt):** + +### Welle S1 — Sicherheitsgrenzen (P0 + Auth/Permission-Kette) — ZUERST +- **F01 (P0)** agent_loop._execute_tool: Tool-Ausführung ohne Allowlist- und Permission-Check — unmittelbar vor Handleraufruf prüfen: Tool in der dem LLM angebotenen Liste, required_permission gegen aktuelle User-Rechte, Verbote, Mandant, Plugin aktiv, ggf. Approval. Abnahme: nicht angebotenes Tool → Ablehnung, Handler bleibt null. +- **F02 (P0)** users.py update_user: globale User.email durch Mandanten-Admin (users:write) änderbar → globale Identitätsänderungen (email, is_system_admin global, Passwort) von Mandantenverwaltung trennen; nur Selbstservice oder echte globale Admin. Abnahme: Tenant-Admin kann globale E-Mail/Aktivstatus fremder Mandanten-Mitglieder nicht ändern. +- **F10** require_permission: Token-Scopes ersetzen User-Rechte (early-return) → effektive Rechte = Schnittmenge(User, Token-Scopes, Delegation), Verbote vorrangig. Abnahme: Token mail:write + User ohne mail:write → 403. +- **F05** require_active_plugin läuft vor Auth/ohne Mandantenkontext → Plugin-Gate an authentifizierten Kontext binden, fehlender Kontext = ablehnen. Abnahme: mandantendeaktiviertes Plugin → 403 auch bei gültiger Session. +- **F03** Session-Widerruf: Deaktivierung/Austritt/Löschen/Passwortwechsel müssen in Redis- UND DB-Fallback-Sessionpfaden wirken; Widerruf dauerhaft speichern. Abnahme: Widerruf wirkt auch bei Redis-Ausfall. +- **F11** Approval-Resolution: approver_id/Ablauf/Gruppe/Atomarität prüfen, Entscheider getrennt speichern, Approval an Aktion+Argumente+Revision binden. +- **F15** Workflow-HTTP: aufgelöste IPv4/6-Ziele gegen Privatnetz prüfen, Verbindung an geprüfte Auflösung binden, Redirects prüfen. +- **F20** prestart überschreibt gezielte Rechte-Entzüge (0100) mit pauschalem GRANT DELETE → Tabellenschutz nur migrieren; keine Rechteanhebung beim Start. +- **F21** test_migrations.sh: MIGRATION_DATABASE_URL überschreiben + Zielidentität vor DDL prüfen (sonst Gefahr für echte DB). +- **F23** Tenant-Backup-API triggert datenbankweiten Restore → Gesamtrestore als globale Betriebsoperation mit separater Berechtigung. +- **F30** Admin-Standardpasswort bei unkonfiguriertem Start → verpflichtendes Secret oder sicherer Einmal-Generierung. + +### Welle S2 — Ausführung verbinden (Worker, Jobs, Contracts, Migrationen) +- **F06** Worker registriert keine der 44 Plugin-Event-Handler (BasePlugin.register_event_handlers ist leer) → API und Worker dieselbe idempotente Registrierung; Abnahme über echten Outbox-Durchgriff (Kontakt anlegen → Worker → Suchindex). +- **F07** Hintergrundjobs verlieren Mandantenkontext/Transaktionen → Mandant+Auftraggeber im Job-Payload Pflicht; Kontext vor erstem SQL; fachliche Änderung+Audit+Outbox gemeinsam committen. +- **F08** External-Agent-API: require_permission an Cookie-Auth gebunden (Bearer nie erreicht) + get_db() ist kein Contextmanager (TypeError) → gemeinsamer geprüfter Auth-Kontext für Cookie+Token; Session-Factory statt get_db. +- **F09** CRM-/MCP-Tools senden nicht anerkannte interne Header → Delegationsmechanismus (delegation_token.py) einbinden; UI und Agent gleiche Rechte-Antwort. +- **F12** Workflow approve/reject: approval["id"] auf ORM-Objekt (TypeError) + falsche resolve-Signatur → an zentralen Vertrag anpassen, wartende Freigabe auflösen statt Selbst-Genehmigung. +- **F13** Workflow-Engine: acquire_lock ohne Aufrufer, Idempotenz unvollständig, Resume ungesperrt → Engine als verbindlichen Zustandsübergang; Abnahme: Worker-Neustart + parallele Resume → keine Doppel-Mails. +- **F14** enforce_data_policy lässt Strings ungefiltert + läuft nur vor der Schleife mit db=None → strukturierte Filterung vor Serialisierung; JEDE LLM-Anfrage (inkl. Tool-Antworten) durch Policy; nicht ladbare Policy = Versand-Stop. +- **F16** Plugin-Lifecycle: prestart reaktiviert absichtlich deaktivierte Plugins; Aktivierungsfehler lassen DB-Zustand aktiv → gewünschten Zustand von Installation/Mandantenfreigabe/Laufzeitgesundheit trennen; Abnahme: Deaktivierung überlebt Neustart. +- **F17** 6 Produktionsstellen rufen ContractRegistry.get() auf (existiert nicht; nur get_contract) → Aufrufer fixen; Abnahme über reale Einstiegspunkte (Miniapp-Tools, proaktive Hinweise, Report-Jobs). +- **F18** Drei Schema-Verfahren (Alembic/Plugin-SQL/sync_plugin_schema) mit Sync-Verlust bei Unique/Partial-Indizes → einen Migrationsbesitzer pro Objekt; Startup-Sync als lesender Driftbericht. +- **F19** alembic/env.py lädt nur app.models (46/129 Tabellen; Sortierung scheitert) → deterministische vollständige Modelldiscovery. +- **F31** Provider-Registry vs. Reindex-Listen divergieren → Plugin-Beiträge als gemeinsame Quelle; Abnahme: neuer Provider wird vollständig indiziert. +- **F37** SMTP-Env-Namen (SMTP_USER vs smtp_username u.a.) → Compose/Config/Doku angleichen; Abnahme: Reset-/Alarm-Mail authentifiziert. +- **F40** Plugin-Migrationen nur Dateiname-Tracking → Hashes speichern und prüfen; Sollzustand vorhandener Tabellen (Spalten/FKs/Policies) vergleichen. +- **F41** Agenten-Stundenlimit zählt ab jetzt() statt letzte Stunde → timedelta(hours=1); Kontingent atomar reservieren. + +### Welle S3 — Fachliche Integrität (Daten- und UI-Korrektheit) +- **F25** CSV-Import: Rollback vernichtet frühere Zeilen, Zähler behalten Erfolge, RLS-Kontext weg → Savepoints pro Zeile, Original-Zeilennummern; Zähler = Persistenz. +- **F26** DMS-Dedup vermischft Identität (fremder Datensatz statt eigener Upload) → Content-Storage vs. Fachobjekt trennen; jeder Upload eigene Identität/Rechte. +- **F27** Kalender: SQL-Filter wirft Serien weg bevor Wiederholungen berechnet werden; end_at-Dauer; Mehrtagesüberlappung → Serie nach Laufzeit selektieren, Wiederholungen im Fenster erzeugen. +- **F28** Import/Export ohne Fachrechte (import_export:write ≠ contacts:write; Export ohne Feldrechte) → Fachrechte UND Importrecht; Feldfilter vor Dateierzeugung. +- **F32** Suche: entity_types=[] = alle (soll 0), Filter nach Top-N, Offset unwirksam, before_search zu spät → None/[] unterscheiden; Filter vor Limit; Hook vor Parametern. +- **F33** Workspace-Wechsel invalidiert fachliche Querykeys nicht → Workspace in Query-Identität oder kontrolliert verwerfen. +- **F34** Mandantenwechsel: alte Daten bis Refetch sichtbar → kontrollierter Kontextwechsel (Abbrechen, Caches leeren, Header synchronisieren). +- **F38** pluginStore-Fehler → Dauerspinner (loaded bleibt false) → Fehler/Leer/Erfolg getrennt rendern, Retry anbieten. +- **F39** Office-Edit-Session verweist auf /preview (PDF-only) + Callback-Route existiert nicht → funktionsfähigen Ablauf anbinden oder Feature als nicht-betriebsbereit kennzeichnen. + +### Welle S4 — Betriebsfreigabe (inkl. korrigierter Phase R) +- **F22** Backup im Container nicht betriebsfähig (pg_dump fehlt, Pfade nicht persistiert, Kontext-/User-Bugs) → dokumentierter Ablauf mit Programmen, Rechten, persistiertem Ziel. +- **F24** /health/ready liefert 200 bei not_ready; Worker-Check meldet up ohne Worker → korrekte HTTP-Codes (503), Heartbeat-Alter statt Queue-Länge. +- **F29** CI ohne Lockfiles/Testdienste/tatsächliches Artefakt → reproduzierbare Pipeline gegen eigenes Image. +- **F36** Komponenten-Map-Generator nicht verbindlich im Build → Check an npm-Build/Dockerfile/CI hängen. +- **F04** Suche: autocomplete/similar ohne Objekt-/Feldrechte; Snippet/Titel unfiltert zur LLM → ein Schutzpfad für ALLE Suchvarianten vor Snippet- und LLM-Übergabe. +- **F35** PWA abgeschaltet, aber Offline-Banner verspricht Schreibspeicherung → PWA wiederherstellen ODER Banner an Realität anpassen. +- Phase-R-Korrektur (siehe unten, bereits eingearbeitet). + +**Abnahmeszenarien quer über alle Wellen (Astra-Vorschlag, verbindlich):** +1. Kontaktanlage → Audit/Outbox → separater Worker → Suchindex → erlaubte KI-Abfrage (F06, F07, F17, F04) +2. Mailentwurf → Freigabe → einmaliger Versand → nachvollziehbares Ergebnis (F11, F12, F13) + +**Reihenfolge-Logik:** S1 zuerst (jede nicht autorisierte Aktion verboten), S2 parallel startbar nach S1-P0s, S3/S4 danach. Nach S1+S2 verifizierter Welle: Aufwand neu schätzen (Astra-Hinweis: die 9-14 Tage aus Phase R sind keine Schätzung für 41 Findings). + +## Phase R — Betriebssicherheit & 95%-Produktionsreife (geplant, user-abgestimmt 2026-09-16 — läuft in S4 auf; korrigiert 2026-09-17 nach Astra-Kritik) **Ziel:** Von „Produktion läuft stabil" zu „Produktion verlässlich": stille Ausfälle werden automatisch erkannt und alarmiert (Minuten statt Wochen), die Test-Suite wird zum vertrauenswürdigen Regressionsschutz, Schema-Drift wird automatisch erkannt, Kernprozesse werden nach jedem Deploy regressionsgetestet, Backups sind nachweislich wiederherstellbar. @@ -1824,14 +1888,19 @@ Der AI Assistent ist ein paralleles System das die Kommunikation-Plattform dupli - Outbox: 158 failed Events wochenlang unbemerkt (#380) - Suite-Isolation und alembic-check-Blockade verhindern verlässliche Regressionsschutz-Gates -**95%-Definition (messbar, Betriebssicht) — die 5 Abnahmekriterien:** -1. Kein kritisches Subsystem kann länger als 30 Minuten unbemerkt ausfallen (Auto-Alarm) -2. Komplette pytest-Suite UND komplette Vitest-Suite laufen in je einem Durchlauf grün (verbindliches Regressionsschutz-Gate) -3. Schema-Drift (Alembic vs. Models vs. Plugin-Migrationen) wird automatisch erkannt (alembic check + Hash-Checks in CI) -4. Kern-Flows sind per E2E nach jedem Full-Deploy verifiziert -5. Backup-Restore ist per Drill nachweislich funktioniert und dokumentiert (RTO/RPO) +**95%-Definition (korrigiert 2026-09-17 nach Astra-Kritik):** Die fünf Kriterien sind kein mathematischer Reifegrad, sondern **konkrete Freigabekriterien**. Dokumentiert wird: erfüllte Kriterien, verbleibende Risiken und bekannte Grenzen (Battle-Testing im Echtbetrieb). „95 %" = Zustand, in dem jeder Ausfall laut statt still wird; die restlichen ~5 % sind Echtbetriebs-Edge-Cases, die nur echte Nutzung findet. -Verbleibende ~5% = Battle-Testing im Echtbetrieb: Edge-Cases, die nur echte Nutzung findet, plus neue Features. 100% existiert bei lebenden Systemen nicht — der Unterschied ist, dass bei 95% jeder Ausfall laut wird, statt still. +**Astra-Kritik an Phase R (8 Punkte, 2026-09-17) — eingearbeitet:** +1. ARQ-Heartbeat überwacht sich nicht selbst → zusätzlich externe Überwachung außerhalb der ARQ/Redis-Ausfallkette (z.B. Cron auf Host oder externer Uptime-Check gegen /health/ready). +2. „Installiert aber inaktiv"-Alarm trifft absichtliche Deaktivierung → **Sollzustand** (DB desired state) mit tatsächlicher Betriebsbereitschaft vergleichen; nur Abweichung alarmiert. +3. Leere Queue ≠ laufender Worker → Worker-Heartbeat-ALTER und Verarbeitungsnachweis messen, nicht Queue-Länge. +4. Komplette Suite grün reicht nicht (Mocks/Admin-Tests können Rechtefehler verdecken) → zusätzlich echte API-/Worker-Prozesse mit tatsächlichen Laufzeitrollen prüfen. +5. Ein FK-Fix + Migrationshash genügt nicht → vollständige Modelldiscovery (F19) und eindeutige Schema-Verantwortung (F18) sind Voraussetzung; R3 hängt an S2. +6. E2E-Normalfälle prüfen Rechteentzug/Neustart nicht → Mehrmandanten-, Rollen-, Fehler- und Wiederaufnahme-Szenarien ergänzen. +7. Monatlicher Restorejob beweist keine sichere Zielwahl → isoliertes Ziel und tatsächliche DB-+Datei-Wiederherstellung nach Containerersatz nachweisen. +8. „95 % Produktionsreife" ist keine messbare Zahl → konkrete Freigabekriterien + verbleibende Risiken dokumentieren (siehe oben). + +**Aufwandskorrektur (Astra):** Die 9-14 Tage gelten NICHT für die Behebung aller 41 Audit-Findings (Phase S). Neue Schätzung nach Abschluss von S1+S2. ### R1 — Stille-Ausfälle-Wächter + Alerting (2-3 Tage) — PRIORITY 1, größter Risikoreduktor - ARQ-Heartbeat-Job (alle 5 Min) prüft: (a) /api/v1/plugins — installiert aber nicht active → ALARM (exakt der #389-Fall), (b) /health/ready — DB/Redis/Storage/Worker, (c) Outbox-DLQ — failed > 0 (#380-Klasse), (d) Worker-Queue-Länge diff --git a/PROGRESS.md b/PROGRESS.md index d2645d0..5ecd80a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -95,7 +95,8 @@ Phase P Notizen-App oder UI-Backlog-Module 2-16. 7. **Marketplace ist leer:** Keine Listings in der DB (API 200, listings=0). Demo-Listings koennen via Admin-API (MarketplaceListingCreate, marketplace:admin) angelegt werden — User fragen. **Offene Roadmap-Phasen (user-abgestimmt, startklar):** -- **Phase R** — Betriebssicherheit & 95%-Produktionsreife (R1-R6, user-abgestimmt 2026-09-16). **NÄCHSTE PHASE, vor O/P.** R1 Alerting gegen stille Ausfälle [#390](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/390) (PRIORITY 1 — Evidenz: KI-Chat 4 Wochen still down #389), R2 Suite verlässlich [#391](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/391), R3 Schema-Integrität [#392](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/392), R4 E2E-Kernflows [#393](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/393), R5 Backup-Restore-Drill [#394](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/394), R6 Ops-Runbook [#395](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/395). Milestone 15. 5 messbare Abnahmekriterien = die 95%-Definition (Details + DoD: Roadmap Phase R). +- **Phase S** — Astra-Sanierung (S1-S4, bestätigt 2026-09-17). **NÄCHSTE PHASE.** Externaudit Astra: 41 Findings (2 P0, 29 P1, 10 P2), 10 stichprobenartig intern verifiziert — alle korrekt. Volltext: [docs/audits/astra-audit-2026-09-17.md](docs/audits/astra-audit-2026-09-17.md). Wellen: S1 Sicherheitsgrenzen [#396](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/396) (F01 KI-Tool-Freigabe P0, F02 globale E-Mail P0, F03, F05, F10, F11, F15, F20, F21, F23, F30), S2 Ausführung verbinden [#397](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/397) (F06-F09, F12-F19, F31, F37, F40, F41), S3 Fachliche Integrität [#398](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/398) (F25-F28, F32-F35, F38, F39), S4 Betriebsfreigabe [#399](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/399) (F04, F22, F24, F29, F36 + korrigierte Phase R). Milestone 16. Abnahmen quer: Kontaktanlage→Outbox→Worker→Suchindex→KI-Abfrage; Mailentwurf→Freigabe→einmaliger Versand. Neue Aufwandsschätzung nach S1+S2. +- **Phase R** — Betriebssicherheit & 95%-Produktionsreife (R1-R6, user-abgestimmt 2026-09-16). Läuft in Phase S Welle 4 auf; korrigiert 2026-09-17 nach Astra-Kritik (8 Punkte in Roadmap eingearbeitet). R1 Alerting gegen stille Ausfälle [#390](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/390) (PRIORITY 1 — Evidenz: KI-Chat 4 Wochen still down #389), R2 Suite verlässlich [#391](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/391), R3 Schema-Integrität [#392](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/392), R4 E2E-Kernflows [#393](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/393), R5 Backup-Restore-Drill [#394](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/394), R6 Ops-Runbook [#395](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/395). Milestone 15. 5 messbare Abnahmekriterien = die 95%-Definition (Details + DoD: Roadmap Phase R). - **Phase M** — MiniApp-Plattform & Dashboard-Builder (M1-M6). **M1 ✓** (Universal-Registry, `/api/v1/miniapps`), **M2 ✓** (persönliche Dashboards: Tabelle, CRUD, Seed, RLS), **M3 ✓** (Dashboard-Builder: Edit-Modus, Drag&Drop, Palette, Tabs), **M4 ✓** (System-Rückbau, Core = reiner Host), **M5 ✓** (Plugin-MiniApps), **M6 ✓ erledigt — PHASE M KOMPLETT** (Windows-Host + AI-Agenten-Tool send_miniapp — siehe Phase-M6-Section). - **Phase N** — Workspace-Scopes (N1-N4). **N1 ✓** (Scope-Registry via Contract), **N2 ✓** (Dynamischer Scope-Editor), **N3 ✓** (Backend-Filterung contacts/dms/mail/calendar + Frontend-Defaults), **N4 ✓ erledigt — PHASE N KOMPLETT** (7 weitere Module: Tasks nur-meine, Kommunikation-Räume, Wiki-Kategorien-Subtree, Reports-Vorlagen, Agents, Tags, Search-Entity-Types + Navigation Startseite/Menü-Reihenfolge + Dashboard-Schnittstelle — siehe Phase-N4-Section). **Nächster Schritt:** Phase O UI-Overhaul (offen: 1.2 Kontakte-Drag-Drop in Ordner, 1.3 MoveDialog) oder Phase P Notizen-App (P1-P5). - **Phase O** — UI-Overhaul (umbenannt von Doppel-L, Bug-Verifikation steht im Roadmap-Eintrag: 5/7 Bugs bereits erledigt, offen: 1.2 Kontakte-Drag-Drop in Ordner, 1.3 MoveDialog) diff --git a/docs/audits/astra-audit-2026-09-17.md b/docs/audits/astra-audit-2026-09-17.md new file mode 100644 index 0000000..f1de906 --- /dev/null +++ b/docs/audits/astra-audit-2026-09-17.md @@ -0,0 +1,799 @@ +# Externaudit Astra — Vollständiger Prüfbericht (17.09.2026) + +> **Geprüft:** leocrm-full-2026-09-16_ee5545d.zip (Stand ee5545d, Alembic-Head 0144) +> **Prüfmodell:** Astra (neues Modell, 2026-09-17) +> **Bearbeitung:** Interne Verifikation 2026-09-17 — 10 Findings stichprobenartig am Code bestätigt (F01, F02, F05, F08, F10, F12, F17, F24, F37, F41, alle korrekt). Sanierung: PHASE S in PLATFORM_ROADMAP.md, Milestone + Wellen-Issues in Forgejo. +> **Volltext unverändert übernommen aus der Modellausgabe.** + +--- + +## Gesamturteil + +LeoCRM hat eine brauchbare modulare Grundarchitektur. Der geprüfte Stand rechtfertigt aber noch keine belastbare Produktionsfreigabe für mandantenfähige, durch KI gesteuerte Geschäftsprozesse. + +Das Hauptproblem sind widersprüchliche oder unterbrochene Verbindungen zwischen vorhandenen Komponenten: + +- Berechtigungen werden je nach Zugangsweg unterschiedlich geprüft. +- API und Worker registrieren unterschiedliche Funktionen. +- Mandantenkontext geht an Session- und Transaktionsgrenzen verloren. +- Migrationen und Startup-Skripte verändern dieselben Zustände unterschiedlich. +- Erfolgsanzeigen entsprechen teilweise nicht dem tatsächlichen Ergebnis. + +Die Sanierung kann auf dem bestehenden System aufbauen. Vorrang haben gemeinsame Ausführungspfade, verlässliche Transaktionen und durchgängige Autorisierung. + +Die Befunde verteilen sich auf: + +| Priorität | Bedeutung | Anzahl | +|---|---|---| +| P0 | Grundlegende Sicherheitsgrenze verletzt; zuerst absichern | 2 | +| P1 | Erheblicher Sicherheits-, Funktions-, Integritäts- oder Betriebsfehler | 29 | +| P2 | Konkreter Integrations-, Konsistenz- oder Wartbarkeitsfehler | 10 | + +Die Einstufungen sind Auditprioritäten, keine CVSS-Bewertungen. + +## Was tatsächlich geprüft wurde + +Das Archiv wurde inventarisiert; relevante Aufrufketten wurden gelesen und mit Build-, Test- und Laufzeitprüfungen ergänzt. 23 isolierte Proben untersuchten konkrete Fehlerbedingungen und Bestände mit echtem Anwendungscode und gezielt ersetztem I/O. + +| Prüfung | Ergebnis | +|---|---| +| Python-Compile für Anwendung und Migrationen | Erfolgreich | +| Backend-Testcollection | 2.362 Tests gefunden | +| test_phase_f_agents.py | 42 Testkörper bestanden; 42 Teardown-Fehler wegen fehlendem Redis | +| Ruff | 7 Meldungen | +| TypeScript und Frontend-Produktionsbuild | Erfolgreich | +| Vollständiger Vitest-Versuch | Kein abgeschlossener Ergebnisdatensatz | +| BlockRenderer.test.tsx gezielt | 76 Tests: 68 bestanden, 8 fehlgeschlagen | +| Generierte Plugin-Komponentenmap | Aktuell, 43 Komponenten | +| Cross-Plugin-Checker | 497 Dateien geprüft, 0 gemeldete Verstöße | +| Alembic-Head | Genau ein Head: 0144 | +| Alembic-Metadatensortierung | Fehler reproduziert | +| Geladener ORM-Bestand | 129 Tabellen, 120 mit tenant_id, 231 FK-Constraints | + +Die acht Fehler der gezielten UI-Testdatei betreffen sieben fehlende QueryClient-Fixtures und eine überholte Erwartung an bereinigtes HTML. Daraus folgt keine nachgewiesene XSS-Lücke. + +Prüfgrenze: Kein vollständiger PostgreSQL-/Redis-Integrationstest, kein separater produktiver Worker, kein vollständiger Browser-E2E-Lauf und kein tatsächlicher Restore. Die Produktionsinstanz wurde nicht verändert. Der Projektcode blieb unverändert. + +## Findings + +### F01 · P0 — KI führt nicht freigegebene Werkzeuge aus + +Die ReAct-Schleife übernimmt Werkzeugnamen aus der Modellantwort und sucht diese im gesamten ToolRegistry. _execute_tool prüft weder die tatsächlich angebotene Werkzeugliste noch required_permission oder aktuelle Benutzerrechte. + +Reproduziert: Angeboten wurde nur audit_allowed. Das Modell lieferte audit_restricted, registriert mit system:admin. Dessen Handler wurde trotzdem ausgeführt. + +Die konkrete Schadenswirkung hängt davon ab, ob der jeweilige Handler zusätzliche eigene Rechteprüfungen besitzt. + +Korrektur: Unmittelbar vor jedem Handleraufruf Werkzeugfreigabe, Argumente, Benutzerrechte, Verbote, Mandant, Plugin-Aktivierung und gegebenenfalls Genehmigung prüfen. + +Abnahme: Ein registriertes, aber nicht freigegebenes Werkzeug wird abgewiesen; Handleraufrufe bleiben null. Rechteentzug während eines Agentenlaufs wirkt auf die nächste Aktion. + +Fundstelle: app/ai/agent_loop.py, Zeilen 116 und 344. + +### F02 · P0 — Mandantenverwaltung kann globale Anmeldeidentitäten verändern + +Mit users:write kann die globale User.email eines Mitglieds des aktuellen Mandanten verändert werden. + +User ist jedoch mandantenübergreifend. Derselbe Datensatz trägt Passwort und Systemadmin-Flag. Der Passwort-Reset verwendet die veränderbare E-Mail-Adresse. + +Ein Benutzerverwalter aus Mandant A kann dadurch bei einem gemeinsamen Benutzer dessen globale Reset-Adresse verändern. Der vollständige Übernahmeablauf wurde nicht gegen ein reales Konto ausgeführt. Die Änderung des globalen Feldes wurde isoliert reproduziert. + +Korrektur: Globale Identitätsänderungen von der Verwaltung einer Mandantenmitgliedschaft trennen. E-Mail-Wechsel nur über verifizierten Selbstservice oder ausdrücklich berechtigte globale Administration. + +Abnahme: Ein Tenant-Verwalter kann weder die globale E-Mail-Adresse noch den globalen Aktivstatus eines gemeinsamen Benutzers verändern. + +Fundstellen: app/routes/users.py, Zeile 176; app/services/user_service.py, Zeile 193; app/services/auth_service.py, Zeile 218. + +### F03 · P1 — Sitzungswiderruf ist unvollständig + +Mehrere Fälle sind inkonsistent: + +- Der Cookie-Pfad prüft is_active aus der Redis-Session. Eine Deaktivierung invalidiert aber hauptsächlich den Berechtigungscache. +- Eine fehlende UserTenant-Mitgliedschaft wird nicht zwingend abgewiesen. +- Die Benutzerlöschung invalidiert weder Sessions noch Berechtigungscache zuverlässig. +- Passwortwechsel löschen Redis-Sessions, lassen PostgreSQL-Fallback-Sessions bestehen. + +Bei einem späteren Redis-Ausfall kann deshalb eine noch nicht abgelaufene alte DB-Session wieder akzeptiert werden. Bei gesundem Redis wird ein fehlender Session-Schlüssel dagegen korrekt abgewiesen. + +Korrektur: Widerruf dauerhaft speichern und in beiden Sessionpfaden prüfen. Aktive Mitgliedschaft und Kontostatus zwingend durchsetzen. + +Abnahme: Deaktivierung, Austritt und Passwortwechsel wirken unmittelbar – auch bei Redis-Ausfall. + +Fundstellen: app/deps.py, Zeilen 101 und 131; app/core/auth.py, Zeilen 88 und 221; app/routes/users.py, Zeile 319. + +### F04 · P1 — Suche umgeht Teile der Objekt- und Feldberechtigungen + +Hauptsuche, Vorschläge, Facetten und Ähnlichkeitssuche verwenden unterschiedliche Schutzmechanismen. + +autocomplete und find_similar_all_types filtern nach Mandant, berücksichtigen aber keine vollständige Benutzer-/Objektsichtbarkeit. Innerhalb desselben Mandanten reicht das nicht zum Schutz privater Daten. + +Im Hauptpfad wird nur result.data feldweise gefiltert. title und snippet bleiben unverändert. Die KI-Zusammenfassung bekommt die Ergebnisse bereits vor dieser Feldfilterung. + +Korrektur: Modulrechte, Plugin-Aktivierung, Objektsichtbarkeit und Feldfreigaben vor Snippet-Erzeugung und LLM-Übergabe anwenden. Alle Suchvarianten müssen denselben Schutzpfad nutzen. + +Abnahme: Private Dateien oder Mails erscheinen weder in Vorschlägen noch in Ähnlichkeiten, Facetten oder KI-Zusammenfassungen. + +Fundstellen: app/plugins/builtins/unified_search/search_engine.py, Zeilen 186 und 258; routes.py, Zeilen 197 und 209. + +### F05 · P1 — Plugin-Sperre läuft vor der Authentisierung + +require_active_plugin wird als Router-Dependency vor der Authentisierung der jeweiligen Route ausgeführt. + +Sie liest den Mandanten aus dem DB-Kontext. Fehlt dieser, kehrt sie ohne Sperre zurück. + +Reproduziert: Die Plugin-Prüfung lief mit leerem Mandantenkontext; erst danach setzte die Auth-Dependency den Mandanten. Der Endpunkt antwortete mit HTTP 200. + +Korrektur: Die Plugin-Prüfung ausdrücklich vom bereits geprüften Benutzer-/Mandantenkontext abhängig machen. Fehlender Kontext muss zur Ablehnung führen. + +Abnahme: Global aktives, im aktuellen Mandanten deaktiviertes Plugin liefert auch bei gültiger Sitzung und passenden Modulrechten 403. + +Fundstellen: app/deps.py, Zeile 409; app/main.py, Zeile 656. + +### F06 · P1 — Worker registriert keine der 44 deklarierten Plugin-Ereignisbehandlungen + +Der Worker überspringt on_activate und ruft stattdessen register_event_handlers auf. + +Diese Basismethode ist leer. Keines der 27 gefundenen Plugins überschreibt sie. Die eigentliche Registrierung findet in BasePlugin.on_activate statt. + +Reproduziert: 27 Plugins, 44 Ereignisdeklarationen, 0 Registrierungen über den Worker-Aufruf. + +API und Worker sind getrennte Prozesse; ihre Registries werden nicht geteilt. Gesonderte Core- oder Wildcard-Handler ändern diesen Befund nicht. + +Korrektur: Prozessregistrierung und mandantenbezogenes Seeding trennen. API und Worker müssen dieselbe idempotente Registrierung ausführen. + +Abnahme: Kontakt oder Datei anlegen → Outbox → separater Worker → zuständiger Plugin-Handler → persistierter Suchindex. + +Fundstellen: app/core/worker.py, Zeile 150; app/plugins/base.py, Zeilen 51 und 155. + +### F07 · P1 — Hintergrundjobs verlieren Mandantenkontext und Transaktionen + +Mehrere Suchjobs und run_agent öffnen neue Sessions und lesen zunächst einen mandantenbezogenen Datensatz, um dessen Mandanten zu bestimmen. Unter der vorgesehenen RLS fehlt bereits für diese erste Abfrage der erforderliche Kontext. + +Der Workflow-Timeoutjob setzt ebenfalls keinen Kontext und beendet Änderungen teilweise mit flush, ohne commit. + +Zusätzlich verspricht TenantMixin automatische ORM-Filterung, implementiert im untersuchten DB-Modul aber lediglich die Spalten. + +set_config(..., true) gilt nur für die aktuelle Transaktion. Nach Commit oder Rollback muss eine neue Transaktion ihren Kontext erneut erhalten. + +Korrektur: Mandant und Auftraggeber im Job-Payload verpflichtend übergeben; Kontext vor dem ersten fachlichen SQL setzen; Änderungen, Audit und Outbox gemeinsam committen. + +Abnahme: Indexierung, Agentenstart und Timeout-Verarbeitung funktionieren mit crm_worker, erzwungener RLS und zwei Mandanten. + +Fundstellen: app/core/db/__init__.py, Zeilen 52 und 280; unified_search/jobs.py, Zeile 31; automation/agent_runner.py, Zeile 26; automation/workflow_timeout.py, Zeile 18. + +### F08 · P1 — Externe Agenten-API funktioniert für reine Bearer-Clients nicht + +Die externen Agentenrouten verlangten zunächst require_permission, das an die Cookie-Authentisierung gebunden ist. + +Die Bearer-Verifikation im Handler wird deshalb bei einem normalen API-Client ohne Cookie nicht erreicht. + +Reproduziert: Statusabfrage mit ausschließlich Bearer-Header liefert 401. + +Im /run-Pfad steckt ein weiterer Fehler: `async with get_db() as stream_db:` — get_db() ist ein FastAPI-Asyncgenerator und kein solcher Contextmanager. Das erzeugt einen TypeError. + +Korrektur: Cookie und API-Token über einen gemeinsamen geprüften Auth-Kontext auflösen. Für selbst verwaltete Sessions die vorhandene Sessionfactory beziehungsweise den richtigen Contextmanager verwenden. + +Abnahme: Gültiger Bearer ohne Cookie funktioniert für Status, Run und Stream; ungültige Tokens werden abgelehnt. + +Fundstellen: app/plugins/builtins/ai_assistant/external_api.py, Zeilen 44 und 126; app/deps.py, Zeile 299. + +### F09 · P1 — Generische CRM-/MCP-Werkzeuge haben keinen funktionierenden Delegationspfad + +Zwei Implementierungen senden X-Internal-Call, X-Tenant-Id, X-User-Id. Diese Header werden von der geschützten API nicht als gültige Authentisierung akzeptiert. + +Reproduziert: Der generische CRM-Aufruf erhält „Not authenticated“. + +Im separaten Worker zeigt der lokale Standardhost außerdem auf den Worker selbst. Der vorhandene Delegationstoken-Code ist nicht in diese Aufrufe eingebunden. + +Korrektur: Fachservices und vorhandenen Delegationsmechanismus verbinden. Einen gemeinsamen geprüften Ausführungskontext verwenden und die beiden generischen Aufrufimplementierungen konsolidieren. + +Abnahme: Dieselbe Fachaktion ist für denselben Benutzer über UI und Agent gleichermaßen erlaubt oder gesperrt; das Audit nennt Benutzer, Agent und Mandant. + +Fundstellen: ai_assistant/crm_api_tool.py, Zeile 114; mcp_server/tool_definitions.py, Zeile 104; app/core/delegation_token.py. + +### F10 · P1 — Token-Scopes vergeben Rechte, statt sie einzuschränken + +require_permission akzeptiert einen passenden _token_scopes-Eintrag durch ein frühzeitiges return, auch wenn die aktuellen Benutzerberechtigungen leer sind. + +Isoliert reproduziert: mail:write wird trotz fehlender Benutzerberechtigung erlaubt. + +Der Cookie-Zwang aus F08 verdeckt diesen Fehler auf vielen HTTP-Pfaden. Eine alleinige Reparatur der Bearer-Unterstützung kann ihn erst erreichbar machen. + +Korrektur: Effektive Rechte als Schnittmenge aus Benutzerrechten, Token-Scopes und Delegation berechnen. Explizite Verbote müssen Vorrang haben. + +Abnahme: Token mit mail:write, Benutzer ohne mail:write → 403. Späterer Rechteentzug wirkt auf bestehende Tokens. + +Fundstelle: app/deps.py, Zeile 314. + +### F11 · P1 — Freigaben sind nicht sicher an Entscheider und Aktion gebunden + +resolve_approval_request prüft Mandant und Status, aber nicht ausreichend: Ablaufdatum, vorgesehenen Genehmiger, Genehmigergruppe, konkurrierende Entscheidungen. + +Die ursprüngliche approver_id wird mit dem tatsächlichen Entscheider überschrieben. + +Reproduziert: Eine abgelaufene Anfrage konnte von einem anderen Entscheider genehmigt werden. + +Für Agenten fehlt außerdem eine durchgängige, dauerhaft gespeicherte Fortsetzung genau des genehmigten Werkzeugaufrufs. + +Korrektur: Zuordnung und Entscheider getrennt speichern; Ablauf und Berechtigung prüfen; Zustand atomar ändern. Genehmigung an Aktion, Argumente, Revision und Kontext binden. + +Abnahme: Falscher Entscheider, abgelaufene Anfrage und doppelte Entscheidung werden abgewiesen. Genehmigung führt exakt die geprüfte Aktion einmal aus. + +Fundstellen: app/core/approval.py, Zeile 106; app/ai/agent_loop.py, Zeile 384; app/routes/approvals.py, Zeile 236. + +### F12 · P1 — Workflow-Genehmigung und -Ablehnung scheitern an falschen Aufrufen + +Die Workflow-Routen behandeln die Rückgabe von create_approval_request als Dictionary: approval["id"]. Tatsächlich wird ein ORM-Objekt zurückgegeben. + +Reproduziert: TypeError: 'ApprovalRequest' object is not subscriptable. + +Danach wäre auch der Aufruf von resolve_approval_request falsch: tenant_id fehlt, und decided_by wird statt approver_id verwendet. + +Korrektur: Beide Routen an den tatsächlichen zentralen Vertrag anpassen und die bestehende wartende Freigabe auflösen. Nicht bei jeder Entscheidung eine neue, sofort selbst genehmigte Anfrage erzeugen. + +Abnahme: Beide tatsächlichen URLs funktionieren; Zustandswechsel, Audit und Freigabe stimmen. Wiederholung erzeugt keinen zweiten Fortschritt. + +Fundstellen: app/routes/workflows.py, Zeilen 507 und 525; app/core/approval.py, Zeile 106. + +### F13 · P1 — Dauerhafte Workflow-Ausführung ist nur teilweise verbunden + +Die Engine dokumentiert Locks, Wiederaufnahme und Idempotenz. Im Anwendungscode hat acquire_lock jedoch keinen Aufrufer. + +idempotency_key existiert als Modellfeld, wird aber von den Seiteneffekt-Handlern nicht durchgängig verwendet. resume verarbeitet Schritte ohne wirksame Sperre. + +Der manuelle Trigger legt eine Instanz an; eine durchgängige automatische Ausführung und Wiederaufnahme fälliger Wait-Schritte ist nicht vollständig angeschlossen. + +Korrektur: Die vorhandene Engine zum verbindlichen Zustandsübergang machen. Start und Wiederaufnahme über registrierte Jobs verbinden; Instanzen atomar übernehmen und stabile Schritt-/Idempotenzzustände persistieren. + +Abnahme: Ein Ablauf übersteht Worker-Neustart. Gleichzeitige Resume-Aufrufe versenden eine Mail nicht doppelt. Unklarer Ausgang eines externen Aufrufs wird nachvollziehbar abgeglichen. + +Fundstellen: app/workflows/engine.py, Zeilen 526 und 567; app/models/workflow.py, Zeile 72; app/routes/workflows.py, Zeile 338. + +### F14 · P1 — KI-Datenrichtlinie schützt nicht den gesamten Nachrichtenstrom + +enforce_data_policy filtert Dictionary-Inhalte, lässt Strings aber unverändert. + +Reproduziert: Ein JSON-String mit einem synthetischen smtp_password passiert unverändert. + +Der Agentenrunner ruft die Policy nur vor der ReAct-Schleife und mit db=None auf. Dadurch wird keine Providerkonfiguration geladen. Werkzeugantworten entstehen erst später innerhalb der Schleife. + +Korrektur: Strukturierte Daten vor der Serialisierung filtern. Jede LLM-Anfrage einschließlich Toolantworten und RAG-Kontext muss denselben geprüften Pfad passieren. Verpflichtende, nicht ladbare Richtlinien müssen den Versand verhindern. + +Abnahme: Gesperrte Felder fehlen am tatsächlichen Provider-Eingang sowohl im Startkontext als auch nach Werkzeugaufrufen. + +Fundstellen: app/ai/data_policy.py, Zeile 35; automation/agent_runner.py, Zeile 242; app/ai/agent_loop.py, Zeile 346. + +### F15 · P1 — Workflow-HTTP-Schutz lässt interne Hostnamen durch + +_is_url_safe blockiert IP-Literale und einige fest eingetragene Hostnamen, lässt andere Hostnamen ungeprüft passieren. + +Reproduziert: http://postgres:5432/ wird vom Validator akzeptiert. + +Auch ein externer Domainname mit privater DNS-Auflösung kann passieren. Welche internen Dienste tatsächlich erreichbar wären, hängt vom Deployment ab. Die Probe sendete keine Netzwerkanfrage. + +Korrektur: Aufgelöste IPv4-/IPv6-Ziele prüfen, die Verbindung an die geprüfte Auflösung binden und erlaubte Ziele beziehungsweise Ports begrenzen. Weiterleitungen ebenfalls prüfen oder sperren. + +Abnahme: Interne Servicenamen, private DNS-Ziele und DNS-Wechsel werden abgefangen. + +Fundstelle: app/workflows/step_handlers.py, Zeilen 169 und 197. + +### F16 · P1 — Plugin-Lifecycle hat widersprüchliche Aktivierungswege + +Mehrere Mechanismen verwalten denselben Zustand: + +- prestart aktiviert absichtlich deaktivierte Plugins erneut. +- main.py führt einen eigenen Aktivierungsablauf aus. +- Seeding erfolgt im Kontext des ersten gefundenen Mandanten. +- Aktivierungsfehler werden protokolliert, ohne einen bereits aktiven DB-Zustand zuverlässig zurückzunehmen. +- Der Worker lädt Jobmodule vor seinem DB-Aktivierungsabgleich. + +Global installiert, mandantenbezogen freigegeben und im Prozess betriebsbereit werden dadurch vermischt. + +Korrektur: Registry-/PluginService-Lifecycle konsolidieren. Gewünschten Zustand, Installation, Mandantenfreigabe und Laufzeitgesundheit getrennt behandeln. + +Abnahme: Absichtliche Deaktivierung bleibt nach Neustart erhalten. Ein Aktivierungsfehler erscheint nicht als gesund. Mandanten erhalten ihre Seeds unabhängig von der Ladefolge. + +Fundstellen: prestart.sh, Zeile 148; app/main.py, Zeilen 309 und 315; app/core/worker.py, Zeile 221. + +### F17 · P1 — Integrationen rufen eine nicht vorhandene Registry-Methode auf + +ContractRegistry besitzt get_contract, aber kein get. + +Mindestens sechs Produktionsstellen verwenden trotzdem get_contract_registry().get(...). + +Betroffen sind unter anderem Agenten-/Workflow-Kommunikation, Miniapp-Werkzeuge, proaktive Hinweise und Berichtjobs. Teilweise wird nur geloggt, sodass erwartete Nachrichten oder Ergebnisse still fehlen. + +Korrektur: Aufrufer auf den tatsächlichen Contract-Zugriff umstellen. Öffentliche Funktionen typisieren und ihre Verträge prüfen. + +Abnahme: Betroffene Funktionen über ihre realen Einstiegspunkte ausführen; Nachrichten, Miniapps und Berichtablage müssen tatsächlich persistiert werden. + +Fundstellen: app/plugins/builtins/contracts.py, Zeile 49; app/ai/miniapp_tools.py, Zeile 26; report_generator/jobs.py, Zeile 80; ai_proactive/services.py, Zeile 78. + +### F18 · P1 — Drei Schema-Verfahren erzeugen Drift + +Alembic, Plugin-SQL und sync_plugin_schema.py verändern dasselbe Datenbankschema. + +Der Startup-Sync verliert bei Indizes wichtige Eigenschaften: UNIQUE, partielle WHERE-Bedingungen, PostgreSQL-Indexmethoden und Optionen. + +Reproduziert: Aus dem partiellen eindeutigen Workspace-Default-Index erzeugt der Sync lediglich einen normalen Index auf tenant_id. + +Abgefangene DDL-Fehler können außerdem die gemeinsame Transaktion unbrauchbar machen, während der Ablauf weiterläuft. + +Korrektur: Pro Tabelle und Änderung einen verbindlichen Migrationsbesitzer bestimmen. Startup-Sync zunächst als lesenden Driftbericht verwenden; Reparaturen ausdrücklich migrieren. + +Abnahme: Frischinstallation und Upgrade ergeben dieselben Spalten, Constraints, Indizes und Policies. + +Fundstellen: prestart.sh, Zeile 135; scripts/sync_plugin_schema.py, Zeilen 177 und 188. + +### F19 · P1 — Alembic kennt nicht alle benötigten Modelle + +alembic/env.py importiert nur app.models. + +In einem frischen Prozess umfasst das 46 Tabellen. Die Metadatensortierung scheitert bereits an einer Referenz von contact_merge_history auf contacts. + +Reproduziert: NoReferencedTableError. + +Nach Laden der Pluginmodelle sind 129 Tabellen bekannt. Nur den in der Roadmap genannten einzelnen Attachment-FK zu reparieren reicht daher nicht. + +Korrektur: Deterministische, nebenwirkungsfreie Modelldiscovery für Alembic und Schema-Prüfungen einführen. + +Abnahme: Metadatensortierung funktioniert im frischen Prozess; alembic check läuft nach Frischinstallation und Upgrade gegen PostgreSQL durch. + +Fundstellen: alembic/env.py, Zeile 15; app/models/contact_merge.py, Zeile 28. + +### F20 · P1 — Startup überschreibt beabsichtigte Datenbankrechte + +Migration 0100 entzieht API und Worker Löschrechte für besonders geschützte Tabellen, darunter audit_log. + +Danach erteilt prestart erneut: GRANT DELETE ON ALL TABLES IN SCHEMA public ... + +Damit wird der beabsichtigte Löschschutz wieder aufgehoben. + +Zusätzlich setzt der Start mehrere DB-Rollen auf dasselbe aus der privilegierten Verbindung entnommene Passwort. API und Worker erhalten die Migrationsverbindung im Environment. + +Korrektur: Tabellenspezifische Rechte ausschließlich nachvollziehbar migrieren. Keine pauschale Rechteanhebung beim Start. Separate Zugangsdaten und ein abgegrenzter Migrationsschritt. + +Abnahme: API und Worker können Auditdaten schreiben, aber nicht löschen. Laufzeitcontainer benötigen keinen frei verwendbaren Migrationszugang. + +Fundstellen: prestart.sh, Zeilen 58 und 90; alembic/versions/0100_restrict_delete_grants.py, Zeile 39; docker-compose.yaml, Zeile 78. + +### F21 · P1 — Migrationstest kann die falsche Datenbank verändern + +test_migrations.sh überschreibt nur DATABASE_URL. Alembic bevorzugt jedoch MIGRATION_DATABASE_URL. + +Wenn diese Variable auf eine echte Instanz zeigt und die vorherigen Skriptschritte erfolgreich laufen, können Upgrade und Downgrade dort statt in der Testdatenbank ausgeführt werden. + +Zusätzlich wird eine SQLAlchemy-URL mit postgresql+psycopg2:// an psql übergeben. Das passt nicht zum erwarteten Verbindungsformat. + +Das Skript wurde wegen dieses Befunds nicht ausgeführt. + +Korrektur: Beide Alembic-Variablen auf das explizite temporäre Ziel setzen. Vor jeder DDL-Aktion Zielidentität prüfen und Aufräumen per trap sicherstellen. + +Abnahme: Selbst bei anders gesetzter MIGRATION_DATABASE_URL verändert der Test ausschließlich die erzeugte Testdatenbank. + +Fundstellen: scripts/test_migrations.sh, Zeilen 24 und 70; alembic/env.py, Zeile 27. + +### F22 · P1 — Backup ist im ausgelieferten Container nicht vollständig betriebsfähig + +Mehrere Probleme greifen zusammen: + +- Das Runtime-Image installiert libpq5, aber nicht die benötigten Programme pg_dump und pg_restore. +- API-Backups landen in /data/backups, CLI-Backups standardmäßig in /backups. +- Compose persistiert diese Backupverzeichnisse nicht. +- Backup-Konfiguration wird ohne erforderlichen Mandantenkontext gelesen. +- Fehlerbenachrichtigung verwendet User.tenant_id, obwohl User global ist. +- Die vorgesehene eingeschränkte Laufzeitrolle kann keinen vollständigen RLS-Dump garantieren. + +Korrektur: Einen dokumentierten Backupablauf mit passenden Programmen, gesonderten Rechten, dauerhaftem Ziel, Ergebnisregistrierung und gemeinsamer DB-/Dateisicherung herstellen. + +Abnahme: Sicherung im gebauten Image erstellen, Container ersetzen und anschließend Datenbank und Dateien auf isoliertem Ziel wiederherstellen. + +Fundstellen: Dockerfile, Zeile 56; app/services/backup_service.py, Zeile 19; app/core/backup_job.py, Zeilen 26 und 208. + +### F23 · P1 — Mandanten-Backup-API führt einen datenbankweiten Restore aus + +Die Restore-Route ist über automation:admin eines Mandanten erreichbar. Sie wählt einen Backupdatensatz dieses Mandanten aus. + +Der eigentliche Restore bearbeitet jedoch die gemeinsame Datenbank ohne Mandantenfilter. + +Die Fehler aus F22 verhindern derzeit viele Aufrufe. Das ist aber keine Sicherheitsgrenze: Eine bloße Reparatur der Programme und Rechte kann den gefährlichen Wirkungsbereich erst freilegen. + +Der Kommentar, pg_restore --clean sei atomar, ist ebenfalls falsch. --clean löscht zu restaurierende Objekte; Atomarität benötigt ausdrücklich passende Transaktionsoptionen. + +Korrektur: Gesamtrestore als globale Betriebsoperation mit separater Berechtigung, Wartungszustand und isoliertem Ziel behandeln. + +Abnahme: Ein Tenant-Admin kann keinen Gesamtrestore auslösen. + +Fundstellen: app/routes/backups.py, Zeile 58; app/services/backup_service.py, Zeilen 175 und 224. + +### F24 · P1 — Healthchecks melden Ausfälle als Erfolg + +Reproduziert: + +- /health/ready liefert bei not_ready weiterhin HTTP 200. +- check_worker meldet up, wenn Redis erreichbar und die Queue leer ist – selbst ohne Worker. + +Ein curl -f erkennt den ersten Fehler nicht. Der Compose-Workercheck prüft ebenfalls Redis statt die tatsächliche Verarbeitung. + +Korrektur: Fehlerzustände mit passenden HTTP-Codes zurückgeben. Worker über zeitlich begrenzten Heartbeat und Verarbeitungsnachweis prüfen. + +Abnahme: DB aus → Readiness 503. Worker aus bei gesundem Redis → Worker down. Festhängende Verarbeitung wird innerhalb einer definierten Zeit erkannt. + +Fundstellen: app/routes/health.py, Zeile 39; app/core/monitoring.py, Zeile 219; docker-compose.yaml. + +### F25 · P1 — CSV-Import meldet zurückgerollte Zeilen als erfolgreich + +Mehrere Zeilen werden in derselben Transaktion geschrieben. Scheitert eine spätere Zeile, ruft der Import db.rollback() auf. + +Dabei verschwinden auch frühere unbestätigte Zeilen. Die Zähler succeeded und created behalten diese aber bei. + +Mit Transaktionsmodell reproduziert: partial_success mit einer erfolgreichen Zeile, obwohl anschließend keine Zeile bestätigt wird. + +Der Rollback entfernt zudem den lokalen RLS-Kontext. Fehlerzeilen werden nach Vorvalidierung teilweise neu nummeriert. + +Korrektur: Atomaren Import oder Teilimport ausdrücklich festlegen. Für Teilimport Savepoints pro Zeile verwenden und Originalzeilennummern erhalten. + +Abnahme: Bei einem Fehler in der mittleren von drei Zeilen stimmen gespeicherte Daten, Erfolgszähler und Fehlernummern überein. + +Fundstelle: app/services/import_export_service.py, Zeilen 131 und 138. + +### F26 · P1 — DMS-Deduplizierung vermischt Dateiinhalt und Dateiidentität + +Beim Upload wird nach gleichem Inhalt im ganzen Mandanten gesucht. Bei Treffer wird der bestehende DmsFile-Datensatz zurückgegeben. + +Dabei werden Besitzer, Ordner, Dateiname und Sichtbarkeit nicht passend zum neuen Upload behandelt. + +Reproduziert: Der Upload eines zweiten Benutzers erhält ID, Name und uploaded_by des vorhandenen fremden Datensatzes; es entsteht kein eigener Dateieintrag. + +Korrektur: Binären Inhalt und fachliches Dateiobjekt trennen. Gleiche Bytes dürfen intern wiederverwendet werden; jeder fachliche Upload braucht aber seine eigene Identität und Rechtezuordnung. + +Abnahme: Zwei Benutzer können dieselbe Vorlage in unterschiedliche Ordner hochladen, ohne fremde Metadaten zu erhalten. Löschen einer Datei beschädigt die andere nicht. + +Fundstelle: app/plugins/builtins/dms/routes.py, Zeilen 146 und 159. + +### F27 · P1 — Kalenderfilter entfernt Serientermine vor ihrer Berechnung + +list_entries filtert zunächst CalendarEntry.start_at >= start_dt. + +Eine wöchentliche Serie, die im Januar beginnt, wird dadurch für eine Februaransicht bereits aus der SQL-Ergebnismenge entfernt. Ihre Februar-Wiederholungen werden anschließend gar nicht berechnet. + +Bei erzeugten Wiederholungen wird außerdem start_at verändert, während end_at auf dem Ausgangstermin bleiben kann. Auch mehrtägige Termine mit Beginn vor dem Sichtfenster fehlen. + +Korrektur: Einzeltermine nach Überschneidung und Serien nach relevanter Laufzeit auswählen. Danach Wiederholungen im Sichtfenster erzeugen und Endzeit um dieselbe Dauer verschieben. + +Abnahme: Januarserie erscheint im Februar; Dauer, mehrtägige Überschneidung und Sommerzeitwechsel stimmen. Ungültige Datumsparameter liefern einen Validierungsfehler. + +Fundstelle: app/plugins/builtins/calendar/routes.py, Zeilen 369, 407 und 433. + +### F28 · P1 — Import/Export besitzt einen abweichenden Berechtigungspfad + +Die generischen Routen prüfen import_export:read/write. + +Der Kontaktvertrag schreibt importierte Kontakte direkt als Modelle. Eine zusätzliche vollständige Prüfung von contacts:write und Feldschreibrechten ist dort nicht enthalten. + +Beim Export werden Objektsichtbarkeit und eine feste Sensitivitätsliste berücksichtigt, aber individuelle Feldberechtigungen nicht vollständig übergeben. + +Korrektur: Import-/Exportrechte als zusätzliche Rechte zu den Fach- und Feldrechten behandeln. Vorhandene Fachservices nutzen und Feldfilter vor Dateierzeugung anwenden. + +Abnahme: Importrecht allein erlaubt keine Kontaktanlage. Ein verborgenes Feld fehlt gleichermaßen in API, CSV, XLSX und JSON. + +Fundstellen: app/routes/import_export.py, Zeile 294; app/plugins/builtins/contacts/contracts.py, Zeilen 194 und 257. + +### F29 · P1 — CI liefert keinen reproduzierbaren vollständigen Qualitätsnachweis + +Der gezeigte Forgejo-Workflow: + +- installiert requirements.txt, aber nicht ausdrücklich die Testabhängigkeiten, +- definiert keine vollständige PostgreSQL-/Redis-Testumgebung, +- verwendet Versionsbereiche, während das Containerimage den Lockfile verwendet. + +Der Docker-Frontendbuild ruft Vite direkt auf und überspringt die im normalen npm-Build enthaltene TypeScript-Prüfung. + +Ein optionaler Smokecheck fragt eine bestehende externe Instanz statt das gerade gebaute Image ab. + +Korrektur: Eine reproduzierbare Pipeline mit identischen Locks, Testdiensten, echten DB-Rollen und Prüfungen des erzeugten Images herstellen. + +Abnahme: Ein sauberer Runner besteht Backend-/Frontend-Suite, Typcheck, Migrationen und E2E gegen sein eigenes Artefakt. + +Fundstellen: .forgejo/workflows/ci.yml, Zeile 21; Dockerfile, Zeile 19; scripts/ci_pipeline.sh, Zeile 113. + +### F30 · P1 — Frischinstallation besitzt ein bekanntes Administrator-Standardpasswort + +Compose und seed_admin.py enthalten einen festen Passwort-Fallback für das initiale Administratorkonto. + +Wenn die Konfiguration fehlt, wird ein Konto mit bekanntem Zugang angelegt. + +Der Befund betrifft den Installationspfad. Er beweist nicht, dass auf dem laufenden Server noch dieses Passwort gilt. + +Korrektur: Initiales Geheimnis verpflichtend machen oder sicher einmalig erzeugen. Unkonfigurierter Produktivstart muss vor Benutzeranlage abbrechen. + +Abnahme: Ein frisches Volume ohne gesetztes Geheimnis erzeugt kein nutzbares Konto mit festem Standardpasswort. + +Fundstellen: docker-compose.yaml, Zeile 95; scripts/seed_admin.py, Zeile 79. + +### F31 · P2 — Provider-Discovery und Reindexierung passen nicht zusammen + +auto_register_providers erhält eine DB-Session, verwendet sie aber nicht zum Aktivierungsabgleich. Stattdessen wird eine feste Providerliste registriert. + +Die Discovery registriert 13 effektive Suchtypen. Reindex- und Ähnlichkeitspfade verwenden daneben eine feste Liste aus vier Tabellen. + +Ein neuer Provider bedeutet deshalb nicht automatisch vollständige Indexierung, Reindexierung und Ähnlichkeitssuche. + +Korrektur: Beiträge aktiver Plugins zur gemeinsamen Quelle für Such- und Indexfähigkeiten machen. Jeder Provider beschreibt seine tatsächlich implementierten Fähigkeiten und Indexjobs. + +Abnahme: Ein neuer Testprovider wird allein über seinen Pluginbeitrag gefunden und vollständig indiziert. Deaktivierung wirkt in API und Worker. + +Fundstellen: unified_search/provider_registry.py, Zeile 118; search_engine.py, Zeile 19; jobs.py, Zeile 245. + +### F32 · P2 — Suchscopes, Filter und Pagination liefern falsche Ergebnisse + +Vier konkrete Probleme: + +- entity_types=[] wird zu „alle Typen“. +- Datum-/Tagfilter laufen erst nach der Begrenzung auf die besten Treffer. +- Der Request-Offset wird nicht wirksam in die Ergebnisabfrage umgesetzt. +- search.before_search läuft erst nach Erzeugung von Suchausdruck und Embedding. + +Reproduziert: Trotz leerer Typenmenge wird ein Provider aufgerufen. + +Korrektur: None und leere Menge unterscheiden. Fachfilter vor Limit und Offset anwenden; Hook vor Ableitung der Suchparameter ausführen. + +Abnahme: Leere Workspace-Schnittmenge ergibt null Treffer. Seite zwei enthält die richtigen Folgetreffer; Filter finden auch Datensätze außerhalb der ursprünglichen Top-N. + +Fundstellen: unified_search/search_engine.py, Zeilen 94 und 116; routes.py, Zeile 182. + +### F33 · P2 — Workspace-Wechsel aktualisiert den Datenkontext nicht zuverlässig + +Der Wechsel verändert Store und globalen HTTP-Header. Fachliche Querykeys wie für Aufgaben enthalten aber keinen Workspace und werden dabei nicht passend invalidiert. + +Dadurch können Daten aus dem vorherigen Arbeitsbereich sichtbar bleiben. + +Beim Wiederherstellen aus sessionStorage wird der API-Header nicht entsprechend synchronisiert; auch Reset behandelt beide Zustände nicht vollständig gemeinsam. + +Korrektur: Workspace in die Queryidentität aufnehmen oder betroffene Queries kontrolliert verwerfen. Wiederherstellung und Reset mit dem API-Kontext synchronisieren. + +Abnahme: Wechsel und Neuladen zwischen zwei unterschiedlich gefilterten Workspaces zeigen sofort passende Daten. + +Einordnung: Das ist ein Arbeitskontextfehler; Workspaces sollen weiterhin keine Rechte vergeben. + +Fundstellen: frontend/src/hooks/useWorkspace.ts, Zeile 56; frontend/src/store/workspaceStore.ts; frontend/src/api/tasks.ts, Zeile 118. + +### F34 · P2 — Mandantenwechsel lässt alten Clientzustand bestehen + +useSwitchTenant setzt den Mandanten und invalidiert Queries. Alte Daten, laufende Anfragen sowie Workspace-/Pluginzustand werden aber nicht vollständig entfernt beziehungsweise zurückgesetzt. + +Viele fachliche Querykeys enthalten keinen Mandanten. + +Dadurch können alte Mandantendaten bis zum Refetch sichtbar bleiben oder verspätete Antworten den neuen Zustand überlagern. + +Korrektur: Mandantenwechsel als kontrollierten Kontextwechsel ausführen: Anfragen abbrechen, betroffene Caches und Stores leeren, Auth-/CSRF-Kontext aktualisieren und neue Plugin-/Workspacezustände laden. + +Abnahme: Eine langsame Anfrage aus A liefert nach Wechsel zu B keine sichtbaren A-Daten. + +Einordnung: Eine serverseitige Umgehung der Mandantentrennung wurde damit nicht nachgewiesen. + +Fundstellen: frontend/src/api/auth.ts, Zeile 100; frontend/src/api/tasks.ts, Zeile 118; frontend/src/api/client.ts, Zeile 34. + +### F35 · P2 — PWA-Anforderung wurde durch Abschaltung ersetzt + +main.tsx deregistriert die Service Worker. Die Vite-Konfiguration aktiviert die PWA nicht. + +Gleichzeitig verspricht der Offlinebanner: „Änderungen werden gespeichert, wenn die Verbindung wiederhergestellt ist.“ + +Ein entsprechender Offline-Schreibpfad ist dort nicht vorhanden. + +Korrektur: Die vereinbarte PWA für Installation und kontrolliertes Caching statischer Assets wiederherstellen. API-/ERP-Daten und Schreiboperationen nicht pauschal offline cachen. Den Banner an die tatsächliche Funktion anpassen. + +Abnahme: Installierbarkeit und Assetupdates funktionieren; Offline-Schreibaktionen werden nicht fälschlich als später gespeichert dargestellt. + +Fundstellen: frontend/src/main.tsx, Zeile 7; frontend/src/App.tsx, Zeile 49; frontend/vite.config.ts. + +### F36 · P2 — Generierte Plugin-Komponentenmap ist nicht verpflichtend geprüft + +Die neue Map ist vorhanden und aktuell: 43 Komponenten, Check grün. + +Der Generator beziehungsweise Aktualitätscheck wird aber nicht verbindlich von npm-Build, Dockerfile und der gezeigten CI aufgerufen. + +Ein künftiger Manifestbeitrag kann deshalb fehlen, während die alte Map erfolgreich gebaut wird. + +Korrektur: Generatorcheck an den normalen Build-/CI-Pfad hängen. + +Abnahme: Ein neuer Manifestpfad ohne aktualisierte Map macht CI rot. Nach Generierung ist die Seite im Produktionsbundle enthalten. + +Fundstellen: scripts/generate_component_map.py, Zeile 71; frontend/package.json, Zeile 8; Dockerfile, Zeile 19. + +### F37 · P1 — SMTP-Variablen heißen in Compose anders als in Settings + +Settings erwartet: SMTP_USERNAME, SMTP_FROM_EMAIL, SMTP_USE_TLS. + +Compose setzt dagegen: SMTP_USER, SMTP_FROM, SMTP_TLS. + +Reproduziert: Trotz gesetzter Compose-Namen bleibt der Benutzername leer; Standardabsender und Standard-TLS-Wert bleiben bestehen. + +Betroffen sind insbesondere Systemmails wie Reset, Einladungen und geplante Alarmierung. + +Korrektur: Environmentnamen zwischen Settings, Compose und Dokumentation angleichen. + +Abnahme: Aus dem tatsächlichen Compose-Environment entstehen die erwarteten Settings; Reset- und Alarmmail funktionieren mit korrekter Anmeldung und Absender. + +Fundstellen: app/config.py, Zeile 57; docker-compose.yaml, Zeile 89. + +### F38 · P2 — Manifestfehler können einen endlosen Spinner erzeugen + +pluginStore.setError setzt loading=false, lässt aber loaded=false. + +PluginRouteRenderer betrachtet nur loaded und zeigt weiterhin den Spinner. Der Fehlerzustand wird dort nicht ausgewertet. + +Korrektur: Laden, erfolgreich leeres Ergebnis und Fehler getrennt darstellen. Fehleranzeige und erneuten Versuch anbieten. + +Abnahme: Manifest-API liefert 500 → verständliche Fehlermeldung statt Dauerspinner. Ein erneuter Versuch kann die Oberfläche wiederherstellen. + +Fundstellen: frontend/src/store/pluginStore.ts, Zeile 118; PluginRouteRenderer.tsx, Zeile 74. + +### F39 · P2 — DMS-Office-Editiersitzung verweist auf unpassende Endpunkte + +create_edit_session akzeptiert Office-Dateien, gibt aber /preview als Dokument-URL zurück. Diese Route akzeptiert ausschließlich PDFs. + +Die erzeugte Callback-URL /files/{id}/callback besitzt im DMS-Router keine Implementierung. + +Damit ist die zurückgegebene Konfiguration noch kein funktionsfähiger Office-Bearbeitungsablauf. + +Korrektur: Geeignete Download-/Speicher-Routen und einen begrenzten, geprüften Editorzugang anschließen. Bis dahin die Funktion nicht als betriebsbereit darstellen. + +Abnahme: DOCX öffnen, ändern, speichern und erneut laden ergibt den aktualisierten Inhalt; fremde oder abgelaufene Editiersitzungen können nicht schreiben. + +Fundstellen: app/plugins/builtins/dms/sharing_routes.py, Zeile 36; routes.py, Zeile 592. + +### F40 · P2 — Plugin-Migrationen werden nur anhand des Dateinamens erkannt + +Der Runner überspringt angewandte Dateien anhand von migration_file. + +Wird das SQL nachträglich verändert, bleibt dies unbemerkt. + +Die RLS-Einrichtung betrifft außerdem vor allem neu angelegte Tabellen. Der vollständige Sollzustand bereits vorhandener Plugin-Tabellen wird damit nicht garantiert. + +Korrektur: Angewandte Migrationen unveränderlich machen und Hashes speichern. Zusätzlich tatsächliche Spalten, FKs, Indizes, Policies und Grants vergleichen. + +Abnahme: Sowohl eine veränderte angewandte Migration als auch eine bestehende Tabelle mit falscher Policy werden erkannt. + +Fundstelle: app/plugins/migration_runner.py, Zeilen 95 und 153. + +### F41 · P2 — Stündliches Agentenlimit verwendet das falsche Zeitfenster + +Der Code setzt: one_hour_ago = datetime.now(UTC). + +Die Stunde wird nicht abgezogen. + +Die anschließende Abfrage zählt deshalb nicht die Läufe der vergangenen Stunde, sondern nur Einträge ab dem aktuellen Zeitpunkt. Das konfigurierte Stundenlimit schützt so nicht zuverlässig vor wiederholten Starts. + +F07 kann den Job bereits früher blockieren. Nach dessen Reparatur bleibt dieser weitere Fehler bestehen. + +Korrektur: Zeitfenster korrekt berechnen und Kontingente atomar reservieren, damit parallele Starts die Begrenzung nicht umgehen. + +Abnahme: Ausschöpfung des Stundenlimits blockiert weitere Starts; nach Ablauf des Fensters sind sie wieder erlaubt. + +Fundstelle: app/plugins/builtins/automation/agent_runner.py, Zeile 68. + +## Architektur- und Strukturbewertung + +Die Zahl der Dateien oder ihre Länge ist nicht das Hauptproblem. Große Mail-/Kalender-Routen, PluginRegistry und Agenten-/Workflowmodule sind zwar Wartungsschwerpunkte. Ihr bloßes Aufteilen würde die beschriebenen Fehler jedoch nicht beheben. + +Die grundlegenden Strukturprobleme sind: + +1. Mehrere Stellen verwalten denselben Zustand. Pluginaktivität wird in Startup, Registry, API und Worker unterschiedlich interpretiert. Drei Mechanismen verändern das Schema. +2. Contracts garantieren zu wenig Verhalten. Exportierte ORM-Modelle, Any und dynamische Funktionszugriffe erlauben, Fachlogik außerhalb ihres Moduls neu zusammenzusetzen. +3. API- und Worker-Ausführung sind nicht gleichwertig. Kontext und Registrierungen werden stillschweigend als vorhanden angenommen, obwohl ein neuer Prozess oder eine neue Session beginnt. +4. Berechtigungsprüfungen liegen zu weit vom Seiteneffekt entfernt. UI-Sperren und angebotene Toollisten schützen nicht automatisch die ausgeführte Fachoperation. +5. Statusanzeigen sind teilweise von der tatsächlichen Funktion entkoppelt. „Aktiv“, „fertig“ oder „gesund“ kann trotz fehlender Registrierung, Persistenz oder Verarbeitung erscheinen. + +Die bestehende Aufteilung sollte verbindlicher werden: + +| Bereich | Verantwortung | +|---|---| +| Core | Identität, Mandant, Rechte, Transaktionen, Audit, Outbox, Jobs, gemeinsame Registries | +| Fachplugin | Fachmodell, Regeln, Services, API, UI und Pluginbeiträge | +| Pluginvertrag | Öffentliche, typisierte Fachoperationen und klar beschriebene Fähigkeiten | +| Ausführungskontext | Mandant, Benutzer, Herkunft, Delegation, aktuelle Rechte und Nachverfolgbarkeit | +| Workspace | Navigation, Ansichten, Defaults und einschränkende Abfragefilter | +| Suche | Gemeinsamer Schutzpfad für Hauptsuche, Vorschläge, Facetten, Ähnlichkeit und RAG | +| Betrieb | Migrationen, Sicherungen, Wiederherstellung und unabhängige Ausfallüberwachung | + +UI, externe API, Agent und Worker sollten dieselben fachlichen Operationen mit demselben geprüften Kontext aufrufen. Fachänderung, Audit und Outbox gehören in eine gemeinsame Transaktion. Wiederholte Ereignisverarbeitung muss beherrscht werden. + +## Datenmodell + +Die gemessenen 129 Tabellen und 231 FK-Constraints sind der geladene ORM-Bestand. Daraus lässt sich nicht ableiten, welche Constraints auf dem Produktionsserver tatsächlich bestehen. + +Für die Schemaabnahme müssen insbesondere nachgewiesen werden: + +- Mandantenbezogene Referenzen können nicht auf fremde Mandanten zeigen. +- Globale Benutzeridentität und Mandantenmitgliedschaft haben getrennte Änderungsrechte. +- Softdelete und Eindeutigkeitsregeln passen zusammen. +- Default-Objekte sind im vorgesehenen Umfang eindeutig. +- Suchindex und fachliche Daten besitzen einen konsistenten Lebenszyklus. + +Ein einfacher Fremdschlüssel auf eine UUID erzwingt nicht automatisch die Mandantengleichheit. Ob sämtliche notwendigen zusammengesetzten Fremdschlüssel bereits wirksam migriert sind, muss an einer vollständig aufgebauten PostgreSQL-Datenbank gemessen werden. + +## Konzept: Business-KI-Plattform + +Das Ziel bleibt sinnvoll: Fachmodule bringen ihre Domänenlogik selbst mit; die Plattform stellt einen verbindlichen technischen und sicherheitsbezogenen Rahmen bereit. + +Was aktuell fehlt, ist vor allem der Nachweis vollständiger Abläufe über diese Grenzen hinweg. + +Die ersten beiden Abnahmen sollten deshalb sein: + +1. Kontaktanlage → Audit/Outbox → Worker → Suche → erlaubte KI-Abfrage. +2. Mailentwurf → Freigabe → einmaliger Versand → nachvollziehbares Ergebnis. + +Damit werden Fachservice, Rechte, Transaktionen, Pluginbeiträge, Worker und KI gemeinsam geprüft. + +## Kalender und spätere LeoRental-Integration + +Der Kalendervertrag exportiert derzeit vor allem Modelle und Workspacebeiträge. Ein verbindlicher CalendarPort-/CalendarProjection-Vertrag ist im gelieferten Stand noch nicht zu finden. + +Vor der späteren Rental-Anbindung müssen feststehen: + +- Welches Fachmodul besitzt den Termin? +- Welche stabile Referenz verbindet Projekt und Kalendereintrag? +- Wie werden Änderungen, Storno und Löschung übertragen? +- Welches System ist führend? +- Wie werden Zeitzonen und Konflikte behandelt? + +LeoCRM Calendar soll dabei die führende Plattformfähigkeit bleiben. Zuerst muss jedoch der vorhandene Serien-/Zeitraumfehler F27 behoben werden. Das ist eine offene Integrationsaufgabe; ein bereits geliefertes Rental-Modul wurde hier nicht geprüft. + +## Import/Export + +Die bestehende modulare Contract-Anbindung ist die richtige Richtung. Sie benötigt vollständige Autorisierung, Transaktionsregeln und dieselben Fachvalidierungen wie die normale API. + +Die gemeinsame Basis kann Parsing, Mapping, Vorschau und Fehlerberichte übernehmen. Fachliche Regeln und Persistenzverhalten bleiben beim zuständigen Modul. + +## Fehler und Lücken im bestehenden Bauplan + +Die geplante Phase R mit Betriebssicherheit, Teststabilität, Schemaabgleich, E2E und Restore ist richtig priorisiert. Ihr Entwurf muss aber korrigiert werden: + +| Planung | Problem | Erforderliche Änderung | +|---|---|---| +| ARQ überwacht Worker und Redis | Wenn ARQ oder Redis ausfällt, läuft auch der Wächter nicht mehr | Ergänzende Überwachung außerhalb dieser Ausfallkette | +| Alarm bei „installiert, aber inaktiv“ | Absichtliche Deaktivierung ist erlaubt | Gewünschten Zustand mit tatsächlicher Betriebsbereitschaft vergleichen | +| Queue-Länge als Workerbeweis | Leere Queue kann auch fehlenden Worker bedeuten | Heartbeat-Alter und tatsächliche Verarbeitung messen | +| Komplette Suite grün | Richtiger Ansatz, aber Mock-/Admin-Tests können reale Rechtefehler verdecken | Echte API-/Workerprozesse und tatsächliche Laufzeitrollen prüfen | +| Ein FK-Fix plus Migrationshash | Behebt unvollständige Modelldiscovery und konkurrierende Schemawege nicht | Vollständige Metadaten und eindeutige Schema-Verantwortung herstellen | +| E2E pro Kernfunktion | Erfolgreicher Normalfall prüft weder Rechteentzug noch Neustart | Mehrmandanten-, Rollen-, Fehler- und Wiederaufnahmeszenarien ergänzen | +| Monatlicher Restorejob | Ein Jobname beweist keine sichere Zielwahl oder vollständige Sicherung | Isoliertes Ziel und tatsächliche DB-/Dateiwiederherstellung nachweisen | +| „95 % Produktionsreife“ | Die fünf Kriterien ergeben keinen mathematischen Reifegrad | Konkrete Freigabekriterien und verbleibende Risiken dokumentieren | + +Die genannten 9–14 Arbeitstage der Roadmap sind keine belastbare Aufwandsschätzung zur Behebung aller hier dokumentierten Fehler. Nach der ersten verifizierten Reparaturwelle sollte neu geschätzt werden. + +Die Umsetzung gehört in die vorhandene PROGRESS.md-/Roadmap-Struktur. Es sollte kein zusätzlicher konkurrierender Status-Tracker entstehen. + +## Empfohlene Reparaturreihenfolge + +| Welle | Aufgaben | Erforderlicher Nachweis | +|---|---|---| +| 1: Sicherheitsgrenzen | F01–F05, F10–F11, F15, F20–F21, F23, F30 | Keine unberechtigten Aktionen oder globalen Identitätsänderungen; garantiert isolierte Tests | +| 2: Ausführung verbinden | F06–F09, F12–F19, F31, F37, F40–F41 | API und separater Worker funktionieren mit echten Rollen, Kontexten und Pluginzuständen | +| 3: Fachliche Integrität | F25–F28, F32–F35, F38–F39 | Importzählung, Dateiidentität, Termine, Feldrechte und Kontextwechsel stimmen | +| 4: Betriebsfreigabe | F22, F24, F29, F36 und korrigierte Phase R | Frischinstallation, Upgrade, Vollsuite, E2E, unabhängiger Ausfallalarm und Restore bestehen | + +Alarmierung und Testinfrastruktur können bereits während der ersten Welle vorbereitet werden. + +## Was bereits sinnvoll gelöst ist + +- Die Pluginrouten verwenden echte Routermechanismen für Parameter und Routenpriorität. +- Die generierte Komponentenmap ist aktuell. +- dms_file wird als Entityalias registriert; der Name ist für sich kein Fehler. +- Workspaces sind ausdrücklich als Arbeitskontext ohne Rechtevergabe modelliert. +- Einige Core-Workerjobs setzen den Mandantenkontext bereits korrekt. +- Logout leert den QueryClient bereits. +- Der Frontend-Produktionsbuild und TypeScript laufen erfolgreich. + +Diese Verbesserungen sollten bei der Sanierung erhalten bleiben. + +## Was für eine belastbare Freigabe noch nachgewiesen werden muss + +1. PostgreSQL mit echten Rollen: Frischinstallation, Upgrade, RLS, Grants, Fremdschlüssel sowie Verhalten bei fehlendem Kontext. +2. Separater Worker: Pluginregistrierung, Outbox, Retry, Fehlerablage, Deaktivierung und Wiederaufnahme nach Neustart. +3. Browser mit mehreren Mandanten und Rollen: Suche, Kontextwechsel, Pluginfehler, Upload, Serientermine und KI-Freigaben. +4. Reale Testintegrationen: SMTP/IMAP, Storage, Editor und LLM-Anbieter einschließlich Fehlerfällen. +5. Wiederherstellung: Datenbank und Dateien auf isoliertem Ziel nach tatsächlichem Containerersatz. +6. Weitere getrennte Prüfungen: Aktueller Abhängigkeits-/CVE- und Lizenzscan, Lasttests und vollständige Prüfung der produktiven Konfiguration. Diese wurden durch das vorliegende Codeaudit nicht ersetzt.