Repository navigation
Commit a00cf99
feat(spec,plugin-approvals): enable.approvalsVisibleToReaders, the per-object opt-in for the read-only record-reader approval tier (#22660)
Fixes #22560
Clause-②: yes (widening)
## What this does
An object's own metadata can now turn on the ruled read-only
record-reader approval tier (#8652). Before this, the only switch was
the `ApprovalsPluginOptions.recordReaderVisibleObjects` constructor
option. A config-driven app cannot set it, because its host builds the
plugin with no options.
- **Spec:** `ObjectCapabilities` (the object's `enable` block) gains
`approvalsVisibleToReaders: boolean`, default `false`. Its `.describe()`
states what `true` grants and to whom: a caller who can read a record of
the object sees that record's approval requests and full action history,
read-only. This holds on the approvals API and the generic data API
alike, on a read that names the record. No approval action is offered.
- **Plugin:** `ApprovalService.addRecordReaderVisibleIds` asks a new
private `recordReaderTierOn(object)` per call. It answers true if the
host's constructor set holds the object, or if the object's live
registered definition (`this.engine.getSchema(object)`) declares
`enable.approvalsVisibleToReaders === true`. This is the seat's decision
B on the round-1 fork (`6094828840`), AGENTS.md "Startup registry reads"
cure 1.
- `approvals-plugin.ts`: doc text only. The constructor option stays for
hosts that build the plugin themselves.
The ruling is unchanged (#8652, `5299823744`, maintainer 「同意」): "A user
with read access to the target business record may view that record's
approval requests and full action history, **read-only** … Enabled by a
per-object or plugin-level switch, **default OFF** … **the downstream
project opts in**."
### Why the name `approvalsVisibleToReaders`
It sits in the `enable` block beside `trackHistory`, `apiEnabled`,
`files`, `feeds`, `activities` and `clone`. Like `apiEnabled`, it is a
flag phrase that says what turning it on does. It names the subject
(approvals), the grant (visible, so read-only and not actionable) and
the grantee (readers of the record). A bare `approvals` was rejected.
Under `enable`, it reads as "turn approvals on for this object", and an
author or an AI would set it to get approval processes. Approval
processes need no such flag, so that reading would silently widen
visibility instead.
### Where the declaration is read, and why there
The service read the option once, in its constructor, which the plugin
calls in `start()`. A round-1 kernel probe showed that at that moment
the registry holds only objects registered in `init()`. Objects from
installed packages (`kernel:ready`), from a later `start()`, from Studio
edits and from dev reloads arrive afterwards. A set collected at start
would leave those declarations inert. It would also keep a removed
declaration in force until restart, an exposure the author believes is
closed. So the flag is read where it is used, from the registry as it is
at the read.
- **Default OFF cost:** at most one in-memory registry lookup per read
that names a record, and none for an untargeted read (the inbox). The
business record is never probed for an object that declares nothing: the
size-0 early return could no longer stand alone, so the check moved
behind the object/record test.
- **Fails closed:** an engine without `getSchema` (the optional member
of `ApprovalEngine`), an unregistered name, a throwing lookup, or any
value but literal `true` reads as not declared.
- **One visibility definition, both doors:** unchanged.
`requestVisibilitySourceOf` hands the same `visibleRequestIds` to
`bindRequestReadGate` and `bindRequestChildReadGates`. The flag widens
`sys_approval_request`, `sys_approval_action` and
`sys_approval_approver` on both doors together. The new pins read all
three on both doors.
### Declaration debts
- Liveness row `packages/spec/liveness/object.json` →
`enable.children.approvalsVisibleToReaders`: `live`, evidence anchored
on `approval-service.ts#recordReaderTierOn`.
- Studio: the object form's Capabilities section gets the toggle with a
help text (`object.form.ts`). The metadata-forms bundles are regenerated
by `node scripts/check-i18n-bundles.mjs --write`. The zh-CN, ja-JP and
es-ES leaves are hand-written, and
`object-collapsed-sections-echo-decisions.test.ts` carries a decided row
for each of the two new leaves, with its counts moved (capabilities 9 →
11 leaves).
- Generated: `authorable-surface/data.json` and
`authorable-defaults/data.json` (from the spec build),
`content/docs/references/data/object.mdx` (`gen:docs`) and
`liveness/state-counts/object.md` (`gen:liveness-counts`), as
`check:generated` named them. `authorable-surface.base.json` is
untouched.
- Changesets: `@objectstack/spec` minor and
`@objectstack/plugin-approvals` minor, each `Clause-②: yes (widening)`;
and `@objectstack/platform-objects` patch, `Clause-②: no`, for the form
row's translated leaves (contract review round 1 `6096311080`).
## Tests
All runs below went through `scripts/pm/os-verify-lock.sh`, and each
quotes the run's own summary line.
- **`@objectstack/plugin-approvals`**, at `fe81af4642`: `vitest run` →
`Test Files 70 passed (70)`, `Tests 1011 passed (1011)`. `typecheck`
(tsc + scripts + `check:test-typecheck`) → `VERDICT command-exit 0`.
`tsc -p tsconfig.test.json --listFiles` includes
`record-reader-opt-in.integration.test.ts` (1 hit).
- **`@objectstack/spec`**, at `fe81af4642`: `vitest run --project local`
→ `Test Files 642 passed (642)`, `Tests 19150 passed | 1 todo`.
`typecheck` → exit 0.
- **`@objectstack/platform-objects`**, at `e0c562f1a9`: `vitest run` →
`Test Files 69 passed (69)`, `Tests 1082 passed (1082)`. `typecheck` →
exit 0. At `fe81af4642`, one pin went red: the catalog-wide
translated-label control in
`object-lifecycle-panel-echo-decisions.test.ts` read 668 against 667,
because this PR adds one authored label. `e0c562f1a9` moves that count.
`git diff fe81af4 e0c562f` touches only that file, so the spec and
plugin readings above stand for the final head.
- **New pins**, `record-reader-opt-in.integration.test.ts` (8 cases). It
uses a real ObjectQL engine over better-sqlite3, the real
`ApprovalsServicePlugin.start()` and the real data-door normalizer. Each
pin compares one reading: the approvals door (list, by id, history) plus
the data door's list and by-id reads of all three request tables.
- An object declaring the flag, with the host option empty: a reader of
the record sees everything, read-only. `viewer` is `{can_act: false,
…}`; decide, reassign, comment and recall refuse with `FORBIDDEN:`; the
data-door row carries no `viewer`.
- A reader on another record, and a caller with no read: nothing, and
404 `RECORD_NOT_FOUND` on the data door.
- A read that names no record: the inbox is not widened.
- Controls: an undeclared object is unchanged, participants are
unchanged, and the host option works alone.
- Late registration: an object registered after `start()` with the flag
widens; re-registered without it, it stops, with no restart. The
registry's own answer flipping is asserted as that pin's control.
- **Spec pins** (`object.test.ts`): the default is `false`; the key is
accepted on `ObjectSchema` and read back `true`; a string value is
refused with `invalid_type`.
- **Ablations**, through `scripts/ablation-replace.mjs` (wrap mode;
anchor must hit; restore proven by blob equality with HEAD and an empty
`git diff HEAD`). The subject resolves from `src` by relative import, so
no `dist/` leg applies. The mutated file's HEAD blob is `8d7983b7e922`,
the same blob at the final head.
- (1) The declared-key read replaced by `return false`: `Tests 3 failed
| 5 passed (8)`. Red: the declared reader pin, the read-only pin and the
late-registration pin. Green, as expected: the cannot-read, inbox and
control pins.
- (2) The size-0 early return put back before the declared read: `Tests
3 failed | 5 passed (8)`, the same three red. The first attempt at (2)
was refused by the tool before any test ran: the replacement contained
its own anchor, so the anchor count could not drop. It was re-anchored
on the comment line above. Both restores read `blob == HEAD
(8d7983b) and git diff HEAD is empty`.
- **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) at `e0c562f1a9` derived 115
commands. All were run, plus the 48 artifact-roster commands that need
no PR context. Result: 161 exit 0, 1 exit 1, 1 NOT MEASURED. `--ran`
reconciles: `115 derived, 114 run, 0 NOT-MEASURED, 1 UNRUN` (the unrun
one is `check:dual-build-cjs-loads`).
- Exit 1: `pnpm check:platform-checklist`, red on `origin/main`
`86da194919` too. Its 7 problems are symbol anchors in
`docs/qa/platform-checklist/areas/access-security.json` and
`attachments-storage.json` naming symbols absent from
`metadata-protocol/src/protocol.ts` and
`service-storage/src/attachment-access-hooks.ts`. This diff touches none
of those files.
- `check:dual-build-cjs-loads`: NOT MEASURED, reason: it needs a
whole-workspace build, which this dispatch rules out.
- Verdict lines: `check:generated` → `✓ All 15 generated artifacts are
up to date`. `check:liveness` → `object 54 classified (live 53, planned
1)`, every `path#symbol` anchor resolves. `check:i18n` → `OK (9
package(s) — all bundles in sync…)`. `check:i18n-stale-fill` → `0
stale-fill`. `check:api-surface` → `unchanged ✓`. `check:nul-bytes` →
`OK`. `check:type-check-debt` → `none above its recorded number`.
## Acceptance notes
- **Kernel probe (throwaway, not committed).** ObjectKernel +
ObjectQLPlugin, with producers registering through the manifest service
in `init()`, in a `start()` composed after approvals, and on
`kernel:ready`. Each object declares the flag. The reading was taken
inside approvals' `start()`, after boot, and after the `kernel:ready`
producer re-registered its object without the flag: `seenAtStart {init:
true, start: false, ready: false}`, `afterBoot {init: true, start: true,
ready: true, plain: false, never_registered: false}`, `afterRemoval
false`. An engine with no `getSchema` answers `false`. What `getSchema`
returns through that door is the authored literal: the declared object's
`enable` reads `{"approvalsVisibleToReaders":true}` with no defaults
filled in, and an object with no block reads `undefined`. So an absent
block or flag reads as the spec default, `false`.
- The option's doc linked `ApprovalService.recordReaderVisibleIds`, a
member that does not exist. It now links `addRecordReaderVisibleIds`, in
the hunk this PR edits anyway.
- The en label is the extractor's humanize of the key, "Approvals
Visible To Readers" (the form declares no label), like its siblings in
that block.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 317cddd commit a00cf99
20 files changed
Lines changed: 604 additions & 30 deletions
File tree
- .changeset
- content/docs/references/data
- packages
- platform-objects/src/apps/translations
- plugins/plugin-approvals/src
- spec
- authorable-defaults
- authorable-surface
- liveness
- state-counts
- src/data
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
Lines changed: 13 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
430 | 430 | | |
431 | 431 | | |
432 | 432 | | |
| 433 | + | |
433 | 434 | | |
434 | 435 | | |
435 | 436 | | |
| |||
533 | 534 | | |
534 | 535 | | |
535 | 536 | | |
| 537 | + | |
536 | 538 | | |
537 | 539 | | |
538 | 540 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
296 | 300 | | |
297 | 301 | | |
298 | 302 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
296 | 300 | | |
297 | 301 | | |
298 | 302 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
296 | 300 | | |
297 | 301 | | |
298 | 302 | | |
| |||
Lines changed: 35 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
316 | 334 | | |
317 | 335 | | |
318 | 336 | | |
| |||
487 | 505 | | |
488 | 506 | | |
489 | 507 | | |
490 | | - | |
491 | | - | |
492 | | - | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
493 | 512 | | |
494 | 513 | | |
495 | 514 | | |
| |||
500 | 519 | | |
501 | 520 | | |
502 | 521 | | |
| 522 | + | |
503 | 523 | | |
504 | 524 | | |
505 | 525 | | |
506 | | - | |
507 | | - | |
508 | | - | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
509 | 529 | | |
510 | 530 | | |
511 | 531 | | |
| |||
685 | 705 | | |
686 | 706 | | |
687 | 707 | | |
688 | | - | |
| 708 | + | |
689 | 709 | | |
690 | 710 | | |
691 | 711 | | |
| |||
918 | 938 | | |
919 | 939 | | |
920 | 940 | | |
921 | | - | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
922 | 946 | | |
923 | | - | |
| 947 | + | |
924 | 948 | | |
925 | 949 | | |
926 | 950 | | |
| |||
1063 | 1087 | | |
1064 | 1088 | | |
1065 | 1089 | | |
1066 | | - | |
| 1090 | + | |
1067 | 1091 | | |
1068 | 1092 | | |
1069 | 1093 | | |
1070 | 1094 | | |
1071 | 1095 | | |
1072 | 1096 | | |
1073 | | - | |
| 1097 | + | |
1074 | 1098 | | |
1075 | 1099 | | |
1076 | 1100 | | |
| |||
Lines changed: 4 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1177 | 1177 | | |
1178 | 1178 | | |
1179 | 1179 | | |
1180 | | - | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
1181 | 1184 | | |
1182 | 1185 | | |
1183 | 1186 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
296 | 300 | | |
297 | 301 | | |
298 | 302 | | |
| |||
0 commit comments