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: 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.
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: _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).
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.
- 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)
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.
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.