System-Prompts (per-tenant, 3-Level-Hierarchie)
Per-Tenant versionierter System-Prompt-Stack mit Tenant / Scope / User
Levels, Rollback, Audit-Trail und automatischer Inject-Pipeline in
/v1/chat/completions und /c1/chat. Stack-Owner können ihren globalen
Default selbst pflegen (Cut 2.35).
Use-Case 1 (häufig): "Wir wollen unseren tenant-globalen Prompt (z.B. Brand-Voice, Compliance, Persona) einmal hinterlegen und in jedem Chat-Call automatisch wirksam haben." → tenant-level POST.
Use-Case 2: "Wir wollen pro Application/Bereich einen anderen Prompt (z.B. 'support' vs 'sales')." → scope-level POST + Header.
Use-Case 3 (selten): "Wir wollen pro Endnutzer einen Prompt-Override (Personalisierung)." → user-level POST.
3-Level-Hierarchie
| Level | Scope | Lookup-Key | Required Headers |
|---|---|---|---|
tenant | gilt für alle Calls des Tenants | level='tenant' | — |
scope | gilt pro scope_id (z.B. App-Name oder Use-Case) | level='scope', scope_id=… | X-Scope-Id |
user | gilt pro (scope_id, user_id) | level='user', scope_id=…, user_id=… | X-Scope-Id, X-User-Id |
Komposition: tenant + scope + user werden bei jedem Chat-Call als XML-Stack
zusammengefügt (von <tenant_default> über <scope_default> bis
<user_prompt>) und als system-Message vor jeder anderen Message
eingefügt. Leere Levels werden übersprungen.
Permission-Modell
6 Scopes, granular delegierbar pro Token in tokens.yaml:
system_prompt:read:tenant
system_prompt:read:scope
system_prompt:read:user
system_prompt:write:tenant
system_prompt:write:scope
system_prompt:write:user
write impliziert automatisch read auf gleichem Level (siehe
expand_writes() in auth/scopes.rs).
Default-Mapping pro Role:
| Role | tenant w | scope w | user w | tenant r | scope r | user r |
|---|---|---|---|---|---|---|
tenant_admin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
tenant_user | — | — | ✓ | — | ✓ | — |
audit_reader | — | — | — | ✓ | ✓ | ✓ |
Custom-Tokens können zusätzliche Scopes via extra_scopes bekommen.
Beispiel godelmann-gocreate-{test,prod} (Cut 2.35): tenant_user +
extra_scopes: [system_prompt:write:tenant, system_prompt:write:scope].
Endpoints
Alle 8 Endpoints sind im OpenAPI-Schema (/openapi.json) ab Cut 2.35
und können mit TypeScript-Codegen (openapi-generator) integriert werden.
| Method | Path | Beschreibung |
|---|---|---|
POST | /v1/system-prompts/{level} | Neue Version anlegen, wird automatisch is_current=1 |
GET | /v1/system-prompts/{level} | Current Version lesen (null wenn keine) |
GET | /v1/system-prompts/{level}/versions | Alle Versionen auflisten (für History-UI) |
GET | /v1/system-prompts/{level}/v/{n} | Spezifische Version lesen |
PUT | /v1/system-prompts/{level}/current/{n} | Rollback: alte Version aktivieren |
DELETE | /v1/system-prompts/{level} | Soft-delete der current-Version (is_current=0) |
DELETE | /v1/system-prompts/{level}/v/{n} | Hard-delete einer archivierten Version (409 wenn current) |
GET | /v1/system-prompts/effective | Composed XML-Stack für aktuelle Token+Scope+User-Kombination |
Beispiele — globalen tenant-Prompt setzen
BEARER="$YOUR_TENANT_BEARER"
# 1) Tenant-globalen Prompt setzen
curl -X POST https://dgx.spass.fun/v1/system-prompts/tenant \
-H "Authorization: Bearer $BEARER" \
-H "Content-Type: application/json" \
-d '{
"content": "Du bist ein präziser, höflicher Assistent. Antworte auf Deutsch...",
"comment": "Brand-Voice v1.0, 2026-05-16"
}'
# → { "id": 42, "level": "tenant", "version": 1, "is_current": true, ... }
# 2) Aktuelle Version lesen
curl https://dgx.spass.fun/v1/system-prompts/tenant \
-H "Authorization: Bearer $BEARER"
# 3) Versions-Historie
curl https://dgx.spass.fun/v1/system-prompts/tenant/versions \
-H "Authorization: Bearer $BEARER"
# 4) Effective-Stack verifizieren
curl https://dgx.spass.fun/v1/system-prompts/effective \
-H "Authorization: Bearer $BEARER"
# → { "tenant": {"version": 1, "content": "...", "is_current": true},
# "scope": null, "user": null,
# "effective": "<tenant_default>...</tenant_default>",
# "byte_size_visible": 234 }
Auto-Inject Pipeline (Cut 2.20+)
Wichtigster Fakt: sobald ein tenant-Prompt gesetzt ist, wird er
automatisch bei jedem Call zu /v1/chat/completions und /c1/chat
in messages[] injiziert. Kein system_prompt_ref-Feld nötig, kein
Frontend-Code-Inject — der Stack-Builder läuft per-request server-side.
# Nach POST /v1/system-prompts/tenant ist KEIN client-side prefix mehr nötig:
curl -X POST https://dgx.spass.fun/c1/chat \
-H "Authorization: Bearer $BEARER" -H "SPASS-User-Id: $USER_ID" \
-d '{"model":"...", "message":"Hallo"}'
# Response-Header zeigt was injiziert wurde:
# SPASS-Augment-Applied: system-prompt=1items, server-tools=..., memory=...
Deaktivieren per-call mit SPASS-Augment: system-prompt=off (z.B.
für admin-Diagnose-Calls). Default ist on.
Versions-Management & Rollback
Jede POST legt eine neue Version mit auto-incrementiertem version
an und macht sie is_current=1. Alle vorherigen Versionen bleiben in
der DB (append-only-history für audit).
# v2 anlegen
curl -X POST https://dgx.spass.fun/v1/system-prompts/tenant \
-H "Authorization: Bearer $BEARER" -d '{"content":"v2 prompt..."}'
# Falls v2 schlecht: zurück auf v1
curl -X PUT https://dgx.spass.fun/v1/system-prompts/tenant/current/1 \
-H "Authorization: Bearer $BEARER"
# → audit-event: system_prompt_rollback
# v2 endgültig löschen (NUR wenn nicht current!)
curl -X DELETE https://dgx.spass.fun/v1/system-prompts/tenant/v/2 \
-H "Authorization: Bearer $BEARER"
# → 409 wenn v2 noch current ist
Audit-Trail
Jede POST/PUT/DELETE schreibt ein Event in audit.jsonl mit:
actor_token_hash(SHA-256 des aufrufenden Bearer-Tokens)actor_user_id(falls header gesetzt)action: einer vonsystem_prompt_create | system_prompt_rollback | system_prompt_unset_current | system_prompt_delete_versionmetadata.{level, version, comment}
Operator-Filter:
jq 'select(.action | startswith("system_prompt_"))' \
/home/dietmar/dgx-llm/data/audit/audit.jsonl.$(date +%F)
Plus die system_prompts-Row selbst speichert created_by (= token_hash)
und created_at (unix-ts). Über GET /v1/system-prompts/{level}/versions
sieht man die ganze Historie inkl. wer-wann-was.
Effective-View Redaction
GET /v1/system-prompts/effective zeigt die ganze Stack-Komposition, aber
redactet Levels die der Token nicht lesen darf. Beispiel mit einem
Token der nur system_prompt:read:scope hat:
{
"tenant": {"redacted": true},
"scope": {"version": 5, "content": "...", ...},
"user": {"redacted": true},
"effective": "<scope_default scope=\"...\">...</scope_default>",
"byte_size_visible": 482
}
Das ist LLM07-Compliance ("show only what the token has scope to see"). Das Modell-im-Call sieht aber alle Levels — die Redaction wirkt nur auf die Operator-Introspection.
Limits
content-Länge: per-Level konfigurierbar viaread_max_bytes_per_level(). Defaults: tenant 32 KB, scope 16 KB, user 8 KB.- Versions-Limit: keine harte Grenze, aber
GET /versionsreturnt alle. Operator-side cleanup-Jobs für ältere Versionen sind nicht eingebaut (manuell via DELETE wenn nötig). - Rate-Limit: kein dedicated bucket — fällt unter den globalen per-Token-Rate-Limit (default 1/s, 30 burst).
Cross-References
response-headers.md—SPASS-Augment-AppliedHeader zeigt was injiziert wurdeexamples-conversation.md— Cut 2.33 Hybrid-Schema für per-conversation-override (system_prompt_ref)changelog.md— Cut 2.20 (inject-pipeline) + Cut 2.35 (self-service)