adr vertraulich owner: matus
ADR-0005 — Eigene Capability-Authority, eigener IdP, eigene Virtual Identity
ADR-0005 — Eigene Capability-Authority, eigener IdP, eigene Virtual Identity
Status: Angenommen · Datum: 2026-08-10
Kontext-Regeln: GG-ARCH-SERVICES-0005
(Zwirn-ADR-0027),
GG-GOV-SECURITY-0003,
GG-DEVOPS-0003
Ersetzt: den Zustand „Platzhalter-URL auf einen Dienst, den es nicht gibt" aus
agentic-engineering/backlog/capability-authority.md
Kontext
GG-ARCH-SERVICES-0005.5 macht eine Capability-Authority vor dem ersten Nutzer, der nicht der
Entwickler ist fällig. Bis dahin genügt ein auf das dev-Profil begrenzter Stub plus ein benannter
Backlog-Punkt — beides war gegeben, und der Konformitätsstand der Regel führte tuxametrics deshalb
ausdrücklich als „nicht rückwirkend zu reparieren".
Diese Frist ist abgelaufen. Der Auslöser ist kein technischer: es sollen Ärztinnen und Ärzte mit
echten Patientendaten arbeiten. Ab diesem Moment ist ein Platzhalter kein „bewusst offen" mehr,
sondern ein Regelverstoß — so steht es in -0005.5 wörtlich.
Damit fallen drei Entscheidungen gleichzeitig an, weil keine ohne die anderen tragfähig ist:
- Welche Form die Authority hat (
-0005.1: eigen oder fremd). - Welcher IdP Tokens ausstellt — ohne ihn gibt es keinen
actorKey, gegen den ein Grant liefe. - Wie eine Praxis ihre Handelnden führt — Personen, Funktionspostfächer und, neu, agentische Identitäten.
Entscheidung
1. Eigene Authority: tuxametrics-platform-service
Von den zwei zulässigen Formen aus -0005.1 fällt fremd aus: eine Authority ist immer die eines
Plattform-Repos desselben Kunden, und es gibt kein solches Repo. Die frühere Annahme, es gäbe eine
mitbenutzbare „Zwirn-Plattform-Authority", war nachweislich falsch — Zwirn liefert die Konsumentenseite
und das Muster, aber keinen betriebenen Dienst.
Umfang: exakt -0005.2, gebaut nach dem belegten Minimalbauplan
dkfz-prozesswerkstatt/prozesswerkstatt-platform-service (34 Java-Dateien). Nicht DOTs 72-Datei-Umfang
(Agent-Sessions, Keycloak-Client-Provisioning, Profile, OPA, Outbox, Drift-Scheduler) — für keinen dieser
Bausteine gibt es hier einen benennbaren Bedarf, und -0005.2 verlangt genau diese Begründungspflicht.
Port 8125, eigene Postgres auf 5524, eingetragen in
service-landscape.md.
2. IdP: Keycloak, Realm digital-labs
Der Realm-Name ist nicht neu erfunden: er stand seit der Erstanlage als Default in
application.properties und .env.example. Diese Entscheidung löst ihn ein, sie ersetzt ihn nicht.
Keycloak beantwortet ausschließlich wer bist du; was darfst du beantwortet die Authority
(GG-GOV-SECURITY-0003, „AuthN vs. AuthZ" — IdP-Kopplung auf einer Ebene ist unvermeidbar, auf zwei
wäre sie ein Migrationsrisiko).
Zwei Clients (keycloak/realm-export.json): tuxametrics-ui (public, PKCE) und
tuxametrics-services (confidential, service-account).
3. Eine Virtual Identity — obwohl sie kein Pflichtbaustein ist
GG-ARCH-SERVICES-0005 grenzt ausdrücklich ab: kein {produkt}-virtual-identity-service verlangt.
Der empirische Befund ist gemischt (drei Produkte haben eine, dkfz/eventmanagement hat keine, evenwes
hat seine wieder entfernt), und wer eine baut, baut ein Fachmodul, keine Baseline.
Wir bauen trotzdem eine, und zwar aus einem fachlichen Grund, nicht aus Symmetrie: ein Mandant ist eine
Praxis, und in einer Praxis handeln mehrere. Das Fachmaterial führt den Widerspruch „Praxis bzw.
Therapeut" (GM-6) als offene Frage OF-12, mit der ausdrücklichen Feststellung, dass ein Prototyp mit
einem Therapeuten sie umgeht und die erste Praxis mit zwei Therapeuten nicht. Eine Identity-Registry ist
der Ort, an dem diese Frage überhaupt beantwortbar wird. Ein Keycloak-Konto allein leistet das nicht: es
kennt Anmeldungen, keine Rollen im Behandlungsablauf, und ein Funktionspostfach hat gar keins.
Port 8126, eigene Postgres auf 5525. Gegenüber der Authority ist die VID Konsument, nie
Gegenstück — so verlangt es die Abgrenzung in -0005.
Die vier Entscheidungen im Detail, die nicht aus dem Bauplan folgen
A. Die drei GM-7-Capabilities sind im Katalog festgenagelt
Das ist die inhaltlich wichtigste Zeile dieser ganzen Arbeit.
txm.empfehlung.decide (Therapeutenentscheidung), txm.uebersteuerung.create (informierte
Übersteuerung, GM-7.2) und txm.freigabe.grant (ärztliche Freigabe) tragen dauerhaft
allowedActors=[USER]. Durchgesetzt an vier Stellen
(tuxametrics-platform-service/util/Gm7Schutz, belegt in Gm7SchutzTest):
- Der In-Process-Registrar der Authority legt sie beim Start selbst an — es gibt kein Zeitfenster, in dem sie fehlen und ein Grant deshalb ungeprüft durchginge.
CapabilityService.upsertweist jeden abweichenden Upsert mit 422 ab — auch eine leereallowedActors-Liste, die im übrigen Katalog „keine Einschränkung" bedeutet und hier die Katastrophe wäre.CapabilityGrantService.createweist einen Grant mitactorType != USERmit 422 ab. Der Check würde ihn ohnehin verwerfen — aber ein existierender Grant, der nie greift, steht in jeder Übersicht und behauptet ein Recht, das es nicht gibt.- Der Check selbst vergleicht
allowedActorsvor dem Grant-Lookup.
Warum das kein Widerspruch zu GM-7 ist — und diese Unterscheidung trägt die ganze Entscheidung:
GM-7 verbietet eine technische Sperre gegen eine klinische Entscheidung. Eine Empfehlung darf nie
unwiderruflich blockiert werden, eine Übersteuerung ist ausdrücklich vorgesehen, und Leitplanke 1 in
CLAUDE.md sagt: „Wer eine Sicherheitsregel als harte Validierung implementiert, widerspricht GM-7
frontal." Hier wird nichts davon gesperrt. Gesperrt wird ausschließlich, wer handeln darf: ein
Mensch, kein Agent. Das ist dieselbe Aussage wie GM-7 selbst — nur durchsetzbar gemacht statt
zugesichert. Gm7SchutzTest enthält die Gegenprobe ausdrücklich als eigenen Test: ein Therapeut mit
Grant darf freigeben.
Was vorher galt und warum es nicht reichte: „kein Agent gibt frei" war bis hierher nur dadurch wahr,
dass die MCP-Tools des Laborauswertungs-Service ausnahmslos lesend sind. Das ist eine Eigenschaft des
Codes, die ein einziger neuer @Tool still gekippt hätte — eine Zusage, kein Mechanismus.
B. Agentische Identitäten: eine Achse, eine Ableitung
Zu entscheiden war, ob „agentisch" eine zweite Achse neben PERSON/FUNCTIONAL_MAILBOX ist. Sie ist
es nicht. Eine Identität ist entweder ein Mensch, ein von Menschen geteiltes Postfach oder Software;
die drei schließen sich aus. IdentityType bekommt deshalb einen dritten Wert AGENT, kein zweites
Feld.
Der technische Actor-Typ (ActorType: USER/AGENT/SERVICE), gegen den allowedActors prüft, ist
daraus vollständig ableitbar und wird deshalb nicht gespeichert: IdentityType.AGENT.actorType()
ist konstant AGENT, PERSON und FUNCTIONAL_MAILBOX sind USER. Zwei Spalten, die dasselbe sagen,
können auseinanderlaufen — und ein Datensatz mit identityType=AGENT, actorType=USER wäre exakt der
Zustand, gegen den Punkt A absichert. Der Domain-Record liefert actorType trotzdem mit, damit niemand
die Ableitung nachbaut und dabei falsch liegt.
Folgerichtig ist identityType nicht editierbar: aus einer agentischen Identität nachträglich eine
Person zu machen würde einen bestehenden Ausschluss lautlos aufheben. Wer den Typ ändern will,
deaktiviert und legt neu an — dann bleibt die Änderung in createdBy/createdDate sichtbar.
C. Der Bootstrap-Grant wird angelegt, nicht dokumentiert
PUT /api/platform/v1/capabilities/{key} ist selbst mit platform.capability.create gegatet —
konsequent aus GG-GOV-SECURITY-0003 („keine ungegatete Funktion, ohne Ausnahme"), aber ein Henne-Ei:
ein Service braucht bereits einen Grant, um sich registrieren zu können.
prozesswerkstatt-platform-service führt das als bekannte operative Lücke mit einem Operator-Vorgang.
Hier ist sie geschlossen. Der In-Process-Registrar legt genau einen Grant an: der
Service-Actor svc-tuxametrics-services darf platform.capability.create im Sentinel-Mandanten
platform. Ein Actor, eine Capability, ein Mandant, idempotent. Begründung: „der Operator macht das
schon" ist bei einem Produkt mit Gesundheitsdaten kein Betriebszustand, und ein Registrar, der beim
ersten Start jedes Services stumm mit 403 scheitert, ist die Art von Fehler, die man erst bemerkt, wenn
ein Endpunkt unerklärlich sperrt.
D. Demo-Grants sind ein Schalter, und er steht auf false
Ein frisch hochgezogener Stack hat keinen einzigen Grant für einen Menschen und ist damit vollständig
fail-closed. Das ist richtig und für einen Demo-Durchlauf unbrauchbar. TXM_DEMO_GRANTS=true legt den
Mandanten praxis01 an und grantet dem Demo-Nutzer alle 18 txm.*-Capabilities.
Der Default ist false, auch im %docker-Profil — nur im %dev-Profil ist er an. Ein Stack, der
sich beim Hochfahren selbst Rechte erteilt, wäre genau die stille Vorentscheidung, die dieses Produkt an
anderer Stelle (RegulatorikGuard, ADR-0002/0003) ausdrücklich verweigert.
Konsequenzen
Gut:
- Die Kette
registrieren → granten → erzwingenist erstmals vollständig. Vorher existierte nur Schritt 3; die 18txm.*-Schlüssel waren nirgends deklariert, also konnte sie niemand granten. CapabilityRegistrarTestbindet die registrierten Schlüssel maschinell an die tatsächlichencapabilityGuard.require(...)-Aufrufe. Beide Driftrichtungen — gegatet-aber-nicht-registriert und registriert-aber-nicht-gegatet — brechen jetzt den Build statt still zu wirken.- Die Zahl ist jetzt nicht mehr driftfähig: es sind 18 Capabilities (17 schreibende plus
txm.auswertung.read). Dass es nicht 14 sind, wurde am selben Tag unabhängig inimplementation-gaps.mdkorrigiert; neu ist, dass der Build es ab jetzt selbst prüft. Genau daran war die falsche 14 über vier Dokumente mitgelaufen — zwei Listen ohne Bindung aneinander. - Der
zwirn-feedback-servicekann sich erstmals registrieren — seinTXM_SERVICE_CLIENT_IDstand aufUNGEKLAERT. - Die
Tenant-Registry ist die erste der beiden Vorbedingungen für Isolationsstufe I2 (isolationsstufe-gesundheitsdaten.md). Die zweite (Flyway) fehlt weiterhin.
Teuer:
- Aus 10 Containern werden 15, aus 4 Postgres werden 7. Ein Kaltstart dauert spürbar länger.
- Ein neuer Nutzer ist jetzt zwei Vorgänge: Keycloak-Konto mit den Attributen
user_key/tenant_keyund CapabilityGrants in der Authority. Das ist die Konsequenz aus der AuthN/AuthZ-Trennung und nicht abkürzbar, ohne genau die Kopplung wieder einzuführen, dieGG-GOV-SECURITY-0003vermeidet.
Bewusst nicht getan:
- Kein Keycloak-Admin-Client, kein Client-Provisioning.
-0005grenzt das ab; es kommt erst mit Agenten-Actors und Service-Clients je Tenant (Zwirn-ADR-0022/0039). - Kein OPA in der Entscheidung.
-0005nennt „Kein OPA-Zwang" als eigene Abgrenzung, der Minimalbauplan entscheidet in Java. Der OPA-Container im Compose-Stack stammt aus der Zeit davor und ist an der Capability-Entscheidung nicht beteiligt — das steht jetzt so indocker-compose.yml. Ihn zu entfernen ist eine eigene kleine Entscheidung. - Ein geteilter Service-Account statt drei.
tuxametrics-serviceswird von drei Services genutzt; alle teilen sich damitactorKeyund Grants. Bewusste DEV-Vereinfachung, benannt inkeycloak/README.md, für einen echten Betrieb durch Zwirn-ADR-0039 (dedizierte Service-Actors) zu ersetzen. - Kein
IdentityStatusGuard. Eine deaktivierte agentische Identität verliert ihre Grants nicht automatisch. Die generische Kopplung dafür (Zwirn-ADR-0008) ist strikt opt-in und heute in keinem Produkt verdrahtet. Ehrlich benannt im Javadoc vonIdentityService.deactivateund inimplementation-gaps.md. tuxametrics-process-hub-cockpitbleibt JVM. Flowable/Spring stützt sich stark auf Reflection und dynamische Proxies; ein natives Image dafür ist ein eigenes Projekt mit eigener Reachability-Konfiguration — dieselbe Entscheidung wie beidkfz-prozesswerkstatt. Eine benannte Ausnahme vonGG-DEVOPS-0003, kein Versehen.
Was diese Entscheidung ausdrücklich nicht berührt
tuxametrics.regulatorik.einstufungundtuxametrics.pilot.betriebsmodusbleibenUNGEKLAERT.RegulatorikGuardbricht den Start unverändert ab, wenn dort etwas anderes steht (ADR-0002, ADR-0003). Das sind Fragen an Dritte (Regulatory/Recht), keine Infrastrukturfragen — dass die Autorisierung jetzt entschieden ist, sagt über sie nichts.- Der fachliche Schnitt. Keine neue Fachzahl, kein neuer Enum-Wert im Domänenmodell,
E2kommt weiterhin nirgends vor, die vier unbequemen Elemente aus ADR-0004 sind unverändert. GM-7selbst. Siehe Punkt A: es gibt weiterhin keine technische Sperre gegen eine Therapeutenentscheidung.
Alternativen, verworfen
| Alternative | Warum nicht |
|---|---|
Fremde Authority konsumieren (-0005.1) |
Es gibt kein Plattform-Repo desselben Kunden. Über Kundengrenzen zu poolen verbietet Zwirn-ADR-0006 — tenantKey ist nur produktweit eindeutig |
| Beim Dev-Stub bleiben | -0005.5: mit dem ersten echten Nutzer ist der Stub im gebauten Artefakt ein Regelverstoß. Er antwortet auf jeden Capability-Check allowed: true — bei Gesundheitsdaten nicht diskutabel |
| Keycloak Authorization Services statt eigener Grants | GG-GOV-SECURITY-0003: Vendor-Lock-in auf zwei Ebenen. Ein IdP-Wechsel wäre dann eine Neumodellierung aller Policies statt eines Claim-Mappings |
allowedActors nur dokumentieren, nicht erzwingen |
Genau der Zustand, aus dem diese Entscheidung entstanden ist: eine Zusage ohne Mechanismus. Bei Freigabe und Übersteuerung ist der Unterschied nicht akademisch |
actorType auf Identity speichern |
Redundant zu identityType und damit divergenzfähig — siehe Punkt B |
Verweise
- Werdegang und was vorher fehlte:
agentic-engineering/backlog/capability-authority.md - Wire-Vertrag:
tuxametrics-platform-service/CAPABILITY_CHECK_CONTRACT.md - Ports und Zuständigkeiten:
guidelines/project/service-landscape.md - Realm, Clients, Claims:
keycloak/README.md