732 lines
31 KiB
Markdown
732 lines
31 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/mcpserver/dubletten.go`
|
|
und `kuratiert.go`.
|
|
|
|
### 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 |
|
|
| `/p/{projekt}/` | Einträge durchsuchen, nach Schlagwort filtern, 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) |
|
|
| `/p/{projekt}/e/{bezeichner}` | Eintrag lesen, mit Rückverweisen |
|
|
| `/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 (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 |
|
|
| `/p/{projekt}/trash` | Papierkorb: wiederherstellen oder endgültig löschen (Schreibrecht) |
|
|
| `/hilfe` | Eingebaute Onboarding-Hilfe für alle Rollen: Einstieg, Begriffe, MCP einrichten, Arbeiten mit Agenten, Weboberfläche, Zugänge. Die Seite für Plattform-Admins sieht nur der Plattform-Admin. |
|
|
| `/healthz` | Healthcheck, ohne Anmeldung |
|
|
|
|
### 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.
|
|
|
|
Vorschau, Autovervollständigung 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. |
|
|
| `DATA_DIR` | `data` | Datenverzeichnis: Kontroll-Datenbank (`control.db`) und `workspaces/` mit je einer Datei pro Workspace |
|
|
| `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.
|
|
|
|
### 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.** 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).
|
|
|
|
**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. Nur
|
|
deshalb lässt sich das Programm als statisches Binary ohne Laufzeitumgebung
|
|
ausliefern.
|
|
|
|
Das SQL vermeidet bewusst alles, was nur SQLite kann (kein `AUTOINCREMENT`, keine
|
|
Datumsfunktionen, keine `INSERT OR REPLACE`), damit ein Umstieg auf PostgreSQL
|
|
offenbleibt. Die Regeln stehen als Kommentar in
|
|
`internal/store/migrations/001_init.sql`.
|
|
|
|
Es gibt zwei Schemata mit je eigenem Migrationsordner:
|
|
|
|
| Ordner | Gilt für |
|
|
|---|---|
|
|
| `internal/store/migrations/` | jede Workspace-Datei (Projekte, Einträge, Keys, Protokoll) |
|
|
| `internal/store/migrations_control/` | die Kontroll-Datenbank `control.db` (Workspaces, Zuordnung Key → Workspace) |
|
|
|
|
**Schema ändern:** im passenden Ordner eine neue Datei mit der nächsten Nummer
|
|
anlegen, für Workspace-Dateien derzeit `005_….sql`. Sie wird automatisch
|
|
angewandt und vermerkt: für die Kontroll-Datenbank beim Start, für eine
|
|
Workspace-Datei beim ersten Öffnen. Bestehende Migrationen werden nie verändert.
|
|
|
|
---
|
|
|
|
## 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.
|