ORISO Dokumentation

ADR-008: Supervision — keep per-session agency-scoped reach; move side-channels out of the client's room

Status: Proposed — 2026-06-28 (grill-with-docs session); U25 cohort + go-live date resolved 2026-06-30 (minors are in U25 scope; go-live 2026-10-01, no 30.06 launch). The separate-room side-channel exclusion is therefore a confirmed pre-go-live blocker (with runway). Still pending: the data-protection officer's exact disclosure wording and the add-supervisor authority/consent details. · Datum: 2026-06-28

  • Status: Proposed — 2026-06-28 (grill-with-docs session); U25 cohort + go-live date resolved 2026-06-30 (minors are in U25 scope; go-live 2026-10-01, no 30.06 launch). The separate-room side-channel exclusion is therefore a confirmed pre-go-live blocker (with runway). Still pending: the data-protection officer's exact disclosure wording and the add-supervisor authority/consent details.
  • Date: 2026-06-28
  • Deciders: Frank (product) + AI (engineering); data-protection officer (open)
  • Related: CONTEXT-conversation-types.md (Supervision, Peer, Coordinator, Selective in-room visibility), ADR-002-silent-room-membership-and-access-control-curtain (same "is it actually enforced?" class), ADR-004/ADR-005 (Megolm is now on and durable post-rebuild — but it does NOT provide in-room confidentiality here: room members decrypt every event, see below), memory oriso-case-handover-cho-01 (reason+consent+audit pattern), oriso-agency-admin-cross-traeger-leak (the unscoped-access leak class). Driver: U25 suicide counselling with Peers coordinated by Coordinators.

Context

Verification (2026-06-28) corrected two assumptions:

  1. Supervision reach is well-built, not an unbounded global IDOR. There are two distinct things: (A) a cosmetic frontend flag isSupervisorView = consultant.id !== userData.userId (UI only); and (B) the real feature — a SessionSupervisor entity + real Matrix membership. addSupervisor (SessionSupervisorFacade) genuinely inviteUserToRoom + setUserPowerLevel(10, read-only observer) + joinRoom; read paths are gated by isSupervisor(consultant, session) = findBySessionIdAndSupervisorConsultantIdAndIsActiveTrue. So a supervisor reaches exactly the sessions explicitly assigned to them, and the add-gate is agency-scopednot cross-Träger, not all-cases. The "built globally" impression is the cosmetic flag (A); the real reach (B) is per-session and agency-bounded.

  2. The real risk is the side-channel direction, and it is HIGH for U25 minors. A supervisor legitimately reading the client room is intended. The breach is the reverse: the supervisor's feedback ([SUPERVISOR_FEEDBACK] prefix) and the coordinator↔peer asides ([VISIBLE_TO:uid…] prefix) are posted into the same room as the minor and hidden only by a React return null (MessageItemComponent.tsx). The Matrix retrieval path filters nothing (getRoomMessages streams every event); Megolm is now on but does not help here — the minor is a legitimate room member and decrypts every event; there is no server ACL — so the full plaintext aside is delivered to the minor's own client and is trivially recoverable (DevTools, network tab, any non-ORISO Matrix client). For a suicidal minor, an exposed clinical aside or supervisor critique is a safeguarding hazard, not a cosmetic bug. This is the concrete form of the "Matrix isn't deep enough = technical debt" question.

A second, lesser risk (AXIS 2): the addSupervisor authority gate is "assigned consultant or same agency," so any same-agency consultant can attach any is_supervisor colleague to any in-progress case, silently to the minor (notification, no veto), with only a free-text note.

Decision (proposed)

  1. Keep reach per-session-explicit and agency-scoped. Do not widen supervision to cross-agency/cross-Träger "global" oversight; that re-opens the unscoped-access leak class ORISO already suffered. If cross-agency oversight is a genuine U25 requirement, it must get an explicit tenant-aware design, not a relaxed areFromSameAgency check.
  2. Stop relying on in-room prefixes for confidentiality. Move all side-channels to a separate room. Supervisor↔counsellor feedback and coordinator↔peer asides live in a separate Matrix room the client is never invited to. The supervisor still joins the client room read-only for observation; only their feedback moves out. This is transport-agnostic — it does not wait for the Megolm/homeserver rebuild (ADR-005).
  3. Treat [VISIBLE_TO:]/[SUPERVISOR_FEEDBACK] visual hiding as non-confidential. It may remain as a UX affordance only where every room member is allowed to see the content; it must never be the boundary protecting a minor from a clinical aside.
  4. Tighten add-supervisor authority + record consent/justification, mirroring the Case-Handover (CAR-CHO-01) reason+consent+audit pattern: restrict who may attach a supervisor, require a recorded justification, and disclose supervision to the ratsuchende (via the agency's privacy policy + Impressum on chat entry). Exact disclosure wording is pending the data-protection officer; the disclose-vs-conceal toggle is trivial to add.

Considered options (hiding the side-channel)

  • In-room prefix + frontend return null (current). Rejected as confidentiality — the data still reaches the minor's device.
  • Separate Matrix room for asides (chosen). Real exclusion, transport-agnostic, ships now. Cost: more rooms to provision + UI thread/context-linking.
  • Megolm per-recipient subset (one room, cryptographic). The only way to keep a single room and hide asides, but blocked on Megolm being off (ADR-004/005) — deferred, not the launch answer.
  • Server-side filtering in getRoomMessages. Defence-in-depth only; a client hitting Synapse /sync directly (E2EE off) still gets everything. Not sufficient alone.
  • Matrix power levels / ACL. Govern who may send, not who may read existing events — does not hide content within a shared room.

Consequences

Positive: the U25 safeguarding leak is closed with a transport-agnostic change (no waiting for the crypto rebuild); supervision reach stays safely bounded; authority/consent gains an auditable trail. Negative / cost: the side-channel becomes a real separate room to manage and link in the UI; "one room with cryptographically hidden asides" remains a future (post-Megolm) nicety, not the near-term shape.

Open (pending Frank's final requirements + data-protection officer)

  • Is visual-only hiding acceptable as an interim before U25 onboards real minors, or is the separate-room exclusion a hard pre-launch blocker? Resolved (Frank, 2026-06-30): there is no 30.06 go-live; go-live is 2026-10-01 and U25 will include real minors. The separate-room exclusion is a hard pre-go-live blocker (must land before 2026-10-01), not an acceptable interim — but it has runway (the late-June two weeks are build time, not a launch). It stays transport-agnostic, so it does not wait on the ADR-005 homeserver rebuild; build it within the runway.
  • Who may attach a supervisor, and must the ratsuchende/guardian consent or be able to veto? Resolved twice, see below: authority resolved 2026-07-04 (US#302); consent/veto resolved 2026-07-13 (this addendum).
  • Is cross-agency/cross-Träger supervision a real requirement, or is agency-scoped (current) correct?
  • Audit/retention: is the SessionSupervisor addedDate/removedDate row sufficient, or is a richer "who supervised which minor's case when" record required?

Prompted by a Shazia bug report about Supervision "bleeding into" Case Handover. Investigation found no regression — the side-room fix from this ADR (shipped as US#293/FE#367, see oriso-matrix-hardening-plan memory) is intact. The actual gaps were (a) no standing supervisor assignment exists — addSupervisor is still a manual, per-session action (US#302, 2026-07-04: authority = agency-admin for their own agency) — and (b) the consent/veto question above was still open.

Decisions:

  1. New standing mechanism: "Supervision (auto-assigned)" (full definition: CONTEXT-conversation-types.md). An agency-admin sets exactly one standing Supervisor per counsellor (a Supervisor may hold this for many counsellors). The moment that counsellor accepts any case, the assigned Supervisor auto-attaches read-only via the existing SessionSupervisor/addSupervisor primitive — no new access mechanism, just a standing default that removes the manual per-session step. Purely additive: the existing ad-hoc, per-session, manually-authorized add-supervisor flow (US#302) is unchanged and continues to exist alongside it. Rationale: reduce "whose turn is it" friction for training/coaching-style oversight, without touching the reach/authority model this ADR already bounded.
  2. No fallback on supervisor unavailability. If the assigned Supervisor is on leave when a case is accepted, supervision simply lapses for that case — no auto-reassignment. Supervision is read-only oversight, never a precondition for the counsellor to handle the case, so nothing blocks. The agency-admin can repoint the standing assignment at any time.
  3. Reason code: CLINICAL_OVERSIGHT, not TRAINING — the standing relationship is ongoing oversight, not a one-off training-labeled ask.
  4. Consent/veto — resolved, and this changes the original design: all four Supervision reasons (PEER_SUPPORT, CLINICAL_OVERSIGHT, SAFEGUARDING_U25, TRAINING) move off the earlier binary "consent required y/n" split (which had only the two clinical reasons gated) onto the same default-on, client-facing opt-out already decided for Case Handover (ORISO-UserService/CONTEXT.md, "Client opt-out mechanics" — in-chat system-message switch, default ON, effective from the moment of switching, reversible). No Supervision reason keeps an upfront blocking consent gate under this model. This directly supersedes the "disclose-vs-conceal is a deferred toggle" framing in the original ADR-008 body — disclosure is no longer a passive toggle only, it is paired with an active client opt-out.

Storage (resolved 2026-07-13, implementation): the opt-out is a nullable is_supervision_opted_out bit on the session table (entity Session.supervisionOptedOut, default false), added by Liquibase changeset 0066 following the idempotent ADD COLUMN IF NOT EXISTS pattern of 0056. Session-scoped and queryable — chosen over packing it into session_supervisor.notes (that column is per-supervisor, the opt-out is per-client-session) and over a separate table (unnecessary for a single boolean). Deploy caveat: clusters that force SPRING_LIQUIBASE_ENABLED=false (the dev cluster configmap) need the column applied manually, same as the case-handover 0057 tables.

Implementation — decision 4 (opt-out), shipped on branch feat/supervision-consent-opt-out, TDD: the per-reason PENDING consent park in SessionSupervisorFacade.addSupervisor is deleted — every reason provisions the ADR-008 side room + client-room observation immediately. New setSupervisionOptedOut(sessionId, optedOut) facade method + client-guarded POST /users/sessions/{id}/supervision/opt-out endpoint: opting out deactivates active supervisors and kicks them from both rooms; opting back in only clears the block (no auto-reactivation — explicit re-add required). The dead consent endpoints (.../consent, .../pending-consent) + their SecurityConfig matchers were removed.

Implementation — decisions 1–3 (standing auto-assignment), same branch, TDD: new Consultant.assignedSupervisorId (Liquibase 0067, idempotent, no FK — a deleted supervisor degrades to a no-op attach rather than blocking the delete); kept distinct from is_supervisor, which remains the "may BE a supervisor" capability gate. SessionSupervisorFacade.attachStandingSupervisorIfAssigned(sessionId, consultant) attaches as CLINICAL_OVERSIGHT and is best-effort by contract — it never throws: the accept path (AssignEnquiryFacade.assignEnquiry) rolls the whole assignment back on any exception, so a supervision problem must degrade to "this case is unsupervised", never to "the counsellor cannot accept the case". Hooked into assignRegisteredEnquiry after the case is committed — registered/Matrix only, since Live Chat (assignAnonymousEnquiry) is out of supervision scope per ADR-002 and has no room to observe. The agency admin sets it via UpdateAdminConsultantDTO.assignedSupervisorId (validated: target must exist, be isSupervisor, and not be self; empty string clears).

Not yet done (follow-ups): the FE opt-out toggle UI; the Admin-board column UI (backend + DTO are ready); auto-attach on re-assignment (AssignSessionFacade.assignSession) and on Case-Handover takeover — currently only enquiry-accept auto-attaches, so a case that changes hands does not pick up the new counsellor's standing supervisor (open question, may be intended); the reasons endpoint still exposes the now-informational clientConsentRequired per reason (left intact to avoid an uncoordinated FE contract change).

Still open: cross-agency/cross-Träger scope; audit/retention sufficiency (both carried over, unchanged by this addendum).

Auf dieser Seite