ADR-0130: Break-Glass — the Project Administrator's Escalation Is a First-Class, Witnessed, Self-Expiring Act¶
- Status: accepted (2026-09-01 — register row 48, layers ii + iii. He opened with the opposite proposal — the project administrator as "the 'oh shit I have a problem' person with all rights" — was argued out of standing content rights and ruled: "I concede :) Your chain of arguments is convincing and I like your proposal". He then extended the design twice, both adopted: "make a temporary function so that the additional staffing gets removed after a specified time (30 min, 60 min, 1 day)" and "a visual indicator if one or more temporary roles are active on the user, e.g. as a marker at the top of the screen".)
- Date: 2026-09-01
- Deciders: Nicolas Burri
- Relates to: ADR-0012 (admin ≠ content — the boundary this ADR keeps rather than bends),
ADR-0121 (the Project Administrator's permission cut — unchanged by this decision),
ADR-0117 (substitution rejected; time-boxed staffing named as the sanctioned shape should the
refusal ever prove insufficient — this ADR is that trigger firing, for a different reason than
the one anticipated), ADR-0129 / V122 (
lqms_role_identity_reachesand the resolver family the expiry filter must respect), ADR-0128 (the permission classification that makes "content act" a structural fact), ADR-0114 (the arrival import's role-mapping step §3 extends), ADR-0124 / V106 (the setup-status dead-end pattern §3 follows), ADR-0064 (workflow role bindings — why neither model can bypass four-eyes), ADR-0110 (the trail is written by every act), open decision 48 (this ADR realizes its ruling).
Context¶
Row 48 began as a bug report with no bug in it. In one customer project nobody could move a document or set
its language: the act's gate (ORGANIZE_DOCUMENTS, the folder-move class) is correct, but the
arrival import's role mapping landed an operating model in which no staffed role held that
permission — the Project Administrator holds administration plus read, the QMB holds
create/edit, the reader roles hold reads. A permission existed that belonged to nobody, and
nothing said so. The immediate remedy was his own act on his own instance (layer i: grant
ORGANIZE_DOCUMENTS to the QMB in that project, done by him as project administrator).
What the incident actually exposed is a doctrine question. His instinct — the project admin has
all rights on project files — is the natural reading of "administrator", and it collides with
the ruled boundary: organizing is classified a CONTENT act, and the Project Administrator holds
no content acts (ADR-0121). The question is not whether the administrator can rescue a stuck
project. He can, today, because ASSIGN_ROLES is already the master key. The question is
whether that rescue looks ordinary or exceptional in the record.
Decision¶
1. The Project Administrator's permission set is UNCHANGED — no standing content rights¶
The permission cut of ADR-0121 stands exactly as it is: administration, staffing, observation,
VIEW, VIEW_TRACEABILITY. Escalation happens by a deliberate, recorded act — not by a standing
grant. Four arguments carried it, and they are recorded because the instinct they overruled was a
good one:
- (a) Capability was never in question. A holder of
ASSIGN_ROLEScan staff himself into any activated role in his scope. Whatever the boundary is, it is not a limit on what he can do — so no argument for standing rights can be made from "he must be able to fix things". He must, and he can. - (b) Break-glass makes the intervention visible AS exceptional. The record then carries an act with a name, a moment and an end, instead of a document quietly moved by the administrator as if that were his ordinary work. The J1 precedent is the same shape at instance scale: the library's revocation wave ran under a staffing taken for the act, and the trail shows precisely that.
- © Standing rights would permanently pair self-granting with acting. The holder of
ASSIGN_ROLESwho also holds the content permissions is the one combination access auditors dislike most — the ability to grant and the ability to use, resident in one role, indistinguishable in the trail. Break-glass separates them in TIME, which is the only separation available once the same person holds both keys. - (d) Neither model bypasses reviews. Four-eyes and release discipline live in the per-document workflow role BINDINGS (ADR-0064) and the review policies, not in the permission set. So the choice between the two models is not about safety of the approval chain at all: it is purely capability surface plus trail honesty, and on both axes break-glass wins.
2. Refusals become doors¶
A permission refusal stops being a wall and becomes the first step of the remedy.
- For a caller holding
ASSIGN_ROLESin the target scope: the refusal names the missing permission, names which ACTIVATED roles in that scope hold it, and offers the audited self-staffing act inline — "Staff yourself as QMB — this will be recorded". One click, one audited act, then the original act proceeds. - For every other caller: the refusal still names the permission and its holding roles, in human terms — role names and the scope's CODE/name, never permission constants, never scope UUIDs — so the person knows whom to ask. This ties into the queued refusal-presentation fix; the presentation rule is the same one, and this ADR is where the requirement becomes binding.
The refusal thereby carries its own remedy, which is the process-dead-end principle applied to the authorization layer: every gate ships the derived answer to "then what?".
3. Permission closure — the door must lead somewhere¶
A remedy that names roles is worthless when the permission is held by no role at all, which is exactly the state that project was in. Two structural additions close it:
- The arrival import's role-mapping step surfaces the closure: before the operating model lands, the preview states which permissions would end up held by NO role in the target scope. The mapping is the moment the model is still cheap to change (ADR-0114).
- Setup-status gains a dead-end check: permission held by no STAFFED role — a WARN finding
in the established shape (
SetupStatusService, the V106/ADR-0124 pattern: state, not paths), sibling toUNSTAFFED_WORKFLOW_ROLEandNO_ACTIVATED_ROLE, so a scope that has drifted into the same hole says so before someone hits the wall.
This is the structural half of row 48 (layer ii) and the reason the row closes with an ADR rather than a grant.
4. Timed self-staffing — the window is declared, the expiry is enforced by the predicate¶
The break-glass staffing carries a duration, declared at the moment of the act: presets 30 minutes, 60 minutes, 1 day; default 60 minutes. The window is his design, adopted whole.
- Enforcement is in the authorization predicate, never in a job. The assignment row carries
expires_at, and every read that resolves authority filters it live. If the janitor never runs, nothing is over-granted — expiry is a property of the read, not of a sweep. - Implementation caution, recorded because it is easy to get wrong: the ADR-0129 resolver
family is built FOR INLINING.
lqms_role_identity_reachesisIMMUTABLE, plain SQL, noSETclause, precisely so PostgreSQL folds it into every caller's plan. The clock must not go inside it:now()isSTABLE, so pushing it down would demote the function, cost the inlining and — worse — make an immutable predicate lie about its own contract. The expiry filter belongs on the assignment READS (lqms_effective_roles,lqms_scope_users,lqms_role_holders,lqms_primary_role_holders, the fan-out bodies, the permission/authorized-scope resolution and the Kotlin repositories that answer the same question) — one filter written identically everywhere, the same lockstep discipline ADR-0128/-0129 established. Pure predicates stay pure; immutable functions stay immutable. - A janitor only tidies. A scheduler sweep (the ADR-0037 tick) removes expired rows and writes the expiry audit event. It is bookkeeping and legibility, never enforcement.
- The trail distinguishes three endings, because they are three different facts about a person:
removed by a person (
ROLE_UNASSIGNED, an actor), expired per the declared window (its own event, no actor — the system honouring what was declared), and extended — which is a FRESH AUDITED ACT with its own window, never a silent edit of the original row. The staffing act itself records the declared duration and what was being attempted when the door opened. - Ordinary staffing is untouched: staffing other people, long-term, stays permanent and unchanged. Generalizing timed staffing to other cases — external auditors given a review window, per-round reviewers, absence deputization (ADR-0117's original trigger) — is a named possible widening, not built here. The mechanism is deliberately introduced through the one case that argued for it.
5. The marker — a persistent header chip while any timed staffing is active¶
His second extension, adopted whole. While a person holds any unexpired timed staffing, the application header shows a persistent chip: "Acting as QMB — 42 min left", one chip per active role, turning amber near expiry, carrying Extend and End now as actions on the chip itself.
- Server truth, cosmetic countdown. The chip's existence and its remaining time are derived from the same assignment read the resolvers use. The client's ticking number is presentation; it never decides anything.
- A personal surface. The marker is shown to the person who is escalated. Others do not see a badge on him — they read the trail, which is where the fact belongs.
- It doubles as the graceful pre-expiry warning. Nobody is ambushed mid-act: the amber state arrives before the window closes, Extend is one click away on the same chip, and the editor never lets a save bounce as a surprise. Expiry is a declared ending, not a trap.
Amendment 2 (2026-09-07, first live use — row 77): §2's doors open only from refusals, and the surfaces (correctly) never offer acts the caller lacks — so the canonical persona could never reach the door from the UI. Ruled: the hiding is the desired state ("the UI should not tease functions that the user does not have"); the remedy is a deliberate entry — a "Break glass…" action visible only to genuine door-holders — and the escalation reason is mandatory on every take (his extension: "forcing them to give a reason"), enforced in the dialog AND at the API; the refusal path auto-fills it from the refused act, the deliberate path demands the human's own words. Lock-marked affordances are rejected by the same no-tease rule. Aligned 2026-09-08 ("fully agree"): §1(a)'s master key was never a capability limit, but the personal doors listing offers CONTENT-class roles only (ADR-0131 §3's argument, one surface over) while the
ASSIGN_ROLESdoor of the act accepted any activated role — so the offer and the act asked different questions by one edge case. The ACT now carries the constraint on both doors: an administrative target is refused whichever way the caller came in, andextendinherits it through the same door check. Administrative authority stays a deliberate, permanent staffing act; break glass witnesses stepping into CONTENT work. His maxim, recorded with it: "filtering to only show allowed selections from the start is preferrable" — offer equals act, one server-side question.
6. Sequencing¶
Implementation lands after the 1.0-blocking fix waves; nothing here is a 1.0 gate. The ADR itself is the deliverable for now: it is the November-audit story artifact — the written answer to "how does your administrator intervene, and how would we know?".
Amendment (2026-09-01, same day — ADR-0131): "nothing here is a 1.0 gate" no longer holds. His substitution-edges ruling ("I'd like to have it in 1.0") stands on this ADR's timed-staffing mechanism, so the mechanism moves into the 1.0 runway. Ordering within 1.0 is unchanged: after the 1.0-blocking fix waves, mechanism first, the ADR-0131 matrix on top — one arc.
Rejected: standing content rights on the Project Administrator¶
His opening proposal — the administrator simply holds the content permissions — was rejected with its costs recorded, since it is the option any reviewer will reach for again:
- It makes the exceptional intervention indistinguishable from ordinary work in the trail. The audit question "who moved this, and under what authority?" gets the answer "the administrator, as usual", which is no answer.
- It permanently resident-pairs self-granting (
ASSIGN_ROLES) with acting — argument © above. - It would be the first content permission handed to the Project Administrator, breaking the classification that ADR-0121 argued and ADR-0128 made structural, and inviting the next one.
- It buys nothing in capability: everything it enables is already reachable via
ASSIGN_ROLESin one recorded step.
Also rejected: reclassifying organizing as an ADMINISTRATIVE act. That would move the boundary to make one incident disappear, and the boundary is load-bearing in three other places (ADR-0128's cascade, the GLOBAL cross-tenant line, the classification guard). If it is ever wanted, it deserves its own argument and its own ADR — not a side effect of this one.
The dissolved misconception¶
The boundary between administration and content was never a capability limit. It could not be: the master key sits in the administrator's own hand. It is a witness — the thing that makes an administrator stepping into content work a visible, bounded, self-ending event rather than an unremarkable Tuesday. Once the boundary is read that way, his instinct and the ruled doctrine stop disagreeing: the administrator is the "oh shit I have a problem" person with all rights, and the system's job is to hold the glass, not the key.
Consequences¶
- Row 48 closes across all three layers: (i) his grant on the affected project, (ii) closure surfacing at import + the setup-status dead-end check, (iii) this doctrine and its mechanism.
role_assignmentgains a nullableexpires_at; a null window means permanent, so every existing row keeps its meaning and no migration rewrites history. The new column joins the schema-extension lifecycle sweep (RLS inventory, export/import bundle shape, dissolution's delete set).- The resolver family gains one more thing that must be written identically in every member. The V122 comment's inventory is the checklist; the guard test family grows a case that fails when a sibling forgets the filter — an expired assignment must never show up in a roster the gate would refuse, which is the same divergence ADR-0129 pinned.
- Two new audit event kinds sit beside
ROLE_ASSIGNED/ROLE_UNASSIGNED: the expiry (actorless) and the extension (a fresh act). Reports that count staffing must read windows, not row existence. - ADR-0117's named trigger has fired. Its refusal of acting-as/impersonation is untouched and remains the reason this is STAFFING with own-name authority: the trail says "he held QMB, from then until then", never "acting for someone".
- The refusal-presentation work gains a binding requirement (§2) and a second caller class, so it should be built once, for both.