| internal | ||
| scripts | ||
| static | ||
| templates | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTEN-GUIDE.md | ||
| AUSBAUSTUFEN.md | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
| REPOMAP.md | ||
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
- Grundbegriffe
- MCP einrichten
- Die Werkzeuge
- Weboberfläche
- Konfiguration
- Sicherheit
- Betrieb
- Entwicklung
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:
- 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.
- Projekt anlegen unter Projekte
- Zugang erzeugen — der Key wird genau einmal angezeigt
- MCP-Client einrichten (die Anleitung erscheint direkt neben dem neuen Key)
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, 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. 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. |
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.
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/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, Schlagworte für viele Einträge auf einmal ändern |
/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}/usage |
Nutzung: Schreibvorgänge der letzten 30 Tage (Agenten/Menschen, neu/aktualisiert), 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 wierecall. 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 |
| 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 pflegt nur der Plattform-Admin. Ein Team kann seinen eigenen Projektnamen nicht selbst korrigieren. Solange es wenige Projekte gibt, ist eine Berechtigungsstufe weniger den Umweg wert.
- Kein TLS im Server selbst. Für HTTPS gehört ein Reverse-Proxy davor.
- Ein Admin-Token für alle Workspaces. Es gibt keinen Admin je Workspace.
Wer den
ADMIN_TOKENhat, kommt an jeden Workspace. - 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
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.
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. 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.
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.
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.
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/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 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.