Repository navigation
Commit 920bc12
fix(plugin-approvals,plugin-security): the approval request actions name the request in their success copy, and Explain access names the principal as a person (#23202)
Fixes #23157
Clause-②: no — server-authored strings (the approval request actions'
success copy and explain's principal sentence) name the record and the
person; no published schema, translation key or export moves.
Server-authored strings that the console renders verbatim now name what
and who they are about. This is family card #23157, a member of
objectui#12308 (classes 2 and 5). It covers the card's rows 1 and 2,
plus the same-family rows the seat's claim amendment added for the patch
round.
## Row 1 and its family: every request action's success toast names the
request
Every action on `sys_approval_request` in
`packages/plugins/plugin-approvals/src/sys-approval-request.object.ts`
had a bare-verb `successMessage`. Each now interpolates
`${result.request.record_title}`. Approve and Reject reuse the inbox
row's inline wording (objectui's `inlineApproved` and `inlineRejected`).
| action | route (all `type: 'api'`, POST) | service method | was | now
|
|---|---|---|---|---|
| `approval_approve` | `…/requests/ID/approve` | `decide()` |
`Approved.` | `Approved "TITLE"` |
| `approval_reject` | `…/requests/ID/reject` | `decide()` | `Rejected.`
| `Rejected "TITLE"` |
| `approval_reassign` | `…/requests/ID/reassign` | `reassign()` |
`Reassigned.` | `Reassigned "TITLE"` |
| `approval_send_back` | `…/requests/ID/revise` | `sendBack()` | `Sent
back for revision.` | `Sent "TITLE" back for revision` |
| `approval_request_info` | `…/requests/ID/request-info` |
`requestInfo()` | `Information requested.` | `Information requested on
"TITLE"` |
| `approval_remind` | `…/requests/ID/remind` | `remind()` | `Reminder
sent.` | `Reminder sent for "TITLE"` |
| `approval_recall` | `…/requests/ID/recall` | `recall()` | `Recalled.`
| `Recalled "TITLE"` |
| `approval_resubmit` | `…/requests/ID/resubmit` | `resubmit()` |
`Resubmitted.` | `Resubmitted "TITLE"` |
| `approval_comment` | `…/requests/ID/comment` | `comment()` | `Reply
posted.` | `Reply posted on "TITLE"` |
`TITLE` stands for the `${result.request.record_title}` token. The
zh-CN, ja-JP and es-ES values of the same nine keys are updated in step,
and each carries the same token. Reject reuses objectui's own
`inlineRejected` translations. No translation key is added, and the en
bundle equals the source.
## Row 2 and its family: Explain access names people, not raw ids
The principal layer's `detail` in `explain-engine.ts` used to open with
`Principal 1cnVm… holds position(s) …`. It now opens with `Principal Dev
Admin (admin@objectos.ai; id 1cnVm…) holds position(s) …`. Three other
users in the same layer are named the same way:
- the on-behalf-of delegator, in all three arms of that clause;
- the user an ADR-0091 D3 delegated position came from, in the clause
(`approver from Grace Hopper (grace@…; id usr_…) until …`);
- that same user in the position's contributor attribution (`delegation
from Grace Hopper (…) until …`).
The module-private helper `personOf` reads each user row with
`ql.findOne('sys_user', …)` under the **asker's** context: the caller,
or the explained principal on a self-explanation. It reads each distinct
delegating user once. Every data middleware applies, so the sentence
names only what the asker's own read returns:
- When the read returns no row, the asker gets the bare id the sentence
always carried.
- When field-level security masks a field, that field stays out.
- When the read throws or the engine has no `findOne`, the sentence
falls back to the bare id.
The name is presentation only and never feeds a verdict. The decision's
`principal` object is unchanged. Only the principal layer's `detail`
expression and its contributor attribution changed, plus the declared
helper and its reads. `isRlsDenySentinel`, its docblock and PR #23179's
lines are untouched.
**Ruling-A pin change, accepted by the seat (claim amendment on the
card).** Two askers can now be shown different names for the same
principal. `explain-asker-gate.test.ts` compares whole decisions across
askers, and its 9 comparisons now go through `apartFromPrincipalPerson`,
which sets aside only the person segment of the principal sentence. The
record story that ruling A governs is still compared in full. Only the
principal's person label is set aside, read through each asker's own
door, consistent with ruling A's "by the read access of whoever ASKED".
## Measurements: what each route answers
Every route answers its service method's result unwrapped
(`res.json(out)` in `rest-server.ts`'s `decisionRoute`, the recall
route, `flowMoveRoute` and `threadRoute`). Each method returns `request`
from `readBackRequest()`, which reaches `loadRequest()` and then
`enrichRows()`, where `record_title` is set. The console's `apiHandler`
hands a body with no `data` key to the runner as `result.data`.
Measured live on fresh showcase boots of this branch (my own random
port, torn down after). Each row shows the answer's keys and
`request.record_title`:
- **approve:** `decision, finalized, request, resumed, runId`.
`INV-1006` (finalized) and `EXP-2001` (still pending).
- **reject:** `decision, finalized, request, resumed, runId`.
`EXP-DEMO`.
- **reassign:** `request`. `EXP-2001`.
- **revise (send back):** `request, resumed, runId`. `Compliance Audit`,
status `returned`. This request was opened by raising a project budget,
which runs `showcase_budget_approval` and its revise edge. The invoice
flow answered 400 because its node has no `revise` out-edge.
- **resubmit:** `request, resumed, runId`. `Compliance Audit`.
- **request-info:** `request`. `INV-1012`.
- **remind:** `notified, request`. `INV-1012`.
- **recall:** `request, resumed, runId`. `Compliance Audit`, status
`recalled`.
- **comment:** `request`. `INV-1012`.
**Not changed: none.** Every one of the nine routes carries
`request.record_title`, so no copy was left as it was and no route
answer was widened.
`GET /api/v1/meta/object/sys_approval_request` served the approve copy
in en and zh-CN. Explain was measured live as the admin:
- "Me": `Principal Dev Admin (admin@objectos.ai; id …) holds position(s)
[platform_admin, …]`.
- Explaining `usr_showcase_auditor_demo`: `Principal Ada Auditor (demo)
(auditor.demo@example.com; id usr_showcase_auditor_demo) holds …`.
## Pins
- `plugin-approvals/src/approval-service.test.ts` has 12 cells. Each
action's copy is filled from that action's real service answer through
`${result.PATH}`, and every token must resolve:
- approve: finalizing, still pending, and named from the payload
snapshot when the record's row is gone;
- one cell each for reject, reassign, send back, request info, remind,
recall, resubmit and reply;
- a check that every declared action's copy reads `${result.*}`.
-
`plugin-approvals/src/translations/success-copy-names-request-i18n.test.ts`
(renamed from the round-1 approve-only file) checks all nine actions.
Every source copy must carry `${result.request.record_title}`, every
locale must carry the same tokens, en must equal the source, and no
other locale may echo the English source.
- `plugin-security/src/explain-principal-person.test.ts` has 16 cells
and uses the real `SecurityPlugin` over a real `ObjectQL` and
better-sqlite3. Each asker's own `sys_user` read is armed first, and
each cell asserts the sentence that read licenses.
- **Principal:** named for the directory admin, the principal itself and
system. The masked admin gets the name and never the email. **Control:**
the blind admin gets the bare id, and the whole decision contains
neither the name nor the email.
- **D10 delegator:** both people named, only one named, or neither, as
the intersected read returns. A delegator that does not exist is named
by its id.
- **ADR-0091 D3 delegated position:** a real `sys_user_position` row
with `delegated_from` set. The directory admin gets Grace named in the
clause and in the attribution. The masked admin gets the name only.
**Control:** the blind admin gets `from usr_pp_grace` in both, and the
whole decision contains neither Grace's name nor her email.
## Negative-pin ablations
Each ablation was committed first and run through
`scripts/ablation-replace.mjs`, with an EXIT/INT/TERM trap that restores
the file. Every leg proved its restore by blob hash against HEAD with an
empty `git diff HEAD`. Each subject resolves through relative `src`
imports, so no `dist/` leg was needed.
- **A1:** the old principal sentence. 5 cells failed and the controls
held: `expected 'Principal usr_pp_ada holds position(s…' to match
/^Principal Ada Lovelace \(ada@example…/`.
- **A2:** the old on-behalf-of clause. 2 cells failed: `expected
'Principal Release Bot (bot@example.te…' to contain 'Acting on behalf of
Grace Hopper (gra…'`.
- **C1:** the delegated-position user printed raw again (`delegatorOf =
(from) => from`). 2 cells failed and the blind control held: `expected
'Principal Lin Delegate (lin@example.t…' to contain '1 position(s) held
via delegation (AD…'`.
- **B1:** approve's source copy back to `Approved.`. 7 cells failed.
- **B2:** the zh-CN approve value back to `已通过。`. 1 cell failed.
- **D1:** reject's source copy back to `Rejected.`. 5 cells failed:
`reject: names the record` failed with `expected 'Rejected.' to be
'Rejected "Acme Renewal"'`, and the token checks failed with
`approval_reject.successMessage: expected [] to deeply equal [
'${result.request.record_title}' ]`.
- **D2:** the ja-JP recall value back to `取り下げました。`. 1 cell failed:
`ja-JP approval_recall.successMessage tokens: expected [] to deeply
equal [ '${result.request.record_title}' ]`.
## Local verification at HEAD dd9bb74
- **Full package suites, under the shared lock:**
- `@objectstack/plugin-security`: 194 files and 4012 tests passed, 45
skipped.
- `@objectstack/plugin-approvals`: 74 files and 1051 tests passed.
- **Typecheck:** both packages' `typecheck` scripts pass, and the
test-typecheck ledgers held.
- **Derived gates:** `dispatch-gates --commands` derived 66 families
from this branch's changed files, all run, with the results recorded in
the dev report. `--ran` reconciled.
- **Lint, narrowed:** eslint ran over the 10 changed TS files only, with
0 errors and 0 warnings. The population is eslint's own `**/*.{ts,…}`
config, and the count comes from `--format json`. The narrowing excludes
nothing: this config enables no type-aware linting (no
`parserOptions.project`), so the diff cannot move a verdict on an
untouched file.
- **Main:** `origin/main` moved during the round, but not on any file
this PR touches, and PR #23179 is still open. No merge was needed.
- **Left to CI:** the type-check lanes, Test Core, Build Core and
dogfood jobs.
## Acceptance notes
- **Residual in the toasts:** a target with no resolvable title (no
display-field value, and no name, title, subject or label in the
snapshot) would toast with an empty title, for example `Approved ""`.
The inbox's inline path falls back to the formatted record id there, and
the token grammar has no fallback operator. Every request measured
carried a title.
- **Cosmetic:** a display name that itself ends in parentheses reads
`Ada Auditor (demo) (auditor.demo@example.com; id …)`.
- **Cost:** each explanation makes one more `sys_user` read per distinct
person named, and one more in the nested read pass when a by-id write is
explained for a row the principal cannot read.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 08b38cb commit 920bc12
11 files changed
Lines changed: 660 additions & 54 deletions
File tree
- .changeset
- packages/plugins
- plugin-approvals/src
- translations
- plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1288 | 1288 | | |
1289 | 1289 | | |
1290 | 1290 | | |
| 1291 | + | |
| 1292 | + | |
| 1293 | + | |
| 1294 | + | |
| 1295 | + | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
| 1299 | + | |
| 1300 | + | |
| 1301 | + | |
| 1302 | + | |
| 1303 | + | |
| 1304 | + | |
| 1305 | + | |
| 1306 | + | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
| 1321 | + | |
| 1322 | + | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
| 1327 | + | |
| 1328 | + | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
| 1335 | + | |
| 1336 | + | |
| 1337 | + | |
| 1338 | + | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
| 1343 | + | |
| 1344 | + | |
| 1345 | + | |
| 1346 | + | |
| 1347 | + | |
| 1348 | + | |
| 1349 | + | |
| 1350 | + | |
| 1351 | + | |
| 1352 | + | |
| 1353 | + | |
| 1354 | + | |
| 1355 | + | |
| 1356 | + | |
| 1357 | + | |
| 1358 | + | |
| 1359 | + | |
| 1360 | + | |
| 1361 | + | |
| 1362 | + | |
| 1363 | + | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
| 1379 | + | |
| 1380 | + | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
| 1408 | + | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
1291 | 1417 | | |
1292 | 1418 | | |
1293 | 1419 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
471 | 471 | | |
472 | 472 | | |
473 | 473 | | |
474 | | - | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
475 | 483 | | |
476 | 484 | | |
477 | 485 | | |
| |||
501 | 509 | | |
502 | 510 | | |
503 | 511 | | |
504 | | - | |
| 512 | + | |
505 | 513 | | |
506 | 514 | | |
507 | 515 | | |
| |||
524 | 532 | | |
525 | 533 | | |
526 | 534 | | |
527 | | - | |
| 535 | + | |
528 | 536 | | |
529 | 537 | | |
530 | 538 | | |
| |||
544 | 552 | | |
545 | 553 | | |
546 | 554 | | |
547 | | - | |
| 555 | + | |
548 | 556 | | |
549 | 557 | | |
550 | 558 | | |
| |||
559 | 567 | | |
560 | 568 | | |
561 | 569 | | |
562 | | - | |
| 570 | + | |
563 | 571 | | |
564 | 572 | | |
565 | 573 | | |
| |||
586 | 594 | | |
587 | 595 | | |
588 | 596 | | |
589 | | - | |
| 597 | + | |
590 | 598 | | |
591 | 599 | | |
592 | 600 | | |
| |||
628 | 636 | | |
629 | 637 | | |
630 | 638 | | |
631 | | - | |
| 639 | + | |
632 | 640 | | |
633 | 641 | | |
634 | 642 | | |
| |||
643 | 651 | | |
644 | 652 | | |
645 | 653 | | |
646 | | - | |
| 654 | + | |
647 | 655 | | |
648 | 656 | | |
649 | 657 | | |
| |||
691 | 699 | | |
692 | 700 | | |
693 | 701 | | |
694 | | - | |
| 702 | + | |
695 | 703 | | |
696 | 704 | | |
697 | 705 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| |||
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
141 | | - | |
| 141 | + | |
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
| 153 | + | |
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| |||
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
166 | | - | |
| 166 | + | |
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
| |||
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
175 | | - | |
| 175 | + | |
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
| |||
181 | 181 | | |
182 | 182 | | |
183 | 183 | | |
184 | | - | |
| 184 | + | |
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| |||
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
194 | | - | |
| 194 | + | |
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
203 | | - | |
| 203 | + | |
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
212 | | - | |
| 212 | + | |
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
| |||
0 commit comments