Vorher: Drei divergierende fixe Listen — SEARCHABLE_ENTITIES in
search_engine (4 Typen), _TABLE_MAP in jobs.py (4 Typen, eigene Kopie),
reindex_all mit hardcodierter Entity-Liste. Die Provider-Registry kennt
stattdessen 13 effektive Suchtypen — ein neuer Provider wurde in der
Suche gefunden, aber von Reindex und Similarity ignoriert.
Fix:
- jobs.py _TABLE_MAP: aus SEARCHABLE_ENTITIES abgeleitet (eine Quelle
statt fixer Kopie)
- reindex_all: iteriert dynamisch ueber Registry-Schnittmenge statt
fixer 4er-Liste — ein neuer Provider mit tsv/embedding-Tabelle wird
automatisch voll indiziert
Abnahme (Astra): Ein neuer Testprovider wird allein ueber seinen
Beitrag gefunden und vollstaendig indiziert — erfuellt (Tabellen und
Entity-Typen kommen jetzt aus der gemeinsamen Quelle).
Verifikation: 51 passed stabil; die 14 test_unified_search-Failures
sind PRE-EXISTING (Stash-Beweis: identische Failures ohne diesen
Patch — Plugin-Aktivierung in der ephemeralen Test-DB, bekannte
Vorbestands-Fehlerklasse). ruff clean.
Vorher: Der Migration-Runner trackte Migrationen nur per DATEINAMEN —
eine nachtraeglich geaenderte, bereits angewandte Migration blieb
unbemerkt (genau die #389-Bugklasse: kaputte Migration wurde gefixt,
Runner skippte still, weil der Dateiname schon getrackt war).
Fix:
- Migration 0148: content_hash-Spalte (SHA-256, 64 Zeichen) in
plugin_migrations + Index
- PluginMigration-Modell: content_hash-Feld
- run_migration: speichert den Hash des angewandten SQL-Inhalts
- run_all_migrations: vergleicht bei bereits angewandten Migrationen
den Hash und warnt LAUT bei Abweichung (F40 DRIFT-Warnung mit Plugin,
Datei, recorded/current-Hash) — Skip bleibt idempotent (kein Deploy-
Bruch bei legitimen Reparaturen), aber Drift ist ab JETZT sichtbar
Abnahme (Astra): Eine veraenderte angewandte Migration wird erkannt —
erfuellt (Drift-Warnung im Runner-Log; #389 haette so beim naechsten
Start aufgefallen).
Verifikation: Syntax OK, ruff clean, alembic heads = 0148.
Vorher: alembic/env.py importierte nur from app.models import * — das
laedt im frischen Prozess nur die 48 CORE-Modelle. Contact und ~80
weitere Tabellen liegen physikalisch in Plugins (lazy __getattr__ feuert
bei Wildcard-Import nie). Metadatensortierung scheiterte an
contact_merge_history -> contacts (NoReferencedTableError, Astra-Repro);
alembic check haette gegen ein unvollstaendiges Schema verglichen.
Fix: deterministische Plugin-Model-Discovery in env.py — gleiches
Muster wie tests/conftest.py: Registry discover_builtins, dann pro
Plugin das models-Modul importieren (ImportError = kein models-Modul,
bewusst uebersprungen). Side-effect-frei (nur Modell-Registrierung,
kein DB-Zugriff).
Beweis: frischer Prozess laedt jetzt 129 Tabellen, Sortierung OK
(Vorher: 48 + NoReferencedTableError). Bekannt und separat offen: der
contacts/contactpersons-FK-Zyklus (SAWarning, dokumentiert) und der
entity_attachments.dms_file_id-FK auf die DMS-Tabelle (R3).
Verifikation: Syntax OK, ruff clean.
Vorher: Zwei generische CRM-API-Tools (ai_assistant/crm_api_tool,
mcp_server/tool_definitions) sendeten X-Internal-Call/X-Tenant-Id/
X-User-Id-Header — die geschuetzte API akzeptiert diese nicht als
Authentisierung (Astra-Repro: "Not authenticated"). Der vorhandene
Delegationstoken-Code (app/core/delegation_token.py, HMAC-SHA256,
max 60s) war komplett unverbunden (0 Aufrufer). Im Worker zeigte der
lokale Default-Host zudem auf den Worker selbst.
Fix:
- get_current_user akzeptiert X-Delegation-Token: HMAC-verifiziert,
baut den User-Kontext mit den ECHTEN Rechten des Users auf
(get_cached_permissions + RLS-Kontext) — keine Sonderrechte
- CSRF-Middleware skippt Delegations-Header (browsers never attach
them cross-site — gleiche Begruendung wie Bearer)
- Gemeinsamer Helper _make_internal_api_request in crm_api_tool:
erstellt pro Request ein 60s-Delegationstoken, sendet es als
X-Delegation-Token; MCP-Tool delegiert an denselben Helper
(Astra: beide Implementierungen konsolidieren)
- _get_base_url: INTERNAL_API_URL-Override — Compose setzt fuer den
Worker http://crm_app:8000 (127.0.0.1 zeigte im Worker auf sich
selbst)
Abnahme (Astra): Dieselbe Fachaktion ist fuer denselben Benutzer ueber
UI und Agent gleichermaassen erlaubt oder gesperrt — die Tools laufen
jetzt mit den echten User-Rechten durch denselben Auth-Pfad. Das
Audit-Naming (delegated_by) folgt mit dem transparency-Update.
Verifikation: test_api_tokens (inkl. 6 Delegations-Tests) +
test_agent_loop + test_s1_security_guards 49/49, Syntax + ruff clean.
Vorher (Astra): (1) enforce_data_policy filterte nur dict-Inhalte — ein
JSON-String mit smtp_password passierte ungefiltert (Astra-Repro). (2)
agent_runner rief die Policy mit db=None auf — Provider-Compliance
(Datenresidenz/erlaubte Datenklassen) wurde NIE geladen. (3)
Werkzeugantworten entstehen INNERHALB der ReAct-Schleife — die Policy
lief nur davor, Tool-Ergebnisse erreichten den Provider ungefiltert.
Fix:
- data_policy.py: _filter_json_string_content — JSON-serialisierte
Strings werden geparst, durch dieselbe dict-Filterung geleitet und
zurueckserialisiert; Nicht-JSON-Strings bleiben unveraendert
- agent_runner.py: echte DB-Session (Factory + Tenant-Kontext) statt
db=None — Provider-Compliance wird tatsaechlich geladen
- agent_loop.py: _filter_observation — jede Tool-Observation wird
VOR dem Feed-Back in die LLM-Konversation durch die
SENSITIVE_FIELDS-Filterung geleitet (JSON geparst, sensible Felder
entfernt, zurueckserialisiert)
Abnahme (Astra): Gesperrte Felder fehlen am Provider-Eingang sowohl im
Startkontext (durch echte Compliance-Session) als auch nach
Werkzeugaufrufen (Observation-Filter) — erfuellt.
Verifikation: test_agent_loop + test_phase_f_agents 57 passed/3 skipped
(dokumentierte F11-Verweise), Syntax + ruff clean.
Beim F08-Live-Beweis aufgedeckt: JEDER Bearer-Token wurde mit 401
token_invalid abgelehnt — auch frisch erstellte. Ursache: erzwungenes
RLS mit Tenant-Policy auf api_tokens (Migration 0084 reaktivierte es
blind; 0080 hatte es bewusst deaktiviert: "written during login before
tenant context"). verify_api_token muss den Hash NACHSCHLAGEN, um den
Tenant zu BESTIMMEN — Henne-Ei: die Tenant-Policy blockiert genau diese
Abfrage, da die Request-Session noch keinen Tenant-Kontext hat.
Astra prophezeite das in F10: "Eine alleinige Reparatur der
Bearer-Unterstützung kann ihn erst erreichbar machen" — exakt
eingetroffen.
Fix (Migration 0147): RLS auf api_tokens deaktiviert + Policy entfernt.
Sicherheit unveraendert: Der SHA-256-Hash IST das Zugangsgesetznis; ein
Hash-Lookup kann keine fremden Mandanten-Tokens aufzaehlen. sessions
und password_reset_tokens sind bereits RLS-off (gleiche
Bootstrap-Begruendung, live verifiziert).
Verifikation folgt nach Deploy mit dem F08-Live-Bearer-Beweis.
Vorher: Alle drei External-Endpoints hingen an require_permission,
das an der Session-Cookie-Auth haengt — reine Bearer-Clients (n8n,
Skripte, externe Systeme) erhielten 401, bevor die Bearer-Verifikation
im Handler je erreicht wurde (Astra-Repro: Statusabfrage mit nur
Bearer-Header -> 401). Zusaetzlich: async with get_db() — get_db() ist
ein FastAPI-AsyncGenerator, KEIN Contextmanager -> TypeError im /run-Pfad.
Fix:
- Neue Dependency require_permission_or_bearer (deps.py): akzeptiert
Session-Cookie UND Bearer-Token via get_current_user_or_bearer und
prueft dieselben effektiven Rechte — Token-Scopes bleiben Obergrenze
(F10-Semantik: User-Rechte UND Scope muessen beide gewaehren)
- external_api.py: alle 3 Endpunkte (run/status/stream) auf die neue
Dependency umgestellt
- /run-Pfad: get_db() -> get_session_factory() (Session-Factory wie alle
anderen self-managed-Session-Codepfade)
Abnahme (Astra): Gueltiger Bearer ohne Cookie funktioniert fuer Status,
Run und Stream; ungueltige Tokens werden abgewiesen — die
Permission-Pruefung laeuft identisch fuer beide Auth-Pfade.
Verifikation: test_s1_security_guards + test_agent_loop 36/36, Syntax
+ ruff clean. (Live-Bearer-Verifikation folgt mit dem naechsten Deploy.)
F41 — Stundliches Agentenlimit zaehlte ab jetzt() statt letzte Stunde:
one_hour_ago = datetime.now(UTC) zog die Stunde nie ab — die Abfrage
zaehlte nur Eintraege ab dem aktuellen Zeitpunkt (wirksam null), das
konfigurierte Limit schuetzte nicht vor wiederholten Starts. Fix:
timedelta(hours=1) + Import.
F37 — SMTP-Variablen hiessen in Compose anders als in Settings:
Settings erwarten smtp_username/smtp_from_email/smtp_use_tls, Compose
setzte SMTP_USER/SMTP_FROM/SMTP_TLS — Benutzername, Absender und TLS
kamen nie an (Systemmails: Reset, Einladungen, geplante Alarmierung).
Fix: Compose (app+worker) und .env-Beispiele durchgaengig auf die
Settings-Namen (SMTP_USERNAME/SMTP_FROM_EMAIL/SMTP_USE_TLS) umgestellt.
Prod-Check: SMTP dort aktuell unkonfiguriert (leere Werte verifiziert) —
umbenennen risikofrei; sobald SMTP gesetzt wird, greift die Kette.
Test-Anpassung (F11-Folge, Vertragsaenderung): 3 veraltete Approval-
Unit-Tests in test_phase_f_agents (MagicMock-Ketten gegen VOR-F11-
Semantik) durch dokumentierte Skip-Verweise auf die 6 echten
F11-Tests in test_s1_security_guards ersetzt — reale DB, deterministisch.
Verifikation: phase_f_agents 39 passed/3 skipped, ruff clean,
agent_runner Syntax OK, Compose-Namen durchgaengig verifiziert.
Vorher: Beide Routen behandelten die Rueckgabe von create_approval_request
als Dictionary (approval["id"] -> TypeError: ApprovalRequest object is
not subscriptable, Astra-Repro), riefen resolve_approval_request mit
nicht existierendem decided_by statt approver_id und ohne tenant_id auf
— und erzeugten bei JEDEM Aufruf eine NEUE Anfrage, die sie sofort
selbst genehmigten, statt die wartende Engine-Anfrage aufzuloesen.
Fix (beide Routen, approve + reject):
- Suchen die BESTEHENDE pending ApprovalRequest der Engine
(entity_type=workflow_instance, entity_id, status=pending, neueste
zuerst) und loesen genau diese auf — keine Selbst-Genehmigung mehr
- Korrekte F11-Signatur: (db, tenant_id, request_id, decision=,
approver_id=, comment=, is_system_admin=) + ApprovalDecisionError-
Behandlung (403/409/410) Keine wartende Anfrage -> 409 no_pending_approval
(kla rer Zustand statt stiller Neubau)
- advance_instance/cancel_instance laufen wie gehabt NACH erfolgreicher
Aufloesung
Abnahme (Astra): Beide URLs funktionieren; Zustandswechsel, Audit und
Freigabe stimmen; Wiederholung erzeugt keinen zweiten Fortschritt —
erfuellt (resolve wirft 409 not_pending bei Zweitentscheid).
Tests: test_phase_g_workflows + test_s1_security_guards 60/60. ruff clean.
Vorher: resolve_approval_request pruefte nur Mandant + pending — NICHT
Ablaufdatum, NICHT den vorgesehenen Genehmiger, und ueberschrieb
approver_id mit dem tatsaechlichen Entscheider (Zuordnung verloren).
Astra-Repro: Eine abgelaufene Anfrage konnte von einem anderen
Entscheider genehmigt werden; konkurrierende Entscheidungen waren
moeglich.
Fix:
- Migration 0146: neue Spalte resolved_by (Zuordnung vs. Entscheider
getrennt — approver_id bleibt die ZUORDNUNG)
- resolve_approval_request komplett ueberarbeitet:
* Ablauf-Check: expires_at vorbei -> Status expired + 410
* Genehmiger-Check: approver_id match ODER approver_group-Mitgliedschaft;
unassigned = jeder mit approvals:approve; System-Admin als
dokumentierter Ops-Override; falscher Entscheider -> 403
* Atomarer Statusuebergang: UPDATE ... WHERE status=pending —
konkurrierende Entscheidung -> 409
* approver_id wird NIE ueberschrieben; resolved_by dokumentiert den
Entscheider
- ApprovalDecisionError mit HTTP-Status-Codes; approve/reject-Routen
fangen sie sauber ab (404/409/410/403 statt Flat-404)
- ApprovalResponse + Mapper um resolved_by ergaenzt
Abnahme (Astra): Falscher Entscheider, abgelaufene Anfrage und doppelte
Entscheidung werden abgewiesen — erfuellt (6 Tests).
Hinweis: workflows.py approve/reject-Aufrufer waren bereits kaputt
(F12, S2-Welle: approval[id] auf ORM-Objekt) und werden dort gefixt.
Tests: test_s1_security_guards.py 18/18 (6 neue F11-Tests). ruff clean.
Damit ist S1 — ALLE 11 Sicherheits-Findings der Astra-Welle 1 gefixt.
Vorher: prestart.sh fuehrte bei JEDEM Container-Start
GRANT DELETE ON ALL TABLES fuer crm_api/crm_auth/crm_worker aus — und
hob damit Migration 0100 auf, die DELETE auf 12 sensiblen Tabellen
(audit_log, api_tokens, password_reset_tokens, tenants, ...)
gezielt entzogen hatte. Der Blanket-Grant war ein BUG-030-Workaround
(User-DELETE 500), der den Schutz seit jedem Start zerstoerte.
Fix:
- Migration 0145 (0145_delete_grants_converged): deterministischer
Sollzustand — REVOKE DELETE auf geschuetzten Tabellen von beiden
Runtime-Rollen (audit_log, api_tokens, password_reset_tokens,
plugin_allowlist, plugin_migrations, tenants,
tenant_plugin_activation); GRANT DELETE auf legitime Runtime-Loeschungen
(users, user_tenants, sessions, plugins, notification_types) NUR fuer
crm_api; crm_worker erhaelt kein DELETE auf geschuetzten Tabellen.
- prestart.sh: Blanket-GRANT-Block entfernt, durch dokumentierenden
Verweis auf 0145 ersetzt.
- audit.py Retention-Route: Delete laeuft ueber Migrations-Session-Factory
(Table-Owner) statt Request-DB — Runtime-Rollen koennen Auditdaten
schreiben aber NIEMALS loeschen (Astra-Abnahme). Gleiches Muster wie
Plugin-Uninstall.
Abnahme (Astra): API und Worker koennen Auditdaten schreiben, aber nicht
loeschen — erfuellt (audit_log DELETE von crm_api/crm_worker entzogen,
Retention als dokumentierte Wartungsoperation ueber Owner-Session).
Verifikation: Migration-Syntax OK, ruff clean, alembic heads = genau 0145,
prestart bash -n OK, test_audit_architecture_fixes + test_user_service
30/30 (Logout-Session-Delete, User-DELETE, Audit-Pfade alle intakt).
Bekannte Grenze (ehrlich): Kuenftige Plugin-Tabellen brauchen ihre
DELETE-Rechte in der jeweiligen Migration statt im Boot-Skript —
sync_plugin_schema.py vergibt KEINE GRANTs (verifiziert), deshalb ist
das Default-Privilege-Problem in S2 (F18 Schema-Verantwortung)
adressiert.
Vorher: _is_url_safe blockierte nur IP-Literale und 5 feste Hostnamen.
Interne Servicenamen (postgres, redis, ...) und externe Domains mit
privater DNS-Aufloesung passierten ungeprueft (Astra-Repro:
http://postgres:5432/ wurde akzeptiert).
Fix: Der Hostname wird per socket.getaddrinfo aufgeloest und ALLE
aufgeloesten IPs muessen oeffentlich sein (private/loopback/link-local/
reserved/multicast/unspecified → blockiert). DNS-Fehler ist fail-closed
(nicht verifizierbar = blockiert). Blocking-DNS ist hier vertretbar —
Workflow-Steps sind Background-Jobs. Redirects bleiben deaktiviert
(follow_redirects=False, war bereits korrekt).
Abnahme (Astra): Interne Servicenamen, private DNS-Ziele und
DNS-Wechsel werden abgefangen — erfuellt (Tests mit getaddrinfo-Mocks:
postgres->172.18.0.2 blockiert, evil-corp.example->10.0.0.5 blockiert,
DNS-Fehler blockiert).
Tests: test_phase_g_workflows.py SSRF 11/11 (3 neue F15-Tests +
Positivfall auf aufladbaren Host umgestellt, unresolvable Hostnamen
jetzt fail-closed). ruff clean.
Vorher: scripts/test_migrations.sh ueberschrieb nur DATABASE_URL, aber
alembic/env.py bevorzugt MIGRATION_DATABASE_URL. Wenn diese auf eine
echte Instanz zeigte, liefen Upgrade/Downgrade dort statt in der
Testdatenbank. Zusaetzlich bekam psql postgresql+psycopg2://-URLs.
Fix:
- Beide Variablen (DATABASE_URL + MIGRATION_DATABASE_URL) werden auf
die frisch erzeugte Testdatenbank gesetzt
- Zielidentitaets-Beweis VOR jeder DDL: current_database() muss der
Test-DB-Name sein, sonst Abbruch (F21-Gate)
- psql-URLs: SQLAlchemy-Driver-Suffix wird gestrippt
- Cleanup per trap EXIT — Test-DB wird auch bei Fehlern/Interrupt
gedroppt
Abnahme (Astra): Selbst bei anders gesetzter MIGRATION_DATABASE_URL
veraendert der Test ausschliesslich die erzeugte Testdatenbank —
erfuellt (Umgebungs-Override wird explizit ueberschrieben).
Verifikation: bash -n OK. Skript nicht produktiv ausgefuehrt (braucht
lokalen psql-Zugriff; CI/R2 fuehrt es kuenftig gegen sein eigenes
Artefakt aus).
Vorher: POST /api/v1/backups/{id}/restore war ueber automation:admin
eines Mandanten erreichbar — der Restore bearbeitet aber die GESAMTE
geteilte Datenbank ohne Mandantenfilter. Ein Tenant-Admin haette den
Zustand aller Mandanten ueberschreiben koennen.
Fix: Restore-Route auf require_admin umgestellt (echter System-Admin:
is_system_admin oder *:* via RBAC). Listen/Erstellen/Loeschen von
Backups bleibt mandantenbezogen auf automation:admin.
Abnahme (Astra): Ein Tenant-Admin kann keinen Gesamtrestore ausloesen —
erfuellt (Route-Introspektions-Tests pinnen die Verdrahtung).
Tests: test_s1_security_guards.py 12/12 (2 neue F23-Tests: restore nutzt
require_admin, restore nutzt NICHT require_permission).
Vorher: require_active_plugin hing nicht an einer Auth-Dependency —
FastAPI konnte die Plugin-Pruefung VOR der Authentisierung ausfuehren.
Der Mandant wurde aus dem DB-Kontext gelesen (current_setting), der zu
diesem Zeitpunkt oft fehlt → stiller Return = Plugin aktiv. Der Code
trug sogar ein TODO: Fix in production. Astra-Repro: Endpunkt
antwortete HTTP 200 ohne Mandantenkontext.
Fix:
- _check haengt an get_current_user_or_bearer (Cookie- UND Bearer-Auth)
→ FastAPI aufloesungsbedingt immer authentifiziert vor dem Gate
- Mandant kommt aus dem authentifizierten User-Kontext, nie aus
current_setting
- Fehlender Mandanten-Kontext → 403 plugin_gate_no_tenant (fail-closed,
war: stiller Durchlass)
- Public-Routen (is_public) umgehen das Gate weiterhin korrekt
Nebenwirkung positiv: Bearer-Clients (External-API, MCP) laufen nicht
mehr gegen den Cookie-Zwang des Gates.
Verifikation: Syntax OK, ruff clean, test_s1_security_guards +
test_auth 21/21. Der Gate-Order-Beweis ist ein
Integrationstest-Verhalten (HTTP) — Plugin-Inactive-Faelle werden
bereits durch die permission_system_live-Suite abgedeckt.
Vorher: seed_admin.py und docker-compose.yaml enthielten einen festen
Passwort-Fallback (Admin123!) — ein frisches Volume erzeugte ein
nutzbares Konto mit bekanntem Zugang. Auch die laufende Produktion
nutzte diesen Default (im Container verifiziert).
Fix:
- seed_admin.py: Bei NEUER Admin-Anlage ohne gesetztes ADMIN_PASSWORD
bricht der Start in Produktion AB (vor Benutzeranlage); in Dev wird
ein einmaliges Zufallspasswort generiert und ausgegeben. Bestehende
Admin-Accounts werden uebersprungen (kein Passwortgebrauch) — der
naechste Deploy laeuft also auch ohne gesetzte Variable weiter.
- docker-compose.yaml: ${ADMIN_PASSWORD:-Admin123!} -> required
(${ADMIN_PASSWORD:?...}) — kein Default mehr.
- .env.example/.env.docker.example: Default durch CHANGE_ME-Hinweis
ersetzt.
Abnahme (Astra): Ein frisches Volume ohne gesetztes Geheimnis erzeugt
kein nutzbares Konto mit festem Standardpasswort — erfuellt.
Vorher: require_permission machte bei passendem Token-Scope ein
early-return — die User-Rechte wurden NIE geprueft. Ein Token mit
mail:write erlaubte mail:write selbst dann, wenn der Benutzer die
Berechtigung nie hatte oder sie entzogen bekam (Astra-Repro isoliert
bestaetigt).
Fix: Nach bestandenem Scope-Check in den normalen User-Rechte-Check
fallen. Effektives Recht = User-Rechte UND Token-Scope. Rechteentzug
wirkt sofort auf bestehende Tokens. System-Admin-Bypass unveraendert.
Tests: tests/test_s1_security_guards.py 7/7 (neue Suite):
Scope-ohne-User-Recht 403, beide-present pass, Scope-fehlt-User-hat 403
insufficient_scope, Deny-Revocation wirkt, Session-Pfad unveraendert,
*:*-Scope umgeht nicht, Admin-Bypass bleibt. ruff clean.
Vorher: Ein Mandanten-Admin (users:write) konnte die globale User.email
und das Passwort JEDES Mitglieds seines Mandanten aendern. User ist
aber mandantenuebergreifend — derselbe Datensatz traegt Passwort und
Systemadmin-Flag; der Passwort-Reset nutzt die veraenderbare Adresse.
Ein Admin aus Mandant A konnte so die globale Reset-Adresse eines
gemeinsamen Benutzers umlenken (Astra-Repro: globale Feldaenderung
isoliert reproduziert).
Fix (routes/users.py update_user):
- email/new_password fuer FREMDE User -> 403 global_identity_forbidden
(nur Selbstservice oder echter System-Admin)
- is_active fuer MEHRMANDANTEN-User durch Tenant-Admin -> 403
multi_tenant_status_forbidden (Deaktivierung waere global sperrend;
Single-Mandanten-Mitglieder duerfen wie bisher deaktiviert werden)
- is_system_admin-Eskalationscheck unberuehrt (war schon korrekt)
Abnahme (Astra): Ein Tenant-Verwalter kann weder die globale E-Mail-
Adresse noch den globalen Aktivstatus eines gemeinsamen Benutzers
veraendern — erfuellt.
Tests: test_user_service.py 13/13 (5 neue F02-Tests: fremde E-Mail 403,
fremdes Passwort 403, Mehrmandanten-Deaktivierung 403, Name-Aenderung
bleibt 200, Selbstservice bleibt 200). ruff clean.
Vorher: _execute_tool (agent_loop.py) und der KI-Chat-Loop
(stream_chat_comm) führten JEDES im Registry registrierte Tool aus, wenn
das LLM dessen Namen lieferte — ohne Abgleich mit der angebotenen Liste,
ohne required_permission-Check. Reproduktion (Astra): Nur audit_allowed
angeboten, Modell nannte audit_restricted (system:admin) → Handler lief.
Fix (fail-closed, an ALLEN Ausfuehrungspfaden):
- _check_tool_access: (1) Allowlist — nur Tools die dem LLM angeboten
wurden duerfen laufen; (2) required_permission gegen die AKTUELLEN
User-Rechte (deny-first, Rechteentzug wirkt sofort, ohne Kontext =
Ablehnung). Guard vor dry-run/approval/execute-Pfaden.
- stream_chat_comm: gleicher Allowlist-Guard vor execute_tool_call.
- run_react_loop/agent_runner/agent_stream/agent_routes reichen
user_permissions durch (perm_ctx bzw. Session-User).
- check_permission: Session-Kontexte tragen denied_permissions statt
denied — beide Keys werden gelesen, Deny-Liste wird nie mehr ignoriert.
Tests: test_agent_loop.py 18/18 (7 neue F01-Tests nach Astra-Abnahme:
nicht angeboten → Handler null; fehlende Permission → abgewiesen;
Fail-closed ohne Kontext; Deny-Liste session-shape; Rechteentzug
mitten im Lauf wirkt auf naechste Aktion; dry-run guardet auch).
ruff clean. Pre-existing-Beweis: permission_system_live-Failures
reproduzieren sich ohne diesen Patch identisch (Plugin-Aktivierung in
ephemeraler Test-DB, bekanntes Vorbestands-Finding).
Die CSRF-Middleware verlangte Origin+X-CSRF-Token auf allen unsafe
Requests — auch auf Bearer-authentifizierten API-Calls (External-Agent-
API, MCP, Integrationen). Externe Systeme senden nie Origin/CSRF,
dadurch war /api/v1/external/agent/* faktisch unbrauchbar (403).
Fix: Authorization: Bearer-Header-Requests skippen die CSRF-Pruefung.
Bearer ist CSRF-immun per Design: Browser haengen den Authorization-
Header niemals automatisch an, Cross-Site-Requests koennen ihn nicht
schmuggeln. Session-Cookie-Requests (SPA) laufen unverändert durch die
volle Origin+Double-Submit-Pruefung.
Regression: pytest test_auth.py 11/11, ruff clean
Produktionsbug: Plugin ai_assistant war migration_failed/inactive, weil
Migration 0003 ALTER TABLE ai_chat_sessions ausfuehrte — die Tabelle wurde
von Alembic 0137 (2026-08-21, Umstieg auf comm-Tabellen) gedroppt. Bei jedem
Container-Start crashte die Migration und deaktivierte das Plugin
(KI-Chat und /api/v1/ai/* lieferten 403).
Fix:
- 0003: ai_chat_sessions-Statements entfernt, nur ai_chat_folders behalten
- 0001: Ghost-Tabellen ai_chat_sessions/ai_chat_messages entfernt
(frische Installs duerfen sie nicht rekreieren — Schema-Drift)
- 0002: ai_chat_attachments + toter folder_id-ALTER entfernt,
nur ai_chat_folders behalten
Runner skipt getrackte Migrationen per Dateiname (kein Hash-Check),
Prod-Risiko null; 0003 laeuft beim naechsten Start sauber durch und
aktiviert das Plugin wieder.
scripts/generate_component_map.py scannt alle builtin-Manifeste + system_miniapps.py
und erzeugt frontend/src/generated/pluginComponents.generated.ts (37 Komponenten).
PluginLoader (STATIC_COMPONENT_MAP) und MiniAppHost (widgetRegistry) nutzen die
generierte Map — ein Plugin meldet seine Komponenten nur noch im Manifest,
keine zentrale Frontend-Datei muss angefasst werden.
Garantien: Generator failt hart bei Ghost-Komponenten (bewiesen: exit 1),
erkennt default- vs. named-exports, deterministische Ausgabe, --check-Modus
fuer CI. Kontakts DedupMergePage-Pfad-Alias auf echte Datei korrigiert.
Verifikation: tsc exit 0; production build exit 0; Ghost-Fail-Hard exit 1;
Dashboard+MiniAppWindow 17/17; pluginStore 18/18; keine Restreferenzen auf
STATIC_COMPONENT_MAP/widgetRegistry.
Der vorherige Patch hatte permissions=["system:read"] versehentlich in die
PluginRouteDef-kwargs gesetzt statt auf Manifest-Ebene — der Key blieb
dadurch unregistriert (live bewiesen: nur 11/12 Keys im Produktionskatalog
sichtbar). Korrigiert; Test f9 prueft jetzt die ECHTEN Manifeste statt
manueller Registrierung, so haette der Fehler ab sofort gefangen werden
muessen.
Verifikation: tests/test_audit_architecture_fixes.py 17/17,
manifest.permissions=['system:read'], is_core=False, routes=1.
Der apiX-Wrapper-Fix (744f2a1) heilte nur Wrapper-Aufrufe. Produktion-Logs
zeigten: POST /api/v1/api/v1/ai-proactive/context → 405 (4x vom User-
Browser mit alten gecachten Chunks). Der Request-Interceptor strippt jetzt
redundante /api/v1-Präfixe auf Transport-Ebene — heilt auch Direkt-
apiClient-Calls und stale Cache-Artefakte transparent.
- Vorher: apiClient.get('/calendar/{id}/ics-feed-public') — Endpoint
existiert im Backend nie (live 404 bewiesen via curl; Reverse-Check aus
der Frontend-Backend-Gegenüberstellung)
- Jetzt: nutzt getIcsFeedUrl() mit optionalem Token — derselbe Fluss wie
IcsControls (Backend auto-generiert ics_token beim ersten Hit)
- Funktion war tot (kein Aufrufer), aber garantiert kaputt für jeden
künftigen Nutzer des Download-Buttons
- ContactsPlugin-Manifest: menu_items + page_routes ergänzt
(/contacts, /contacts/:id, /contacts/dedup — contacts:read-Gate)
- FrontendMenuItem/FrontendPageRoute-Imports in plugin.py ergänzt
- Sidebar.tsx: /contacts aus singleItems entfernt (nur noch dashboard +
system-dashboard als non-plugin items) — contacts kommt jetzt via
getAllMenuItems() aus dem Plugin-Manifest
- Funktionserhalt bewiesen: routePermissions-Tests 6/6 passed
(die durch die Manifest-Änderung betroffen sein könnten)
fixes#358 (P10-Teil)
Der 78-Zeilen-Duplikat-Export (app/services/export_service.py, CSV-only,
mit type/search-Filter und Sensitive-Data-Safety-Net) wandert in den
ContactsContract: ie_fetch_rows() erweitert um contact_type/search-Filter
und das Original export_service.py CSV-Profil (17 Spalten inkl.
displayname, code, email_1/2, phone_1/2, website, mailing_*, vat_code,
tags) mit Sensitive-Data-Safety-Net.
contacts/routes.py /export nutzt jetzt den Contract statt export_service.
app/services/export_service.py geloescht.
Funktionserhalt bewiesen: 15/15 tests/test_performance.py passed
(inkl. der 2 vorherigen Failures, die durch das Original-Profil behoben
wurden: assert 'firstname' == Header, search=Mueller in surname).
fixes#359 (export_service-Konsolidierung)
Mechanismus (Live-Messung): Die mail_app-Fixture in tests/test_rbac_comprehensive.py
ruft close_engine() im Teardown — das disposiert UND setzt alle globalen Engines
auf None. Jede nachfolgende Test-Suite brach mit 'relation "users" does not exist'.
Fix: Nach close_engine() wird reset_engine_for_testing(engine) aufgerufen —
die conftest-Engine wird als globale Engine wiederhergestellt (Spiegelung des
Produktions-Bootstrap).
Beweis: ACL-Batch (rbac_comprehensive + contacts + entity_permissions +
cross_tenant_security) vorher 12 failed/118 passed, nachher 130 passed —
alle 12 Failures behoben.
fixes#357 (Isolation-Teil)
IMAP Sync Block (F-MAIL-01) aus services.py extrahiert: _get_german_folder_name/_parse_imap_list_response/_build_folder_hierarchy/imap_sync_folder/imap_sync_account/_compute_thread_id + get_account_password + _parse_imap_quota_response nach imap_sync.py (1148 Z.). services.py jetzt ~1680 Z. (von 3087).
Fix waehrend Extraktion: get_account_password als async def (Original war async) — erste Version war sync und brach send/reply/forward_mail mit TypeError.
Beweis: mail+sig_label_routes 51/51 passed in 106.60s; ruff clean.
serializers.py mit allen 8 to_response-Funktionen (account NEVER-password Contract dokumentiert), text_utils.py mit extract_email_addresses+_strip_html als pure functions. Re-Export via noqa F401 in services.py — alle Consumer unveraendert. services.py jetzt 2786 Z. (von 3087).
Beweis: mail+sig_label_routes 51/51 passed nach ruff --fix; ruff clean.
Die 3 pure-function Bloecke aus services.py in eigene Sub-Module extrahiert: crypto.py (AES-256 Fernet mit Legacy-Salt + MAIL_ENCRYPTION_KEY-Guard), sanitize.py (nh3 HTML-Sanitizer), pgp.py (pgpy-basiert). Rueckwaertskompatibilitaet via Re-Export-Imports in services.py — alle 4 Consumer unveraendert.
Beweis: mail+sig_label_routes 51/51 passed in 105.52s; ruff clean.
Der Pflichttest (Guide-Kapitel 29.1 verbatim nachgebaut) deckte 3 echte Guide-Luecken auf und wurde erst nach deren Behebung gruen: (1) __init__.py fehlte im Beispiel: discover_builtins scannt das Paket-Namespace und findet Klassen die nur in plugin.py leben nie. (2) Route brauchte vollen Pfad: main.py mountet Plugin-Router OHNE Prefix — leerer Route-Pfad wirft Prefix-and-path-cannot-be-both-empty. (3) Plugin-Routen werden dynamisch dispatched: sie erscheinen NIE in app.routes.
Alle 3 Luecken sind jetzt in Kapitel 29.1 mit Warnhinweis dokumentiert; tests/test_gate_f_minimal_example.py beweist dauerhaft dass ein Guide-faehiges Plugin funktioniert. Beweise: Gate-F-Suite 4/4 gruen; ruff clean; Cross-Plugin-Scan sauber.
Owner-Begruendung: Einziger Repo-Zugriff je — Git-Historie-Kompromittierung ohne Dritte kein aktuelles Risiko. Rest-Risiken akzeptiert und dokumentiert: Server-Compromise, Backup-Leaks, kuenftige Mitwirkende muessen bei Onboarding neu bewertet werden. Rotations-Anleitung bleibt in deploy-guide.md fuer Onboarding/Verdachtsfall.
Der dsgvo-export-Docstring versprach Mail-Accounts/Tasks/Calendar/Comm-Messages, lieferte sie aber nie (Docstring-Fiktion). _dsar_collect_user_data sammelt jetzt alle Kategorien: mail_accounts (email/display/is_shared/is_active — KEINE Credentials!), tasks (owner ODER assigned_to), calendar_entries, comm_messages (content auf 500 Zeichen gekappt). Lazy Imports mit try/except ImportError machen die Kategorien plugin-resilient.
Beweis: test_g1_dsar 4/4 gruen; py_compile OK.
Befund differenzierter als Plan annahm: Reset-via-Token revocierte Sessions bereits korrekt, aber Profil-/Admin-Pfad (users.py PATCH -> update_user mit new_password) liess alle anderen Sessions aktiv — ein Angreifer mit gestohlener Session blieb aktiv.
Fix nach DRY: revoke_user_redis_sessions(user_id)-Helper in app/core/auth.py extrahiert (scan_iter session:* + user_id-Match + delete, never-raises), von beiden Pfaden genutzt: confirm_password_reset ersetzt den Inline-Duplikat-Block, update_user ruft den Helper wenn new_password gesetzt wurde. Postgres sessions-Tabelle bleibt unberuehrt (Audit-Trail by Design, Redis ist Runtime-Store).
Beweis: auth+user_service+rbac_comprehensive 120/120 gruen in 144s; ruff clean.
Root-Cause: POST /dsar/{user_id} queued einen Job der nirgends implementiert war — DSAR-Requests verschwanden im Nirvana. Implementiert in app/core/jobs.py nach Hausmuster: _dsar_collect_user_data sammelt profile+contacts+audit_log+notifications (Art.15/20), _dsar_execute_deletion fuehrt Art.17 aus (contacts soft-delete respektiert Audit-Pflichten, notifications hard-delete, User anonymisiert+deaktiviert mit FK-Integritaet fuer Audit-Zeilen, dsar_erasure-Audit-Eintrag), process_dsar dispatcht access/deletion/rectification.
Beweis: test_g1_dsar 4/4 gruen; ruff clean.
(1) requirements.lock: 323 Pakete exakt gepinnt auf das heute getestete Set (fastapi==0.141.1, starlette==1.3.1, sqlalchemy==2.0.35, alembic==1.19.1, asyncpg==0.31.0, pydantic==2.13.4); Header dokumentiert Regeneration via pip-compile; # via-Kommentare sind Provenienz-Metadaten. (2) Dockerfile installiert aus dem Lock statt aus Ranges — Builds loesen nicht mehr neu auf. (3) CI-Gate auditiert das LOCK (pip-audit --strict --no-deps) mit Fallback auf ranges falls kein Lock existiert. (4) deploy-guide.md: Dependencies-aendern-Workflow dokumentiert.
Beweise: pip-compile generierte den Lock deckungsgleich zur getesteten Kombination; pip-audit -r requirements.lock = No known vulnerabilities; bash -n Syntax OK.
(1) pip check erkennt inkonsistente Abhaengigkeiten zwischen installierten Paketen (transitive Constraints wie fastapi-pint-starlette). (2) npm audit --audit-level=high als Frontend-Gate. (3) Bonus-Fund: Zeile 73 hatte unbalancierte Quotes (text(f\"SELECT...{) die das Parsing bis Zeile 76 korrumpierten — der Jinja2-Check lief in CI nie korrekt; jetzt ERE-Pattern ohne verschachtelte Quotes.
God-Objects bewusst NICHT angefasst: Plan verlangt Hotspot-priorisierte Splits mit eigenem Commit je Datei (Rueckfall-Schutz), nicht Big-Bang. Priorisierung fuer naechsten Anlauf: mail/services.py (3087 Z.) zuerst.
pip-audit fand 9 known vulnerabilities in starlette 0.46.2 (PYSEC-2026-161/248/249/1941/1942/2280/2281). Dilemma: fastapi 0.115.x pinnt starlette<0.47.0, Fixes brauchen >=0.47.2 bis 1.3.1 -> Fix erfordert FastAPI-Bump.
Loesung: fastapi 0.141.1 (verlangt nur starlette>=0.46.0 ohne Obergrenze) + starlette direkt auf 1.3.1 gepinnt in requirements.txt (>=1.3.1,<1.4), damit der Resolver nicht auf vulnerable Versionen fallen kann.
Beweise: pip-audit --no-deps = No known vulnerabilities found; Regressionssmoke auth+api_audit 19/19 + mail+permissions+outbox+audit_middleware+cross_tenant_v2 84/85 (die 1 Failure ist der bekannte Reihenfolge-Vorbestand test_list_permissions_empty, isolat gruen — identisch zum Pre-Bump-Stand).
I-E-4 bis I-E-Triage dokumentiert: BUG-097 Rate-Limiter-Cleanup (10/10), BUG-094 api-audit.md erstellt (9/9), BUG-098 RLS-Haertung FORCE+Rollen-Scoped-Policies (31/31 ueber 3 Suiten), BUG-093/095/096 als durch fruehere Fixes bereits erledigt nachgewiesen.
Regressionssmoke: 84/85 passed; die 1 Failure (test_list_permissions_empty) ist Reihenfolge-Abhaengigkeit — isoliert gruen wie die komplette permissions-Suite 22/22. Keine RLS-Haertungs-Regression.
rls_coverage deckte echte Schema-Luecken auf: kein FORCE ROW LEVEL SECURITY auf 122 Tenant-Tabellen, Policies an PUBLIC statt Runtime-Rollen gescoped, crm_migration BYPASSRLS, Legacy crm_runtime vorhanden.
conftest-Setup gehaertet: (1) FORCE RLS auf allen Tenant-Tabellen, (2) Policies TO crm_api+crm_worker (DROP+RECREATE), (3) Rollen-Haertung crm_api/crm_worker/crm_migration NOSUPERUSER NOBYPASSRLS, (4) Legacy-Drop exception-sicher mit REASSIGN/DROP OWNED.
Zwei Contracts ausbalanciert: cross_tenant v1 verlangt RLS-FREI auf Identity-Tabellen (users/user_tenants/groups/user_groups — Login-Bootstrap ohne Tenant-Context), rls_coverage will alle anderen haerten. Beide erfuellt: conftest nimmt die 4 Tabellen aus, rls_coverage dokumentiert die Bootstrap-Ausnahme. crm_runtime-Test akzeptiert Neutralisierung (NOLOGIN/NOSUPERUSER/NOBYPASSRLS) statt Drop wegen Cross-DB-Grants aus restore_drill.
Beweis: rls_coverage + cross_tenant v1+v2 31/31 passed in 19.33s (vorher 12 failed).
Root-Cause: Rate-Limiter-Zustand akkumulierte ueber Tests hinweg (alle Tests teilen dieselbe Client-IP): InMemoryRateLimiter (process-local) UND Redis rate:* Keys auf der App-DB (REDIS_URL=...db1). Das session-scoped redis_client-Fixture zeigt auf DB0 und cleanupte ins Leere. Fix: autouse _reset_inmemory_rate_limiter + _clear_rate_limit_keys auf get_settings().redis_url.
Beweis: test_auth 10/10 in Kette (vorher 3 PasswordReset-Failures mit 429).
Die PluginLoader-Tests definieren den Contract des Error-Fallbacks (liefen nie gegen sie): Text Failed to load plugin: {name} als zusammenhaengender Knoten + text-red-600 am alert-Container + role=alert. Umgesetzt statt Tests zu biegen — der Fallback ist jetzt konsistent mit dem getesteten Contract.
Beweis: vitest PluginLoader.test.tsx 6/6; tsc exit=0.
Root-Cause der 35 Suite-Timeouts: test_delete_folder trigger imap_delete_folder -> echter aioimaplib.IMAP4_SSL-Connect zu imap.example.com blockiert bis Netzwerk-Timeout; der blockierte Call vergiftet Event-Loop fuer alle nachfolgenden Tests (Kaskade ab 12. Test).
Fixes: (1) tests/conftest.py: autouse mock_imap_connections-Fixture mit deterministischem Fake-IMAP-Client (_FakeIMAPResponse, alle Client-Methoden) via monkeypatch auf services.aioimaplib.IMAP4_SSL. (2) create_mail_account setzt owner_id=user_id gemaess OwnedMixin-Contract — vorher NULL -> get_effective_access read statt admin -> 403 bei assign_shared_users (echter Production-Bug). (3) test_download_attachment: storage_path relativ zum Storage-Root — Path-Traversal-Guard hat korrekt gearbeitet. (4) GET /mail/threads gibt Plain Array zurueck — konsistent mit Geschwister-Routen und fetchThreads(): Promise<ThreadResult[]>.
Beweise: 46/46 passed in 94.41s (vorher 1 failed, 10 passed, 35 errors in 1109.94s); conftest-ruff-Findings auto-gefixt (8), Rest = Vorbestand E402 dynamische Plugin-Imports; Test nach Fix verifiziert.
Verifiziert: useDeleteNotification und useAgentSkills haben NULL Komponenten-Importeure (nur Definitionsdateien). Die echten Komponenten nutzen andere Hooks (useNotifications, useMarkNotificationRead, useUnreadNotificationCount; useAgentTools/useAgentToolsFull). Nach AGENTS.md 0.2 keine Backend-Shims fuer tote Calls: beide Hooks entfernt, ungenutztes apiDelete-Import in notifications.ts bereinigt.
Damit sind alle 12 API-Braeche aus dem D5-Triage abgeschlossen: ai/sessions x5 (3e5f13f), policies x4 (86c96f0), mail x4 (86c96f0), notifications DELETE (hier), agents/skills (hier). tsc exit=0.
Root-Cause: Frontend-Komponenten (SignatureManager, LabelManager) rufen Endpunkte auf die das Backend nie hatte (404/405 in Production). Anders als ai/sessions sind diese Funktionen ECHT in Komponenten eingebunden -> Backend-Routen nachbestellt statt Frontend-Calls zu loeschen:
(1) PATCH+DELETE /mail/signatures/{id}: MailSignatureUpdate-Schema neu, Tenant-Scoped + Owner-Check (403 bei fremder Signatur), is_default-Exklusivitaet beim Setzen. (2) DELETE /mail/labels/{id}: gleicher Stil. (3) updateDraft Frontend: apiPatch -> apiPut (Backend hat PUT /drafts/{id} bereits). Beweistest tests/test_mail_sig_label_routes.py 5/5 gruen (PATCH-Werte, DELETE+Liste-leer, 404-Faelle).
Verifikation: create_app registriert beide neuen Routen (563 total); ruff clean; tsc exit=0.
Root-Cause: Backend hat KEIN /ai/sessions-CRUD (nur Conversations-Routen im kommunikation/ai_assistant). Frontend-Nutzer war NUR AISidebar — dessen Chat-Tab renderte nie einen echten Chat sondern nur Platzhalter gesteuert von Session-Calls auf 404. Nach AGENTS.md 0.2/0.3 keine Backend-Shims gebaut: (1) Geister-Tests ChatWindow.test.tsx + SessionList.test.tsx geloescht — importierten nicht existierende Komponenten @/components/ai/ChatWindow + SessionList (BUG-099-Muster, Plan sanktioniert Loeschung). (2) AISidebar: tote fetchSessions/createSession-Calls + sessionId/loading-State entfernt; Chat-Tab zeigt jetzt Verweis-Link auf existierende /ai-assistant-Seite (962e0ee). (3) api/ai.ts 253→170 Zeilen: tote Interfaces ChatFolder/ChatSession/ChatMessage/ChatAttachment + Folders/Sessions/Attachments-Sektionen entfernt; fetchMessages/streamChat bleiben (genutzt von AiChatPanel/Communication).
Beweise: tsc --noEmit exit=0; vitest src/__tests__/ai/ 26/26 gruen (vorher 2 Geister-Suites mit Import-Error); ruff unberuehrt.
Der integrierte Overhaul-Plan hatte eine eigene Zusammenfassungs-Sektion direkt vor der echten Roadmap-Zusammenfassung — fuer zukuenftige KIs eindeutig benannt.
Gemaeß AGENTS.md-Regel "PLATFORM_ROADMAP.md ist EINZIGE Planungs-Datei": Der 348-Zeilen UI-Overhaul-Plan (7 Phasen: Bugfixes, AI-in-Kommunikation, Wiki/Tasks/Kalender/Tags-UI) ist jetzt als Phase L in der Roadmap integriert (Ueberschriftenebenen angepasst, ASCII-Mockups erhalten). Vollstaendiges Original abrufbar via git show c807aac:UI_OVERHAUL_PLAN.md.
Konflikt-Notiz ergaenzt: Phase 2 plant "AI Assistant Page entfernen", aber 962e0ee hat die Seite bewusst gebaut um die Geister-Route zu fixen — VOR Phase-2-Umsetzung neu entscheiden. AGENTS.md benoetigt keine Aenderung (Datei wurde dort nie referenziert); repo-weit existierten 0 Referenzen.
Root-Causes und Fixes: (1) conftest.py: crm_api-Rolle (NOSUPERUSER NOBYPASSRLS) mit Grants, RLS auf 117 Tenant-Tabellen aktiviert, tenant_isolation-Policies erstellt — vorher liefen Tests als Superuser (RLS bypassed). (2) test_rls_blocks_cross_tenant_insert: asyncpg fuehrt eagerly aus, RLS-Violation kommt direkt bei execute() nicht erst bei flush() — Doppel-Exception-Erwartung durch Message-Assertion ersetzt. (3) test_rls_tenant_a_insert_own_succeeds: 6 NOT NULL numeric Spalten (discount_*) im Raw-INSERT ergaenzt (Model hat Python-Defaults, DB keine server_defaults). (4) seed_data: commit() fuer Cross-Connection-Sichtbarkeit (crm_api verbindet separat) + Teardown-Cleanup gegen Datenlecks. (5) admin_session: ohne conn.begin() — sonst conditional_savepoint und commit() wirkungslos. (6) sees_only_rows x2: UUID/String-Vergleich normalisiert (asyncpg liefert UUID-Objekte).
Vorher: 9 von 10 Tests vakuum-trivial gruen (leere DB, Superuser). Nachher: echte RLS-Assertions mit Seed-Daten als unprivilegierte Rolle.
- /sw.js und /service-worker.js liefern jetzt einen Self-Unregister Service Worker
- index.html bekommt Cache-Control: no-cache, no-store, must-revalidate
- Fixt das wiederkehrende weiße-Seite-Problem nach Deploys
- Complete rebuild of Tasks.tsx with 3-column explorer layout
- Left: TaskTree with status/priority grouping, expandable sections
- Middle: List view or Kanban board (switchable via toolbar)
- Right: TaskDetail panel with status selector, edit/delete actions
- PluginToolbar registration with new-task button and view-mode selector
- ResizablePanel for tree and detail columns
- Mobile responsive with single-pane view switching
- Search bar in list view
- Kanban board with 4 status columns (Offen, In Bearbeitung, Blockiert, Erledigt)
tsc clean
5.1: Toolbar already has PluginToolbar with navigation, view mode, actions — no changes needed
5.2: Calendar visibility toggle fix:
- calendarStore.setCalendars now initializes visibleCalendarIds with all calendar IDs
- CalendarTree.tsx visibility check simplified to visibleCalendarIds.has(cal.id)
- No more empty-set-means-all-visible confusion
tsc clean
Migration 0137: Drop AI chat tables (ai_chat_sessions, ai_chat_messages, ai_chat_attachments, ai_conversations, ai_messages)
Backend:
- Remove AIChatSession, AIChatMessage, AIChatAttachment models from ai_assistant/models.py
- Remove AIConversation, AIMessage from app/models/__init__.py
- Remove session/message/stream/attachment routes from ai_assistant/routes.py
- Add new streaming route POST /ai/conversations/{conversation_id}/stream using comm tables
- Add new messages route GET /ai/conversations/{conversation_id}/messages using comm tables
- Add stream_chat_comm, get_comm_messages, save_comm_message to services.py
- Update external_api.py to use CommConversation/CommMessage instead of AIChatSession/AIChatMessage
- Update unified_search ai_chat_provider to search comm_messages with conversation_type=ai
- Remove ai_copilot router from main.py and routes/__init__.py
- Remove ai_conversation from entity_permissions.py and owner_transfer_service.py
- Update ai_assistant/plugin.py get_entity_models to remove AIChatSession
- Guard ai_copilot_service.py imports with try/except
Frontend:
- Remove AIAssistant.tsx, AIAssistantStandalone.tsx, SessionList.tsx, ChatWindow.tsx
- Remove AI Assistant routes from routes/index.tsx
- Update api/ai.ts: streamChat uses /ai/conversations/{id}/stream, fetchMessages uses /ai/conversations/{id}/messages
- Update Communication.tsx: use convId for AI streaming, remove aiSessionId, use fetchAiMessages for AI conversations
- Update AiChatPanel.tsx: create comm conversation instead of AI session, use new fetchMessages
- Update AISidebar.tsx: remove ChatWindow import, show placeholder
tsc clean, build successful, backend import OK
Replace direct {error} JSX rendering with typeof check + .message fallback.
When error is an object (not a string), React showed [object Object].
Now renders error.message or fallback string.
- Add OwnedMixin to 29 model files (78 tables that had owner_id in DB but not in model)
- Add search/embedding columns to 9 model files (18 columns: search_tsv, embedding, indexed_at, content_text, content_tsv, body_tsv, company_id, deleted_at)
- Fix import syntax errors in calendar/models.py, mail/models.py, notification.py, contact.py
- Fix nullable constraints on search_tsv columns
- Remove ForeignKey from mails.company_id (companies table not always loaded in test context)
- All 36 tests pass (24 Phase J + 12 Phase K)
- Models now match production DB schema
All wiki service functions expect db as first argument but routes
were passing tenant_id as first argument. This caused 500 on
/wiki/articles, /wiki/categories, and all wiki endpoints.
New plugins (self_improvement, knowledge) were discovered but never activated
in the production DB. prestart.sh now auto-installs and activates all
discovered builtin plugins on every container start.
- get_cached_permissions() returned None when _get_current_permission_version failed
- deps.py get_current_user() crashed with AttributeError: NoneType.get()
- Fix: fall through to DB resolution instead of returning None
- Fix: add None guard in deps.py as safety net
- Admin user was created without is_system_admin=True, causing sidebar
to be empty (all permission checks failed)
- seed_default_workspace() was never called, so no workspaces existed
- Now seed_admin.py ensures is_system_admin=True for existing admins
and creates a default workspace if none exists
Fixes: sidebar empty, settings inaccessible
Phase B Security-Fix (5d1b239) entfernte den Default-Wert für MAIL_ENCRYPTION_KEY,
aber der Key wurde nie in docker-compose.yaml/.env.docker.example/deploy-guide.md
aufgenommen. Das führte zu Container-Crashs beim Deploy da der Key zwingend
erforderlich ist (mail/services.py RuntimeError).
- docker-compose.yaml: MAIL_ENCRYPTION_KEY in crm_app und crm_worker environment
- .env.docker.example: MAIL_ENCRYPTION_KEY mit Generierungs-Anleitung
- docs/deploy-guide.md: MAIL_ENCRYPTION_KEY in Zugänge-Liste aufgenommen
Commit 48e6b15 renamed services to crm_app/crm_worker, but fast-deploy.sh
still searched for containers with UUID prefix only. Now falls back to
matching UUID + 'app' in container name.
- Robuster gegen undefined API-Daten in Mail, ContactDetail, ContactsList, Settings, Sidebar
- E2E-Mocks korrigiert für Kontakt-Detail, Mail-Liste/Folders und Plugin-Toggle
- Auth-Store mit persist-Middleware für E2E-Login
- test-results/ in .gitignore aufgenommen
Playwright E2E: 34/34 passed
Core migration 0026/0110 tried to add password_salt to mail_accounts
but the table did not exist during core migration run. Added
password_salt directly to the mail plugin initial migration.
All built-in plugins should be auto-activated. The is_core flag was
only set on some plugins, leaving Mail, DMS, Calendar, Automation,
Unified Search etc. inactive by default.
Instead of a static list, find ALL tables with tenant_id but without
owner_id and add the column. This catches all plugin tables that were
created after core migration 0054 ran.
crm_migration role is created by migration 0085, so the first
alembic run on a fresh DB must use crm_user (POSTGRES_USER).
Fixes initial deployment failure on fresh databases.
- Integrate ABAC policies into apply_visibility_filter() (allow/deny with priority)
- Add field whitelist (ABAC_ALLOWED_FIELDS) for build_sql_condition() security
- Add request-level ContextVar for user principals (group_ids, role_id)
- Set principals in deps.py (session + bearer auth)
- Use ContextVar in visibility.py and permission_resolver.py (N+1 fix)
- Add version validation to get_cached_visible_ids() (cache strategy unification)
- Deactivate delegation route (parked — not integrated into resolve_permissions)
- Add 7 ABAC integration tests
All 70 tests pass (7 ABAC + 63 existing). No regressions.
- Add Role records for editor, viewer, guest in seed_full_data
- Link UserTenant.role_id to Role records (legacy string roles no longer grant permissions)
- Fixes 10 test failures caused by removal of Legacy Role Bypass
- All 33 tests now pass
- Remove import from app.plugins.builtins.test_sample (deleted in Phase 4)
- Define SamplePlugin inline in test_plugins.py with same lifecycle behavior
- Replace all test_sample/TestSamplePlugin references with sample_plugin/SamplePlugin
- Create migration SQL files: 0001_sample_plugin.sql, 0001_bad_migration.sql
- Update discover_builtins test to check for tags plugin instead
P34: Remove test_sample plugin from production code
P35: Remove CompanyContact=None dead code from contact.py
P36: Change Plugin.config from Text to JSONB (model + migration 0117 + service)
P37: Add AI comment about workspace overengineering in workspace.py
P38: Add container resource limits to docker-compose.yaml
P39: Guest TTL 1800 not found — already migrated to regular users
P40: Add AI comment about missing IP/Device binding in session.py
P41: Fix Redis healthcheck to use auth password
P42: RLS migration history comment already present in alembic/env.py
Phase 2 cleanup: contact_folder_permission.py model was removed from
models/__init__.py and is no longer imported anywhere. The service,
schema, and routes remain as they delegate to EntityPermission.
Deleted:
- app/models/contact_folder_permission.py (dead model class)
Kept (still actively used):
- app/services/contact_folder_permission_service.py (delegates to EntityPermission)
- app/schemas/contact_folder_permission.py (pure Pydantic schemas)
- app/routes/contact_folder_permissions.py (registered in main.py)
Problem 1: Remove legacy role bypass
- Remove role="admin" string bypass in permissions.py resolve_permissions()
- Remove role="admin"/"editor" bypass in auth.py check_permission()
- Remove legacy role string fallback in deps.py require_admin/require_write
- Add migration 0112: Create Role records for built-in roles and link role_id
- KI-Kommentar: Legacy Role Bypass entfernt — alle Admins müssen echte role_id haben
Problem 2: Enforce API token scopes
- Add _token_scopes check in require_permission() in deps.py
- When _token_scopes is set (API token auth), required permission must be in scopes
- When _token_scopes not set (session auth), normal permission check applies
Problem 3: Migration chain verification
- Chain is already linear: 0027→0028_rls_force→0028_user_preferences→0029
- user_preferences table confirmed exists in DB
- No duplicate revision IDs found
Problem 4: RLS for remaining tenant tables
- Add migration 0111: Dynamic RLS activation for any remaining tables with tenant_id
- Login tables and global tables explicitly excluded
- DB check shows 0 tables currently missing RLS (safety net migration)
Problem 5: Permission cache invalidation on tenant switch
- Add invalidate_permission_cache() call in switch_tenant() for old tenant
- Stale cached permissions from old tenant no longer leak
Problem 6+7: Guest system removal
- Remove get_current_guest() from deps.py
- Remove guest_auth.py router from main.py and routes/__init__.py
- Rewrite guests.py to use regular User/UserTenant with role=guest
- Remove GuestUser/GuestInvitation from models/__init__.py
- Add migration 0113: Migrate guest_users to regular users, drop guest tables
- Update frontend GuestLogin/GuestContacts to redirect to normal pages
- KI-Kommentar: Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
- Add tests/test_permission_system_live.py: 33 live tests against real PostgreSQL
testing RBAC, ABAC, RLS, cross-tenant isolation, guest access, entity sharing,
field-level permissions, role invalidation, group permissions, membership suspension
- fix(contacts): delete route uses contacts:delete instead of contacts:write
The delete_contact and delete_contact_person routes were checking contacts:write
permission instead of contacts:delete, allowing users without delete permission
to delete contacts.
- fix(contacts): DeleteContactCommand passes is_system_admin to service
DeleteContactCommand.run() was not passing is_system_admin from the session
to contact_service.delete_contact(), causing system admins to be blocked
by the row-level admin access check.
- fix(contacts): allow deletion of tenant-owned contacts
contact_service.delete_contact() required admin-level entity access for ALL
contacts, including tenant-owned ones (owner_id=None). Tenant-owned contacts
can now be deleted by any user with contacts:delete permission (already
verified by the route via require_permission).
Plugin route registration in main.py was mutating module-level router
singletons by appending require_active_plugin dependencies directly to
router.routes. This persisted across app instances, causing test routes
to inherit require_active_plugin checks and return 403 "plugin inactive"
when tests created their own FastAPI apps with those routers.
Fix: use app.include_router(router, dependencies=[plugin_dep]) which
adds dependencies at the app level without modifying the shared router.
Fixes 35 test failures across 4 test files:
- test_agent_memory.py (6 failures)
- test_external_agent_api.py (15 failures)
- test_graph_rag.py (7 failures)
- test_marketplace.py (7 failures)
- Use migration engine (crm_migration, BYPASSRLS) for default data seeding
in app/main.py instead of crm_api role which is RLS-enforced
- Skip domains PATCH for dockercompose apps in deploy_api() to avoid 422
- Regenerate migration_hashes.txt for 0085_restore_tenant_rls.py
- Add migration 0110: password_salt column to mail_accounts
LOGIN_TABLES (users, user_tenants, tenants, sessions, password_reset_tokens)
added to skip list. RLS on these tables blocked crm_auth from reading users
during login → 401 Invalid email or password.
crm_auth grants applied directly (no RLS) matching 0085 AUTH_TABLES.
⚠️ LOGIN-TABELLEN DÜRFEN KEIN RLS BEKOMMEN — RLS blockiert crm_auth beim Login.
Siehe 0085 AUTH_TABLES für die korrekten Grants.
Plugin migrations run after core migration 0085 which sets up RLS for
all known core tables. Plugin-created tables were left without RLS,
creating a critical multi-tenant isolation gap (84 tables affected).
The migration runner now automatically enables RLS on all newly created
tenant tables after validation:
- ENABLE + FORCE ROW LEVEL SECURITY
- Idempotent DROP IF EXISTS + CREATE fail-closed tenant isolation policy
- GRANT CRUD to crm_api and crm_worker
- ALTER TABLE OWNER TO crm_migration
Global tables (-- GLOBAL TABLE comment) are skipped.
Plugin activation was broken for ALL inactive plugins because
sync_notification_types() tried to INSERT search_reindex_complete (22 chars)
into type_key VARCHAR(20), causing StringDataRightTruncationError.
Alembic head: 0106 → 0107
unified_search plugin activation calls sync_notification_types() which
DELETEs stale rows from notification_types. App DB user (crm_api) lacked
DELETE permission, causing plugin activation to fail with
InsufficientPrivilegeError.
Alembic head: 0105 → 0106
Fixes 3 issues found by API integration tests:
1. migration_runner.py: Add GLOBAL TABLE exemption for tables without tenant_id
- New _extract_global_table_names() method parses -- GLOBAL TABLE: comments
- marketplace_listings is intentionally global (no tenant_id)
2. unified_search/migrations/0002_embeddings.sql: Remove companies table (does not exist),
add DO $$ BEGIN END $$ blocks to check table existence before ALTER
3. marketplace/migrations/0001_initial.sql: Add -- GLOBAL TABLE: marketplace_listings comment
Two critical bugs found by API integration tests:
1. contacts.embedding (vector(768)) — ORM model updated in Phase 5.3 but
plugin migration 0002_embeddings.sql was never run as Alembic migration.
Also adds embedding columns to mails, companies, files, calendar_entries, tags.
2. audit_log.created_at, updated_at, deleted_at — AuditLog inherits TenantMixin
which expects these columns, but they were never added to the DB table.
Also adds to deletion_log.
Migration uses IF NOT EXISTS checks for all columns/indexes.
Alembic head: 0103 → 0104
The CircuitBreakerMiddleware was blocking all requests (503 circuit_open) because
every exception in get_db() — including 401 Unauthorized, 403 Forbidden, 404 Not Found —
was calling record_failure() on the DB circuit breaker. This caused the circuit to
trip after 5 non-DB errors (e.g. failed login attempts during security testing).
Fix: Only call record_failure() when _is_transient_db_error(exc) returns True,
filtering out HTTP exceptions that are not DB-related.
- app/core/resilience.py: CircuitBreaker (CLOSED/OPEN/HALF_OPEN), retry_db,
redis_call_with_fallback, InMemoryRateLimiter, CircuitBreakerMiddleware
- app/core/auth.py: get_session_data now falls back to PostgreSQL sessions
table when Redis is unavailable
- app/core/permissions.py: get_cached_permissions falls back to direct DB
resolution when Redis circuit is open
- app/core/rate_limit.py: check_rate_limit falls back to in-memory limiter
when Redis is down; reset_rate_limit clears both Redis and in-memory
- app/core/middleware.py: CSRF validation uses get_session_data (Redis+DB
fallback); sliding session TTL is best-effort during outage
- app/core/db/__init__.py: get_db() wraps session creation with retry_db
for transient connection errors; records circuit breaker success/failure
- app/deps.py: refresh_session_ttl wrapped in try/except for Redis outage
- app/main.py: CircuitBreakerMiddleware registered (returns 503 when DB
circuit is OPEN, skips health/metrics endpoints)
- app/config.py: Added resilience settings (thresholds, cooldown, retries)
- tests/test_resilience.py: 30 tests covering all patterns
30/30 resilience tests pass. No regressions in plugin lifecycle tests.
- .env.example: Add ADMIN_EMAIL/ADMIN_PASSWORD
- SANIERUNGS_FORTSCHRITT.md: Update UUIDs
- IMPLEMENTATION_PLAN.md: Update UUID
- promptinclude: Worker/DB/Redis are now part of Docker-Compose-App
- Replace old UUID stvabl4vaqru7jclx4ittzr3 with dx4pqdziu4uj6x9fxs1u5z0x
- Remove old worker UUID asxqaq3566to108xordck0ff
- Update INSTALL.md: Stand 2026-08-04, Commit 0ebc411, Alembic-Head 0103
- Update DEPLOY.md: New UUID and auto-resolve via APP_DOMAIN
- prestart.sh: runs seed_admin.py after migrations
- seed_admin.py: reads ADMIN_EMAIL and ADMIN_PASSWORD from env vars
- Creates default tenant + admin role + admin user if not exists
- No custom crm-net network — Coolify manages networking
- No custom Traefik labels — Coolify generates them
- No hardcoded domains — all from environment variables
- Simplified volumes — no custom names
- deploy.py: UUIDs from env vars or Coolify API lookup by name
- fast-deploy.sh: No hardcoded UUIDs, APP_DOMAIN from env
- docker-compose.yml: All secrets from env vars, no hardcoded values
- .env.example: All required vars documented
- Deleted obsolete fast-frontend-deploy.sh with hardcoded container name
- Beide WebSocket Endpoints pruefen jetzt Plugin-Aktivierung (global + tenant)
- Fail-closed bei Fehlern
- Pruefung direkt im WebSocket-Endpunkt (Router-Dependency greift bei WS nicht)
ApiToken Modell erbt von TenantMixin (TimestampMixin) das updated_at erwartet.
Migration 0001 hat api_tokens ohne updated_at erstellt.
Migration 0083 hat deleted_at hinzugefuegt aber updated_at verpasst.
4.5 Modul-Konfiguration pro Workspace:
- WorkspaceManager: Config-Editor pro Modul (JSON textarea)
- Pro Modul kann JSON config bearbeitet werden (z.B. sichtbare Ordner-IDs)
- Generisch: jedes Modul definiert selbst was in seiner config steht
4.6 Bugfixes:
- Sidebar useMemo: isModuleVisible zu Abhaengigkeiten hinzugefuegt
- Bei Workspacewechsel wird Sidebar jetzt sofort neu berechnet
Tests: 17 Backend + 13 Frontend = 30/30 bestanden
- Tag :latest auf neuestes Commit-Image (Coolify taggt mit Hash, nicht latest)
- Verbinde Worker mit coolify Netzwerk nach Restart (für Redis/Postgres DNS)
- Kein update_service mehr (überschreibt Coolify-Konfiguration)
- Worker-Compose auf Server korrigiert (coolify Netzwerk in Service-Definition)
- recover_stuck_events: Reset processing events stuck >120s back to pending
- cleanup_published_events: Delete published events older than 30 days
- Replay now resets outbox_deliveries for clean retry
- Worker: hourly retention cleanup cron job
- API: /recover-stuck and /cleanup-published endpoints
- process_outbox_batch: auto-recovery at start of each tenant iteration
- 23/23 tests passing (5 new tests)
- Phasen-Status: Phase 0-3 abgeschlossen, 4-10 offen
- Gates: Alle 5 bestanden
- Produktions-Setup: Coolify Ressourcen, DB-Rollen, Volumes
- Deployment: deploy.py Befehle dokumentiert
- Wichtige Dateien und Regeln für nächsten Agenten
- Was erledigt ist und was als nächstes zu tun ist
Tests:
- Registry initialization and engine requirement
- Plugin registration and discovery
- Load order with and without dependencies
- Core plugin deactivation blocked
- Deactivation blocked by active dependents
- Event handler registration on activate
- Event handler unregistration on deactivate
- Activate → deactivate → reactivate cycle
- Idempotent activate when already active
- Idempotent deactivate when already inactive
Phase 3 (Plugin-Lifecycle) verified:
- install: idempotent, dependency checks, migrations via crm_migration
- activate: idempotent, per-tenant with RLS context, event handlers
- deactivate: idempotent, core protection, dependency check, handler cleanup
- uninstall: deactivate first, then optional drop tables
- main.py: per-tenant activation with set_tenant_context
- Worker: event handlers only for active plugins (Gate 5)
- Router: only in API, not in worker
- 74 Tabellen erhalten FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
- 10 globale Tabellen ausgeschlossen (sequences, system_settings, currencies, etc.)
- Orphan-Cleanup: SET tenant_id = NULL für verwaiste Einträge
- Idempotent: IF NOT EXISTS für alle Constraints
- Downgrade: Drop aller FK-Constraints
- Komplette Schritt-für-Schritt-Installation von Grund auf
- Alle DB-Rollen und Bootstrap-Reihenfolge dokumentiert
- Alle ENV-Variablen für API, Worker, DB dokumentiert
- Vollständige docker-compose.yml Referenz
- Coolify-Setup-Anleitung
- prestart.sh Startup-Ablauf
- seed_admin.py Admin-Erstellung
- Verifikationsschritte (Health, Login, Alembic, RLS, DDL)
- SMTP-Konfiguration
- Backup und Restore mit Grant-Hinweis
- Häufige Probleme und Lösungen
- Architektur-Übersicht und Datei-Struktur
Gate 5 (Worker und Eventhandler) bestanden:
- Worker healthy, verarbeitet Outbox-Jobs und enqueued Jobs
- 18 Worker-Funktionen registriert
- Plugin-Eventhandler nur für aktive Plugins
- Per-Tenant Outbox-Processing mit RLS-Kontext
- Worker verwendet crm_worker (get_worker_session_factory)
- Keine Plugin-Router im Worker
- Migration 0090: Drop legacy tenant_isolation policies on companies_old,
company_contacts_old, contacts_old that used app.tenant_id variable.
Create new policies using app.current_tenant_id for crm_api/crm_worker.
- seed_admin.py: Rewrite to use migration engine (crm_migration) for
bootstrap, set tenant context, create Tenant + Role + User + UserTenant.
No longer passes tenant_id as User parameter.
Fixes: 3 legacy app.tenant_id policies found in Gate 2 verification.
Fixes: seed_admin.py incompatible with current User model.
Migration 0070 creates roles without passwords. On fresh DB, API cannot authenticate.
prestart.sh now extracts password from MIGRATION_DATABASE_URL and sets it for all roles.
- main.py: registry.initialize(get_migration_engine()) instead of get_engine()
- main.py: plugin migrations run via get_migration_session_factory() not async_session()
- registry.py: upgrade_plugin, install_plugin, uninstall_plugin all use migration session for DDL
- db/__init__.py: get_migration_engine() raises RuntimeError if MIGRATION_DATABASE_URL missing (no fallback)
- Fixes fresh-install failure: crm_api has no DDL rights, plugin migrations need crm_migration
- Migration 0086: Remove FORCE RLS from 5 global tables
- Migration 0085: crm_migration keeps BYPASSRLS for data migrations
- Migration 0085: Remove audit_log from crm_auth grants
- auth_service.py: Audit log via separate API session (crm_api with tenant context)
- tests/test_no_legacy_tenant_var.py: CI test for app.tenant_id in policies
- auth_service.py: set tenant context before audit log write in login
- migration 0085: add sessions and audit_log to AUTH_TABLES for crm_auth
- Login now works on production with RLS enabled
- config.py: add auth_database_url, worker_database_url, migration_database_url
- db/__init__.py: separate engines for auth/worker/migration + get_auth_db/get_worker_db
- auth.py: all auth endpoints use get_auth_db (crm_auth role)
- auth_service.py: remove login fallback, require active membership, check status
- auth_service.py: switch_tenant checks active membership status
- alembic/env.py: use migration_database_url for Alembic
- docker-compose.yml: add AUTH_DATABASE_URL, WORKER_DATABASE_URL
- .env.example: add all 4 DB URLs with separate roles
- migration 0085: transfer ownership to crm_migration, fix BYPASSRLS,
enable RLS+FORCE on all tenant tables, drop old policies, create new
fail-closed policies scoped to crm_api+crm_worker, revoke excessive grants,
grant minimal crm_auth access, drop crm_runtime, set default privileges
- tests/test_rls_coverage.py: automated RLS coverage check (13 tests)
- tests/test_cross_tenant_security_v2.py: RLS tests with unprivileged role
- Fix report_generator/jobs.py: use DmsContract instead of direct DMS import
- Remove app.tenant_id from set_tenant_context (only app.current_tenant_id)
- Create tests/test_cross_tenant_security_v2.py with real RLS tests using
unprivileged crm_api role (NOSUPERUSER, NOBYPASSRLS)
- Fix existing tests referencing app.tenant_id
- Git baseline tag v-phase0-baseline at 11d6faa
- Production DB backup at /tmp/crm_backup_20260731_015514.dump
2026-07-31 01:57:51 +02:00
1145 changed files with 137534 additions and 49267 deletions
**IMPORTANT**: For `GET /api/public/share/{token}` (AC14, AC15) — T11 permissions plugin ALREADY implements this endpoint. Do NOT create a duplicate. If T11's endpoint already handles password-protected links (401 without password), then AC14 and AC15 are already satisfied. Verify by reading `app/plugins/builtins/permissions/routes.py`.
- OnlyOffice: generate config only, don't run OnlyOffice server
- For AC14/AC15: check if T11 permissions plugin already satisfies these. If yes, write tests that verify existing endpoint. If no, implement in DMS plugin.
## Deliverables
- All plugin files created
- Plugin registered in builtins __init__.py
- tests/test_dms.py with all 19 ACs tested
- All tests passing
- Coverage ≥80%
- Report: files created, test count + pass/fail, coverage %
- API client: src/api/client.ts (axios, withCredentials, 401→login, 422→validation)
## Backend API Endpoints
- GET /api/v1/companies?page=1&page_size=25&search=...&industry=...&sort_by=...&sort_order=...
- GET /api/v1/companies/{id} → CompanyDetailResponse (includes contacts[])
- POST /api/v1/companies
- PATCH /api/v1/companies/{id}
- DELETE /api/v1/companies/{id}
- GET /api/v1/companies/export?format=csv&search=...&industry=...
- POST /api/v1/companies/import (CSV upload)
- GET /api/v1/contacts?page=1&page_size=25&search=...
- GET /api/v1/contacts/{id} → ContactDetailResponse (includes companies[])
- POST /api/v1/contacts
- PATCH /api/v1/contacts/{id}
- DELETE /api/v1/contacts/{id}
- GET /api/v1/users?page=1&page_size=25
- GET /api/v1/users/{id}
- POST /api/v1/users
- PATCH /api/v1/users/{id}
- DELETE /api/v1/users/{id}
- GET /api/v1/notifications
- GET /api/v1/plugins
- GET /health
Note: No dedicated audit log or global search API endpoint exists yet. For audit log, create a frontend page that calls GET /api/v1/audit (may 404 — handle gracefully with empty state). For global search, call useCompanies and useContacts with search param in parallel.
# T08c: Frontend Mail UI + Global Search UI — Implementation Briefing
## Task
Implement frontend UI for Mail plugin (folder tree, mail list, reading pane, compose, templates, signatures, rules, labels, PGP, vacation, shared mailbox, delegates) and enhance Global Search UI with tabs.
**Assessor:** Security Data Engineer (A0 Orchestrator)
**Project:** LeoCRM at `/a0/usr/workdir/leocrm-fix`
---
## Summary
| Severity | Count |
|----------|-------|
| CRITICAL | 5 |
| HIGH | 8 |
| MEDIUM | 8 |
| LOW | 5 |
| **Total**| **26**|
---
## CRITICAL Issues
### C-1: Redis Default Password `changeme` in docker-compose.yml
**File:** `docker-compose.yml:53`
**Risk:** Redis stores session data, CSRF tokens, and rate-limit counters. The default password `changeme` is trivially guessable. If Redis port 6379 is exposed, an attacker can read/modify all sessions, steal CSRF tokens, and bypass rate limits.
**Remediation:** Remove the default fallback. Require `REDIS_PASSWORD` as a mandatory variable (`${REDIS_PASSWORD:?REDIS_PASSWORD is required}`). Use a strong randomly generated password in production.
### C-2: No SECRET_KEY in `.env` — Insecure Default Active in Development
**Risk:**`.env` has no `SECRET_KEY`. The config defaults to `"change-me-in-production-use-a-secure-random-string"`. While `get_settings()` raises in production mode, `.env` sets `ENVIRONMENT=development`, so the default key is silently used. Any signing/token operation using `secret_key` is compromised.
**Remediation:** Add a strong random `SECRET_KEY` (min 32 chars) to `.env`. Fail-fast in all environments if the default key is detected, not just production.
### C-3: PostgreSQL and Redis Ports Exposed to Host
**File:** `docker-compose.yml:37-38, 56-57`
**Risk:**`ports: "5432:5432"` and `ports: "6379:6379"` expose the database and Redis to the host network. Combined with weak/default credentials, this allows direct external access to all session data and the entire database.
**Remediation:** Remove port mappings for production. Use Docker internal networking only (`crm-net`). If debug access is needed, bind to `127.0.0.1:5432:5432` and document it as dev-only.
### C-4: Unauthenticated Error Endpoint Forwards Data to External Forgejo
**Risk:** The `/api/v1/errors` endpoint requires no authentication. CSRF middleware explicitly bypasses token checks for this path (line 48 of `middleware.py`). Any unauthenticated attacker can POST arbitrary error data (message, stack, URL, userAgent, and **arbitrary context dict**) which gets forwarded to an external Forgejo instance as a public issue. The `context` field accepts `dict[str, Any]` with no size limit on individual keys — an attacker can exfiltrate data or inject malicious content into Forgejo issues.
**Remediation:** Require authentication for error reporting. If unauthenticated errors are needed, strip the `context` field entirely, add strict schema validation with size limits on all fields, and add a CAPTCHA or stricter rate limiting.
### C-5: Plaintext Database Password in `.env`
**File:** `.env:1`
**Risk:**`DATABASE_URL=postgresql+asyncpg://leocrm:leocrm@localhost:5432/leocrm` embeds the DB password `leocrm` in plaintext. While `.gitignore` covers `.env`, the password is weak and identical to the username. If the file is accessed via any path traversal, backup leak, or container escape, the database is fully compromised.
**Remediation:** Use a strong unique password. Separate `DATABASE_URL` construction from credential storage where possible (e.g., use individual `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_HOST`, `POSTGRES_DB` env vars and construct the URL in code).
---
## HIGH Issues
### H-1: Rate Limiter Trusts X-Forwarded-For Without Validation
**File:** `app/core/rate_limit.py:43-45`
**Risk:**`get_client_ip()` blindly trusts the `X-Forwarded-For` header. An attacker can set arbitrary values to bypass rate limits on login, password reset, and other endpoints. Each request with a different spoofed IP creates a new rate-limit counter.
**Remediation:** Only trust `X-Forwarded-For` from known proxy IPs. Configure a trusted proxy list and validate the header chain. Use Starlette's `ProxyHeadersMiddleware` or validate against a `TRUSTED_PROXIES` env var.
**File:** `app/core/auth.py:53-66` and `app/core/auth.py:94-96`
**Risk:** Two `get_redis()` functions exist. The first (line 53) returns a singleton. The second (line 94) creates a **new Redis connection on every call**. Code importing `get_redis` may use either version. The middleware (line 69) creates its own Redis connection per request. This leads to connection pool exhaustion under load.
**Remediation:** Remove the second `get_redis()` (line 94-96). Ensure all code uses the singleton version. The middleware should use `get_redis()` from `app.core.auth` instead of creating its own connection.
### H-3: CSRF Middleware Creates New Redis Connection Per Request
**File:** `app/core/middleware.py:69-90`
**Risk:** For every unsafe HTTP request, the middleware creates a new `aioredis.from_url()` connection, uses it, then closes it. Under load, this creates thousands of connections and can exhaust Redis connection limits.
**Remediation:** Use the global Redis singleton via `from app.core.auth import get_redis`. Remove the per-request connection creation and the `finally: await redis.close()` block.
### H-4: CSRF Token Stored Plaintext in PostgreSQL
**Risk:** The CSRF token is stored as plaintext in the PostgreSQL `sessions` table (audit trail). If the database is compromised, all active CSRF tokens are available for CSRF attacks.
**Remediation:** Store only a hash of the CSRF token in PostgreSQL (like `hash_token()` already exists for session tokens). Compare hashes during validation.
### H-5: No File Upload Validation in Storage Backend
**File:** `app/core/storage.py:69-128`
**Risk:**`LocalStorage` performs no validation on uploaded files:
- No path traversal protection: `os.path.join(self.base_path, path)` with a malicious `path` containing `../../` can write anywhere on the filesystem
- No file type/extension whitelist
- No file size limit
- No content-type validation
-`get_url()` returns the full filesystem path, leaking internal directory structure
**Remediation:** Sanitize `path` with `os.path.realpath()` and verify it's within `base_path`. Enforce file size limits, extension whitelist, and MIME type validation. Return relative paths from `get_url()`, not absolute filesystem paths.
**Risk:** Both WebSocket managers accept connections via `connect(websocket, user_id)` without verifying that `user_id` is authenticated. The security depends entirely on the calling route. If any WebSocket route passes an untrusted `user_id` (e.g., from query params), an attacker can impersonate any user. There is also no origin verification on WebSocket connections.
**Remediation:** Verify session cookie inside `connect()` before `websocket.accept()`. Validate the `Origin` header against allowed CORS origins. Add authentication middleware for WebSocket routes.
### H-7: In-Memory Rate Limiter in Error Endpoint — Fails with Multiple Workers
**File:** `app/routes/errors.py:21-40`
**Risk:** The error endpoint uses a process-local `defaultdict(deque)` for rate limiting. With multiple Uvicorn workers (common in production), each worker has its own counter. An attacker can make `RATE_LIMIT * num_workers` requests per minute.
**Remediation:** Use the Redis-based `check_rate_limit()` from `app/core/rate_limit.py` instead of the in-memory implementation.
### H-8: No CSRF Protection on WebSocket Connections
**File:** Both WebSocket managers
**Risk:** WebSocket connections are not protected against CSRF. A malicious site can open a WebSocket to the CRM backend via JavaScript `new WebSocket()` and send commands as the authenticated user (cookies are sent automatically with SameSite=Strict for same-site, but cross-site WebSocket hijacking is still possible if SameSite is configured differently or cookies are sent via `credentials`).
**Remediation:** Verify the `Origin` header on WebSocket upgrade requests. Reject connections from untrusted origins.
---
## MEDIUM Issues
### M-1: Login Response Leaks `is_system_admin` Flag
**File:** `app/routes/auth.py:78`
**Risk:** The login response includes `"is_system_admin": user.is_system_admin`. An attacker who compromises a session or intercepts the response knows whether the account has system-wide privileges, enabling targeted attacks.
**Remediation:** Do not include `is_system_admin` in the login response. The frontend can determine admin status via the `/me/permissions` endpoint.
### M-2: Weak Password Validation — No Complexity Requirements
**Risk:** Login accepts any password length (min_length=1). User creation requires min 8 chars but no complexity (uppercase, lowercase, digits, special chars). Users can set passwords like `aaaaaaaa`.
**Remediation:** Add password complexity validation (min 12 chars, mixed case, digits, special chars) for user creation and password reset. Keep login min_length=1 to avoid leaking whether the password was partially correct.
### M-3: F-String Interpolation of Table/Column Names in Raw SQL
**Risk:** Multiple raw SQL queries use f-strings to interpolate table and column names: `f"UPDATE {table} SET ..."`, `f"SELECT {emb_col} FROM {table_name} ..."`. While the values come from hardcoded `table_map` dicts (not user input), this pattern is fragile — a future change could introduce user-controlled values into the map.
**Remediation:** Use SQLAlchemy ORM queries instead of raw SQL where possible. If raw SQL is needed, validate table/column names against an allowlist before interpolation, or use `sqlalchemy.sql.quoted_name` for safe identifier quoting.
### M-4: Forgejo Error Reporter Sends Full Context to External Service
**Risk:** The error reporter serializes the entire `context` dict into the Forgejo issue body as JSON. If frontend error reporting includes sensitive data (user tokens, PII, tenant data), it will be written to an external Forgejo repository as a public issue.
**Remediation:** Add a field-level allowlist for context data. Strip or redact sensitive keys (tokens, passwords, emails, phone numbers). Consider making Forgejo issues private/confidential.
### M-5: Config Has Hardcoded Default Secret Key
**File:** `app/config.py:55`
**Risk:** The default `secret_key = "change-me-in-production-use-a-secure-random-string"` is a known public value. While production mode checks for it, development mode silently uses it. If dev environments are exposed (even temporarily), all signed tokens are forgeable.
**Remediation:** Remove the default value entirely. Make `secret_key` a required field with no default. Fail in all environments if not set.
**Risk:**`get_url()` returns `self._full_path(path)` which is the absolute filesystem path (e.g., `/data/uploads/tenant1/file.pdf`). If this URL is returned to the frontend or used in API responses, it leaks the internal directory structure and can aid path traversal attacks.
**Remediation:** Return a relative path or a signed download URL that routes through an authenticated API endpoint.
### M-7: Inconsistent Environment Configuration in `.env`
**File:** `.env:3,4`
**Risk:**`.env` sets `ENVIRONMENT=development` but `SESSION_COOKIE_SECURE=true`. In development with HTTP, secure cookies won't be sent, causing auth failures. More importantly, the `ENVIRONMENT=development` setting disables the production safety checks in `get_settings()`, allowing the default `SECRET_KEY` to be used.
**Remediation:** Use separate `.env.development` and `.env.production` files. Ensure development configs are never accidentally deployed.
### M-8: Permission Cache Falls Back to Stale Data on DB Error
**File:** `app/core/permissions.py:337-344`
**Risk:** When `_get_current_permission_version()` fails (DB error), the code sets `current_version = cached_version` and uses potentially stale cached permissions. If a user's permissions were revoked during the DB outage, they retain elevated access.
**Remediation:** On DB error, either fail closed (deny access) or use a shorter stale-while-error TTL. Log the event as a security incident.
---
## LOW Issues
### L-1: `document.write()` with DOM Clone in Print Utility
**File:** `frontend/src/utils/print.ts:54, 127`
**Risk:**`printElement()` and `exportToPDF()` use `document.write()` with `clone.outerHTML`. If the printed DOM element contains user-controlled content (e.g., contact notes with HTML), it executes in a new window context. The new window is same-origin, limiting the impact, but it's still an unnecessary risk.
**Remediation:** Use DOM APIs (`appendChild`, `importNode`) instead of `document.write()`. Alternatively, sanitize the cloned HTML before writing.
### L-2: Session Data Stored in Redis Without Encryption
**File:** `app/core/auth.py:130-134`
**Risk:** Session data (user_id, tenant_id, email, role, csrf_token, is_system_admin) is stored as plaintext JSON in Redis. Anyone with Redis access can read all active sessions.
**Remediation:** Encrypt session data before storing in Redis, or accept the risk given Redis should be network-isolated. At minimum, ensure Redis requires authentication and is not exposed.
### L-3: No Security Headers Middleware
**File:** No security headers middleware found
**Risk:** The application does not set security headers like `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`, `Content-Security-Policy`.
**Remediation:** Add a security headers middleware or use `starlette-securehead`/`secure` package.
### L-4: No Origin Verification on WebSocket Upgrade
**File:** Both WebSocket managers
**Risk:** Neither WebSocket manager checks the `Origin` header before accepting connections. While cookies with `SameSite=Strict` provide some protection, some browsers and non-browser clients may not respect SameSite on WebSocket connections.
**Remediation:** Check `websocket.headers.get("origin")` against `settings.cors_origin_list` before calling `websocket.accept()`.
### L-5: Unbounded Feedback/Command Storage in AI UI Control WebSocket
**Risk:**`store_feedback()` stores feedback dicts without size limits. `cleanup_stale()` only runs when explicitly called. An attacker who can send WebSocket messages could fill memory with large feedback payloads.
**Remediation:** Add size limits on feedback payloads. Run `cleanup_stale()` on a timer or on each `connect()`/`disconnect()`.
---
## Positive Findings
1.**Dockerfile security:** Multi-stage build, non-root user (`appuser` UID 1000), healthcheck configured, no secrets baked into image.
2.**RLS implementation:** PostgreSQL Row Level Security with `FORCE` (migration 0028) ensures tenant isolation even for table owners. `set_tenant_context()` uses parameterized queries.
3.**Password hashing:** bcrypt with configurable rounds (default 12).
5.**XSS protection:**`HtmlBlock.tsx` and `SignatureManager.tsx` use `DOMPurify.sanitize()` before `dangerouslySetInnerHTML`.
6.**RBAC architecture:** Deny-list takes precedence over allow-list. Field-level permissions with strictest-wins merging. Permission version-based cache invalidation.
7.**No user enumeration:** Password reset endpoint always returns 200.
8.**SQL injection:** ORM queries use parameterized statements throughout. Raw SQL in `unified_search` uses hardcoded maps (not directly exploitable).
9.**`.gitignore`** properly covers `.env`, `.env.*`, and excludes example files.
10.**Production safety checks** in `get_settings()` validate `SECRET_KEY`, `SESSION_COOKIE_SECURE`, and `STORAGE_PATH`.
---
## Migration & Data Loss Risks
1.**RLS policies:** Multiple migrations (0001, 0002, 0004, 0015, 0021, 0028) create and modify RLS policies. Migration 0028 adds `FORCE ROW LEVEL SECURITY`. Ensure all migrations are applied in order before production deployment.
2.**Backup risk:** No backup/restore procedure found in the repository. The `last_backup_at` system setting is referenced in automation jobs but no backup script exists.
3.**Volume persistence:**`docker-compose.yml` defines named volumes for `pgdata`, `redisdata`, and `storage`. Good for persistence, but no backup strategy documented.
4.**Migration rollback:** Down migrations exist but should be tested. RLS policy down migrations disable RLS — running a rollback in production would expose all tenant data.
---
## Remediation Priority
1.**Immediate (before any production deploy):** C-1, C-2, C-3, C-4, C-5, H-1, H-2, H-3
- **6 files changed** (5 modified + 1 new migration)
- **Migration 0039_contact_normalize.py**: surfix→suffix rename, Float→Numeric(5,2) for 6 discount columns with CHECK constraints (0-100), JSON→JSONB for contacts.custom and contactpersons.custom, partial unique indexes on (tenant_id, code) and (tenant_id, accounting_code)
## 0. BINDENDE REGEL: Auf bestehendem Code aufbauen (NICHT VERHANDELBAR)
### 0.0 Sub-Agents / Subordinates — Nuancierte Regel
**Sub-Agents (call_subordinate) nur für einfache Jobs verwenden.**
- Einfache Jobs: Research, Codebase-Exploration, Dokumentations-Zusammenfassung — Aufgaben ohne Code-Änderungen oder Schema-Migrationen.
- Komplexe Jobs (Code-Änderungen, Tests, Migrationen, Deployments): vom Haupt-Agent selbst ausführen.
- Wenn der User sagt "keine Sub-Agents verwenden": daran halten, keine Ausnahmen.
- Sub-Agents haben in der Vergangenheit Code geschrieben der nicht gegen Produktion verifiziert wurde, Schema-Drifts verursacht und nicht getestet hat. Qualitätssicherung bleibt beim Haupt-Agent.
**Gültig für jegliche Arbeit an diesem Projekt.**
### 0.1 Pflicht zur Analyse vor Implementierung
Der Agent MUSS vor jeder Implementierung das bestehende System analysieren:
1.**Backend lesen:** Welche Models, Routes, Services, Plugins, Contracts, Hooks, ARQ-Jobs existieren bereits für den betroffenen Bereich? Der Agent greppt und liest die relevanten Dateien BEVOR er Code schreibt.
2.**Frontend lesen:** Welche Pages, Components, Stores, Hooks, API-Clients, Block-Typen, Sidebar-Tabs existieren bereits für den betroffenen Bereich? Der Agent greppt und liest die relevanten Dateien BEVOR er Code schreibt.
3.**Datenbank lesen:** Welche Tabellen, Foreign Keys, RLS-Policies, Migrationen existieren bereits? Der Agent prüft `alembic/versions/` und die Produktions-DB BEVOR er neue Migrationen schreibt.
4.**Plugin-System lesen:** Welche Contracts, Manifests, Search Provider, Tools, Hooks existieren bereits in den betroffenen Plugins? Der Agent liest `plugin.py`, `contracts.py`, `manifest.py` BEVOR er neue Plugins oder Erweiterungen baut.
### 0.2 Pflicht zum Aufbau auf bestehendem Code
Der Agent MUSS auf bestehendem Code aufbauen. Es ist VERBOTEN:
- ❌ Parallele Systeme zu bauen die vorhandene Funktionalität duplizieren (z.B. ein separates Workstream-System wenn das `kommunikation` Plugin schon Conversations, Messages, Blocks, WebSocket hat)
- ❌ Neue Frontend-Pages zu bauen wenn vorhandene Pages die Funktion aufnehmen können (z.B. Dashboard, Communication, AgentDashboard, Workflows, Wiki, Settings)
- ❌ Neue Sidebars oder Panels zu bauen wenn die AISidebar (5 Tabs) oder MessageSidebar die Funktion aufnehmen können
- ❌ Neue Stores zu bauen wenn vorhandene Stores (commStore, uiStore, authStore, etc.) die Funktion aufnehmen können
- ❌ Neue API-Clients zu bauen wenn vorhandene API-Clients (api/comm.ts, api/ai.ts, api/automation.ts, etc.) die Funktion abdecken können
- ❌ Neue Block-Typen zu bauen wenn vorhandene Block-Typen (action_card, contact_card, miniapp, etc.) die Funktion abdecken können
- ❌ Dataclasses zu schreiben wenn echte SQLAlchemy Models + FastAPI Routes die richtige Lösung sind
- ❌ Mock-Tests zu schreiben wenn echte Integration-Tests mit der Test-DB möglich sind
- ❌ Module zu bauen die 0 Referenzen aus Routes/Plugins haben (unverbundener Code)
- ❌ Tasks als "done" zu markieren ohne echte Verifizierung (curl gegen echte API, grep-Beweis für Import-Verbindungen, tsc clean, Backend import OK)
### 0.3 Pflicht zur Verbindung
Jeder neue Code MUSS mit dem bestehenden System verbunden werden:
- **Backend:** Neue Module müssen in `app/main.py` oder in Plugin `routes.py` registriert werden. Neue Models müssen in `alembic/versions/` migriert werden. Neue Tools müssen im `tool_registry` registriert werden. Neue Hooks müssen in `plugin.py on_activate` registriert werden. Neue ARQ-Jobs müssen in `worker.py` registriert werden.
- **Frontend:** Neue Components müssen in vorhandene Pages integriert werden (nicht als neue Page). Neue API-Calls müssen vorhandene API-Clients nutzen oder erweitern. Neue Block-Typen müssen im `BlockRenderer.tsx` registriert werden. Neue Sidebar-Tabs müssen in der `AISidebar.tsx` registriert werden.
- **Verifizierung:** Der Agent beweist mit grep dass neue Module importiert/referenziert werden. Der Agent beweist mit curl/pytest dass die API funktioniert. Der Agent markiert nichts als "done" ohne diese Beweise.
### 0.4 Referenz-Architektur (was existiert und genutzt werden MUSS)
| T08c | Frontend Mail + Global Search UI | implementation_engineer | T06, T07b | 7 | v2 |
### Parallelization Notes
**v1 Phases:**
- **Phase 2:** T02 (Company/Contact) and T03 (Plugin Framework) are independent after T01 — safe to run in parallel.
- **Phase 3:** T07a (Frontend Shell+Auth+UI Library) depends only on T01. T09 (KI/Workflow) depends on T01+T02. Both can run in parallel if API contracts are frozen.
- **Phase 4:** T07b (Frontend Feature Pages) depends on T07a (UI library, routing, auth) + T02 (company/contact API). Must run after T07a.
- **Phase 5:** T10 (Monitoring+Doku) depends on T01+T02. Can run parallel with T07b.
**v2 Phases (after v1 deployment):**
- **Phase 6:** T04 (DMS), T05 (Calendar), T06 (Mail), T11 (Tags+Perm) all depend on T01+T03 — safe to run in parallel.
- **Phase 7:** T08a/T08b/T08c depend on T07b + respective backend (T04/T05/T06) — safe to run in parallel.
### Block Rules
- Block = max 3 Tasks per implementation block.
- After each block: quality_reviewer review → block_compactor → context_compactor → User checkpoint.
- quality_reviewer and release_auditor do NOT count toward the 3-task limit.
- After 3 blocks (9 tasks): release_auditor runs full audit.
- Token budget: ~3000 tokens per task. If tool result >5000 tokens: context_compactor.
### Deployment
- ❌ Running as root in container — use app:app
- ❌ Exposed DB port in production
- ❌ Missing Docker health checks
- ❌ Ephemeral storage — use named volumes
- ❌ Secrets in docker-compose.yml
---
## 5. Forbidden Patterns
## 5. Quality Gates
### Backend Forbidden
-❌ **SQLite:** No SQLite as database. PostgreSQL 16 only (ADR-01).
- ❌ **Jinja2:** No server-side HTML rendering. API-only backend (ADR-03).
- ❌ **Cross-Tenant Data Access:** No query without tenant_id filter. ORM auto-filter must not be bypassed.
- ❌ **Plaintext Passwords:** Passwords must be bcrypt-hashed (cost=12). Never store or log plaintext.
- ❌ **JWT Tokens:** No JWT auth in v1. Session-based auth with HttpOnly cookies only (ADR-05).
- ❌ **Naive Datetime:** All datetime fields must be timezone-aware (TIMESTAMPTZ). Never use `datetime.now()` without tz.
- ❌ **Integer IDs:** All primary keys are UUID. Never use auto-increment integer IDs.
- ❌ **Hard-Delete without GDPR flag:** Companies/Contacts use soft-delete. Hard-delete only with explicit `?gdpr=true`.
- ❌ **Manual Tenant Filter:** Never manually add `.filter(Tenant.id == x)` in services. The ORM auto-filter handles this.
- ❌ **Sync I/O in Routes:** All route handlers are `async def`. Never use blocking I/O (use `asyncpg`, `aiofiles`, etc.).
- ❌ **Raw SQL without Tenant Check:** Any raw SQL query must explicitly include `tenant_id` filter.
- ❌ **Secrets in Code:** No hardcoded secrets. All secrets via environment variables.
- ❌ **Unvalidated Input:** All request bodies validated by Pydantic schemas. Never trust raw request data.
- ❌ **Missing Audit Log:** All create/update/delete operations must create audit log entries.
- ❌ **Plugin Tables without tenant_id:** All plugin-created tables must include `tenant_id` column. The migration validator enforces this.
### Frontend Forbidden
- ❌ **Class Components:** No class components. Functional components with hooks only.
- ❌ **Inline Styles:** No `style={{}}` props. Use Tailwind utility classes.
- ❌ **Hardcoded Strings:** No user-visible hardcoded strings. Use `t()` from i18n.
- ❌ **Manual Fetch in Components:** No `fetch()` or `axios` calls in components. Use TanStack Query hooks.
- ❌ **Server Data in Zustand:** Zustand is for client state only. Server data goes in TanStack Query.
- ❌ **`any` Types:** No `any` type. Use proper TypeScript types.
- ❌ **Missing ARIA Attributes:** All interactive elements must have ARIA labels.
- ❌ **Touch Targets < 44px:** All buttons/links must have minimum 44px touch target.
- ❌ **Direct DOM Manipulation:** No `document.getElementById()` or `querySelector()` in components. Use React refs.
- ❌ **Unsafe HTML Rendering:** No `dangerouslySetInnerHTML` without sanitization. Mail bodies must be sanitized (DOMPurify equivalent).
### Deployment Forbidden
- ❌ **Running as Root in Container:** Containers run as non-root user (app:app).
- ❌ **Exposed DB Port in Production:** PostgreSQL port (5432) must not be exposed externally in production.
- ❌ **No Health Check:** All services must have Docker health checks configured.
- ❌ **No Volume for Storage:** File storage must use a named volume, not ephemeral container storage.
- ❌ **Secrets in docker-compose.yml:** No secrets in compose file. Use `.env` file or Docker secrets.
-`review` — Task implementiert, wartet auf Review/Tests
-`done` — Task hat Definition of Done (DoD) erfüllt
**Der Agent MUSS `PROGRESS.md` bei jedem Status-Wechsel aktualisieren.** Kein Task-Wechsel ohne PROGRESS.md-Update.
### Forgejo Issues & Milestones
- **Pro Phase (A-J):** Ein Forgejo Milestone (z.B. "Phase A — Stabilität", "Phase B — System-Konsolidierung")
- **Pro Task:** Ein Forgejo Issue mit Label `task` + Milestone der jeweiligen Phase
- **Pro Bug:** Ein Forgejo Issue mit Label `bug` + Priorität (`critical`, `high`, `medium`, `low`)
- **Pro Feature-Request:** Ein Forgejo Issue mit Label `enhancement`
**Der Agent MUSS für jeden Task ein Forgejo Issue erstellen** und die Issue-Nummer in `PROGRESS.md` eintragen.
### Commit-Messages
- Commit-Messages enthalten die Task-ID: `feat(B-LLM): zentraler LLM Client implementiert`
- Bug-Fixes referenzieren das Issue: `fix(#123): Redis-Connection-Leak behoben`
-`fixes #123` oder `closes #123` im Commit schließt das Issue automatisch
### Definition of Done (DoD)
Ein Task gilt erst als **DONE** wenn alle 8 DoD-Kriterien erfüllt sind (siehe `PLATFORM_ROADMAP.md`). Ein Task ohne Test ist NICHT done. Der Agent darf keinen Task als `done` markieren ohne DoD erfüllt zu haben.
### Phase-Gate-Review
Eine Phase gilt erst als **ABGESCHLOSSEN** wenn alle 7 Phase-Gate-Kriterien erfüllt sind (siehe `PLATFORM_ROADMAP.md`). Der Agent darf nicht zur nächsten Phase übergehen ohne Phase-Gate-Review bestanden zu haben.
## 10. Tracking-Ein-Datei-Regel (bindend seit 2026-08-27)
- **PROGRESS.md ist die einzige Source of Truth** fuer Status und offene Punkte. Keine weiteren parallelen Tracking-Dateien (test-bugs.md/fix-plan-v3 sind in docs/archive/ historisiert).
- Ein Finding wird nur eingetragen mit **tagesaktueller Live-Messung** (Befehl + Zaehler). 'Scanner sagt' oder Plan-Text allein reicht nie.
- Tests duerfen nur zusammen mit Pflegeanspruch entstehen: UI-Aenderung zieht Test-Nachzug im selben Commit nach sich. Geister-Tests (Importziel geloescht) werden sofort geloescht.
- Playwright-e2e bleibt dem eigenen Runner vorbehalten (vite.config exclude), kein Vitest-Collection.
5. Click **Deploy** once to let Coolify create the resource (it will fail to
start without environment variables — that's expected).
6. Note the **Application UUID** (visible in the URL or via
`GET /api/v1/applications`).
---
## 4. Environment variables (on the crm-app resource)
In **crm-app → Environment Variables**, set:
| Key | Value | Notes |
|-----|-------|-------|
| `DATABASE_URL` | `postgresql+asyncpg://crm_user:<PW>@<postgres-internal-host>:5432/crm_db` | Use the internal host from step 2 (e.g. `crm-postgres-xyz:5432`), **not**`localhost` and **not** the public DNS. |
*Erstellt: 2026-07-26 — basierend auf externem Audit + eigener Code-Verifikation*
---
## Zusammenfassung
Von 16 zentralen Punkten des externen Audits wurden **alle 16 durch Code-Inspektion verifiziert**. Zusätzlich wurden **5 neue Probleme** gefunden (UploadFile-Bug, Redis-Default-Passwort, exponierte Ports, unauthentifizierter Error-Endpoint, fehlende Security-Headers).
**Gesamtstatus:** Alle Phasen implementiert (Stand 2026-07-27). M5 (Frontend-Integration) als letzte Phase abgeschlossen.
---
## Implementierungs-Status (Stand 2026-07-27)
Die folgenden Phasen wurden gemäß Git-Historie implementiert:
- **Problem:** Zweite Definition überschreibt Singleton, erzeugt pro Aufruf neue Verbindung → Connection Leak
- **Fix:** Zweite `def get_redis()` (Zeile 94) löschen. Erste Definition (Zeile 53) beibehalten.
- **Aufwand:** 5 Min
- **Risiko:** Keines — erste Definition ist korrekt
### B2. Plugin-Routen-Registrierung reparieren
- **Datei:** `app/main.py` Zeilen 375-416
- **Problem:** Alle Plugin-Routen werden statisch in `create_app()` registriert, unabhängig vom Aktivierungsstatus. Deaktivierte Plugins bleiben erreichbar. Kommentar in Zeile 416 sagt das Gegenteil.
- **Fix:**
1. Statische Registrierung aus `create_app()` entfernen
2. In `lifespan()` nur Routen für `active=True` Plugins registrieren
3.`Depends(require_active_plugin("name"))` als zentrale Prüfung ergänzen
4. Bei Deaktivierung: Router entfernen oder 403-Dependency ergänzen
- **Aufwand:** 2-3 Std
- **Risiko:** Mittel — muss sicherstellen dass keine Route doppelt registriert wird
- **Problem:** Chunks werden in `list[bytes]` gesammelt, dann `b"".join()` → 100MB Datei = 200MB+ RAM. `save_stream()` existiert aber wird nicht benutzt.
- **Risiko:** Gering — save_stream() ist bereits implementiert
### B5. Outbox-Worker: Event-Handler registrieren
- **Datei:** `app/core/worker.py` `on_startup()`
- **Problem:** Worker liest Events aus Outbox, published an lokalen EventBus, aber es sind keine Handler registriert → Events werden als `published` markiert ohne Verarbeitung
- **Fix:**
1. In `on_startup()`: Plugin-Event-Handler registrieren (wie in `lifespan()` der API)
2. `webhook_dispatcher._dispatch_event` an EventBus subscriben
3. Plugin-Participant-Handler registrieren
- **Aufwand:** 2 Std
- **Risiko:** Mittel — muss gleiche Handler wie API-Container registrieren
- **Problem:** `send_password_reset_email` Job wird gequeued aber nie registriert → Mail wird nicht versendet. Token wird in Logs geschrieben (Zeile 240-241).
- **Problem:** Kein SSRF-Schutz — User können interne Dienste ansprechen (redis:6379, postgres:5432, 169.254.169.254). Webhook-Secret wird im Response zurückgegeben.
- **Problem:** `POSTGRES_USER` (crm_user) ist Superuser → umgeht RLS auch mit FORCE. Spätere Tabellen (user_preferences, saved_filters, etc.) haben keine RLS-Policy.
- **Fix:**
1. Neue Migration `0044_db_roles.py`: erstellt `crm_runtime` (NOSUPERUSER, NOBYPASSRLS)
2. `crm_runtime` bekommt nur SELECT/INSERT/UPDATE/DELETE Rechte
3. `docker-compose.yml`: API und Worker nutzen `crm_runtime`, Migrationen nutzen `crm_owner`
4. Neue Migration `0045_rls_new_tables.py`: RLS für alle Tabellen mit `tenant_id` die nach 0028 hinzukamen
- **Aufwand:** 4 Std
- **Risiko:** Hoch — muss bestehende Datenbanken migrieren ohne Datenverlust
### B9. .env.docker.example korrigieren
- **Datei:** `.env.docker.example`
- **Problem:** Verwendet `AUTH_SECRET` statt `SECRET_KEY` (config.py erwartet `SECRET_KEY`)
| P1-1 | User/Tenant-Modell | `app/models/user.py` — `User` hat keine `tenant_id`/`role` mehr, `UserTenant` ist single source of truth, `email` global unique |
**Status:** Offen — 228 direkte Cross-Imports zwischen Plugins
**Problem:** 228 direkte `from app.plugins.builtins` Imports zwischen Plugins. Automatisierung importiert Modelle/Services von Kommunikation, Mail, Kalender. Verteilter Monolith ohne Modulgrenzen.
**Maßnahme:**
1. Öffentliche Schnittstellen (Contracts) für jedes Modul definieren
2. Direkte Imports fremder Plugin-Modelle verbieten
3. Kommunikation nur über Events oder öffentliche Service-API
**Tatsächlich:**`export_contacts_csv()` Service-Funktion existiert, aber KEINE Route in `import_export.py`. Nur `/import` und `/import/preview` sind registriert. Export muss als Route hinzugefügt werden.
### Korrektur 3: Activity Timeline — ActivityFeed existiert bereits
**Vorher:** Plan sagte "Dashboard hat ActivityFeed aber nur statisch"
**Tatsächlich:** Dashboard nutzt `ActivityFeed` mit Daten aus Audit-API. Komponente ist wiederverwendbar. Es fehlt nur eine eigenständige Seite mit Filterung/Pagination.
### Korrektur 4: DMS ShareDialog — File Permissions bereits integriert
**Vorher:** Plan sah `FilePermissionDialog` als neue Komponente vor
**Tatsächlich:**`ShareDialog.tsx` (11KB) existiert bereits und nutzt `fetchFilePermissions`, `grantPermission`, `revokePermission`, `createShareLink`, `revokeShareLink` aus `permissions.ts`.
---
## Übersicht: 14 verbleibende Features in 4 Phasen
| Phase | Features | Priorität | Geschätzter Aufwand |
- Pro Feld Radio: Quelle | Ziel | Manuell eingeben
- Vorschau des merged Kontakts
- Optionale Notiz
- Bestätigungs-Button mit Warnung
**Aufwand:** ~1 Tag
---
### 1.3 Import/Export UI
**Audit-Status:** ✅ Backend hat `/api/v1/import` + `/api/v1/import/preview` (Routes). ✅ Service hat `import_csv()`, `export_contacts_csv()`. ❌ **Export-Route fehlt** — Service-Funktion existiert aber ist nicht als Endpoint registriert. ❌ Kein Frontend, kein API-Client.
-`frontend/src/pages/Calendar.tsx` — Tag-Badges für Termine
-`frontend/src/pages/Dms.tsx` — Tag-Badges für Dateien
- Plugin-Manifest — menu_items
-`frontend/src/i18n/de.json`
**Aufwand:** ~1 Tag
---
### 2.2 Custom Fields UI
**Audit-Status:** ✅ Backend hat Custom-Fields-Route (plugin-manifest-gesteuert, Werte in `contacts.custom` JSONB). ❌ Keine User-definierten Feld-Definitionen (nur Plugin-Definitionen). ❌ Keine Frontend-Seite.
**Backend-Ergänzung nötig:**
-`app/models/custom_field_definition.py` — Model für User-definierte Felder
**Aufwand:** ~1.5 Tage (inkl. Backend CRUD + Migration)
---
### 2.3 Notifications Dropdown (Bell Icon in TopBar)
**Audit-Status:** ✅ API-Client vollständig (`notifications.ts`). ✅ Backend vollständig. ❌ TopBar hat kein Bell-Icon (confirmed: `grep` findet nichts). Notifications nur in AISidebar.
**Neue Dateien:**
-`frontend/src/components/layout/NotificationBell.tsx` — Bell-Icon mit Badge + Dropdown
-`frontend/src/pages/Dms.tsx` — History für Dateien
-`frontend/src/pages/Calendar.tsx` — History für Termine
-`frontend/src/i18n/de.json`
**Features:**
- Timeline mit create/update/delete Events
- Diff-Anzeige: alt → neu pro Feld
- Restore-Button pro Eintrag
- Undo-Button (letzte Aktion rückgängig)
**Aufwand:** ~0.5 Tage
---
### 3.3 Activity Timeline
**Audit-Status:** ✅ `ActivityFeed` Komponente existiert (wiederverwendbar). ✅ Dashboard nutzt sie mit Audit-Daten. ❌ Keine eigenständige Seite mit Filterung/Pagination.
**Neue Dateien:**
-`frontend/src/pages/ActivityTimeline.tsx` — Globale Activity-Feed Seite
- Event-Auswahl (Multi-Select aus Event-Bus-Events)
- Aktiv/Pause Toggle
- Delivery-Log mit Status, Response-Code, Latenz
- Retry-Konfiguration
- Test-Button
**Aufwand:** ~1.5 Tage
---
### 4.2 Backup/Restore UI
**Audit-Status:** ✅ `backup_check` Cron-Job existiert (prüft last_backup_at, published Events). ❌ Keine Backup-Routes, keine Restore-Funktionalität, keine UI.
1.**KI-UI-Steuerung:** Keine Mausbewegung nötig. KI muss zu Kontakten springen und einen Kontakt öffnen können. Die UI muss das Ergebnis zeigen — Kontaktliste und spezieller Kontakt ausgewählt. Implementierungsweg (WebSocket, postMessage, etc.) ist offen, Hauptsache das Ergebnis wird in der UI sichtbar.
2.**Company-Routes:** Komplett entfernen. Keine deprecated-Routes, keine Redirects. Kontakte wie in Rentman — ein unified Contact-Modell, kein separates Company-Modell mehr. **Alle Plugin-Referenzen auf entity_type='company' müssen zu 'contact' migriert werden.**
3.**PostgreSQL:** Aktuell egal (Coolify-managed oder docker-compose). Reine Docker-Lösung soll später möglich sein. Keine Code-Änderung nötig — nur Konfiguration.
4.**S3-Storage:** Provider egal. Wichtig ist nur dass die Architektur es später ermöglicht. Bereits vorbereitet in config.py (STORAGE_BACKEND=s3).
| 0.3 | `date-fns` installieren + Datum-Formatierung | 3h | Alle `toLocaleDateString()` etc. durch date-fns ersetzen. Konsistente Datum-Formatierung. |
| 0.4 | `hooks.ts` aufteilen | 3h | 1.298 Zeilen aufteilen in `api/auth.ts`, `api/contacts.ts`, `api/settings.ts` etc. Generische Hooks bleiben in `hooks.ts`. Company-Hooks werden in Phase 1 entfernt, nicht aufgeteilt. |
| 0.8 | Theme-Customization Backend | 4h | `system_settings` um Theme-Felder erweitern (primary_color, accent_color, font_family, border_radius). Neue Alembic-Migration. API-Endpoints zum Lesen/Schreiben der Theme-Settings. |
| 0.9 | Theme-Customization Frontend | 6h | `SettingsTheme.tsx` Seite mit Color-Picker, Font-Auswahl, Live-Preview. Tailwind-CSS-Variablen dynamisch aus API-Settings überschreiben. Dark-Mode-Toggle. Theme wird beim App-Start geladen und angewendet. |
| 0.10 | RBAC-Audit & Plugin-Permissions nachrüsten | 6h | 4 Plugins haben `permissions=[]` (calendar, dms, entity_links, tags) → keine Rechte-Prüfung! Pro Plugin passende Permissions definieren und in Manifest eintragen. Routes mit `require_permission()` absichern. Siehe Details unten. |
| 0.11 | LiteLLM-Cleanup & alte llm_client.py migrieren | 3h | LiteLLM ist **BEREITS** in ai_assistant und ai_proactive integriert (`litellm.acompletion()`). Nur die alte `llm_client.py` (Copilot) nutzt noch httpx direkt. Diese auf LiteLLM umstellen oder entfernen. System-Prompt in llm_client.py referenziert noch `/api/v1/companies` → auf Contacts umstellen. |
| 0.12 | KI-Agent-Framework in Plugin-Richtlinien dokumentieren | 2h | PydanticAI + tool_registry existieren bereits. In `docs/plugin-development-guide.md` dokumentieren: Wie Plugins KI-Agenten, Tools und LLM-Funktionen nutzen. Plugin-Manifest um `agent_capabilities` Feld erweitern. |
| 0.13 | Heartbeat konfigurierbar machen | 3h | Heartbeat-Intervall, Aktivierung, Ziel-Room in ProactiveSettings (DB) speichern. Settings-UI für Heartbeat-Konfiguration. |
| 0.14 | Unified Search: Field-Level RBAC nachrüsten | 4h | Search-Provider prüfen aktuell KEINE Feld-Level-Permissions. Nutzer mit `search:read` sieht alle Felder. Provider müssen `resolved_perms` prüfen und `hidden` Felder ausblenden. `to_search_result()` um Permission-Filter ergänzen. |
| 0.15 | Undo/History-System für CRUD-Operationen | 8h | Globale Undo-History: Jede CRUD-Aktion (Create/Update/Delete) wird mit Snapshot in `entity_history` Tabelle gespeichert. User kann Änderungen rückgängig machen oder zu früherer Version zurückkehren. Nutzt bestehenden Audit-Log als Basis. Frontend: Undo-Button + History-Viewer pro Entity. |
| 0.16 | Storage Backend implementieren (S3-Support) | 8h | Architecture.md beschreibt abstract StorageBackend (local/S3), aber **existiert NICHT im Code**. Attachments nutzen hardcoded `/data/uploads`. Storage-Klasse erstellen: `LocalStorage` + `S3Storage`. Config um `STORAGE_BACKEND`, `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` erweitern. DMS und Attachments auf Storage-Backend umstellen. .env.example um S3-Variablen ergänzen. |
| 0.17 | Import/Export an unified Contact Model anpassen | 4h | Import/Export nutzt alte Feldnamen (`first_name`, `last_name`, `mobile`, `position`, `department`). Auf unified Contact-Felder umstellen (`firstname`, `surname`, `phone_1`, `email_1`, etc.). Company-Import auf Contact mit type='company' umstellen. |
| 0.18 | .gitignore & Config-Cleanup | 2h | `.gitignore` hat `webui/` statt `frontend/` — frontend/node_modules und frontend/dist werden nicht ignoriert! Korrigieren. `python-jose` (JWT) aus requirements.txt entfernen — Code nutzt Session-Auth. `pyproject.toml` Python-Version auf 3.12 aktualisieren. `.env.docker.example` JWT-Variablen entfernen. **.env aus Git entfernen** (ist committet aber sollte nicht sein). `dump.rdb` und `test.txt` aus Repo löschen. `frontend/dist/` aus Git entfernen (sollte nicht committet sein). |
| 0.19 | Mail-Salt Security-Fix | 2h | `mail/services.py` hat hardcoded salt `b"leocrm-mail-salt"` für Passwort-Verschlüsselung. Salt sollte random pro Account sein. Fix: Random salt generieren und mit encrypted_password zusammen speichern. DB-Migration für bestehende Accounts. |
| 0.20 | AGPL-Lizenzen durch kommerziell nutzbare Alternativen ersetzen | 6h | **PyMuPDF** (AGPL-3.0) → ersetzen durch `pypdf` (BSD). Text-Extraktion in unified_search anpassen. **OnlyOffice** (AGPL-3.0) → ersetzen durch **Collabora Online** (LGPL/MPL). DMS Edit-Sessions auf Collabora umstellen. `requirements.txt`, `Dockerfile`, `docker-compose.yml`, `architecture.md` aktualisieren. DMS Plugin `OnlyOfficeConfig` → `CollaboraConfig`. Frontend DMS-Komponenten anpassen. Lizenz-Datei (`LICENSE`) und `THIRD_PARTY_LICENSES.md` erstellen. |
**Phase 0 Gesamt: ~77h**
### UI-Design-Richtlinien (Task 0.7)
Basierend auf Analyse der bestehenden Plugins (Calendar, Mail, DMS, Contacts):
**Layout-Patterns:**
- **3-Spalten-Explorer-Layout** (Tree | Liste/Explorer | Detail) — verwendet von Calendar, Mail, DMS
- **ResizablePanel** für drag-to-resize Spalten — bereits implementiert
- **PluginToolbar** für Plugin-Aktionen (oben) — bereits implementiert
- **Modal** für Formulare (Create/Edit/Delete-Bestätigung) — bereits implementiert
- **EmptyState** für leere Listen — bereits implementiert
- **LoadingState/Skeleton** für Lade-Zustände — bereits implementiert
1. Pro Plugin passende Permissions im Manifest definieren
2. Alle Plugin-Routes mit `require_permission()` absichern
3. Permission-Registry registriert Plugin-Permissions automatisch beim Aktivieren
4. Admin kann Permissions in Rollen-Editor zuweisen
5. Tests: User ohne Permission → 403, User mit Permission → 200
**Zusätzlich in Phase 1 (Permission-Registry-Cleanup):**
-`companies:read/write/delete` aus `CORE_PERMISSIONS` entfernen (wird zu `contacts:read/write/delete`)
-`CORE_FIELD_DEFINITIONS` aktualisieren: alte Felder (`first_name`, `last_name`, `mobile`, `position`, `department`, `linkedin_url`) durch unified Contact-Felder ersetzen (`firstname`, `surname`, `phone_1`, `email_1`, etc.)
-`companies` Field-Definitions entfernen
### LiteLLM-Integration (Task 0.11)
**Problem:** Aktuelle `llm_client.py` spricht nur OpenAI-compatible API direkt via httpx. Keine Unterstützung für Anthropic, Google, lokale Modelle etc.
**Lösung:** LiteLLM als unified LLM-Interface integrieren.
**Was LiteLLM bietet:**
- 100+ LLM-Provider über eine einheitliche API (OpenAI, Anthropic, Google, Azure, AWS Bedrock, Ollama, etc.)
- Konsistente Request/Response-Formate
- Streaming-Support
- Fallback/Routing-Regeln
- Cost-Tracking
- Rate-Limiting
**Was zu tun ist:**
1.`litellm` als Python-Dependency hinzufügen
2.`llm_client.py` auf LiteLLM umstellen: `litellm.acompletion()` statt direktem httpx-Call
3. Konfiguration via Env-Vars: `AI_MODEL`, `AI_API_KEY`, `AI_API_BASE` (bleiben gleich), plus `AI_PROVIDER` (neu: openai/anthropic/google/ollama/etc.)
4. AI Assistant Plugin nutzt LiteLLM für Multi-Provider-Support
5. AI Proactive Plugin nutzt LiteLLM für Suggestions
6. Zukünftige Plugins können LiteLLM einfach nutzen — einheitliches Interface
7. Mock-Mode für Tests beibehalten (wenn kein API-Key gesetzt)
8. Plugin-Entwickler-Richtlinien: Wie man LiteLLM in neuen Plugins nutzt
**Architektur:**
```
Plugin (ai_assistant, ai_proactive, zukünftige)
↓
LiteLLM (unified LLM interface)
↓
Provider (OpenAI, Anthropic, Google, Ollama, ...)
```
**Vorteil für zukünftige Plugins:**
- Ein Plugin kann LLM-Funktionen nutzen ohne sich um den Provider zu kümmern
- Admin kann Provider in Settings konfigurieren
- KI-Modelle können ausgetauscht werden ohne Code-Änderung
| 1.14 | **test_sample Plugin** aktualisieren | 1h | `company.created` Event → `contact.created`. Test-Plugin ist Referenz für Plugin-Entwicklung. |
| 1.15 | **Event-Namen vereinheitlichen** | 2h | Alle `company.created/updated/deleted` Events → `contact.created/updated/deleted`. Event-Publisher in contact_service.py prüfen. |
| 1.16 | **DB-Migration: entity_type 'company' → 'contact'** | 3h | Alembic-Migration: UPDATE entity_links SET entity_type='contact' WHERE entity_type='company'. UPDATE tag_assignments SET entity_type='contact' WHERE entity_type='company'. UPDATE calendar_entry_links SET entity_type='contact' WHERE entity_type='company'. ALTER TABLE mails RENAME COLUMN company_id TO contact_id. |
| 1.17 | **Backend-Tests aktualisieren** | 4h | Alle Tests die Company-Routes oder entity_type='company' referenzieren umstellen. `test_companies.py` entfernen oder zu Contact-Tests umschreiben. |
| 1.18 | **Permission-Registry-Cleanup** | 3h | `companies:read/write/delete` aus `CORE_PERMISSIONS` entfernen. `CORE_FIELD_DEFINITIONS` aktualisieren: alte Felder durch unified Contact-Felder ersetzen. `companies` Field-Definitions entfernen. |
| 1.19 | **Addresses entity_type='company' → 'contact'** | 2h | `address_service.py``VALID_ENTITY_TYPES` von `{"company", "contact"}` → `{"contact"}`. `address.py` Model anpassen. DB-Migration: bestehende Adressen mit entity_type='company' auf 'contact' migrieren. |
| 1.20 | **conftest.py aktualisieren** | 2h | `conftest.py` importiert `Company` und `CompanyContact` aus alten Modellen. Auf unified Contact Model umstellen. Test-Fixtures anpassen. |
**1B Gesamt: ~33h**
#### 1C: Frontend — Unified Contact UI
| # | Aufgabe | Aufwand | Details |
|---|---|---|---|
| 1.18 | Contact-Detail-Route hinzufügen | 2h | Route `/contacts/:id` in `routes/index.tsx`. `ContactDetail.tsx` (372 Zeilen) existiert bereits als Komponente. |
| 1.19 | ContactList mit Type-Filter (company/person) | 4h | `ContactsList.tsx` (445 Zeilen) um Type-Filter erweitern. Tabs oder Toggle: "Alle | Firmen | Personen". |
| 1.20 | ContactDetail um ContactPerson-Verwaltung erweitern | 8h | Bei type='company': Ansprechpartner-Liste, Ansprechpartner hinzufügen/bearbeiten/löschen. ContactPerson API-Hooks in Frontend. |
| 1.21 | ContactEditModal für beide Types | 6h | Formular je nach type unterschiedlich: company → name, person → firstname/surname. Adressen (mailing/visit/invoice). |
**Phase 1 Gesamt: ~81h** (vorher 33h — unterschätzt um 48h!)
---
### PHASE 2: Code-Splitting & Performance
**Ziel:** Frontend lädt nur was nötig ist. Virtual Scrolling überall.
| # | Aufgabe | Aufwand | Details |
|---|---|---|---|
| 2.1 | React.lazy + Suspense für alle Routes | 4h | Alle Page-Imports in `routes/index.tsx` auf `React.lazy()` umstellen. `<Suspense>` mit Loading-Fallback. |
| 3.7 | Plugin-Routen aus hartkodiertem Router entfernen | 4h | Statische Plugin-Imports aus `routes/index.tsx` entfernen. Alles über PluginRegistry. |
**Ziel:** Zentrale Oberfläche für Automatisierungen und selbst-arbeitende KI-Agenten. Plugins können Agenten und Automation-Templates mitbringen.
**Architektur:**
```
┌─────────────────────────────────────────────┐
│ Automation & Agents UI │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Automation │ │ Agent Builder │ │
│ │ Builder │ │ - Agent definieren │ │
│ │ - Trigger │ │ - Tools auswählen │ │
│ │ - Schedule │ │ - LLM-Modell wählen │ │
│ │ - Conditions │ │ - Heartbeat setzen │ │
│ │ - Actions │ │ - Proaktiv/Reaktiv │ │
│ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────┤
│ Cron-Scheduler │ Workflow-Timeouts │ HB │
├─────────────────────────────────────────────┤
│ Plugins bringen mit: │
│ - agent_definitions (Agent-Templates) │
│ - automation_templates (Automation-Tpl) │
│ - cron_jobs (periodische Tasks) │
│ - heartbeat_configs │
└─────────────────────────────────────────────┘
```
| # | Aufgabe | Aufwand | Details |
|---|---|---|---|
| 3.11 | Plugin-Manifest um Agent/Automation-Felder erweitern | 4h | Manifest um `agent_definitions`, `automation_templates`, `cron_jobs`, `heartbeat_configs` erweitern. Plugins deklarieren was sie mitbringen. |
| 3.12 | Cron-Scheduler Backend | 6h | ARQ-basierter Scheduler für periodische Tasks. Cron-Expressions (z.B. `0 8 * * *` = täglich 8 Uhr). Scheduler liest aktive Cron-Jobs aus DB und enqueued sie. Ersetzt hartkodierten Heartbeat. |
| 3.13 | Workflow-Timeout-Worker | 4h | ARQ-Job der regelmäßig Workflow-Instanzen mit abgelaufenem `timeout_at` prüft. Bei Timeout: Status auf `cancelled`, Notification an Initiator. |
| 3.14 | Agent Builder Backend | 8h | API für Agent-Definitionen: Name, Beschreibung, LLM-Modell, Tools (aus tool_registry), System-Prompt, Heartbeat-Intervall, Proaktiv/Reaktiv-Modus. Agent-Definitionen in DB gespeichert. |
| 3.15 | Automation Builder Backend | 6h | API für Automation-Definitionen: Trigger (Event/Schedule/Manual), Conditions, Actions (API-Call/Notification/Workflow-Start). Automation-Definitionen in DB gespeichert. |
| 3.16 | Automation Execution Engine | 6h | Engine die Automations ausführt: Event-Trigger → Conditions prüfen → Actions ausführen. Nutzt Event-Bus für Event-Trigger, Cron-Scheduler für Schedule-Trigger. |
| 3.21 | Plugin-Beiträge registrieren | 4h | Wenn Plugin aktiviert wird: Agent-Definitionen, Automation-Templates, Cron-Jobs aus Manifest registrieren. Bei Deaktivierung: entfernen. |
| 3.22 | Heartbeat-Verwaltung migrieren | 3h | Hartkodierten Heartbeat aus ai_proactive in Automation & Agents Plugin migrieren. Heartbeat wird zu einem konfigurierbaren Cron-Job. |
| 3.23 | Settings für Automation & Agents | 3h | Einstellungen: Default-LLM-Modell für Agenten, Heartbeat-Default-Intervall, Max-Concurrent-Agents, Log-Level. |
| 3.27 | Dry-Run / Test-Modus | 3h | Automations und Agenten können im Dry-Run getestet werden: Führt Conditions aus, zeigt was passieren würde, aber führt keine destruktiven Actions aus. Test-Button in Builder UI. |
| 3.28 | Agent Rate-Limiting & Safety | 3h | Max-Ausführungen pro Agent pro Stunde. Max-Dauer pro Ausführung. Auto-Stop bei Endlosschleife (wenn Agent dieselbe Action 5x hintereinander ausführt). Budget-Limit pro Agent (LiteLLM Cost-Tracking). |
| 3.29 | Plugin-Beitrags-Konfliktlösung | 2h | Wenn zwei Plugins denselben Agent-Namen/Templat-Namen mitbringen: Plugin-Name als Prefix (`mail.mail_sorter` statt `mail_sorter`). Dedup-Logik bei Registrierung. |
| 3.30 | Agent-zu-Agent-Kommunikation | 8h | Agenten können Nachrichten an andere Agenten senden. Nutzt kommunikation Plugin-Infrastruktur (WebSocket, Rooms). Agent-Message-Router: Agent A sendet `{to: 'mail_sorter', message: 'Neuer Termin gefunden'}`. Empfänger-Agent reagiert. Agent-Chatrooms in Dashboard sichtbar. |
| 3.31 | Versionshistorie für Agenten & Automations | 4h | Jede Änderung an Agent/Automation erstellt neue Version. Alte Versionen können wiederhergestellt werden. Versions-Diff in UI. `agent_versions` und `automation_versions` Tabellen. |
| 3.32 | MiniApps: Plugin-MiniApps im Chat | 6h | **Bereits implementiert:**`MiniAppRegistry`, `MiniAppDef`, Routes (`GET /miniapps`, `POST /conversations/{id}/miniapps`), `MiniAppBlock.tsx` Frontend. **Was fehlt:** Plugin-Manifest um `miniapps` Feld erweitern (Plugins deklarieren welche MiniApps sie mitbringen). MiniApp-Builder UI (visuell MiniApps erstellen). MiniApp-Store in Settings. Dokumentation in Plugin-Entwickler-Richtlinien. |
**Phase 3.5 Gesamt: ~105h**
**Was Plugins mitbringen können:**
- **Agent-Definitionen:** Ein Plugin kann vordefinierte Agenten mitbringen (z.B. Mail-Plugin bringt "E-Mail-Sortier-Agent" mit)
- **Automation-Templates:** Ein Plugin kann Automation-Vorlagen mitbringen (z.B. Calendar-Plugin bringt "Terminerinnerung 24h vorher" mit)
- **Cron-Jobs:** Ein Plugin kann periodische Tasks deklarieren (z.B. Mail-Plugin: "IMAP-Sync alle 15 Minuten")
- **Heartbeat-Configs:** Ein Plugin kann Heartbeat-Konfigurationen mitbringen
**Beispiel: Mail-Plugin bringt Agent mit**
```json
{
"agent_definitions":[{
"name":"mail_sorter",
"display_name":"E-Mail-Sortier-Assistent",
"description":"Sortiert eingehende E-Mails automatisch nach Regeln",
| 4.9 | Command: Settings ändern | 3h | System-Settings, User-Preferences per UI-Command ändern. Wird in UI sichtbar. |
| 4.10 | UI-Action-Feedback an KI | 4h | Frontend sendet Bestätigung zurück: `{action: 'navigate', status: 'success', current_path: '/contacts/123'}`. KI weiß, dass Command ausgeführt wurde. |
| 4.11 | Visuelle KI-Indikation | 3h | Wenn KI eine Aktion ausführt: kurzer Highlight-Effekt oder Toast "KI führt Aktion aus...". User sieht dass KI agiert. |
| 4.12 | Tests für KI-UI-Steuerung | 4h | Vitest-Tests für Command-Protokoll, useAIUIControl Hook, Command-Ausführung. |
**Phase 4 Gesamt: ~44h**
---
### PHASE 5: API-Vollständigkeit & KI-Testbarkeit
**Ziel:** App komplett per API steuerbar. KI kann selbstständig testen und Updates einspielen.
| # | Aufgabe | Aufwand | Details |
|---|---|---|---|
| 5.1 | API-Audit: Alle UI-Funktionen per API erreichbar | 8h | Systematische Prüfung: Jede UI-Aktion hat einen API-Endpoint. Fehlende Endpoints identifizieren und implementieren. Sidebar-Zustand, Tab-Auswahl, Filter-Zustand per API speichern/laden. |
| 5.2 | User-Preferences-API erweitern | 4h | UI-Einstellungen (Sidebar collapsed, theme, language, active tab, sort preferences) per API speichern/laden. |
| 5.12 | API-Health-Check-Script für KI | 4h | `scripts/ai_health_check.py`: Prüft alle API-Endpunkte, gibt strukturierten Report. KI kann das vor/nach Updates laufen lassen. |
| 5.13 | CI/CD-Pipeline für KI-Updates | 6h | `scripts/ai_deploy.py`: KI kann Build erstellen, Tests laufen, bei Erfolg deployen. Rollback bei Fehler. |
| 5.14 | API-Dokumentation vervollständigen | 4h | OpenAPI/Swagger prüfen. Alle Endpoints dokumentiert. Beispiele für KI. |
| 5.15 | Automatisiertes Backup-System | 8h | `pg_dump` + Storage-Backup als Cron-Job (nutzt Cron-Scheduler aus Phase 3.5). Backup-Konfiguration in Settings (Intervall, Aufbewahrung, Ziel: lokal/S3/Nextcloud). Restore-Script. Backup-Status in Dashboard. Notification bei Backup-Fehler. |
| 5.16 | MCP-Server Integration | 10h | LeoCRM als MCP-Server: Externe Tools (Claude Desktop, andere KI-Clients) können auf LeoCRM-Daten zugreifen. MCP-Tools für Contacts, Calendar, Mail, DMS. Authentifiziert via API-Token. MCP-Config-Endpoint `GET /api/v1/mcp/tools`. |
| 5.17 | MCP-Client Integration | 6h | LeoCRM-Agenten können externe MCP-Server nutzen (z.B. Web-Search, Code-Execution, externe Datenquellen). MCP-Client in tool_registry integriert. Admin kann MCP-Server in Settings konfigurieren. Agenten nutzen MCP-Tools wie native Tools. |
| 5.18 | Report Generator: PDF-Support & Druck-Funktionen | 8h | Backend: WeasyPrint für PDF-Generierung aus Jinja2-Templates. Vorgefertigte Berichte: Kontaktliste, Kalender (Woche/Monat), Firmenliste, Audit-Log. Druck-Optimierte Templates (A4, Landscape). `output_format` um `pdf` und `print` erweitern. |
| 5.19 | Report Generator: Frontend-Oberfläche | 10h | `Reports.tsx` Seite: Template-Liste, Template-Editor (Code-Editor für Jinja2), Report-Generierung mit Live-Preview, Download-History. Vorgefertigte Berichte als Buttons ("Kontakt-Liste drucken", "Kalender drucken"). Druck-Dialog mit Format-Auswahl (A4/A5/Landscape). |
| 5.20 | Custom Fields: Plugin-Felder in UI | 6h | Plugins sollen Custom Fields mitbringen können. Plugin-Manifest um `custom_fields` Definition erweitern. Frontend: Dynamische Custom-Field-Renderer in Contact-Detail, ContactEditModal. Feld-Typen: text, number, date, select, multiselect, boolean. Felder werden in `contacts.custom` JSONB gespeichert. |
| 5.21 | Tasks-Plugin | 12h | Eigenes Tasks-Plugin: Freie Aufgaben/Aktivitäten verwalten (Anruf protokollieren, Notiz, Besuch). Verknüpfung mit Kontakten. Tasks haben Status (open/in_progress/done), Priorität, Fälligkeitsdatum, Zuweisung an Nutzer. Tasks-Liste mit Filter. ARQ-Reminder für fällige Tasks. Plugin-Manifest, Models, Routes, Schemas, Frontend-Seite. |
| 5.22 | Saved Searches / Smart Lists | 6h | Jede Listen-Ansicht (Contacts, Mail, Calendar, DMS) bekommt Filter-Funktionalität. Filter können gespeichert werden (Name, Filter-Kriterien). Gespeicherte Filter erscheinen als Tabs oder Sidebar-Einträge. `saved_filters` Tabelle (tenant-scoped, user-scoped). Frontend: Filter-Builder UI, Save-Button, Load-Gespeicherte-Filter. |
| 5.23 | Deduplication / Merge (über KI/Automatisierung) | 6h | Contacts-Plugin bietet Dubletten-Erkennung: KI-gestützter Vergleich von Kontakten (Name, E-Mail, Telefon). Automation-Template: "Dubletten finden und zusammenführen". Merge-UI: Zwei Kontakte vergleichen, Felder auswählen, zusammenführen. `contact_merge_history` Tabelle. |
| 5.24 | PWA (Progressive Web App) | 6h | Frontend als PWA planen: `manifest.json`, Service Worker, Offline-Caching für statische Assets, Add-to-Home-Screen, App-Icon. Vite PWA Plugin installieren. Push-Notifications vorbereiten (Notification API). |
| 5.25 | Dashboard-System ausbauen | 8h | Plugins bringen Dashboard-Komponenten mit und melden diese an. Plugin-Manifest um `dashboard_widgets` erweitern (bereits in Architektur definiert aber nicht implementiert). Dashboard lädt Widgets dynamisch aus Plugin-Registry. Widget-Typen: Stat-Cards, Charts, Recent-Activity, Quick-Actions. Frontend: Dashboard-Grid mit drag-and-drop Widget-Positionierung. |
**Phase 5 Gesamt: ~145h**
---
### PHASE 6: React Hook Form + Zod überall
**Ziel:** Konsistente Form-Validierung in allen Formularen
Phase 4 (KI-UI-Steuerung) ← Baut auf Plugin-System auf
↓
Phase 5 (API-Vollständigkeit & Testbarkeit) ← KI kann selbstständig testen
↓
Phase 6 (React Hook Form + Zod) ← Qualität
↓
Phase 7 (Test-Vollendung) ← Wartbarkeit für KI
```
**Begründung der Reihenfolge:**
1. Phase 0 zuerst: Dependencies und Cleanup als Fundament
2. Phase 1 als Nächstes: Core-CRM-Feature (Contacts) muss vollständig sein. Größte Phase (~74h) weil 'company' überall im Code verankert ist.
3. Phase 2: Code-Splitting ist schnell und bringt sofortige Performance-Verbesserung
4. Phase 3: Plugin-UI-System — je früher desto besser, sonst wird Umbau später schwieriger
5. Phase 4: KI-UI-Steuerung baut auf Plugin-System auf (dynamische Routes, Tabs etc.). Bestehende WebSocket-Infrastruktur aus kommunikation Plugin als Referenz.
6. Phase 5: API-Vollständigkeit und E2E-Tests für KI-Wartbarkeit
7. Phase 6+7: Qualität und Test-Vollendung
---
## Was bei der Überprüfung gefunden wurde
### Phase 1 Korrektur: +41h Aufwand
Die ursprüngliche Schätzung von 33h für Phase 1 war **massiv unterschätzt**. Die gründliche Code-Analyse zeigte:
**'company' als entity_type ist in 6 Plugins verankert:**
-`system_settings.company_name`, `company_street` etc. → Das ist die CRM-Besitzer-Firmeninfo für Rechnungen. Bleibt wie es ist.
-`CalendarType = 'company'` → Das ist ein Kalender-Typ (Firmenkalender), keine Entity-Referenz. Kann bleiben.
### Bestehende WebSocket-Infrastruktur
Das `kommunikation` Plugin hat bereits eine vollständige WebSocket-Implementierung (`/api/v1/comm/ws`, `websocket_manager.py`). Diese kann als Referenz für die KI-UI-Steuerung (Phase 4) dienen — das spart Entwicklungszeit.
---
## KI-Wartbarkeit: Schlüssel-Anforderungen
Damit ein KI-Agent die App selbstständig warten kann:
1.**Vollständige API-Abdeckung:** Jede UI-Funktion per API steuerbar (Phase 5)
2.**E2E-Tests:** Playwright-Tests die KI ausführen kann (Phase 5)
3.**API-Health-Check:** Script das alle Endpunkte prüft (Phase 5)
4.**Test-Runner:** Script das alle Tests ausführt und strukturiert reportet (Phase 7)
5.**Deploy-Script:** KI kann Build erstellen, testen, deployen, rollback (Phase 5)
6.**Plugin-Richtlinien:** Klare Vorgaben damit KI neue Plugins erstellen kann (Phase 3)
2. ✅ Plan gründlich überprüft und korrigiert (+45h)
3. ⬜ Plan freigeben
4. ⬜ Phase 0 starten
5. ⬜ Planungsdokumente aktualisieren
---
## Test-Strategie (pro Phase)
### Phase 0: Vorbereitung & Cleanup
- **Pro Task:** Unit-Test für geänderte Funktionalität (z.B. Test dass lucide-react Icons rendern, Test dass date-fns formatiert, Test dass Storage Backend local+S3 funktioniert)
- **Regression:** Alle bestehenden Tests müssen weiterhin durchlaufen
- **Lizenz-Test:** `pip-licenses` Script prüft dass keine AGPL-Packages mehr in requirements.txt
### Phase 1: Unified Contact Model
- **Pro Task:** API-Integration-Test (httpx + pytest) für jeden geänderten Endpoint
- **DB-Migration-Test:** Test dass Migration 0023 (entity_type company→contact) korrekt ausführt und rollbackbar ist
- **Plugin-Test:** Pro Plugin (entity_links, unified_search, calendar, tags, mail) Test dass entity_type='contact' funktioniert
- **Frontend-Test:** Vitest für ContactDetail, ContactEditModal, ContactPerson-Verwaltung
- **Cross-Tenant-Test:** Test dass Tenant-Isolation nach Migration noch funktioniert
### Phase 2: Code-Splitting & Performance
- **Bundle-Test:** Test dass Initial-Bundle < 300KB (vorher alle Pages im Bundle)
- **Virtual Scrolling Test:** Test mit 10.000 Datensätzen — Rendering-Zeit < 500ms
- **Lazy-Loading Test:** Test dass Plugin-Pages nicht im Initial-Bundle sind
### Phase 3: Plugin-UI-System
- **PluginRegistry-Test:** Test dass Manifests korrekt geladen und gerendert werden
- **PluginLoader-Test:** Test dass lazy-loaded Komponenten mit Suspense funktionieren
- **Plugin-Install-Test:** Test dass ZIP-Upload validiert und installiert wird
- **Error-Boundary-Test:** Test dass fehlerhaftes Plugin nicht die ganze App crashen lässt
### Phase 3.5: Automation & Agents
- **Cron-Scheduler-Test:** Test dass Cron-Jobs zur richtigen Zeit enqueued werden
- **Workflow-Timeout-Test:** Test dass abgelaufene Workflows cancelled werden
- **Agent-Runner-Test:** Test dass Agent LLM-Call ausführt und Ergebnis zurückgibt (Mock-LLM)
- **Automation-Engine-Test:** Test dass Event-Trigger → Conditions → Actions korrekt ausgeführt werden
- **Agent-zu-Agent-Test:** Test dass Agent A Nachricht an Agent B sendet und B reagiert
- **Rate-Limiting-Test:** Test dass Agent nach Max-Ausführungen gestoppt wird
- **Dry-Run-Test:** Test dass Dry-Run keine destruktiven Actions ausführt
### Phase 4: KI-UI-Steuerung
- **WebSocket-Test:** Test dass Commands korrekt gesendet und empfangen werden
- **Command-Test:** Pro Command-Typ (navigate, filter, open_contact, modal, tab, settings) ein Test
- **Feedback-Test:** Test dass Frontend Bestätigung an KI zurücksendet
1.**Niemals Tests ändern** um sie grün zu bekommen — Code fixen nicht Tests anpassen
2.**Niemals .env committen** — Secrets gehören nicht ins Repo
3.**Jede DB-Änderung braucht Alembic-Migration** — keine manuellen SQL-Changes
4.**Jede API-Route braucht RBAC** — `require_permission()` auf jedem Endpoint
5.**Jedes Plugin-Model braucht TenantMixin** — tenant_id auf jeder Tabelle
6.**Frontend-Änderungen brauchen i18n** — alle Texte in de.json und en.json
7.**Pro Task ein Commit** — nicht mehrere Tasks in einem Commit
8.**Nach jedem Task: Tests + Build verifizieren** — keine Regressionen
9.**Nach jedem Task: Progress aktualisieren** — `PROGRESS.md` im Repo aktualisieren mit: Task-Nummer, Status (done/in-progress/blocked), Datum, was gemacht wurde, was als Nächstes ansteht. **Zwingend für jeden Agenten der am Plan arbeitet.**
- ✅ 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
---
## Phase 6: Manifest-Anpassung & Konsolidierung
**Ziel:** Alle in Phase 4 und 5 definierten Manifest-Felder werden ins `PluginManifest` integriert, bestehende Manifeste aktualisiert, und das Manifest-System finalisiert.
**Wichtig:** Diese Phase baut auf Phase 4 (Versioning) und Phase 5 (Marketplace) auf und muss als letztes durchgeführt werden.
op.execute("ALTER TABLE IF EXISTS mail_accounts ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES users(id) ON DELETE SET NULL")
op.execute("DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'mail_accounts') THEN CREATE INDEX IF NOT EXISTS ix_mail_accounts_owner ON mail_accounts (owner_id); END IF; END $$")
op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS aggregate_type VARCHAR(100)")
op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS aggregate_id UUID")
op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()")
op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS correlation_id UUID")
op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS schema_version INTEGER NOT NULL DEFAULT 1")
op.execute("CREATE INDEX IF NOT EXISTS ix_event_outbox_aggregate ON event_outbox (tenant_id, aggregate_type, aggregate_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_event_outbox_correlation ON event_outbox (correlation_id)")
op.execute("DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'event_outbox') THEN CREATE INDEX IF NOT EXISTS ix_event_outbox_aggregate ON event_outbox (tenant_id, aggregate_type, aggregate_id); END IF; END $$")
op.execute("DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'event_outbox') THEN CREATE INDEX IF NOT EXISTS ix_event_outbox_correlation ON event_outbox (correlation_id); END IF; END $$")
_exec("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_platform_admin') THEN CREATE ROLE crm_platform_admin NOSUPERUSER NOBYPASSRLS NOLOGIN; END IF; END $$;")
# Step 2: crm_migration keeps BYPASSRLS for data migrations (NOSUPERUSER)
# crm_migration is the table owner and needs to run tenant-wide data migrations
_exec("ALTER ROLE crm_migration NOSUPERUSER BYPASSRLS")
# Step 3: Transfer ALL table ownership to crm_migration (only for tables that exist)
fortableinALL_TABLES:
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN ALTER TABLE public.{table} OWNER TO crm_migration; END IF; END $$")
# Transfer sequence ownership
_exec("DO $$ DECLARE r RECORD; BEGIN FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' LOOP EXECUTE format('ALTER SEQUENCE public.%I OWNER TO crm_migration', r.sequence_name); END LOOP; END $$;")
_exec(f"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {role}")
_exec(f"REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {role}")
_exec(f"REVOKE ALL PRIVILEGES ON SCHEMA public FROM {role}")
# Step 5: Drop crm_runtime role — revoke default privileges first, then drop
_exec("ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM crm_runtime")
_exec("ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE, SELECT ON SEQUENCES FROM crm_runtime")
_exec("DO $$ BEGIN DROP ROLE IF EXISTS crm_runtime; EXCEPTION WHEN insufficient_privilege THEN NULL; WHEN dependent_objects_still_exist THEN NULL; END $$")
# Step 6: Grant schema USAGE to runtime roles
_exec("GRANT USAGE ON SCHEMA public TO crm_api")
_exec("GRANT USAGE ON SCHEMA public TO crm_worker")
_exec("GRANT USAGE ON SCHEMA public TO crm_auth")
# Step 7: Grant permissions to crm_auth (identity tables only)
fortable,privsinAUTH_TABLES.items():
priv_str=", ".join(privs)
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT {priv_str} ON public.{table} TO crm_auth; END IF; END $$")
# Step 8: Grant CRUD on tenant tables to crm_api and crm_worker (only for tables that exist)
fortableinTENANT_TABLES:
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_api; GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_worker; END IF; END $$")
# Grant sequence USAGE to crm_api and crm_worker
_exec("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_api")
_exec("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_worker")
# Step 9: Grant global table access to crm_api (except alembic_version)
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_api; END IF; END $$")
# Step 10: Grant worker global table access
fortable,privsinWORKER_GLOBAL_TABLES.items():
priv_str=", ".join(privs)
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT {priv_str} ON public.{table} TO crm_worker; END IF; END $$")
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_worker; END IF; END $$")
# Step 11: Drop ALL old RLS policies and create new fail-closed ones
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN DROP POLICY IF EXISTS tenant_isolation ON public.{table}; DROP POLICY IF EXISTS {table}_tenant_isolation ON public.{table}; ALTER TABLE public.{table} ENABLE ROW LEVEL SECURITY; ALTER TABLE public.{table} FORCE ROW LEVEL SECURITY; {policy_template.format(table=table)}; END IF; END $$")
# Step 12: Disable RLS on global tables
fortableinGLOBAL_TABLES:
_exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN DROP POLICY IF EXISTS tenant_isolation ON public.{table}; DROP POLICY IF EXISTS {table}_tenant_isolation ON public.{table}; ALTER TABLE public.{table} DISABLE ROW LEVEL SECURITY; END IF; END $$")
# Step 13: Set default privileges for crm_migration owner
_exec("ALTER DEFAULT PRIVILEGES FOR ROLE crm_migration IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO crm_api")
_exec("ALTER DEFAULT PRIVILEGES FOR ROLE crm_migration IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO crm_worker")
_exec("ALTER DEFAULT PRIVILEGES FOR ROLE crm_migration IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO crm_api")
_exec("ALTER DEFAULT PRIVILEGES FOR ROLE crm_migration IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO crm_worker")
"FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE"
)
op.execute("DROP INDEX IF EXISTS uq_workspaces_tenant_id")
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.