# Bifröst – AI Workspace Kontextgebundene KI-Arbeitsplattform, zusammen mit Hugin (Register für Regeln, Skills und Befehle; von dort stammt auch das Layout der Oberfläche) und Munin (Gedächtnis). Bifröst ist die Brücke zwischen den Welten: Sie verbindet Daten, KI und die Menschen, die jeden Schritt nach außen freigeben. In Texten *Bifröst*, technisch ohne Umlaut (*bifrost*); CLI `aiw`, Variablen `AIW_*` und Metriken `aiw_*` bleiben unverändert. Architektur und Entscheidungen: [ai_workspace_architekturkonzept.md](ai_workspace_architekturkonzept.md). Stand: **Phase 3 plus MCP-Server zentral und je Workspace, Uploads mit DuckDB-Analyse, lesende Datenbanken (PostgreSQL, Oracle) als MCP-Verbindungen, bereinigtes Markdown, Munin als Gedächtnis, Regeln, Skills und Befehle aus Hugin, Bereich *Repository*, abgeglichene Ordner mit Dokumenten-Auslese, feinere Policies mit globalen Approvern sowie Nutzung, Kosten und Audit-Log im UI.** Es gibt Anmeldung, Teams, Nutzer, Workspaces mit Rollen, Git-Quellen und Chat. Jede Nachricht wird als Run im Worker verarbeitet; das Modell antwortet über die Responses API und liest Repositories mit eingestuften, lesenden Git-Tools. Jeder Chat hat seinen eigenen Branch `ai/…`: Die KI committet Änderungen frei darauf (`commit_change`); pushen und den Pull Request (Bitbucket Data Center) erstellen nur Menschen. Tools, die eine Freigabe brauchen, pausieren den Run, ohne einen Worker zu belegen. Antworten und Tool-Schritte werden per SSE gestreamt. Modelle, API-Keys und Tool-Einstufung werden im UI verwaltet. Antworten werden als Markdown serverseitig gerendert und mit `nh3` bereinigt (keine externen Bilder, Linkziele sichtbar). Chats lassen sich umbenennen und löschen. ## Lokal starten Voraussetzungen: Docker, [uv](https://docs.astral.sh/uv/), git. ```bash docker compose up -d # PostgreSQL 18 auf 127.0.0.1:55433 cp .env.example .env uv sync uv run aiw gen-key # Ausgabe als AIW_SECRET_KEY in .env eintragen uv run aiw migrate uv run aiw seed-demo # Team "demo": admin (Plattform-Admin), entwicklung, fachbereich, freigabe uv run aiw demo # optional: Demo-Workspaces Bestellservice und MCP-Entwicklung uv run aiw web # http://127.0.0.1:8000 uv run aiw worker # zweites Terminal ``` Kürzer mit dem Startskript (PostgreSQL, Migrationen, Web und Worker in einem Terminal; Ersteinrichtung mit `--setup`, von null mit `--reset`, Oracle mit `--oracle`; Details in [docs/lokal-starten.md](docs/lokal-starten.md#von-null-starten)): ```bash scripts/dev.sh # http://127.0.0.1:8765, Strg+C stoppt alles scripts/dev.sh --reset # von null mit Demo-Workspaces; löscht alle lokalen Daten (mit Rückfrage) ``` Zum Entwickeln mit sichtbaren Logs, Health-Checks, Metriken und Oracle-Quellen: [docs/lokal-starten.md](docs/lokal-starten.md). Vergleich mit OpenCode, Lücken und Arbeitsplan: [docs/opencode-vergleich.md](docs/opencode-vergleich.md). Anmelden mit `admin` und dem Passwort aus `AIW_DEMO_PASSWORD`. Weitere Nutzer legt ein Plattform-Admin unter *Administration → Nutzer* an; sie bekommen ein Einmal-Passwort und setzen beim ersten Login ein eigenes. Ohne Demo-Passwort (z. B. auf einem Server) den ersten Admin mit `uv run aiw create-admin ` anlegen. Danach einen Workspace anlegen und ein Git-Repository verbinden. Zum Ausprobieren reicht ein lokales Repository, z. B. `file:///pfad/zu/einem/repo` (dafür `AIW_GIT_ALLOW_LOCAL=true` setzen). Der Chat erscheint erst, wenn eine Quelle erfolgreich getestet wurde. **Demo-Workspaces:** `uv run aiw demo` legt drei Workspaces an (Inhalte in `app/demo/`, reproduzierbar). Mitglieder sind jeweils die Demo-Nutzer, benannt nach ihrer Rolle: `admin` (Workspace-Admin), `entwicklung` und `fachbereich` (Mitglieder), `freigabe` (Freigeber). Die beiden Workspaces mit Daten haben je ein lokales Bare-Repository unter `data/demo/` (drei Wochen Verlauf von Admin, Entwicklung und Fachbereich, ein Feature-Branch), einer Datei zur Analyse, einem Dokumentenordner und sechs Beispielfragen. Protokolle sind relativ zu den Commits datiert, damit die Geschichte an jedem Tag stimmt. - **Demo: Bestellservice** (`bestellservice`): kleiner Python-Dienst, `bestellungen_2026.csv` (1200 Bestellungen), *Projektunterlagen* (Protokolle, Wartungsvertrag, Betriebshandbuch). Eingebauter Widerspruch: Der Code rechnet mit 10 % Rabatt ab 500 €, die spätere Abstimmung beschließt 5 % ab 1.000 €. Dazu die **Shop-Datenbank** (`app/demo/shop_db.py`): eine eigene Datenbank `_demo_shop` im PostgreSQL von Bifröst mit Kunden, Artikeln, Bestellungen, Positionen und Retouren (2025/2026) und der nur lesenden Rolle `demo_leser`, angebunden über den PostgreSQL-Treiber. Läuft der DB-Dienst aus `scripts/dev.sh` (Port 8791), registriert die Demo den Treiber (oder nutzt einen vorhandenen mit derselben URL), stuft seine lesenden Tools ein und verbindet die Datenbank; sonst überspringt sie diesen Teil mit Hinweis. In einem schon vorhandenen Demo-Workspace kommt die Datenbank ohne `--reset` dazu. URL und Schlüssel des Dienstes lassen sich mit `AIW_DEMO_DBMCP_URL` und `AIW_DEMO_DBMCP_KEY` ändern. - **Demo: MCP-Entwicklung** (`mcp`): MCP-Server `ticket-mcp` für ein Service-Desk (FastMCP, Streamable HTTP, Tests, `AGENTS.md`), `tool_aufrufe_2026_10.csv` (2000 Aufrufe aus der Testumgebung), *MCP-Unterlagen* (interner Leitfaden, MCP in Kürze, Anbindung an Bifröst, Service-Desk-API, Review-Protokoll). Eingebaut: `close_ticket` ist als `readOnlyHint` markiert, Bifröst würde es also als lesend vorschlagen; `search_tickets` hat keine Obergrenze. Das Review hat beides beanstandet, das Log zeigt die Folgen (große, langsame Suchen). - **Demo: Nur Chat** (`chat`): keine Datenquellen; die Einstellung *Ohne Datenquellen chatten* ist an. Drei Beispielfragen stehen wörtlich auch im Bestellservice (Wartungsvertrag, Rabatt laut Protokoll, Retouren), dazu allgemeine Aufgaben. So sieht man nebeneinander, was die Datenanbindung ausmacht: Hier sagt die KI, dass sie Vertrag, Code und Datenbank nicht kennt. Ein zweiter Aufruf ändert nichts (außer die Shop-Datenbank nachzutragen bzw. neu zu befüllen); `aiw demo mcp --reset` legt eine Demo neu an (löscht dabei ihre Chats). Braucht `AIW_GIT_ALLOW_LOCAL=true`; das Modell richtet weiterhin ein Plattform-Admin ein. Die eingebauten Tools stuft der Befehl nach Vorschlag ein, falls noch nicht geschehen. **Beispielfragen und Rückmeldungen:** Workspace-Admins hinterlegen unter *Einstellungen → Allgemein* bis zu acht Beispielfragen (`workspaces.settings["examples"]`); Übersicht und leerer Chat zeigen sie, ein Klick stellt die Frage. Unter jeder Antwort fragt Bifröst „Hilfreich?“ (👍/👎, optional Kommentar; `answer_feedback`, eine Rückmeldung je Person und Antwort). *Nutzung und Kosten* zeigt den Anteil hilfreicher Antworten, je Modell und die Kommentare mit Link zur Antwort, dazu einen CSV-Export für die Auswertung des PoC. **Alles integrieren:** Schritt für Schritt, was nach dem Start noch einzurichten ist (Modell, Tools, Munin, Hugin, Dokumenten-Auslese, Bitbucket, Benachrichtigungen), steht in [docs/einrichtung.md](docs/einrichtung.md), zum Abhaken in [docs/checkliste.md](docs/checkliste.md). **Modell einrichten** (einmalig, als Plattform-Admin `admin`): unter *Modelle und Zugänge* einen Endpunkt anlegen (`https://api.openai.com/v1`), einen API-Key für *alle Teams* hinterlegen und das Modell `gpt-6-luna` hinzufügen. Das erste Modell wird automatisch Standard. Mit *Verbindung testen* prüfen. Danach unter *Tools* die Git-Tools einstufen (*Vorschläge übernehmen*); bis dahin sind sie gesperrt. Später kommt das AI Gateway als weiterer Endpunkt hinzu, mit einem Key je Team. **Schutz vor Prompt Injection (`app/security`):** Die harte Grenze sind Plattform-Regeln, die nicht vom Modell abhängen (angebotene Tools, Policies, Freigaben, Rechte, Push nur durch Menschen). Zusätzlich gehen alle fremden Inhalte (Tool-Ergebnisse, Dateien, Kommentare, Konfliktdateien, Dienst-Anleitungen) eingefasst als `` an das Modell; reservierte Tags im Inhalt werden entschärft. `signals.py` erkennt typische Injection-Formulierungen, blockiert nichts, sondern protokolliert (`untrusted_signal`, Metrik `aiw_untrusted_signals_total`) und kennzeichnet den Inhalt. Neue Muster dort ergänzen und ein Beispiel in `tests/injection_corpus.py` aufnehmen; `tests/test_security.py` spielt Szenarien mit einem Modell durch, das der Injection folgt, und prüft, dass alle schreibenden Routen ein CSRF-Token verlangen. **Datenbank-Treiber (`services/db-mcp`):** Lesende MCP-Server für Datenbanken, ein Dienst je Datenbanktyp mit gemeinsamem Kern: `db-mcp-postgres`, `db-mcp-oracle`. In Bifröst heißen sie Datenbank-Treiber: Ein Plattform-Admin registriert jeden einmal unter *Administration → Datenbank-Treiber* (URL und Schlüssel des Dienstes) und stuft seine vier Tools ein. Ein Treiber hat keine eigenen Daten; sein Schlüssel öffnet nur den Dienst. Die Zugangsdaten der Datenbanken tragen Workspace-Admins ein; Bifröst schickt sie je Aufruf als Header. Details im [README des Dienstes](services/db-mcp/README.md). **Online-Hilfe:** Der Knopf *Hilfe* oben führt zur passenden Stelle der Hilfe für die aktuelle Seite (`/hilfe`). Sie ist nur nach Anmeldung erreichbar und enthält Anleitungen für Nutzer, Freigeber und Prüfer, Workspace- und Plattform-Admins, dazu Begriffe und häufige Fragen, mit Suche. Die Seiten liegen als Markdown in `app/help/pages/` und werden mit dem Code gepflegt. Ein Test meldet, wenn ein neues eingebautes Tool in der Admin-Hilfe fehlt. **Projektregeln:** Liegt im Repository eine `AGENTS.md` (Code-Stil, Struktur, Befehle, Commit-Nachrichten), gibt die Plattform sie der KI als Projektkonvention mit, vom selben Stand, den die Tools lesen (Chat-Branch, sonst Ziel-Branch), höchstens 8 KB. Sie ist nachrangig zu den Regeln der Plattform und kann keine Rechte erweitern. Jede Antwort zeigt, welche Regeldatei galt. **Plan und Planungsmodus:** Das Tool `update_plan` (Vorschlag `write_internal`) unter *Tools* einstufen. Bei größeren Aufgaben legt die KI damit einen Plan aus Schritten an, die sie als offen, in Arbeit oder erledigt markiert. Er steht oben im Chat, geht in jede Anfrage, in die Arbeitsnotiz und in die PR-Beschreibung ein, und *Weiterarbeiten* setzt beim nächsten offenen Punkt an. Mit *Erst planen* am Eingabefeld (bei vorhandenem Plan: *Plan überarbeiten*) schaltet man den Chat in den Planungsmodus, beliebig oft im Wechsel mit dem normalen Chat: Die KI bekommt nur lesende Tools und `update_plan` und ändert nichts. Ruft sie trotzdem ein schreibendes Tool auf, wird der Aufruf abgewiesen. *Plan umsetzen* beendet den Modus und startet die Umsetzung. **Lange Sessions:** Der Verlauf geht nach Token-Budget an die KI (`AIW_HISTORY_MAX_TOKENS`, Standard 60 000, höchstens 60 % des Kontextfensters). Ältere Tool-Ausgaben werden gekürzt, und was nicht mehr passt, fasst die KI in einer *Arbeitsnotiz* zusammen, die oben im Chat einsehbar ist. Die Tool-Schritte je Antwort stellt der Workspace-Admin unter *Einstellungen → Allgemein → Antworten* ein (Standard `AIW_MAX_TOOL_ROUNDS`=12, höchstens `AIW_MAX_TOOL_ROUNDS_LIMIT`=40). Ist das Limit erreicht, fasst die KI den Stand zusammen, und *Weiterarbeiten* setzt fort. Jede Antwort zeigt, wie voll der Kontext war, und der Chat zeigt Tokens und Kosten bisher. **Code ändern (Chat = Branch):** Die Tools `commit_change` (Vorschlag `write_internal`, erlaubt) und `git_pr_comments` (lesend) unter *Tools* einstufen. Der Ablauf: 1. **Committen:** Die KI legt Änderungen mit `commit_change` als Commits auf den Branch des Chats `ai/c-`. Der Branch entsteht beim ersten Commit und beginnt auf dem aktuellen Stand des Ziel-Branches. Je Commit gibt es keine Freigabe; die lesenden Git-Tools sehen danach den Stand des Branches. 2. **Prüfen:** Die Ansicht *Änderungen* oben im Chat zeigt Branch, Commits und Diff. Noch nicht übertragene Commits lassen sich einzeln zurücknehmen (*Letzten Commit zurücknehmen*). 3. **Übertragen:** Die Commits bleiben in der Plattform, bis ein Mitglied des Workspace klickt: - *Branch pushen*, z. B. um lokal oder in der CI zu testen. - *Pull Request erstellen*: Pushen und den PR anlegen, standardmäßig als Entwurf, später *Als bereit markieren*. Die Beschreibung bekommt Commits, Autor und einen Link zum Chat (mit `AIW_PUBLIC_URL`). - *Änderungen übertragen*: Spätere Commits gehen erst auf diesen Klick in den offenen PR. 4. **Review:** Der Worker fragt offene PRs alle `AIW_PR_POLL_SECONDS` (300) ab; *Status aktualisieren* fragt sofort ab. Bei neuen Kommentaren, Merge oder Ablehnung bekommen Chat-Ersteller und PR-Ersteller eine E-Mail. *Kommentare einarbeiten* startet eine Antwort, in der die KI die Kommentare mit `git_pr_comments` liest und neue Commits anlegt. In Bitbucket antworten kann die KI nicht. Nach Merge oder Ablehnung löscht die Plattform den Branch im Git-Server, wenn er noch auf dem zuletzt übertragenen Stand steht (abschaltbar je Quelle; der technische Account braucht das Recht, `ai/*` zu löschen). 5. **Abschluss:** Nach Merge oder Ablehnung ist der Branch geschlossen; der nächste Commit im selben Chat beginnt einen neuen Branch (`…-2`). `commit_change` nimmt einzelne Änderungen (ersetzen, neu schreiben, löschen) oder einen Unified Diff über mehrere Dateien (`patch`, auch Löschen und Umbenennen). Beim Commit prüft die Plattform Python, JSON, TOML, XML und YAML auf Syntax, ohne Code auszuführen. Fehler sind nur Warnungen: Der Commit entsteht trotzdem, die KI bekommt die Warnungen zurück, und sie stehen in der Ansicht *Änderungen* am Commit. **Repository:** Hat ein Workspace eine Git-Quelle, zeigt die Seitenleiste *Repository*. Der Reiter *Neu* listet die Commits des Ziel-Branches nach Tagen (Filter: Zeitraum, Autor, Pfad) und hebt hervor, was seit dem letzten Besuch der Person dazugekommen ist (`repo_visits`). Ein Klick zeigt den Commit mit Diff. *Branches* listet alle Branches des Servers mit Vorsprung und Rückstand gegenüber dem Ziel-Branch; Chat-Branches verweisen auf ihren Chat und den PR-Status. *Pull Requests* listet offene PRs aus Bitbucket Data Center (Autor, Branches, Reviewer, Kommentare) mit Diff gegenüber dem gemeinsamen Stand; *Dateien* zeigt Baum und Dateien eines beliebigen Branches mit Verlauf. Von dort starten Chats, die neue Commits zusammenfassen, einen Commit oder eine Datei erklären, einen Pull Request reviewen oder auf einem bestehenden Branch arbeiten: Dann lesen die Tools diesen Branch, der Chat-Branch zweigt davon ab, und Abgleich und Pull Request beziehen sich auf ihn (`chat_sessions.git_base`). Die Seite liest aus dem Klon der Plattform und holt höchstens einmal pro Minute alle Branches (`refs/remotes/origin/*`); *Aktualisieren* holt sofort. CI-Status wird nicht angezeigt. **Abgeglichene Ordner:** Für Dokumente auf dem Rechner einer Person, die sich regelmäßig ändern. Der Browser gleicht einen lokalen Ordner ab (`showDirectoryPicker` mit gemerktem Handle in IndexedDB, sonst ``): Er holt `/folders/{id}/manifest` (Pfad → Größe, Änderungszeit), lädt nur Neues und Geändertes einzeln hoch und meldet zum Schluss die vorhandenen Pfade (`prune`). Bifröst speichert nur den Text (`doc_files`, Volltextindex `german`); gleicher Inhalt (SHA-256) wird nicht neu ausgelesen. Textformate dekodiert Bifröst selbst; Office, PDF und Bilder schickt der Worker als Base64 an den zentralen MCP-Server der Art *Dokumenten-Auslese* (Tool und Parameter wählt der Plattform-Admin). Sichtbarkeit je Ordner: *nur ich* (nur in eigenen Chats bei eigenen Nachrichten; die Antworten sehen trotzdem alle Mitglieder) oder *ganzer Workspace* (mit Bestätigung). Die KI liest mit `docs_list`, `docs_search`, `docs_read`. Der Text zählt aufs Speicherkontingent. Lokale stdio-MCP-Server erreicht Bifröst nicht; Dienste laufen zentral über HTTP. **Mit main abgleichen:** Hat der Ziel-Branch neue Commits, zeigt die Ansicht *main ist weiter*. *Mit main abgleichen* übernimmt sie per Merge-Commit in den Chat-Branch. Es gibt kein Rebase, bereits übertragene Commits bleiben also gültig, und der nächste Push ist ein normaler Fast-Forward. Der Diff zeigt danach weiter nur die eigenen Änderungen. Bei Konflikten passiert nichts. Die Ansicht zeigt dann *Konflikt mit main*, und *Konflikte von der KI lösen lassen* startet eine Antwort, in der die KI mit dem Tool `merge_target` (Vorschlag `write_internal`) die Konfliktdateien mit Markern liest und für jede eine Lösung angibt. Auch dieser Merge-Commit bleibt in der Plattform, bis ein Mensch ihn überträgt. Gepusht wird nie mit Überschreiben. Steht der Branch auf dem Server auf einem anderen Stand, z. B. nach Commits von außen, bricht der Push ab. Geht der Klon verloren, holt die Plattform gepushte Branches vom Server oder baut die Commits aus den gespeicherten Änderungen exakt nach. Übertragen wird per `git push` über HTTPS aus dem Klon der Plattform; das Token geht als Header mit, nie in der URL. Ohne Nutzername an der Quelle als Bearer (Bitbucket Data Center), mit Nutzername als Basic mit dem Token als Passwort (Gitea, Forgejo, GitLab, GitHub, klassische HTTPS-Server). SSH wird nicht unterstützt. Für Bitbucket Data Center an der Quelle *Pull Requests: Bitbucket Data Center* wählen und ein HTTP Access Token mit Schreibrecht hinterlegen, das per Branch Permission auf `ai/*` beschränkt ist. Für `ai/*`-Branches sollte die CI keine Secrets bekommen. **Dateien analysieren:** im Workspace unter *Dateien zur Analyse* CSV, TSV, JSON(L) oder Parquet hochladen; jede Datei wird zu einer DuckDB-Tabelle. Die Tools `data_list_tables`, `data_describe_table` und `data_query` einstufen (Vorschlag: lesend, erlaubt). Abfragen laufen schreibgeschützt ohne Datei- und Netzzugriff, mit Zeit-, Speicher- und Zeilenlimit. Ein Workspace mit Upload ist auch ohne Git-Quelle chatbereit. **Datenbank anbinden (lesend):** Im Workspace unter *Einstellungen → MCP-Server → Datenbank hinzufügen* den Datenbanktyp (einen der Treiber), Kurzname, Host, Datenbank, Nutzer und Passwort angeben. Der Host muss in `AIW_DB_ALLOWED_HOSTS` stehen, lokal reicht `AIW_DB_ALLOW_LOCAL=true`. Empfohlen ist ein Nutzer nur mit SELECT; der Verbindungstest meldet Schreibrechte. Jede Abfrage läuft in einer schreibgeschützten, zurückgerollten Transaktion (genau eine Anweisung), mit Zeitlimit und Zeilenlimit. Die Tools heißen nach dem Kurznamen (`shop__list_tables`, `shop__describe_table`, `shop__query`) und haben die Einstufung des Treibers; unter *Administration → Tools* erscheinen nur die Tools des Treibers. Vorgaben im Workspace unter *Tools* gelten je Datenbank. Den SQL-Dialekt beschreibt der Treiber in seiner Anleitung. Ergebnisse mit Spalten und Zeilen erscheinen als Tabelle mit CSV und Diagramm. Zum Ausprobieren: `beispiele/demo_shop.sql` (Anleitung im Kopf der Datei). **Nutzung, Kosten, Audit:** Unter *Administration → Nutzung und Kosten* Tokens und Kosten je Team, Workspace, Modell, Nutzer oder Tag (Preise je 1 Mio. Tokens am Modell, Währung `AIW_COST_CURRENCY`), dazu unter *Administration → Runs* die Run-Liste. *Administration → Audit-Log* mit Filtern und CSV-Export. Workspace-Admins sehen beides für ihren Workspace. Verbrauch wird je Modellaufruf festgehalten und bleibt beim Löschen von Chats erhalten. **Freigaben feiner steuern:** Je Tool legt der Plattform-Admin die Freigabestufe fest (Workspace, also Admin oder Freigeber, oder globaler Approver; globale Approver unter *Nutzer*) und ob das Vier-Augen-Prinzip gilt. Unter *Einstellungen → Allgemein → Freigaben* kann ein Workspace Modus B wählen: Dann braucht jede Schreibaktion in ein Fremdsystem (`write_external`) eine Freigabe, auch wenn das Tool erlaubt ist. Workspaces können zusätzlich Vier Augen verlangen, Tools einer bestimmten Datenbank verschärfen (z. B. `prod__query` nur mit Freigabe) und *Regeln je Quelle* für Repositories setzen. Auch lesende Tools können so eine Freigabe verlangen; die KI erhält das Ergebnis erst nach der Entscheidung. Vor der Ausführung werden die Rechte des Entscheiders erneut geprüft. **Workspace-Aufbau:** Die Übersicht zeigt die Chats (mit Suche über Titel und Nachrichten) und eine Zeile, welche Daten bereitstehen. Unter *Daten* liegen Datenquellen (verbinden, testen und bearbeiten nur Admins) und Dateien zur Analyse (hochladen dürfen alle Mitglieder). Für Admins liegt der Rest unter *Einstellungen* in Reitern: Allgemein (Name, Beschreibung, Modell, Webhook, Löschen), Mitglieder (Rolle direkt ändern), Tools und Regeln, MCP-Server. **Rollen im Workspace:** *Mitglied* chattet und lädt Dateien hoch. *Freigeber* entscheidet zusätzlich Freigabeanträge des Workspace und bekommt die Benachrichtigungen dazu, verwaltet den Workspace aber nicht. *Admin* darf beides und verwaltet Einstellungen, Mitglieder, Quellen und Tools. Anträge der Stufe *globaler Approver* entscheiden weiterhin nur globale Approver. **Speicherkontingent:** Dateien zur Analyse zählen je Workspace gegen ein Kontingent, Standard `AIW_WORKSPACE_STORAGE_MB` (500 MB). Gezählt wird die Größe der DuckDB-Tabelle (mindestens etwa 0,5 MB je Datei, bei großen Dateien meist weniger als das Original). Die Seite *Daten* zeigt die Belegung und warnt ab 80 %. Ist das Kontingent voll, wird ein Upload abgelehnt. Anpassen kann es der Plattform-Admin je Workspace unter *Administration → Teams → Team*. Git-Klone zählen nicht; für sie gilt `AIW_GIT_MAX_REPO_MB` je Repository. **Workspace löschen:** Unter *Einstellungen → Allgemein*, mit Eingabe des Namens. Der Workspace kommt in den Papierkorb (für alle unsichtbar) und lässt sich auf der Workspace-Übersicht `AIW_WORKSPACE_TRASH_DAYS` Tage lang wiederherstellen oder sofort endgültig löschen; danach löscht ihn der Worker. Endgültig entfernt werden Quellen (mit verschlüsselten Zugangsdaten und Git-Klon), Chats, Runs, Freigabeanträge, Uploads, Tool-Vorgaben und MCP-Freischaltungen samt Keys. Audit-Log und Verbrauch bleiben. Nicht möglich, solange Runs laufen oder auf Freigabe warten. **Im Chat:** *Antwort neu erzeugen* bzw. *Erneut versuchen* nach einem Fehler, *Exportieren* als Markdown, Antworten kopieren, Strg+Enter sendet. Ergebnisse von `data_query` und `db_query` erscheinen unter der Antwort als Tabelle mit CSV-Download und, wenn sie passen (eine Beschriftung, bis zu drei Zahlenspalten, höchstens 40 Zeilen), als Balken- oder Liniendiagramm. **Benachrichtigungen:** Wartet ein Antrag, bekommen alle, die ihn entscheiden dürfen, eine E-Mail; die auslösende Person erfährt die Entscheidung. Jeder Workspace kann zusätzlich einen Teams- oder Slack-Webhook eintragen. E-Mail-Adresse und Abbestellen im *Profil* (Klick auf den Namen oben rechts). Zustellung über eine Outbox im Worker mit bis zu fünf Versuchen; Stand unter *Administration → Benachrichtigungen*. Einrichtung über `AIW_SMTP_*` und `AIW_PUBLIC_URL` (für Links). **Administration:** Teams ohne Workspaces, Modelle (außer dem Standard) und Endpunkte ohne Modelle lassen sich löschen. **Munin als Gedächtnis:** MCP-Server mit Art *Gedächtnis* registrieren, *Tools abrufen*, *Empfehlung übernehmen* (`forget` wird gesperrt), im Workspace mit dem Munin-Key des Projekts freischalten. Die Anleitung des Servers geht in den System-Prompt. Unter *Gedächtnis* sehen Mitglieder die Einträge; löschen können nur Workspace-Admins. **Hugin anbinden:** Der Plattform-Admin trägt unter *Administration → Hugin* den Host ein (muss in `AIW_MCP_ALLOWED_HOSTS` stehen; `AIW_HUGIN_URL` füllt ihn nur vor) und sieht dort, welche Workspaces Hugin mit welchem Tenant nutzen. Workspace-Admins tragen unter *Einstellungen → Hugin* den Integrations-Key (`hi_…`) ihres Tenants ein, wählen Pakete, gleichen ab und übernehmen. Versionen und Prüfsummen werden festgeschrieben, Pflichtpakete des Tenants kommen immer mit. Regeln gehen in den System-Prompt, Skills lädt die KI mit `load_skill`/`read_skill_file`, Befehle ruft man im Chat mit `/name` auf. `hugin_feedback` meldet Unklares an Hugin. Skills mit Skripten, Befehle mit Shell-Einbettung und andere Arten werden nicht angeboten. Der Worker gleicht täglich im Hintergrund ab (`AIW_HUGIN_CHECK_HOURS`) und meldet Updates per E-Mail, ohne zu übernehmen. Eine `.hugin.yaml` im Repository lässt sich als Vorschlag in die Auswahl übernehmen. **MCP-Server anbinden:** Zentral unter *Administration → MCP-Server* registrieren (URL muss in `AIW_MCP_ALLOWED_HOSTS` stehen; lokal `AIW_MCP_ALLOW_LOCAL=true` für `http://localhost`), *Tools abrufen*, die Tools unter *Tools* einstufen und im Workspace unter *Einstellungen → MCP-Server* freischalten. Workspace-Admins können dort auch eigene Verbindungen anlegen, mit Kurzname (über alle Workspaces eindeutig), URL, Token und weiteren Headern (verschlüsselt). Nutzt die Plattform die URL schon, ist die Verbindung sofort aktiv. Sonst wartet sie auf die Freigabe eines Plattform-Admins, der per E-Mail benachrichtigt wird. Gleiche Tools desselben Dienstes erkennt Bifröst am Fingerabdruck (URL, Name, Beschreibung, Schema) und übernimmt die Einstufung; neue Tools bleiben gesperrt, bis sie eingestuft sind. ## Tests ```bash docker compose up -d uv run pytest ``` Die Tests legen je Lauf eine eigene Datenbank im selben PostgreSQL an und löschen sie danach. SQLite reicht nicht, weil `SKIP LOCKED`, `LISTEN/NOTIFY` und Teilindizes gebraucht werden. Statische Prüfung mit `uv run ruff check app tests` (Konfiguration in `pyproject.toml`; `--fix` behebt Import-Reihenfolge und Ähnliches). ## Betrieb ### Health-Checks | Endpunkt | Web (Port der App) | Worker (`AIW_WORKER_HTTP_PORT`, Standard 9101) | |---|---|---| | `/live` | Prozess antwortet | Arbeitsschleife lief in den letzten `AIW_WORKER_STALE_SECONDS` (120 s) | | `/ready` | Datenbank erreichbar, Migrationen aktuell, Upload-Verzeichnis beschreibbar, `AIW_SECRET_KEY` gesetzt | wie Web | `/live` prüft bewusst nicht die Datenbank, damit ein DB-Ausfall nicht alle Prozesse neu starten lässt. Bei Problemen antwortet `/ready` mit 503 und nennt im JSON die betroffene Prüfung. `/healthz` und `/readyz` bleiben als Aliase. ### Metriken `/metrics` liefert das Prometheus-Format und ist nur aktiv, wenn `AIW_METRICS_TOKEN` gesetzt ist. Prometheus schickt den Token als `Authorization: Bearer `; ohne Token antwortet der Endpunkt mit 404. - **Web:** HTTP-Requests nach Route-Muster und Status mit Dauer. Dazu der Zustand aus der Datenbank: - Runs nach Status und Alter des ältesten fälligen Runs (`aiw_runs_oldest_queued_seconds`). - Offene Freigaben und Benachrichtigungen (wartend, fehlgeschlagen). - Lebende Worker (`aiw_workers_alive`) und Sekunden seit dem letzten Herzschlag. - Workspaces im Papierkorb, Uploads, belegter Upload-Speicher (`aiw_upload_bytes`), Workspaces ab 80 % Kontingent (`aiw_workspaces_storage_high`), Kosten seit Mitternacht. - **Worker:** - Runs nach Art und Ergebnis mit Dauer. - Modellaufrufe nach Modell und Ergebnis mit Dauer und Tokens. - Tool-Aufrufe (MCP-Tools je Server zusammengefasst). - Zustellversuche und der letzte Schleifendurchlauf. **Ausfälle:** Fällt ein Worker aus, übernimmt nach Ablauf der Lease (`AIW_RUN_LEASE_SECONDS`) ein anderer. Der Stand wird nach jeder Tool-Runde gespeichert; ein neuer Versuch setzt dort fort, statt Commits und Modellaufrufe zu wiederholen. Steckte der Run in einer freigegebenen Schreibaktion, wechselt er nach *Abgleich nötig*. Ist die Datenbank kurz weg, wartet die Worker-Schleife und macht weiter. Zeitpunkte der Warteschlangen (Runs, Benachrichtigungen) kommen von der Uhr der Datenbank; abweichende Uhren von Web- und Worker-Rechnern verzögern also nichts. Pushes und Pull Requests sind wiederholbar: Kam ein Push an, nur die Antwort nicht, erkennt der nächste Versuch das; ein schon angelegter Pull Request wird gefunden statt doppelt angelegt. Die Zähler gelten je Prozess. Prometheus fragt deshalb jeden Web- und Worker-Prozess einzeln ab. Den Zustand aus der Datenbank liefert nur der Web-Prozess. Beispiele für Alarme: ```yaml - alert: AiwKeinWorker expr: max(aiw_workers_alive) == 0 for: 2m - alert: AiwWarteschlangeStaut expr: max(aiw_runs_oldest_queued_seconds) > 300 for: 5m - alert: AiwModellFehler expr: sum(rate(aiw_model_calls_total{outcome!="ok"}[10m])) / sum(rate(aiw_model_calls_total[10m])) > 0.1 for: 10m - alert: AiwBenachrichtigungenFehlgeschlagen expr: max(aiw_notifications{status="failed"}) > 0 - alert: AiwZustandNichtLesbar expr: min(aiw_state_up) == 0 for: 5m ``` ### Logs Mit `AIW_LOG_FORMAT=json` schreibt die App eine JSON-Zeile je Ereignis nach stdout; ohne diese Einstellung ist es lesbarer Text. Das Level stellt `AIW_LOG_LEVEL` ein. Jede Zeile trägt den Kontext, soweit vorhanden: `request_id`, `user_id`, `workspace_id`, `run_id` und `worker_id`. Über die `run_id` findet man alles zu einem Run, auch über Web und Worker hinweg. - **Request-ID:** Eine `X-Request-ID` vom Proxy wird übernommen, sonst erzeugt die App eine. Sie steht in der Antwort und bei unerwarteten Fehlern als „Fehler-ID“ auf der Fehlerseite. - **Zugriffszeile:** eine je Request mit Methode, Route-Muster, Status, Dauer und Nutzer. Query-Strings stehen nicht darin, Probes und Metrik-Abrufe werden nicht geloggt. - **Ereignisse (`event`):** `run_started`, `run_finished` (Ergebnis, Dauer), `model_call` (Modell, Tokens, Dauer, Fehler), `tool_call` (Tool, Status, Entscheidung, Dauer), `notification_error`. - **Nie im Log:** Prompts, Antworten, Abfrageergebnisse und Zugangsdaten. Ein Filter maskiert zusätzlich typische Muster: Bearer-Token, `sk-…`-Keys, `password=…` und Passwörter in URLs. ### Backup Die Datenbank wird zentral gesichert. Außerhalb davon liegen nur: - **Uploads** (`AIW_UPLOAD_DIR`): DuckDB-Dateien, nach dem Hochladen unveränderlich, deshalb im laufenden Betrieb sicherbar. - **Git-Cache:** wird bei Bedarf neu geklont und nicht gesichert. - **`AIW_SECRET_KEY`:** gehört in den Secret-Store. Ohne ihn sind die verschlüsselten Zugangsdaten in der Datenbank wertlos. ```bash uv run aiw backup /backup/aiw # uploads-.tar.gz mit manifest.json (Prüfsummen, Migrationsstand) uv run aiw restore /backup/aiw/uploads-20261022T020000Z.tar.gz [--force] uv run aiw check # Exit-Code 1 bei Problemen ``` - **`backup`** sichert die Uploads, die laut Datenbank verarbeitet sind. Fehlende Dateien meldet es mit Exit-Code 1. - **`restore`** prüft die Prüfsummen und überspringt Uploads, die es in der Datenbank nicht mehr gibt. Vorhandene Dateien mit anderem Inhalt überschreibt es nur mit `--force`. - **`check`** prüft: - Datenbank und Migrationen. - Ob alle Uploads ihre Datei haben; verwaiste Dateien meldet es nur als Hinweis. - Ob `AIW_SECRET_KEY` alle gespeicherten Geheimnisse entschlüsseln kann. - **Zeitpunkt:** Das Backup läuft am besten nach dem Datenbank-Backup, z. B. als CronJob. Nach einem Restore der Datenbank zeigt `aiw check`, ob Datenbank und Dateien zusammenpassen. ## Aufbau | Pfad | Inhalt | |---|---| | `app/main.py` | Web-Prozess: Router, Sicherheitsköpfe (CSP), Request-ID und Zugriffslog, `/live`, `/ready`, `/metrics` | | `app/core/logs.py`, `metrics.py`, `health.py` | JSON-Logs mit Kontext, Prometheus-Metriken, Health-Checks | | `app/tasks/monitor.py` | Worker-Herzschlag und HTTP-Server des Workers für Probes und Metriken | | `app/backup.py` | `aiw backup`, `aiw restore`, `aiw check` | | `app/cli.py` | `aiw migrate \| seed-demo \| demo \| create-admin \| gen-key \| web \| worker \| backup \| restore \| check` | | `app/identity` | Anmeldung (Demo-Provider, später OIDC), Sitzungen | | `app/workspaces` | Workspaces, Mitgliedschaften, Rollen | | `app/data_sources` | Quellen, Git-Connector, lesende Git-Tools, Bitbucket-API | | `app/changes` | Branch je Chat: `commit_change`, `git_pr_comments`, Push, Pull Request, Abfrage des PR-Status, Ansicht *Änderungen* | | `app/demo` | `aiw demo`: Demo-Workspaces Bestellservice und MCP-Entwicklung (Repository, Datei, Dokumente, Beispielfragen) | | `app/feedback` | Rückmeldungen zu Antworten (👍/👎, Kommentar), Auswertung und CSV unter *Nutzung und Kosten* | | `app/documents` | Abgeglichene Ordner: Abgleich aus dem Browser, Auslese über MCP, Volltextsuche, Tools `docs_*` | | `app/repo` | Bereich *Repository*: neue Commits seit dem letzten Besuch, Branches, Commit mit Diff, Chats mit Bezug | | `app/uploads` | Uploads, Umwandlung in DuckDB, gesperrte Abfrageverbindung, Tools `data_*` | | `app/core/markdown.py` | Bereinigte Darstellung von Modellausgaben (markdown-it-py, nh3) | | `app/usage`, `app/audit/views.py` | Verbrauch und Kosten je Modellaufruf, Run-Liste, Audit-Log im UI | | `app/notify` | Benachrichtigungen: Outbox, E-Mail, Webhooks, Profil | | `app/core/charts.py` | Diagramme zu Abfrageergebnissen als Inline-SVG | | `app/hugin` | Hugin: Host der Plattform, Key je Workspace, Auswahl, Abgleich und Festschreiben je Workspace, Laden für Runs, Befehle, Tools `load_skill`, `read_skill_file`, `hugin_feedback` | | `app/mcp/memory.py` | Gedächtnis (Munin): Einstufungsempfehlung, Einträge ansehen und löschen | | `app/mcp` | MCP-Server: zentrale Registrierung, Discovery, Freischaltung je Workspace, Verbindungen der Workspaces mit Freigabe, Datenbank-Treiber, Client (SDK `mcp`), `/admin/mcp`, `/admin/databases` | | `app/mcp/databases.py` | Datenbanken der Workspaces als MCP-Verbindungen über Datenbank-Treiber | | `app/approvals` | Freigabeanträge (z. B. für Datenbankabfragen mit Freigabe-Regel oder MCP-Tools), Entscheidung, Ablauf, Ausführung mit erneuter Prüfung, `/approvals` | | `app/ai` | Endpunkte, Keys, Modelle, Verwaltung `/admin/models`, Provider-Adapter (Responses API) | | `app/chat` | Chats, Nachrichten, Handler für Chat-Runs (`runner.py`) | | `app/tools` | Tool-Registry, Einstufung, Policy Engine, Ausführung, `/admin/tools` | | `app/tasks` | Run-Zustände, Queue mit Lease, Ereignisse, Worker, SSE | | `app/audit` | Audit-Log | | `app/migrations` | Alembic | Neue Migration nach Modelländerungen: ```bash uv run python -c "from alembic import command; from app.cli import alembic_config; from app.core.config import get_settings; command.revision(alembic_config(get_settings()), message='…', autogenerate=True)" ```