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), memoryoriso-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:
-
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 — aSessionSupervisorentity + real Matrix membership.addSupervisor(SessionSupervisorFacade) genuinelyinviteUserToRoom+setUserPowerLevel(10, read-only observer)+joinRoom; read paths are gated byisSupervisor(consultant, session)=findBySessionIdAndSupervisorConsultantIdAndIsActiveTrue. So a supervisor reaches exactly the sessions explicitly assigned to them, and the add-gate is agency-scoped — not 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. -
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 Reactreturn null(MessageItemComponent.tsx). The Matrix retrieval path filters nothing (getRoomMessagesstreams 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)
- 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
areFromSameAgencycheck. - 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).
- 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. - 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/syncdirectly (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
SessionSupervisoraddedDate/removedDate row sufficient, or is a richer "who supervised which minor's case when" record required?
Addendum 2026-07-13 (grill-with-docs): standing auto-assignment + unified opt-out consent
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:
- New standing mechanism: "Supervision (auto-assigned)" (full definition:
CONTEXT-conversation-types.md). An agency-admin sets exactly one standingSupervisorper counsellor (aSupervisormay hold this for many counsellors). The moment that counsellor accepts any case, the assigned Supervisor auto-attaches read-only via the existingSessionSupervisor/addSupervisorprimitive — 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. - 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.
- Reason code:
CLINICAL_OVERSIGHT, notTRAINING— the standing relationship is ongoing oversight, not a one-off training-labeled ask. - 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).
ADR-007: Live-chat liveness — the indicator reads backend Availability, never mirrors or infers it
Status: Accepted — 2026-06-28 (grill-with-docs session). Root causes verified; fix scheduled. · Datum: 2026-06-28
ADR-009: Global Topic/Category ownership and AI-assisted translation infrastructure
Status: Accepted — Frank, 2026-07-01 (grill-with-docs session). Design decided now ("set the points"); implementation deliberately deferred, see Rollout below. · Datum: 2026-07-01