MCP Knowledge Base
Find a file
2026-10-19 10:13:56 +02:00
internal Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
scripts Erweiterung um Postgres und Systemstats, Bugfixes und Erweiterung UI 2026-10-16 22:36:24 +02:00
static Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
templates Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
.dockerignore initial commit after rewrite 2026-09-15 20:36:26 +02:00
.env.example Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
.gitignore Workspaces, Änderungshistorie, Papierkorb, Nutzungsübersicht, Hilfe 2026-09-25 07:44:52 +02:00
AGENTEN-GUIDE.md Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
AUSBAUSTUFEN.md Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
BETRIEB.md Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
docker-compose.dev.yml Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
Dockerfile Erweiterung um Postgres und Systemstats, Bugfixes und Erweiterung UI 2026-10-16 22:36:24 +02:00
go.mod Erweiterung um Postgres und Systemstats, Bugfixes und Erweiterung UI 2026-10-16 22:36:24 +02:00
go.sum Erweiterung um Postgres und Systemstats, Bugfixes und Erweiterung UI 2026-10-16 22:36:24 +02:00
last_test.go Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
LASTTEST.md Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
main.go Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
main_test.go Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
README.md Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00
REPOMAP.md Bugfixes, Security und Umbau auf einheitliche Namen 2026-10-19 10:13:56 +02:00

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

# 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

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:

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:

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

Starten und wo die Daten landen

./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:

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:

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:

{
  "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.

{
  "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:

{
  "$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.


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/duplicates und internal/mcpserver/curated.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/duplicates), 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 32 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.

Docker

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

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:

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.

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:

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.
  • 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

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:

DATA_DIR=/pfad/data BACKUP_DIR=/pfad/sicherungen scripts/backup.sh

Für Cron, täglich um 3 Uhr mit 14 Tagen Aufbewahrung:

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.

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.

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.

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.

# 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

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.

Hilfeseiten pflegen: Die Hilfe unter /hilfe sind Markdown-Dateien in internal/help/pages/, 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 pages in internal/help/help.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/dialect.go, pool.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: TestMigrationDirsMatch (beide Ordner haben dieselben Dateien, läuft immer) und TestSchemaMatches (beide ergeben dasselbe Schema, braucht PostgreSQL):

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 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.