Skip to content

Commit 105f0c1

Browse files
authored
docs(adr): amend ADR-130's approval-surface constraint and gate it (#800)
## Description ADR-130 constraint 3 said `agent_approve` was "doubly unreachable for non-admins today". It named two surfaces, both behind admin gates — and **both still are**: `nrllm_runs` is `access => 'admin'` (`Modules.php:299-301`) and the Playground is too (`:280-282`) with `denyNonAdmin()` on its resume path. **So the premise did not expire. A third surface arrived.** `nrllm_aitasks` is `access => 'user'` and registers `approve`/`submitInput` (`:342-344`, `:375-381`), so the enumeration stopped being exhaustive against a module that did not exist when it was written. That distinction is the useful part. "The premise expired" reads as bad luck. **"A constraint that enumerates surfaces is only as durable as the enumeration"** is a failure mode a reviewer can guard against — and it is the one that bit. The record even predicted its own trigger ("becomes exercisable with the editing module") and the prediction left no mark anywhere a change had to pass. Which record made it reachable was **established, not assumed**: `git log -S"nrllm_aitasks"` returns exactly one commit, `2ee16dfd feat: editor-facing AI Tasks module (ADR-131)`; `git log -S"AgentRunController::class"` returns that and the earlier admin-only module; and ADR-131's own Consequences already said it in prose — *"`agent_approve` through the shared approvals inbox (previously doubly unreachable for non-admins)"*. The author saw the consequence and wrote it down. Only the two lifecycle fields were missed. They are written now, from both ends. ## What the constraint says instead **Not module access alone.** Both the module tick in `be_groups` and the grant are required, and neither substitutes for the other (ADR-131 decision 2). Then three checks on the approve path, in the order `ResumeCoordinator` evaluates them — which is not the order I first wrote, and the code was allowed to decide: 1. `mayActOnRun()` (`:205`) — may this actor act on this run at all 2. ADR-172's `requiresSecondApprover` (`:222-226`) — optional, refuses an approval from the run's own initiator 3. ADR-133's `approverRefusal()` (`:314`) — withholds the release of a write the approver could not run themselves Only the third releases the run, because only it had claimed anything. `submitInput`'s own gates are recorded **beside** the list rather than merged into it: it has no four-eyes check, and its `submitterRefusal` examines every pending call rather than only those declaring a write. ## A prediction in prose is what failed, so the fix is a test `ApprovalSurfaceInventoryTest` holds three lists: which modules register the approval actions and under which `access`; which classes under `Classes/Controller` reach `AgentRuntime::approve()`/`submitInput()`; and that every AJAX route on `resumeAction`/`submitInputAction`/`approveAction` opens with `denyNonAdmin()`. It then requires ADR-130 to **name each module the scan finds**, matched as a whole identifier. That matching detail is not cosmetic. An earlier draft used `assertStringContainsString`, and a module named `nrllm_run` satisfied it through the existing `nrllm_runs` — a substring collision that let a fourth surface through. A second control found worse: a fourth module under a *different* controller was invisible to all three assertions and the suite stayed fully green. Both holes are closed; each guard was then seen to fail on the defect it exists for. **The ADR states what the three lists do not cover**, because they do not cover everything: a class outside `Classes/Controller`, exposed under a fourth action name, still passes. That was tried and it stayed green. Saying so is the point — the check exists to make this record's enumeration falsifiable, not to become a second registry. An earlier draft of this very paragraph also planted the literal `nrllm_run` into the text as an example, which would itself have satisfied the naming check for a module of that name; no example is given now, deliberately. ## Citation rot, found and fixed Eleven line-anchored citations in ADR-169 and ADR-171 pointed at ranges this amendment moved. One predated it: **ADR-171 cited `ResumeCoordinator.php:204` for the `mayActOnRun` check, and that line is blank** — the four-eyes commit inserted a guard above it four days ago. The personas record rotted within a day of merging. All eleven are recomputed. The general problem is filed as #793: 74 such citations exist and `AdrLifecycleTest` checks `:ref:` targets but not these. ## Related Issue Closes #787 — whose own framing this change corrects; the correction is a comment on the issue. Also filed while sweeping: #791 (ADR-119 counts twelve submodules, there are fourteen plus one outside), #792 (ADR-108/094 count 40/41 builtins, there are 46), #793 (line-anchored citations rot unchecked). ## Type of Change - [x] Documentation update - [x] New test (a mechanical gate for this defect class) ## Deliberately not done ADR-130 **constraint 4** is expired too — `tasks_manage` "does not exist yet", and the editing module shipped without it. Its amender is ADR-169, which is `:Status: Proposed`. The lifecycle forbids a proposal writing `:Amends:`, so that amendment is owed by whichever change accepts ADR-169. Pre-empting it would break the same rule in the other direction. ## Gates `ApprovalSurfaceInventoryTest` OK (4 tests, 79 assertions), `AdrLifecycleTest` OK (6 tests, 1187 assertions), all three repo checks exit 0. `rector -n` at the CI-pinned PHP 8.2 did not run — `.Build` here is resolved at 8.4 and `platform_check.php` fatals before it starts. One new PHP file reaches CI without that gate having run locally. ## Checklist - [x] My code follows the project's coding standards - [x] I have added a test that proves the gate works, and watched each assertion fail - [x] I have updated the documentation accordingly - [x] `CHANGELOG.md` entry under the existing `### Changed` - [x] Both ends of the amend link written, verified by `AdrLifecycleTest` - [x] My changes generate no new warnings
2 parents 388be24 + 4d0721f commit 105f0c1

6 files changed

Lines changed: 532 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6565

6666
### Changed
6767

68+
- **ADR-130's constraint 3 is amended by ADR-131** (`#787`). The record called
69+
`agent_approve` doubly unreachable for non-admins because it named two
70+
approval surfaces and both were admin-gated. Both still are: `nrllm_runs` is
71+
`access => 'admin'` and the Playground's resume path still calls
72+
`denyNonAdmin()`. The enumeration was what went out of date — the editor
73+
module `nrllm_aitasks` is `access => 'user'` and registers `approve` and
74+
`submitInput`, so a non-admin whose group has that module ticked and who holds
75+
the grant decides other users' runs today (both switches are required). The
76+
amendment states the three bounds that apply on top of module access — the
77+
module tick is required but bounds nothing on its own — in the order
78+
`ResumeCoordinator::approve()` evaluates them: `mayActOnRun()`, the opt-in
79+
four-eyes refusal (ADR-172), and the approver tool gate (ADR-133) that
80+
withholds a write the approver could not run themselves. That list is the
81+
approve path; `submitInput` is recorded next to it with the same first and
82+
last gate and no four-eyes check (ADR-150). The stale citations ADR-169 and
83+
ADR-171 carried into the same records are corrected with it. A new
84+
`ApprovalSurfaceInventoryTest` pins which modules register `approve` /
85+
`submitInput` under which `access`, which classes under `Classes/Controller`
86+
reach the approval runtime, and that ADR-130 names each module it finds — so a
87+
new approval module cannot be registered without editing the record. The ADR
88+
states what the three lists do not cover. No behaviour changes.
89+
6890
- **One MCP operation now has one timeout, not one per HTTP request** (`#773`,
6991
ADR-170). A tool call against an MCP server is three requests — the protocol
7092
handshake, its confirmation, then `tools/call` — and each carried a full

Documentation/Adr/Adr130BackendUserGrants.rst

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
ADR-130: Capability grants for backend users
55
===========================================
66

7-
:Status: Accepted
7+
:Status: Accepted (constraint 3 enumerated the approval surfaces and the
8+
enumeration was not exhaustive — see :ref:`ADR-131 <adr-131>`)
89
:Date: 2026-08-06
10+
:Amended: 2026-08-06 by :ref:`ADR-131 <adr-131>`
911

1012
Context
1113
=======
@@ -75,12 +77,136 @@ Named constraints, each verified against the code
7577
reachable for grant holders **now**, without any UI — the intended end
7678
semantics, bounded by the per-user budget. This ADR must not be read as
7779
"inert until the editing module ships".
78-
3. **``agent_approve`` is doubly unreachable for non-admins today** — both
79-
human approval surfaces sit behind admin gates (the AgentRun module's
80-
``access => admin``, the Playground's ``denyNonAdmin()``). The
81-
``mayActOnRun()`` branch is real, tested code, but only becomes
82-
exercisable for non-admins with the editing module. Stated here so
83-
nobody reads it as an already-active control.
80+
3. **``agent_approve`` is reachable for non-admins** — amended, see
81+
:ref:`ADR-131 <adr-131>`. This record called the grant doubly
82+
unreachable because it named two human approval surfaces and both sat
83+
behind admin gates.
84+
85+
Both gates still hold. ``nrllm_runs`` is still ``'access' => 'admin'``
86+
in ``Configuration/Backend/Modules.php``, and the Playground's
87+
``resumeAction()`` and ``submitInputAction()`` still open with
88+
``denyNonAdmin()``. The editing module added a *third* surface that
89+
did not exist when this was written: ``nrllm_aitasks`` is
90+
``'access' => 'user'`` and registers ``AgentRunController::approve``
91+
and ``submitInput``. A non-admin whose group has that module ticked
92+
**and** who holds the grant therefore decides other users' suspended
93+
runs today — which is what the grant is for. Both switches are
94+
required, and neither substitutes for the other
95+
(:ref:`ADR-131 <adr-131>` decision 2): the module tick alone grants no
96+
execution, and the grant alone reaches no surface.
97+
98+
This record named its own expiry trigger. The sentence that stood here
99+
said the ``mayActOnRun()`` branch "only becomes exercisable for
100+
non-admins with the editing module" — and then the editing module
101+
shipped, with nothing carrying that prediction into the change that
102+
fired it. So the failure is not that the trigger went unforeseen; it
103+
is that a foreseen trigger left no mark anywhere a change had to pass.
104+
**A constraint that enumerates surfaces is only as durable as the
105+
enumeration.** "Both surfaces are gated" is a claim about a list, and
106+
a list stops being complete the moment someone registers the next
107+
entry, whether or not the record predicted it. Nothing in the codebase
108+
held the list.
109+
110+
A constraint phrased against the *check* is falsifiable where one
111+
phrased against the *inventory* is not — but "no approval action sits
112+
outside an admin gate" is not the check to write, because this record
113+
deliberately made it false. What distinguishes an intended non-admin
114+
surface from an accidental one is the inventory written down and
115+
asserted, which
116+
``Tests/Unit/Configuration/ApprovalSurfaceInventoryTest.php`` now
117+
does. It holds three lists, and each covers a
118+
bounded scope rather than the whole idea of an approval surface. It
119+
pins which modules register ``approve``/``submitInput`` and under
120+
which ``access``. It pins which classes *under* ``Classes/Controller``
121+
reach :php:`AgentRuntime::approve()` or
122+
:php:`AgentRuntime::submitInput()`. And it requires every AJAX route
123+
targeting one of three named actions — ``resumeAction``,
124+
``submitInputAction``, ``approveAction`` — to open with
125+
``denyNonAdmin()``. It then requires this paragraph to name each
126+
module the scan finds, matched as a whole identifier rather than as a
127+
substring, so a name that merely extends one already written here
128+
does not satisfy it. No example of such a name is given, deliberately:
129+
writing one into this paragraph would be enough to satisfy the check
130+
for it.
131+
132+
A fourth surface fails whichever of the three it falls inside.
133+
Registered as a module, it fails the module list and this paragraph's
134+
naming check — under any controller, not only ``AgentRunController``.
135+
Written as a class under ``Classes/Controller`` that calls the
136+
runtime, it fails the controller list before it is registered
137+
anywhere. Exposed as an AJAX route on one of the three named actions,
138+
it fails unless that route guards itself. Whichever list it broke has
139+
to be edited; the module door additionally requires this text. Each
140+
of those failures was produced on purpose, by registering a fourth
141+
surface of that shape and watching the assertion fail, before this
142+
paragraph was allowed to claim it.
143+
144+
What none of the three catches, stated so nobody reads more into them
145+
than they hold: an approval-calling class outside ``Classes/Controller``,
146+
exposed under an action name that is not one of the three. That
147+
combination was tried and the suite stayed green. Widening the scan to
148+
every class in ``Classes`` would close it and was not done here — the
149+
check exists to make the enumeration in this paragraph falsifiable, not
150+
to become a second registry.
151+
152+
What bounds the grant is therefore not module access alone. Three
153+
checks apply to a single approval, in the order
154+
:php:`ResumeCoordinator::approve()` evaluates them. **This list
155+
describes the approve path only** — ``submitInput`` is the other
156+
action this constraint names, and its gates are set out after the
157+
list rather than merged into it, because the middle check does not
158+
exist there and a merged list of three would be false for it:
159+
160+
- :php:`AiActorContext::mayActOnRun()` decides whether this actor may
161+
act on this run at all — owner, admin, or ``agent_approve`` holder.
162+
A refusal is ``RunAccessDeniedException``.
163+
- Only on a configuration with ``require_second_approver`` set, an
164+
*approval* (never a denial) from the run's own initiator is refused,
165+
admin and grant holder alike (:ref:`ADR-172 <adr-172>`). This is
166+
opt-in and narrows who may release one particular run
167+
(``ResumeCoordinatorFourEyesGateTest``).
168+
- :php:`approverRefusal()` resolves the approver's live backend user
169+
and asks :php:`ToolCallPolicyInterface::decide()` about every
170+
pending call that declares a write, throwing
171+
:php:`ApproverNotPermittedException` on a denial
172+
(:ref:`ADR-133 <adr-133>`). Unlike the previous check this one is
173+
not opt-in: ``approve()`` calls it for every approval decision, and
174+
only a *denial* skips it, because a denial executes nothing. Two
175+
things scope it — it looks at pending calls that declare a write, so
176+
a read-only turn passes it unexamined, and the ``requiresAdmin()``
177+
axis it leans on is one the ``tools.dataClassEnforcement: observe``
178+
switch does not relax (that switch governs the trust-zone axis
179+
only). This is what keeps the grant from becoming a write
180+
escalation: the grant admits the *decision*, ADR-133 withholds the
181+
*release* of a write the approver could not run themselves
182+
(``ResumeCoordinatorApproverGateTest``).
183+
184+
None of the three consumes the turn. The first two throw above the
185+
``claimResume()`` call, so the run is never claimed and simply stays
186+
``WAITING_FOR_APPROVAL``; the third claimed the state to read it and
187+
therefore calls ``release()`` before throwing. ``AgentRunController``
188+
turns the latter two into a flash and a redirect, so a run one grant
189+
holder may not release stays decidable by someone who may — the two
190+
gate tests named above assert exactly that, each re-reading the run
191+
and finding it ``WAITING_FOR_APPROVAL``.
192+
193+
None of the three shrinks the set of runs the grant admits a decision
194+
on: that set is ``mayActOnRun()``'s answer, and the other two act on
195+
the *outcome* of a decision already admitted. That is why they are
196+
named here and amend nothing about the grant itself.
197+
198+
``submitInput()`` is gated the same way minus the middle check. It
199+
opens with the same :php:`AiActorContext::mayActOnRun()` call and the
200+
same ``AGENT_APPROVE`` scope, and it ends at
201+
:php:`submitterRefusal()` (:ref:`ADR-150 <adr-150>`), the sibling of
202+
:php:`approverRefusal()` against the submitter's live backend user.
203+
One rule differs: it asks the policy about **every** pending call
204+
rather than only those declaring a write, because an input-requiring
205+
tool declares no write — the input and approval markers are mutually
206+
exclusive at registration — so a write filter would select nothing
207+
and the gate would be decorative. There is no four-eyes check on that
208+
path: :ref:`ADR-172 <adr-172>` refuses a self-*approval*, and
209+
supplying input is not one.
84210
4. **``tasks_manage`` does not exist yet.** The list/wizard actions have
85211
no per-action gate to migrate (they are module-gated only), and the
86212
trait's JSON 403 body is the wrong shape for HTML module actions. The

Documentation/Adr/Adr131EditorModule.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ ADR-131: The editor-facing module
66

77
:Status: Accepted
88
:Date: 2026-08-06
9+
:Amends: :ref:`ADR-130 <adr-130>` (constraint 3: this module is a third
10+
approval surface its enumeration did not have)
911

1012
Context
1113
=======

Documentation/Adr/Adr169RecordManagementUsesTypo3Permissions.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Context
1717

1818
Issue `#691` declines to add a ``MANAGE`` grant case until a management surface
1919
exists, under :ref:`ADR-130 <adr-130>`'s rule that a case arrives together with
20-
its enforcement point (``Adr130BackendUserGrants.rst:32-36``). That rule exists
20+
its enforcement point (``Adr130BackendUserGrants.rst:34-38``). That rule exists
2121
because :ref:`ADR-023 <adr-023>` shipped backend-group checkboxes that gated
2222
nothing and :ref:`ADR-117 <adr-117>` had to remove them: "a control that has to
2323
be labelled 'has no effect' is worse than its absence"
@@ -243,8 +243,8 @@ acceptable grant of ``allowed_groups``.
243243
-------------------
244244

245245
``tasks_manage`` is reserved in three places: ADR-130 named constraint 4
246-
(``Adr130BackendUserGrants.rst:84-88``), ADR-131's "what stays out"
247-
(``Adr131EditorModule.rst:67-68``) and the enum docblock
246+
(``Adr130BackendUserGrants.rst:199-203``), ADR-131's "what stays out"
247+
(``Adr131EditorModule.rst:69-70``) and the enum docblock
248248
(``Classes/Domain/Enum/BackendUserGrant.php:24-27``). `#691` asks for something
249249
wider — providers, models, configurations and tasks.
250250

@@ -277,7 +277,7 @@ first; old routes keep working through kept identifiers plus
277277
an actor-scoped run viewport, but the record states that "'Own runs' for editors
278278
is mostly the approver's view", that agent runs "are currently started from
279279
admin surfaces", and that the ownership filter "matters the moment any non-admin
280-
path starts runs" (``Adr131EditorModule.rst:69-72``). A personal run history is
280+
path starts runs" (``Adr131EditorModule.rst:71-74``). A personal run history is
281281
what that surface will become, not what it is.
282282

283283
What has changed is the count and the constraint. ADR-119 describes twelve
@@ -287,7 +287,7 @@ registered outside it under ``parent => 'web'``
287287
file). It sits there for a verified platform reason: the module menu drops every
288288
top-level module whose own access check fails, so a child of the admin-only
289289
``nrllm`` (``:53``) would be invisible to non-admins
290-
(``Adr131EditorModule.rst:29-33``). A management surface has that same
290+
(``Adr131EditorModule.rst:31-35``). A management surface has that same
291291
constraint.
292292

293293
**Recommendation: reopen ADR-119 — recommended here, not done here.** Without

Documentation/Adr/Adr171PersonasTheCodeAlreadyAssumes.rst

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ per-task, per-category or per-group scoping, and no task ownership field.
111111

112112
**Code.** ``BackendUserGrant.php:47-52``; the grant branch in
113113
:php:`AiActorContext::mayActOnRun()` (``:229``) — "deliberately the only scope
114-
with a grant equivalent" (``Adr130:57-60``); the write side at
115-
``ResumeCoordinator.php:204`` and ``:634``; the list viewport at
114+
with a grant equivalent" (``Adr130:59-62``); the write side at
115+
``ResumeCoordinator.php:205`` and ``:650``; the list viewport at
116116
``AgentRunController.php:267``. It sits in no recommended preset: "granting it is
117117
an explicit trust decision"
118118
(``Documentation/Administration/Permissions.rst:55-59``).
@@ -216,7 +216,7 @@ one of ours. No third-party integrator is evidenced anywhere.
216216

217217
**Code.** None, by design: "a case is only added TOGETHER with its consumer (a
218218
grant nothing reads is worse than none)" (``BackendUserGrant.php:24-27``);
219-
reserved again at ``Adr130:84-88`` and ``Adr131:67-68``.
219+
reserved again at ``Adr130:199-203`` and ``Adr131:69-70``.
220220

221221
**Human (unvalidated).** Curates the AI catalogue for their own team while keys
222222
and endpoints stay administrator-only.
@@ -234,14 +234,20 @@ that should be retired.
234234
Contradictions found
235235
====================
236236

237-
**A. A control ADR-130 said was inactive went live, and the record still says it
238-
is not.** ``Adr130BackendUserGrants.rst:78-83``: "``agent_approve`` is **doubly
239-
unreachable for non-admins today** — both human approval surfaces sit behind
240-
admin gates … only becomes exercisable for non-admins with the editing module.
241-
Stated here so nobody reads it as an already-active control." It shipped:
242-
``Modules.php:375-381`` registers ``AgentRunController::approve`` and
243-
``submitInput`` in ``nrllm_aitasks``, ``'access' => 'user'`` (``:344``). A
244-
non-admin deciding another user's write is an active control. Nothing filed it.
237+
**A. A control ADR-130 said was inactive went live** — RESOLVED, `#787`.
238+
ADR-130 constraint 3 read "``agent_approve`` is **doubly unreachable for
239+
non-admins today** — both human approval surfaces sit behind admin gates …
240+
only becomes exercisable for non-admins with the editing module. Stated here so
241+
nobody reads it as an already-active control." It shipped: ``Modules.php:375-381``
242+
registers ``AgentRunController::approve`` and ``submitInput`` in
243+
``nrllm_aitasks``, ``'access' => 'user'`` (``:344``). A non-admin who holds the
244+
grant and whose group has that module ticked decides another user's write today.
245+
ADR-130 now carries ``:Amended:`` and a rewritten constraint 3
246+
(``Adr130BackendUserGrants.rst:80-198``), and
247+
``Tests/Unit/Configuration/ApprovalSurfaceInventoryTest.php`` pins the module
248+
inventory, so a new approval *module* cannot be registered without ADR-130
249+
naming it. It does not cover every shape an approval surface could take;
250+
ADR-130 says which.
245251

246252
**B. One codebase, two answers to "may this person use this model".** The Editor
247253
Action Center refuses a user outside the configuration's allowed groups —
@@ -253,9 +259,11 @@ module** does not: ``TaskExecutionService.php:63`` calls
253259
configuration, returned unchecked; the method has no actor to check with. A task
254260
pinned to a restricted configuration bypasses that restriction.
255261

256-
**C. The bound that justifies the grant is opt-in.** ``Adr130:55-56`` and
257-
``Permissions.rst:41-43``: "The per-user budget pre-flight … bounds what a grant
258-
holder can spend." ``BudgetService.php:76-79``: with no ``tx_nrllm_user_budget``
262+
**C. The bound that justifies the grant is opt-in.** ``Adr130:57-58``: "The
263+
per-user budget pre-flight … bounds what a grant holder can spend."
264+
``Permissions.rst:43-45`` states it in its own words, not this one's — "Every run
265+
is pre-flighted against the user's own usage budget and attributed to them."
266+
``BudgetService.php:76-79``: with no ``tx_nrllm_user_budget``
259267
record for that uid, an inactive one, or one with no limit set, the check returns
260268
``allowed()``. The budget is per user and opt-in; the grant is per group.
261269
Ticking ``tasks_use`` for a group without creating budget records ships an
@@ -281,7 +289,7 @@ operator" that is a **non-admin** holding one grant, and says so: "identical to
281289
"Each case maps to exactly one enforcement point — there is no wildcard"; the
282290
``TASKS_USE`` docblock names two. There are nine, across task execution, a module
283291
index, the Editor Action Center and a record context menu. That is a role-ladder
284-
rung, which ``Adr130:24`` promised the design would not grow.
292+
rung, which ``Adr130:26`` promised the design would not grow.
285293

286294
.. _adr-171-invented:
287295

@@ -292,12 +300,12 @@ These exist because a gate exists, not because anyone does the job. A reviewer
292300
should read this section first.
293301

294302
- **The approver, as a distinct person.** The gate is real and tested. But
295-
``Adr130:57-60`` calls it "**deliberately** the only scope with a grant
303+
``Adr130:59-62`` calls it "**deliberately** the only scope with a grant
296304
equivalent" — a design choice, not an observed org chart. Until
297305
contradiction A shipped, no non-admin could reach it at all. We built the
298306
separation before meeting anyone who wants it.
299-
- **The input submitter.** ``ResumeCoordinator.php:634`` calls the *same*
300-
predicate as ``:204``, so submitting is the approver's grant.
307+
- **The input submitter.** ``ResumeCoordinator.php:650`` calls the *same*
308+
predicate as ``:205``, so submitting is the approver's grant.
301309
:ref:`ADR-150 <adr-150>` reasons about them as a distinct actor with a
302310
different danger model — and no production tool can produce one:
303311
``InputPauseCoverageTest::INPUT_REQUIRING_TOOLS = []`` pins the empty list,
@@ -368,7 +376,7 @@ What TYPO3 gives us and what it does not
368376

369377
TYPO3 gives **roles**: three enforcement primitives, all of which nr_llm uses.
370378
``admin`` is the whole default posture. ``user`` on a module means one tick in a
371-
group's module list and nothing more — ``Adr131:34-39`` records the verified trap
379+
group's module list and nothing more — ``Adr131:36-41`` records the verified trap
372380
that ``'user,group'`` denies everyone. ``systemMaintainer`` we never check but
373381
depend on.
374382

0 commit comments

Comments
 (0)