858 lines
38 KiB
Markdown
858 lines
38 KiB
Markdown
# Munin
|
||
|
||
Ein projektbezogenes Gedächtnis für KI-Agenten und Menschen.
|
||
|
||
> In der nordischen Überlieferung sind Hugin und Munin die Raben Odins. Munin
|
||
> steht für das Gedächtnis: Er fliegt aus, sammelt und kehrt zurück, um zu
|
||
> berichten — was hier die Agenten tun. Passend zum Torwächter **Heimdall**,
|
||
> unserem AI-Gateway.
|
||
|
||
KI-Werkzeuge (Claude, Cline, OpenCode, eigene Anwendungen) legen darin über
|
||
**MCP** ab, was sie lernen, und rufen es wieder auf. Menschen pflegen dasselbe
|
||
Wissen über eine schlanke Weboberfläche. Beides landet im selben Speicher — der
|
||
Unterschied ist ein Feld am Eintrag, kein zweites System.
|
||
|
||
```
|
||
KI-Werkzeug ──MCP──┐
|
||
├──▶ Workspace ──▶ Projekt ──▶ Einträge
|
||
Browser ──HTTP─────┘ (eine SQLite-Datei je Workspace)
|
||
```
|
||
|
||
---
|
||
|
||
## Inhalt
|
||
|
||
- [Schnellstart](#schnellstart)
|
||
- [Bauen](#bauen)
|
||
- [Grundbegriffe](#grundbegriffe)
|
||
- [MCP einrichten](#mcp-einrichten)
|
||
- [Die Werkzeuge](#die-werkzeuge)
|
||
- [Weboberfläche](#weboberfläche)
|
||
- [Konfiguration](#konfiguration)
|
||
- [Sicherheit](#sicherheit)
|
||
- [Betrieb](#betrieb)
|
||
- [Entwicklung](#entwicklung)
|
||
|
||
---
|
||
|
||
## Schnellstart
|
||
|
||
```bash
|
||
# 1. Konfiguration anlegen
|
||
cp .env.example .env
|
||
|
||
# 2. Geheimnisse erzeugen und eintragen
|
||
openssl rand -hex 32 # → SESSION_SECRET
|
||
openssl rand -hex 24 # → ADMIN_TOKEN
|
||
|
||
# 3. Starten
|
||
go run .
|
||
```
|
||
|
||
Dann `http://localhost:8080` öffnen und mit dem `ADMIN_TOKEN` anmelden.
|
||
|
||
Der weitere Weg:
|
||
|
||
1. **Workspace wählen** — nach der Anmeldung landet der Plattform-Admin unter
|
||
*Workspaces*. Beim ersten Start legt der Server den Workspace „Intern“ selbst
|
||
an; weitere (etwa einer je Abteilung) lassen sich dort anlegen.
|
||
2. **Projekt anlegen** unter *Projekte*
|
||
3. **Zugang erzeugen** — der Key wird genau einmal angezeigt
|
||
4. **MCP-Client einrichten** (die Anleitung erscheint direkt neben dem neuen Key)
|
||
|
||
---
|
||
|
||
## Bauen
|
||
|
||
`go run .` aus dem Schnellstart übersetzt bei jedem Start neu. Für den Betrieb
|
||
baut man stattdessen einmal ein **Binary**: eine einzelne ausführbare Datei, die
|
||
alles enthält. Vorlagen, CSS, JavaScript und Hilfeseiten sind per `//go:embed`
|
||
eingebettet; neben dem Binary braucht es nur die Konfiguration.
|
||
|
||
### Voraussetzungen
|
||
|
||
- **Go 1.26** oder neuer (`go version` prüft das). Eine ältere Go-Fassung ab 1.21
|
||
lädt die passende beim ersten Bauen selbst nach, das steht in `go.mod`.
|
||
- **Kein C-Compiler.** Der SQLite-Treiber (`modernc.org/sqlite`) ist in reinem
|
||
Go geschrieben.
|
||
|
||
Die Abhängigkeiten lädt `go build` beim ersten Mal selbst herunter.
|
||
|
||
### Für den eigenen Rechner
|
||
|
||
```bash
|
||
go build -o munin .
|
||
./munin
|
||
```
|
||
|
||
Das ergibt `munin` im Projektverzeichnis, gut 20 MB groß. Es steht in
|
||
`.gitignore` und landet nicht im Repository. Unter Windows `-o munin.exe`
|
||
schreiben — `-o` übernimmt den Namen genau so, ohne die Endung zu ergänzen.
|
||
|
||
### Für den Betrieb
|
||
|
||
So baut auch das Dockerfile:
|
||
|
||
```bash
|
||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o munin .
|
||
```
|
||
|
||
| Schalter | Wirkung |
|
||
|---|---|
|
||
| `CGO_ENABLED=0` | reines Go, ein statisches Binary ohne Abhängigkeit von Systembibliotheken |
|
||
| `-trimpath` | entfernt die Pfade der Baumaschine aus dem Binary |
|
||
| `-ldflags="-s -w"` | lässt Debug-Symbole weg, das Binary schrumpft auf etwa 16 MB |
|
||
|
||
### Für ein anderes Betriebssystem
|
||
|
||
Go übersetzt ohne weitere Werkzeuge für andere Systeme. `GOOS` und `GOARCH`
|
||
geben das Ziel an, etwa vom Mac aus für einen Linux-Server:
|
||
|
||
```bash
|
||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o munin-linux-amd64 .
|
||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags="-s -w" -o munin-linux-arm64 .
|
||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o munin.exe .
|
||
```
|
||
|
||
Als Container: `docker build -t munin .` (siehe [Docker](#docker)).
|
||
|
||
### Starten und wo die Daten landen
|
||
|
||
```bash
|
||
./munin
|
||
```
|
||
|
||
Zwei Dinge hängen am **aktuellen Verzeichnis**, nicht am Ort des Binarys:
|
||
|
||
- **`.env`** wird von dort gelesen. Fehlt sie, müssen `SESSION_SECRET` und
|
||
`ADMIN_TOKEN` als Umgebungsvariablen gesetzt sein, sonst startet der Server
|
||
nicht.
|
||
- **`DATA_DIR`** ist standardmäßig `data`, also ebenfalls relativ zum aktuellen
|
||
Verzeichnis. Wer das Binary aus einem anderen Verzeichnis startet, bekommt
|
||
dort einen neuen, leeren Bestand.
|
||
|
||
Für den Betrieb deshalb einen absoluten Pfad setzen:
|
||
|
||
```bash
|
||
DATA_DIR=/srv/munin/data ./munin
|
||
```
|
||
|
||
Darin legt der Server beim Start an:
|
||
|
||
```
|
||
data/
|
||
├── control.db Workspaces, Zuordnung Key → Workspace, Workspace-Admins
|
||
└── workspaces/
|
||
└── <id>.db je Workspace: Projekte, Einträge, Keys, Protokolle, Papierkorb
|
||
```
|
||
|
||
Neben jeder Datenbank liegen im Betrieb `…-wal` und `…-shm`. Sie gehören dazu
|
||
und dürfen nicht einzeln gelöscht werden. Welche Pfade der Server benutzt,
|
||
steht in der Startzeile des Logs (`Workspaces: …`).
|
||
|
||
## Grundbegriffe
|
||
|
||
| Begriff | Bedeutung |
|
||
|---|---|
|
||
| **Workspace** | Ein Bereich für eine Abteilung oder ein Team, mit einer eigenen Datenbankdatei. Enthält beliebig viele Projekte. Workspaces sind strukturell getrennt: Keine Abfrage kann über eine Workspace-Grenze hinweg lesen, weil die Daten nicht in derselben Datei liegen. |
|
||
| **Projekt** | Die Einheit der Abschottung innerhalb eines Workspace. Eigene Einträge, eigene Zugänge, eigener MCP-Server. Projekte sehen sich gegenseitig nicht. |
|
||
| **Eintrag** | Ein Stück Wissen: Bezeichner, Titel, Markdown-Inhalt, Schlagworte. Von Menschen oder Agenten geschrieben. |
|
||
| **Bezeichner** (slug) | Kurzer, dauerhafter Name eines Eintrags (`deployment-prozess`). Der Schlüssel gegen Dubletten: Zweimal derselbe Bezeichner aktualisiert, statt eine zweite Fassung danebenzustellen. |
|
||
| **API-Key** | Zugang zu genau einem Projekt, mit Namen und Rolle. Wird nur als Prüfsumme gespeichert. |
|
||
| **Plattform-Admin** | Der `ADMIN_TOKEN`. Legt Workspaces und Projekte an und kommt an jedes Projekt. Arbeitet immer in genau einem gewählten Workspace und wechselt über den Umschalter in der Kopfleiste. |
|
||
| **Fassung** (version) | Zählt bei jeder Änderung eines Eintrags hoch. Die abgelösten Fassungen bleiben erhalten und lassen sich in der Weboberfläche vergleichen. |
|
||
|
||
### Warum Bezeichner statt Ordner
|
||
|
||
Ein Wiki gliedert nach Ordnern. Für ein Gedächtnis, in das Agenten schreiben,
|
||
funktioniert das nicht: Wo eine Erkenntnis „hingehört", ist selten eindeutig, und
|
||
ein Agent trifft diese Entscheidung schlecht.
|
||
|
||
Stattdessen gibt es **Schlagworte** zum Einordnen und **Verweise** zum
|
||
Verknüpfen. Im Inhalt zeigt `[[bezeichner]]` auf einen anderen Eintrag; die
|
||
Gegenrichtung („was verweist hierher") entsteht daraus von selbst und steht in
|
||
der Detailansicht wie auch in der MCP-Antwort.
|
||
|
||
---
|
||
|
||
## MCP einrichten
|
||
|
||
Nach dem Erzeugen eines Keys zeigt die Weboberfläche den passenden Abschnitt an.
|
||
Der Key bestimmt Workspace und Projekt — ein zusätzlicher Parameter ist nicht nötig. Wer mit
|
||
mehreren Projekten arbeitet, trägt mehrere Server mit je eigenem Key ein.
|
||
|
||
Alle Clients sprechen denselben Endpunkt `…/mcp` über Streamable HTTP an. Die
|
||
Konfigurationsdateien sehen trotzdem unterschiedlich aus:
|
||
|
||
| Client | Datei | Schlüssel | Transport heißt dort |
|
||
|---|---|---|---|
|
||
| Claude Code | `.mcp.json` / `~/.claude.json` | `mcpServers` | `"type": "http"` |
|
||
| Cline | `cline_mcp_settings.json` / `~/.cline/mcp.json` | `mcpServers` | `"type": "streamableHttp"` |
|
||
| OpenCode | `opencode.json` | `mcp` | `"type": "remote"` |
|
||
|
||
Die beiden ersten Zeilen sind die Falle: gleicher Schlüssel, anderer Wert für
|
||
`type`. Ein aus Claude Code kopierter Block läuft in Cline nicht.
|
||
|
||
### Claude Code
|
||
|
||
Am schnellsten über die Kommandozeile:
|
||
|
||
```bash
|
||
claude mcp add --transport http munin-plattform https://munin.intern/mcp \
|
||
--header "Authorization: Bearer wk_…"
|
||
```
|
||
|
||
Ohne `--scope` landet der Eintrag in `~/.claude.json` und gilt nur im aktuellen
|
||
Projekt; `--scope user` macht ihn überall verfügbar.
|
||
|
||
Fürs Team gehört er stattdessen als `.mcp.json` ins Repository — dann aber
|
||
**ohne** den Key im Klartext. Claude Code löst `${VAR}` aus der Umgebung auf:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"munin-plattform": {
|
||
"type": "http",
|
||
"url": "https://munin.intern/mcp",
|
||
"headers": { "Authorization": "Bearer ${MUNIN_KEY}" }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Jede Person setzt `MUNIN_KEY` auf ihren eigenen Key. Damit steht im Repository die
|
||
Einrichtung, nicht das Geheimnis — und im Protokoll steht trotzdem, wer geschrieben
|
||
hat.
|
||
|
||
### Cline
|
||
|
||
Über die Oberfläche: *MCP Servers* → *Configure* → *Configure MCP Servers*. In der
|
||
Kommandozeilenfassung liegt dieselbe Datei unter `~/.cline/mcp.json`.
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"munin-plattform": {
|
||
"type": "streamableHttp",
|
||
"url": "https://munin.intern/mcp",
|
||
"headers": { "Authorization": "Bearer wk_…" },
|
||
"disabled": false,
|
||
"autoApprove": ["recall", "get", "list", "related"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`autoApprove` nennt die Werkzeuge, die ohne Rückfrage laufen dürfen. Die vier
|
||
lesenden gehören dort hinein — sonst muss jede Suche einzeln bestätigt werden, und
|
||
ein Agent, der für jeden Blick ins Gedächtnis nachfragen muss, benutzt es nicht.
|
||
`remember` und `forget` gehören ausdrücklich **nicht** hinein: Sie verändern
|
||
gemeinsames Wissen.
|
||
|
||
### OpenCode
|
||
|
||
`opencode.json` im Projektverzeichnis oder global unter
|
||
`~/.config/opencode/opencode.json`. Anderes Schema als bei den beiden anderen —
|
||
der Block heißt `mcp`, nicht `mcpServers`:
|
||
|
||
```json
|
||
{
|
||
"$schema": "https://opencode.ai/config.json",
|
||
"mcp": {
|
||
"munin-plattform": {
|
||
"type": "remote",
|
||
"url": "https://munin.intern/mcp",
|
||
"enabled": true,
|
||
"headers": { "Authorization": "Bearer {env:MUNIN_KEY}" }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
OpenCode ersetzt `{env:VAR}` aus der Umgebung und `{file:~/pfad}` durch den Inhalt
|
||
einer Datei. Ein nicht gesetztes `{env:…}` wird zur leeren Zeichenkette — der
|
||
Server antwortet dann mit `401`, nicht mit einer Fehlermeldung über die
|
||
Konfiguration.
|
||
|
||
### Danach
|
||
|
||
**Ein Key je Person und je Agent.** Nur so steht im Protokoll, wer etwas
|
||
geschrieben hat, und ein einzelner Zugang lässt sich zurückziehen, ohne alle
|
||
anderen zu stören.
|
||
|
||
Verbindung prüfen: Der Agent sollte `recall`, `get`, `list`, `related`,
|
||
`remember` und `forget` anbieten. Tut er das nicht, hilft ein Blick ins Zugriffsprotokoll des
|
||
Servers — ein `401` dort heißt falscher oder zurückgezogener Key, gar kein Eintrag
|
||
heißt, dass der Client den Server nie aufgerufen hat.
|
||
|
||
Wie man Agenten so anspricht, dass sie den Speicher auch nutzen:
|
||
[AGENTEN-GUIDE.md](AGENTEN-GUIDE.md).
|
||
|
||
---
|
||
|
||
## Die Werkzeuge
|
||
|
||
| Werkzeug | Rolle | Zweck |
|
||
|---|---|---|
|
||
| `recall` | read | Durchsucht den Speicher, auf Wunsch mit `synonyms`. Der Haupteinstieg — auch vor dem Schreiben. |
|
||
| `get` | read | Lädt einen Eintrag vollständig, samt Verweisen und Fassungsnummer. |
|
||
| `list` | read | Überblick: zuletzt geändert, alle Schlagworte. |
|
||
| `related` | read | Über `[[Verweise]]` verknüpfte Einträge, direkt und über einen Zwischeneintrag. |
|
||
| `remember` | write | Legt einen Eintrag an oder aktualisiert ihn. Fragt bei ähnlichen und bei kuratierten Einträgen nach (siehe unten). |
|
||
| `forget` | write | Legt einen Eintrag in den Papierkorb (30 Tage wiederherstellbar, nur durch Menschen). |
|
||
|
||
Der Server schickt jedem Client eine **Anleitung** (`instructions`) mit, die das
|
||
Vorgehen beschreibt — vor allem: *vor* dem Schreiben erst `recall` aufrufen. Das
|
||
ist das wirksamste Mittel gegen Dubletten und steht in
|
||
`internal/mcpserver/server.go`.
|
||
|
||
### Schreibbudget
|
||
|
||
`remember` und `forget` sind je Projekt auf **1.000 Aufrufe pro Stunde** gedeckelt
|
||
(`WRITE_LIMIT_HOURLY`). Lesen ist nie begrenzt.
|
||
|
||
Der Grund ist die Kehrseite einer anderen Entscheidung: Ein gültiger Key umgeht
|
||
die IP-Sperre bewusst, weil Deploy-Skript, MCP-Client und Browser eines Büros
|
||
sich nach außen oft eine Adresse teilen. Damit hätte ein Agent, der in einer
|
||
Schleife hängt, sonst überhaupt keine Grenze.
|
||
|
||
Das Budget hängt am **Projekt**, nicht am Key: Ein Limit je Key ließe sich
|
||
umgehen, indem man einen zweiten Key anlegt, und das darf jeder Projekt-Admin
|
||
selbst. Einzelne Projekte bekommen über *Projekte → Bearbeiten* eine eigene
|
||
Obergrenze; die Übersicht zeigt, wie viel im laufenden Fenster noch frei ist.
|
||
Ändern darf sie nur der Plattform-Admin — aus demselben Grund.
|
||
|
||
Ist das Budget erschöpft, bekommt das Modell keinen nackten Fehler, sondern die
|
||
Anweisung, abzubrechen und das Limit zu melden — sonst versucht es sofort wieder
|
||
oder weicht auf ein anderes Werkzeug aus.
|
||
|
||
### Rückfragen statt Schreiben
|
||
|
||
`remember` schreibt in zwei Fällen nicht sofort, sondern antwortet mit
|
||
`judgment_required` — als Antwort, nicht als Fehler, damit das Modell sie liest
|
||
und entscheidet:
|
||
|
||
| Fall | Antwort | Trotzdem schreiben mit |
|
||
|---|---|---|
|
||
| Neuer Bezeichner, aber ähnlicher Titel vorhanden | `candidates` (bis zu fünf) | `force: true` |
|
||
| Eintrag ist kuratiert | Erklärung | `override_curated: true` und `reason` |
|
||
|
||
Im zweiten Fall fällt der Haken „kuratiert" weg, das Protokoll vermerkt die
|
||
Begründung, und die Nutzungsseite zeigt den Eintrag unter *Zur Prüfung*, bis ein
|
||
Mensch ihn wieder kuratiert. Einzelheiten: `internal/dubletten` und
|
||
`internal/mcpserver/kuratiert.go`. Jeder Kandidat nennt die gemeinsamen Wörter
|
||
(`matched_words`).
|
||
|
||
### Gleichzeitiges Schreiben
|
||
|
||
`get` liefert eine `version`. Wird sie bei `remember` als `expected_version`
|
||
mitgegeben, meldet der Server einen Konflikt, falls jemand anderes den Eintrag
|
||
inzwischen geändert hat — statt die fremde Änderung stillschweigend zu
|
||
überschreiben.
|
||
|
||
```
|
||
Agent A: get(deployment) → version 3
|
||
Agent B: get(deployment) → version 3
|
||
Agent A: remember(…, expected_version=3) → OK, jetzt version 4
|
||
Agent B: remember(…, expected_version=3) → Konflikt: neu laden, zusammenführen
|
||
```
|
||
|
||
---
|
||
|
||
## Weboberfläche
|
||
|
||
| Weg | Zweck |
|
||
|---|---|
|
||
| `/login` | Anmeldung mit API-Key oder Admin-Token |
|
||
| `/admin/workspaces` | Workspaces auflisten, anlegen und wählen (nur Plattform-Admin) |
|
||
| `/admin/workspaces/{id}/edit` | Workspace umbenennen oder löschen |
|
||
| `/admin/projects` | Projekte des gewählten Workspace anlegen und auflisten, mit Schreibvorgängen der letzten 30 Tage je Projekt; Markdown-Export anstoßen |
|
||
| `/admin/export.zip` | Alle Projekte des gewählten Workspace als ZIP herunterladen (Plattform- und Workspace-Admin) |
|
||
| `/admin/projects/{projekt}/edit` | Name, Beschreibung und Schreibbudget ändern; Projekt löschen |
|
||
| `/admin/audit` | Protokoll über alle Projekte des gewählten Workspace, filterbar nach Vorgang, Zugang, Projekt und Zeitraum |
|
||
| `/admin/search` | Suche in allen Projekten des gewählten Workspace, nach Projekt gruppiert (Plattform- und Workspace-Admin) |
|
||
| `/admin/overview` | Überblick des Workspace: was zu tun ist (je Projekt mit Link), Projekte, Aktivität der letzten 30 Tage (Workspace- und Plattform-Admin) |
|
||
| `/admin/system` | Zustand (was zu tun ist), Aktivität der letzten 30 Tage, Bestand und Platz je Workspace, Speicher, Bereitschaft, offene Datenbankverbindungen gegen das Budget, Fassung und Laufzeit (nur Plattform-Admin) |
|
||
| `/p/{projekt}/` | Einträge durchsuchen, nach Schlagwort filtern, veraltete zeigen (`?veraltet=1`), für viele Einträge auf einmal Schlagworte ändern oder sie in den Papierkorb legen |
|
||
| `/p/{projekt}/new` | Neuen Eintrag anlegen (`?slug=` belegt den Bezeichner vor), mit Hinweis auf ähnliche vorhandene Einträge |
|
||
| `/p/{projekt}/e/{bezeichner}` | Eintrag lesen, mit Rückverweisen und verwandten Einträgen über einen Zwischeneintrag |
|
||
| `/p/{projekt}/e/{bezeichner}/edit` | Eintrag bearbeiten, mit Vorschau |
|
||
| `/p/{projekt}/e/{bezeichner}/versions` | Alle Fassungen eines Eintrags |
|
||
| `/p/{projekt}/e/{bezeichner}/versions/{n}` | Fassung `n` mit ihrer Nachfolgerin vergleichen |
|
||
| `/p/{projekt}/graph` | Verweise zwischen den Einträgen als Graph |
|
||
| `/p/{projekt}/keys` | Zugänge verwalten, optional mit Ablaufdatum (Rolle `admin`) |
|
||
| `/p/{projekt}/export.zip` | Projekt als ZIP herunterladen, passt direkt in den Import (Rolle `admin`) |
|
||
| `/p/{projekt}/usage` | Nutzung: geänderte kuratierte Einträge zur Prüfung, Schreibvorgänge der letzten 30 Tage (Agenten/Menschen, neu/aktualisiert), Suchen der Agenten samt Anfragen ohne Treffer, Bestand, Verweise ins Leere, aktive Zugänge |
|
||
| `/p/{projekt}/audit` | Protokoll des Projekts, filterbar nach Vorgang, Zugang und Zeitraum |
|
||
| `/p/{projekt}/trash` | Papierkorb: wiederherstellen oder endgültig löschen (Schreibrecht) |
|
||
| `/hilfe` | Eingebaute Onboarding-Hilfe, oben rechts in der Kopfleiste: Einstieg, Begriffe, MCP einrichten, Arbeiten mit Agenten, Weboberfläche, Zugänge. Dazu je eine Seite für Workspace-Admins (sehen Workspace- und Plattform-Admin) und für Plattform-Admins (nur dieser). |
|
||
| `/live` | Läuft der Prozess? Ohne Anmeldung, ohne Datenbank. Für den Neustart des Containers (`/healthz` ist der alte Name und geht weiter) |
|
||
| `/ready` | Kann er Anfragen bedienen? Prüft die Datenbank, sonst `503`. Ohne Anmeldung. Für Load-Balancer und Monitoring |
|
||
|
||
### Funktionen beim Bearbeiten und Lesen
|
||
|
||
- **Vorschau:** Der Reiter *Vorschau* über dem Textfeld zeigt das Ergebnis. Der
|
||
Server rendert es mit demselben Code wie die Detailansicht, deshalb kann die
|
||
Vorschau nicht vom späteren Ergebnis abweichen.
|
||
- **Autovervollständigung für Verweise:** Nach `[[` schlägt das Textfeld passende
|
||
Einträge vor, mit derselben Suche wie `recall`. Auswählen mit Pfeiltasten und
|
||
Enter/Tab oder mit der Maus. Eingefügt wird der Bezeichner, nicht der Titel.
|
||
- **Änderungshistorie:** Vor jeder Änderung wird die bisherige Fassung gesichert,
|
||
auch bei Änderungen durch Agenten. Über *Verlauf ansehen* in der Detailansicht lässt sich jede abgelöste
|
||
Fassung zeilenweise mit ihrer Nachfolgerin vergleichen und mit Schreibrecht per
|
||
*Wiederherstellen* zurückholen — der bisherige Stand bleibt dabei selbst im
|
||
Verlauf.
|
||
- **Papierkorb:** Löschen, ob in der Oberfläche oder per `forget`, legt einen
|
||
Eintrag nur in den Papierkorb des Projekts. Dort bleibt er samt Verlauf 30
|
||
Tage und lässt sich von Menschen mit Schreibrecht wiederherstellen oder
|
||
sofort endgültig löschen. Danach räumt die tägliche Aufgabe ihn ab.
|
||
- **Graph:** Zeigt, welche Einträge aufeinander verweisen. Verweise auf
|
||
Bezeichner, zu denen es noch keinen Eintrag gibt, erscheinen als gestrichelte
|
||
Knoten. Bei mehr als 150 Einträgen bleiben die am stärksten verknüpften
|
||
sichtbar, die Seite weist darauf hin. Zoomen mit dem Mausrad, verschieben durch
|
||
Ziehen.
|
||
- **Schlagworte für viele Einträge:** Mit Schreibrecht hat die Eintragsliste
|
||
Auswahlkästchen. Für die ausgewählten Einträge lassen sich Schlagworte auf
|
||
einmal hinzufügen oder entfernen.
|
||
- **Gibt es schon?** Beim Anlegen zeigt Munin unter dem Titel vorhandene
|
||
Einträge mit ähnlichem Titel und die gemeinsamen Wörter — dieselbe Prüfung
|
||
wie bei `remember` über MCP (`internal/dubletten`), hier nur als Hinweis.
|
||
- **Rückfrage beim Verlassen:** Wer im Formular etwas geändert hat und die
|
||
Seite verlässt, ohne zu speichern, bekommt eine Rückfrage des Browsers.
|
||
- **Auch verwandt:** Unter einem Eintrag stehen neben den Rückverweisen die
|
||
Einträge, die über einen Zwischeneintrag verknüpft sind — wie `related`.
|
||
- **Veraltet:** Einträge, die seit über einem Jahr niemand geändert hat, tragen
|
||
eine Marke; ein Filter neben der Suche zeigt genau diese, die Nutzungsseite
|
||
nennt ihre Zahl.
|
||
- **Tastenkürzel:** `/` Suche, `n` neuer Eintrag, `e` bearbeiten,
|
||
`Strg+Enter` speichern.
|
||
|
||
Vorschau, Autovervollständigung, „Gibt es schon?“, Rückfrage, Tastenkürzel und
|
||
Graph brauchen JavaScript. Alles andere funktioniert auch ohne.
|
||
|
||
### Rollen
|
||
|
||
| Rolle | Darf |
|
||
|---|---|
|
||
| `read` | lesen, suchen |
|
||
| `write` | zusätzlich schreiben und löschen |
|
||
| `admin` | zusätzlich Zugänge des eigenen Projekts verwalten |
|
||
|
||
---
|
||
|
||
## Konfiguration
|
||
|
||
| Variable | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `SESSION_SECRET` | — | **Pflicht.** Signiert Sitzungen und CSRF-Tokens. Mindestens 32 Zeichen. |
|
||
| `ADMIN_TOKEN` | — | **Pflicht.** Zugang des Plattform-Admins. Mindestens 16 Zeichen. |
|
||
| `DB_DRIVER` | `sqlite` | Wo die Daten liegen: `sqlite` oder `postgres` (siehe AUSBAUSTUFEN.md 4.10) |
|
||
| `DATA_DIR` | `data` | Nur SQLite: Datenverzeichnis mit Kontroll-Datenbank (`control.db`) und `workspaces/` mit je einer Datei pro Workspace |
|
||
| `DATABASE_URL` | — | Nur PostgreSQL, dann **Pflicht**: z.B. `postgres://munin:geheim@host:5432/munin`. Schemas `munin_control` und je Workspace `munin_ws_<id>`. Lokal: `docker-compose.dev.yml` |
|
||
| `PORT` | `8080` | Port des Servers |
|
||
| `COOKIE_SECURE` | automatisch | `Secure`-Flag der Cookies. Ohne Angabe: an, außer bei localhost. |
|
||
| `ENABLE_HSTS` | `false` | HSTS-Header senden. Nur bei gültigem Zertifikat. |
|
||
| `TRUSTED_PROXY` | — | IPs/CIDR-Netze, deren `X-Forwarded-For` geglaubt wird |
|
||
| `MAX_API_BODY_BYTES` | `4194304` | Höchstgröße eines MCP-Requests (4 MB) |
|
||
| `WRITE_LIMIT_HOURLY` | `1000` | Schreibende MCP-Aufrufe je Projekt und Stunde. `0` = unbegrenzt. |
|
||
| `EXPORT_DIR` | — | Zielverzeichnis des Markdown-Exports. Leer = abgeschaltet. |
|
||
|
||
Fehlen die Pflichtangaben oder sind sie zu kurz, startet der Server nicht. Ein
|
||
schwaches Geheimnis wäre die Wurzel für alles andere — ein klarer Abbruch ist
|
||
besser als unsicherer Betrieb.
|
||
|
||
---
|
||
|
||
## Sicherheit
|
||
|
||
### Was geprüft wird
|
||
|
||
| Schutz | Wo |
|
||
|---|---|
|
||
| Workspaces in getrennten Datenbankdateien | `internal/workspace/workspace.go`, `internal/store/control.go` |
|
||
| Signierte Sitzungs-Cookies (HMAC-SHA256) | `internal/auth/session.go` |
|
||
| API-Keys nur als SHA-256-Prüfsumme gespeichert | `internal/auth/keys.go` |
|
||
| Zurückgezogener Key beendet laufende Sitzungen sofort | `internal/web/middleware.go` |
|
||
| Neues `ADMIN_TOKEN` beendet alle Sitzungen des Plattform-Admins sofort | `internal/auth/session.go` (`PlatformSubject`) |
|
||
| CSRF-Token in allen Formularen | `internal/auth/csrf.go` |
|
||
| `SameSite=Lax`, `HttpOnly`, `Secure` | `internal/auth/cookies.go` |
|
||
| Rate-Limiting je Adresse, nur auf Fehlversuche | `internal/auth/ratelimit.go` |
|
||
| Content-Security-Policy und weitere Schutz-Header | `internal/web/middleware.go` |
|
||
| Größenlimits für Request-Bodies | `internal/web/middleware.go` |
|
||
| Server-Timeouts gegen Slowloris | `main.go` |
|
||
| Optimistisches Sperren gegen stilles Überschreiben | `internal/store/entries.go` |
|
||
|
||
### Abschottung der Projekte
|
||
|
||
Sie ist **strukturell**, nicht eine Frage der richtigen Filterbedingung:
|
||
|
||
- Jeder Workspace liegt in einer **eigenen Datenbankdatei**. Welche Datei zu
|
||
einem Key gehört, steht in der zentralen Kontroll-Datenbank. Ein Key führt
|
||
deshalb nur in genau eine Datei.
|
||
- Jedes Projekt hat einen **eigenen MCP-Server**, dessen Werkzeuge fest auf
|
||
dieses Projekt verdrahtet sind
|
||
- Jedes Projekt hat einen **eigenen Suchindex** — auch die BM25-Gewichtung wird
|
||
nicht über Projektgrenzen hinweg berechnet
|
||
- Jeder Datenbankzugriff ist auf die Projekt-ID eingeschränkt
|
||
- Ein fremdes Projekt antwortet mit **404, nicht 403** — sonst ließe sich über
|
||
die Statuscodes herausfinden, welche Projekte es sonst noch gibt
|
||
|
||
### Warum SHA-256 für Keys und nicht bcrypt
|
||
|
||
Für Passwörter wären langsame Verfahren richtig: Menschen wählen aus einem
|
||
kleinen, erratbaren Raum. Ein API-Key hier ist aber 256 Bit aus `crypto/rand` —
|
||
diesen Raum durchsucht niemand. Die Langsamkeit brächte keinen Gewinn, würde aber
|
||
jeden Request bremsen. Entscheidend ist der zweite Punkt: Weil SHA-256 schnell
|
||
und deterministisch ist, wird der Key in **einem indizierten Lookup** aufgelöst
|
||
statt in einer Schleife über alle Keys.
|
||
|
||
### Prompt Injection
|
||
|
||
Der Inhalt des Speichers landet direkt im Kontext von KI-Werkzeugen. Wer in ein
|
||
Projekt schreiben darf, schreibt damit indirekt in die Prompts aller anderen, die
|
||
dieses Projekt nutzen.
|
||
|
||
Dagegen hilft kein technischer Schutz, nur Nachvollziehbarkeit: Jeder Eintrag
|
||
trägt Herkunft (`human`/`agent`) und den Namen des verwendeten Zugangs, und jede
|
||
Änderung steht im Protokoll.
|
||
|
||
### Wo die Grenzen liegen
|
||
|
||
- **Keine Benutzerkonten.** Zugänge sind Keys, keine Personen. Ein Key je Person
|
||
ist der Ersatz — geplant ist später Keycloak.
|
||
- **Projekte pflegen Plattform-Admin und Workspace-Admins.** Ein Team mit
|
||
einem Projekt-Key kann seinen eigenen Projektnamen nicht selbst korrigieren,
|
||
dafür braucht es den Admin seines Workspace (AUSBAUSTUFEN.md 4.8).
|
||
- **Kein TLS im Server selbst.** Für HTTPS gehört ein Reverse-Proxy davor.
|
||
- **Ein Plattform-Admin über allem.** Workspace-Admins sind auf ihren
|
||
Workspace beschränkt, aber wer den `ADMIN_TOKEN` hat, kommt an jeden
|
||
Workspace. Ein neues Token beendet alle Sitzungen damit sofort (siehe
|
||
`BETRIEB.md`).
|
||
- **Das Schreibbudget zählt im Arbeitsspeicher.** Nach einem Neustart steht es
|
||
wieder bei null. Als Netz gegen eine Schleife reicht das; eine verbindliche
|
||
Zusage ist es nicht.
|
||
|
||
---
|
||
|
||
## Betrieb
|
||
|
||
Was im Ernstfall zu tun ist (Server startet nicht, Platte voll, Key geleakt,
|
||
Wiederherstellen aus der Sicherung), steht im Betriebshandbuch
|
||
[`BETRIEB.md`](BETRIEB.md).
|
||
|
||
### Docker
|
||
|
||
```bash
|
||
docker build -t munin .
|
||
|
||
docker run -d --name munin \
|
||
-p 8080:8080 \
|
||
-v munin-daten:/data \
|
||
-e SESSION_SECRET="$(openssl rand -hex 32)" \
|
||
-e ADMIN_TOKEN="$(openssl rand -hex 24)" \
|
||
-e ENABLE_HSTS=true \
|
||
-e TRUSTED_PROXY=172.16.0.0/12 \
|
||
munin
|
||
```
|
||
|
||
Die Geheimnisse sollten aus einem Passwortspeicher kommen, nicht bei jedem Start
|
||
neu erzeugt werden — ein neues `SESSION_SECRET` macht alle Sitzungen ungültig.
|
||
|
||
### Mit PostgreSQL
|
||
|
||
Statt der SQLite-Dateien kann Munin eine PostgreSQL-Datenbank nutzen
|
||
(Hintergrund: [AUSBAUSTUFEN.md 4.10](AUSBAUSTUFEN.md)). Darin legt es selbst
|
||
an, was es braucht: das Schema `munin_control` und je Workspace ein Schema
|
||
`munin_ws_<id>`. Die Tabellen sind dieselben wie unter SQLite.
|
||
|
||
**Datenbank einrichten** (einmalig, als Datenbank-Admin). Am einfachsten
|
||
gehört Munin die Datenbank:
|
||
|
||
```sql
|
||
CREATE ROLE munin LOGIN PASSWORD '…';
|
||
CREATE DATABASE munin OWNER munin TEMPLATE template0 LOCALE 'C' ENCODING 'UTF8';
|
||
```
|
||
|
||
Soll die Datenbank einem anderen Benutzer gehören (etwa dem des
|
||
Datenbankbetriebs), reichen diese Mindestrechte:
|
||
|
||
```sql
|
||
CREATE ROLE munin LOGIN PASSWORD '…' NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
||
GRANT CONNECT, CREATE ON DATABASE munin TO munin;
|
||
```
|
||
|
||
Mehr braucht Munin nicht: keine Rechte am Schema `public`, kein Superuser,
|
||
kein `CREATEDB`/`CREATEROLE`. Beide Wege sind erprobt (2026-10-16). Wofür
|
||
welches Recht gebraucht wird, steht in
|
||
[`BETRIEB.md`](BETRIEB.md#rechte-des-datenbankbenutzers).
|
||
|
||
| Anforderung | Warum |
|
||
|---|---|
|
||
| PostgreSQL 13 oder neuer, erprobt mit 18 | Fensterfunktionen und Identitätsspalten; pgx unterstützt 13+ |
|
||
| Munin ist Eigentümer der Datenbank **oder** hat `CONNECT, CREATE` darauf; kein Superuser | Er legt je Workspace ein Schema an. Was er anlegt, gehört ihm; deshalb darf er darin Tabellen anlegen und das Schema beim Löschen des Workspace wieder entfernen |
|
||
| Sortierung `C` (`LOCALE 'C'`) | Sortiert wie SQLite, Byte für Byte. Sonst stünde z.B. `a-c` vor `ab`. Munin warnt beim Start, wenn es anders ist |
|
||
| `max_connections` ≥ Workspaces × 3 + 14 | Je Workspace bis zu 3 Verbindungen, 4 fest, 10 Reserve. Bei 40 Workspaces also 134, `200` ist eine gute Zahl. Reicht es nicht, startet Munin nicht und nennt die nötige Zahl; ebenso wird ein neuer Workspace abgelehnt |
|
||
| Genau **ein** Munin-Prozess | Suchindex und Schreibbudget liegen im Arbeitsspeicher. Ein zweiter Prozess fände veraltete Stände |
|
||
|
||
**Starten:**
|
||
|
||
```bash
|
||
docker run -d --name munin \
|
||
-p 8080:8080 \
|
||
-v munin-daten:/data \
|
||
-e DB_DRIVER=postgres \
|
||
-e DATABASE_URL='postgres://munin@db.intern:5432/munin?sslmode=verify-full&sslrootcert=/certs/ca.pem' \
|
||
-e PGPASSWORD="…" \
|
||
-e SESSION_SECRET="…" \
|
||
-e ADMIN_TOKEN="…" \
|
||
munin
|
||
```
|
||
|
||
- **Passwort** nicht in `DATABASE_URL` schreiben, sondern über `PGPASSWORD`
|
||
oder eine `.pgpass`-Datei (beides liest Munin). Steht es doch in der URL,
|
||
müssen Sonderzeichen URL-kodiert sein (`@` → `%40`).
|
||
- **Verschlüsselung:** im Netz mindestens `sslmode=require`, besser
|
||
`verify-full` mit dem Zertifikat der eigenen CA (`sslrootcert`).
|
||
`sslmode=disable` nur, wenn die Datenbank auf demselben Rechner läuft.
|
||
- **Das Volume `/data`** braucht es nur noch für `EXPORT_DIR`, falls gesetzt.
|
||
- **Ist die Datenbank beim Start noch nicht bereit**, versucht Munin es bis zu
|
||
15-mal im Abstand von 2 Sekunden. Fällt sie im laufenden Betrieb weg,
|
||
schlagen Anfragen fehl, solange sie fehlt; abgebrochene Verbindungen
|
||
verwirft Go (`database/sql`) und baut danach neue auf. Ein Neustart von
|
||
Munin sollte also nicht nötig sein — erprobt ist das aber noch nicht, siehe
|
||
[`BETRIEB.md`](BETRIEB.md#postgresql-ist-weg).
|
||
- **Sicherung:** `scripts/backup.sh` mit `DB_DRIVER=postgres`, siehe unten.
|
||
|
||
Für die Entwicklung startet `docker-compose.dev.yml` eine passend
|
||
eingestellte Datenbank.
|
||
|
||
### Hinter einem Reverse-Proxy
|
||
|
||
```bash
|
||
TRUSTED_PROXY=127.0.0.1 # Adresse des Proxys
|
||
COOKIE_SECURE=true # falls der Proxy TLS beendet
|
||
ENABLE_HSTS=true # nur mit gültigem Zertifikat
|
||
```
|
||
|
||
Ohne `TRUSTED_PROXY` sieht der Server für alle Besucher dieselbe Adresse — das
|
||
Rate-Limiting würde dann entweder niemanden bremsen oder beim ersten Angreifer
|
||
gleich alle aussperren.
|
||
|
||
### Datensicherung
|
||
|
||
**Datenbank (SQLite).** Jeder Workspace (Abteilung) liegt in einer eigenen Datei unter
|
||
`DATA_DIR/workspaces/`, dazu kommt die zentrale Kontroll-Datenbank
|
||
`DATA_DIR/control.db`. Weil SQLite im WAL-Modus läuft, sollte im laufenden
|
||
Betrieb nicht einfach kopiert werden — `scripts/backup.sh` sichert beides
|
||
mit `sqlite3 .backup`, in einem zeitgestempelten Ordner je Lauf:
|
||
|
||
```bash
|
||
DATA_DIR=/pfad/data BACKUP_DIR=/pfad/sicherungen scripts/backup.sh
|
||
```
|
||
|
||
Für Cron, täglich um 3 Uhr mit 14 Tagen Aufbewahrung:
|
||
|
||
```cron
|
||
0 3 * * * DATA_DIR=/srv/munin/data BACKUP_DIR=/srv/backups RETENTION_DAYS=14 \
|
||
/srv/munin/scripts/backup.sh >> /var/log/munin-backup.log 2>&1
|
||
```
|
||
|
||
Da jede Abteilung ihre eigene Datei hat, lässt sich eine einzelne davon auch
|
||
gezielt sichern oder wiederherstellen, ohne die übrigen anzufassen: Server
|
||
stoppen, die gewünschte `.db`-Datei aus einem Sicherungsordner zurückkopieren,
|
||
Server wieder starten. Die genauen Schritte und was dabei zu beachten ist
|
||
(Hilfsdateien `-wal`/`-shm`, wieder gültige Keys) stehen in
|
||
[`BETRIEB.md`](BETRIEB.md#aus-einer-sicherung-wiederherstellen).
|
||
|
||
**Datenbank (PostgreSQL).** Dasselbe Skript sichert mit `pg_dump` jedes Schema
|
||
in eine eigene Datei: `control.dump` und `workspaces/munin_ws_<id>.dump`. Jede
|
||
Datei wird nach dem Schreiben auf Lesbarkeit geprüft. Ein einzelner Workspace
|
||
lässt sich so gezielt zurückspielen.
|
||
|
||
```bash
|
||
DB_DRIVER=postgres DATABASE_URL='postgres://munin@db.intern:5432/munin' \
|
||
BACKUP_DIR=/pfad/sicherungen scripts/backup.sh
|
||
```
|
||
|
||
Gebraucht werden `psql`, `pg_dump` und `pg_restore` in mindestens der Version
|
||
des Servers. Fehlen sie, lassen sie sich aus einem PostgreSQL-Container leihen
|
||
(`PSQL`, `PG_DUMP`, `PG_RESTORE`, siehe Kopf von `scripts/backup.sh`).
|
||
|
||
**Probe der Sicherung.** `scripts/sicherung-pruefen.sh` spielt eine Sicherung
|
||
zurück, startet Munin darauf (eigener Port, eigene Geheimnisse), wartet auf
|
||
`/ready`, vergleicht die Zahl der Workspaces und räumt auf. Für PostgreSQL
|
||
braucht es eine leere Probe-Datenbank (`PROBE_DATABASE_URL`), die einmal
|
||
angelegt wird; ist sie nicht leer, bricht das Skript ab.
|
||
|
||
```bash
|
||
scripts/sicherung-pruefen.sh /pfad/sicherungen/<zeitstempel>
|
||
DB_DRIVER=postgres PROBE_DATABASE_URL='postgres://munin@db.intern:5432/munin_probe' \
|
||
scripts/sicherung-pruefen.sh /pfad/sicherungen/<zeitstempel>
|
||
```
|
||
|
||
**Markdown-Export.** Ist `EXPORT_DIR` gesetzt, schreibt der Server den Bestand
|
||
aller Workspaces täglich als Markdown-Dateien dorthin: ein Ordner je Workspace,
|
||
darin ein Ordner je Projekt, eine Datei je Eintrag und eine `_index.md` als
|
||
Übersicht:
|
||
|
||
```
|
||
export/
|
||
mfk3x9a2-1a2b3c4d/ ← Workspace-ID
|
||
_workspace.md ← Name des Workspace
|
||
plattform/
|
||
_index.md
|
||
deployment.md
|
||
server-zugang.md
|
||
```
|
||
|
||
Die Workspace-Ebene ist nötig, weil Projekt-Bezeichner nur innerhalb eines
|
||
Workspace eindeutig sind. Der Ordner heißt nach der ID und nicht nach dem Namen,
|
||
damit ein Umbenennen keinen neuen Ordner erzeugt. Welcher Workspace dahinter
|
||
steckt, steht in `_workspace.md`.
|
||
|
||
> **Umstieg von älteren Fassungen:** Früher lagen die Projektordner direkt unter
|
||
> `EXPORT_DIR/`. Diese alten Ordner werden nicht mehr angefasst und können nach
|
||
> einem erfolgreichen Export gelöscht werden.
|
||
|
||
Jede Datei trägt alle Felder im Kopf (Bezeichner, Schlagworte, Herkunft, Autor,
|
||
Fassung, Zeitstempel), sodass sich der Bestand daraus auch wieder einlesen ließe.
|
||
|
||
Das eignet sich als Git-Sicherung: Ein Lauf ohne inhaltliche Änderungen fasst
|
||
keine Datei an und erzeugt deshalb auch keinen Diff.
|
||
|
||
```bash
|
||
# Auf Knopfdruck: "Jetzt exportieren" unter /admin/projects
|
||
# (exportiert nur den gerade gewählten Workspace)
|
||
|
||
# Für Cron oder ein Sicherungsskript, alle Workspaces:
|
||
./munin -export
|
||
```
|
||
|
||
Scheitert der Export eines Workspace, laufen die übrigen trotzdem durch.
|
||
`./munin -export` endet dann aber mit einem Fehlercode, damit ein Cronjob die
|
||
Sicherung nicht für vollständig hält.
|
||
|
||
**Download als ZIP.** Unabhängig von `EXPORT_DIR` lässt sich derselbe Inhalt
|
||
auch direkt im Browser herunterladen: ein Projekt über *Als ZIP herunterladen*
|
||
über der Eintragsliste (Rolle `admin`), ein ganzer Workspace über *Alle als ZIP
|
||
herunterladen* unter *Projekte*. Das ZIP entsteht bei jedem Klick frisch und
|
||
wird nicht auf dem Server abgelegt. Es hat denselben Aufbau wie der Export und
|
||
passt deshalb ohne Umpacken in den Import. Jeder Download steht im Protokoll
|
||
(`export_download`). Wer außer Admins herunterladen darf, steht an einer
|
||
einzigen Stelle: `Session.CanDownloadProject` in `internal/web/context.go`.
|
||
|
||
Der Export ist eine Sicherung, keine zweite Wahrheit — Änderungen an den Dateien
|
||
wirken nicht zurück und gehen beim nächsten Lauf verloren.
|
||
|
||
### Protokoll
|
||
|
||
Vorgänge werden 90 Tage aufbewahrt und danach automatisch gelöscht
|
||
(`auditRetention` in `main.go`). Das Protokoll enthält Adressen und Zugangsnamen,
|
||
aus denen sich Personen ableiten lassen — eine Aufbewahrungsfrist ist deshalb die
|
||
Voraussetzung dafür, es überhaupt führen zu dürfen.
|
||
|
||
Getrennt davon steht das **Suchprotokoll**: was Agenten mit `recall` gesucht
|
||
und ob sie etwas gefunden haben, ausgewertet auf der Nutzungsseite. Ohne
|
||
Adressen und Namen, aber mit dem Text der Anfragen — deshalb nur 30 Tage
|
||
(`store.SearchLogRetention`), und es verschwindet mit dem Projekt.
|
||
|
||
Jeder Workspace führt sein eigenes Protokoll in seiner eigenen Datei.
|
||
`/admin/audit` zeigt das Protokoll des gerade gewählten Workspace, nicht alle
|
||
auf einmal. Das tägliche Aufräumen läuft über alle Workspaces, auch über die,
|
||
die erst nach dem Serverstart angelegt wurden.
|
||
|
||
Bei Schreibvorgängen an Einträgen steht in der Spalte *Details*, ob der Eintrag
|
||
**angelegt** oder **aktualisiert** wurde. Daran lässt sich ablesen, ob Agenten
|
||
vorhandene Einträge fortschreiben oder immer neue anlegen.
|
||
|
||
---
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
go run . # starten
|
||
go test ./... # alle Tests
|
||
go vet ./... # statische Prüfung
|
||
gofmt -l . # Formatierung prüfen
|
||
```
|
||
|
||
### Aufbau
|
||
|
||
```
|
||
main.go Konfiguration, Verdrahtung, Serverstart
|
||
internal/
|
||
store/ SQLite: Schema, Migrationen, alle Abfragen
|
||
(Workspace-Dateien und Kontroll-Datenbank)
|
||
workspace/ Öffnet Workspace-Dateien bei Bedarf und hält sie offen
|
||
entry/ Fachlogik: Verweise, Textausschnitte, Zeilenvergleich
|
||
markdown/ Markdown nach HTML — ein Renderer für Einträge und Hilfe
|
||
search/ BM25-Volltextindex, einer je Projekt
|
||
auth/ Sitzungen, CSRF, Cookies, Keys, Rate-Limiting
|
||
export/ Markdown-Export als Sicherung
|
||
web/ Routen, Middleware, Handler
|
||
mcpserver/ MCP-Werkzeuge
|
||
hilfe/ Eingebaute Hilfeseiten (Markdown in seiten/)
|
||
templates/ static/ eingebettet ins Binary
|
||
scripts/backup.sh Sicherung aller Datenbankdateien
|
||
```
|
||
|
||
Eine Übersicht über jede einzelne Datei steht in [REPOMAP.md](REPOMAP.md).
|
||
|
||
**Hilfeseiten pflegen:** Die Hilfe unter `/hilfe` sind Markdown-Dateien in
|
||
`internal/hilfe/seiten/`, die mit ins Binary gebaut werden. Ändert sich eine
|
||
Funktion, gehört die Anpassung der Hilfe in denselben Commit. Eine neue Seite
|
||
braucht eine Datei *und* einen Eintrag in der Liste `seiten` in
|
||
`internal/hilfe/hilfe.go`. Ein Test prüft, dass beides zusammenpasst und dass
|
||
alle Links auf `/hilfe/…` zu existierenden Seiten führen. Seiten mit
|
||
`NurAdmin: true` sieht nur der Plattform-Admin; allgemeine Seiten dürfen
|
||
nicht auf sie verlinken (auch das prüft ein Test). In den Seiten
|
||
werden `{{SERVER}}` und `{{PROJEKT}}` beim Anzeigen durch die Adresse des
|
||
Servers und den Bezeichner des eigenen Projekts ersetzt.
|
||
|
||
Die Richtung der Abhängigkeiten zeigt immer nach innen: `web` und `mcpserver`
|
||
kennen `store`, aber `store` kennt keinen von beiden.
|
||
|
||
### Datenbank
|
||
|
||
SQLite über `modernc.org/sqlite` — ein in Go übersetzter Nachbau, kein C — oder
|
||
PostgreSQL über `pgx`, ebenfalls reines Go. Nur deshalb lässt sich das Programm
|
||
als statisches Binary ohne Laufzeitumgebung ausliefern.
|
||
|
||
Welche Datenbank, entscheidet `DB_DRIVER`. Alle Abfragen sind für beide
|
||
dieselben: Sie werden mit `?` geschrieben und laufen durch einen kleinen
|
||
Dialekt, der für PostgreSQL `$1, $2 …` daraus macht (`internal/store/dialekt.go`,
|
||
`verbindung.go`). Das SQL vermeidet deshalb alles, was nur eine der beiden
|
||
kann. Die Regeln stehen als Kommentar in
|
||
`internal/store/migrations/sqlite/001_init.sql`.
|
||
|
||
Es gibt zwei Schemata mit je eigenem Migrationsordner, darin je Datenbank ein
|
||
Unterordner:
|
||
|
||
| Ordner | Gilt für |
|
||
|---|---|
|
||
| `internal/store/migrations/{sqlite,postgres}/` | jeden Workspace (Projekte, Einträge, Keys, Protokoll) |
|
||
| `internal/store/migrations_control/{sqlite,postgres}/` | die Kontroll-Datenbank (Workspaces, Zuordnung Key → Workspace) |
|
||
|
||
**Schema ändern:** eine neue Datei mit der nächsten Nummer anlegen, derzeit
|
||
`008_….sql` für Workspaces, **in beiden Unterordnern** unter demselben Namen.
|
||
Sie wird automatisch angewandt und vermerkt: für die Kontroll-Datenbank beim
|
||
Start, für einen Workspace beim ersten Öffnen. Bestehende Migrationen werden
|
||
nie verändert.
|
||
|
||
Zwei Tests passen auf: `TestMigrationsordnerGleich` (beide Ordner haben
|
||
dieselben Dateien, läuft immer) und `TestSchemaGleich` (beide ergeben dasselbe
|
||
Schema, braucht PostgreSQL):
|
||
|
||
```bash
|
||
docker compose -f docker-compose.dev.yml up -d
|
||
MUNIN_TEST_PG='postgres://munin:munin@localhost:5432/munin_test?sslmode=disable' go test ./...
|
||
```
|
||
|
||
---
|
||
|
||
## Was noch aussteht
|
||
|
||
[AUSBAUSTUFEN.md](AUSBAUSTUFEN.md) hält fest, was bewusst nicht gebaut wurde —
|
||
mit Messwerten, Begründung und den Auslösern, an denen man merkt, dass es Zeit
|
||
dafür ist. Unter anderem: FTS5 statt eigenem Suchindex, eine Datenbank je
|
||
Projekt, semantische Suche.
|