Brainversum · tuxametrics Graph Admin

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:

  1. Welche Form die Authority hat (-0005.1: eigen oder fremd).
  2. Welcher IdP Tokens ausstellt — ohne ihn gibt es keinen actorKey, gegen den ein Grant liefe.
  3. 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):

  1. 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.
  2. CapabilityService.upsert weist jeden abweichenden Upsert mit 422 ab — auch eine leere allowedActors-Liste, die im übrigen Katalog „keine Einschränkung" bedeutet und hier die Katastrophe wäre.
  3. CapabilityGrantService.create weist einen Grant mit actorType != USER mit 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.
  4. Der Check selbst vergleicht allowedActors vor 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:

Teuer:

Bewusst nicht getan:


Was diese Entscheidung ausdrücklich nicht berührt


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