Repository navigation
plugin-approvals: the ruled record-reader visibility tier (#8652) is reachable only through a constructor option no app can set — declare its opt-in where an app can #22560
Description
Activity
- addedenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guards
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsDeferral note from the
domain:specseat 3 (#18883) · sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T01:53Z. ⛔ Not a claim; the card stayspm:queue, unassigned.- Why not now: this card's Order says plugin-approvals:
sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 "lands first or alongside", because with the tier switched on the data door and the approvals door must honour it from one definition. plugin-approvals:sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 (domain:services,priority:p1) is in flight (pm:dispatched, assigned). This card's acceptance ("sees its approval requests read-only on both doors") cannot be pinned on the data door until plugin-approvals:sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559's gate exists onmain. - What the next claim should know: the declarative key feeds
ApprovalsPluginOptions.recordReaderVisibleObjects(approval-service.ts:1235per the body); measure where plugin-approvals:sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559's data-door gate reads its set, so both doors read one definition. - Re-pick condition: plugin-approvals:
sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 closed. Triage may instead setpm:blockedwithBlocked-by: #22559; this seat does not change the card's state.
Generated by Claude Code
- Why not now: this card's Order says plugin-approvals:
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsDeferral note, updated, from the
domain:specseat 3 (#18883) · sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T04:37Z. ⛔ Not a claim; the card stayspm:queue, unassigned. Supersedes the re-pick condition in6092402036.- plugin-approvals:
sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 has closed (PR fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587 →76bc1e03a3). The data door now servessys_approval_requestby the approvals door's one visibility definition,requestVisibilitySourceOf(service), with the record-reader tier still default-OFF. That was this card's stated order, and it now holds. - A new cross-lane intersection: plugin-approvals:
sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589 (domain:services, in flight; claim surface "packages/plugins/plugin-approvals/only") binds read gates on the sibling tables from that same visibility source. This card's plugin half (feeding the declared opt-in intorecordReaderVisibleObjects,approval-service.tsabout:1263/:1364, andapprovals-plugin.tsabout:88) is in the same package and the same wiring. By the cross-domain rule it lands after plugin-approvals:sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589's PR. - For the next claim:
- Read
requestVisibilitySourceOfand plugin-approvals:sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589's gates onmainfirst, so one switch widens all three tables' doors together. - The per-object form fits
ObjectCapabilities(the object'senableblock,packages/spec/src/data/object.zod.tsabout:239), besidefiles,feedsandactivities: one boolean, defaultfalse, and the constructor option kept for hosts that build the plugin themselves. - That placement is this seat's reading for the claim to confirm, not a ruling.
- Read
- Re-pick condition: plugin-approvals:
sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589 closed.
Generated by Claude Code
- plugin-approvals:
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 6 (#22560: a declarative, per-object opt-in for the ruled record-reader visibility tier (#8652), in the object's
enableblock, feeding the onerecordReaderVisibleObjectsset) · 2026-10-10T06:35Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22560-record-reader-opt-in
Worktree:objectstack-issue-22560
Domain:domain:spec(the plugin half inplugin-approvalsis declared below, as aSeam:-style vertical dispatch)
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main83b8b80728; stop on breach and explain in the report):packages/spec/src/data/object.zod.ts: one boolean inObjectCapabilities(theenableblock, about:239), defaultfalse, besidefiles,feedsandactivities. Its.describe()says what it grants: a caller who can read a record of this object sees that record's approval requests and history, read-only, on both doors, with no approval action. Its name is the dev's, in that block's vocabulary, and the report gives it.packages/plugins/plugin-approvals/src/approvals-plugin.ts(therecordReaderVisibleObjectshand-off, about:213): the set the service receives is the union of the constructor option and every object that declares the new key. ⛔ Notapproval-service.ts, which PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625 (plugin-approvals (17.7.0): opening an approval step notifies none of its approvers — no inbox message, no email; the docs also still say no product surface writesmanager_id#22607) is editing, norrequest-read-gate.tsorsys-approval-token.object.ts(plugin-approvals:sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616). If the declared key cannot be honoured without editingapproval-service.ts(for example, objects registered after start), stop and report it as a fork.- Pins in both packages; the generated spec artifacts the key renders into; the liveness or strictness ledger rows its declaration needs.
.changeset/22560-*.md:@objectstack/specminorand@objectstack/plugin-approvalsat its level, each withClause-②: yes (widening).
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). A new declarable key on a published schema, widening a read surface when opted in: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: yes (widening)
Responsibility:n/a — not a defect card(filing gate ③, executing the 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 ruling)
Thread-read: 6093841398
Serial constraints cleared:- Unblocked: plugin-approvals:
sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 (PR fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587,76bc1e03a3) and plugin-approvals:sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589 (PR fix(plugin-approvals): the generic data door serves sys_approval_action and sys_approval_approver rows by their request's visibility (#22589) #22613,83b8b80728) have landed, so one visibility definition servessys_approval_request,sys_approval_actionandsys_approval_approver(requestVisibilitySourceOf,bindRequestChildReadGates). - Disjoint from the in-flight cards in this package: plugin-approvals:
sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616 (area:access,domain:services) claimssys-approval-token.object.tsonly; PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625 (plugin-approvals (17.7.0): opening an approval step notifies none of its approvers — no inbox message, no email; the docs also still say no product surface writesmanager_id#22607) editsapproval-service.tsand tests, notapprovals-plugin.ts. - No open PR touches
object.zod.ts.
The ruling, verbatim (#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." The seat takes the per-object form, as the card's "matches how other capabilities are declared" reads, and keeps the constructor option for hosts that build the plugin themselves.This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22560,
"status": "needs_decision",
"branch": "claude/issue-22560-record-reader-opt-in",
"pr": null,
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (subagent: the parent PM session's id, the one Claim 6094707451 names)",
"premise_still_valid": true,
"summary": "Stopped at the dispatch's Zone 2 item 3 stop, before any edit: a key that apps declare cannot be honoured without approval-service.ts reading the opt-in where it is used. Today the service turns the option into a private readonly Set once, in its constructor (approval-service.ts:1363, :1384), and the plugin builds the service in start() (approvals-plugin.ts:207). A throwaway kernel probe shows that at that moment the registry holds only objects registered in init(). An object registered in a later plugin's start(), or on kernel:ready (when marketplace-installed apps are rehydrated), is absent then and present after boot. So a start-time union, the only form that fits the claimed file surface, would leave the declaration inert for installed packages, Studio-authored objects and os dev reloads. It would also keep the tier ON after an author removes the key, until restart. Items 1, 2 and 4 hold as the dispatch assumed. The premise holds: serve still builds the plugin with no arguments (packages/cli/src/commands/serve.ts:4712, and the approvals capability has no options arm). The branch is pushed at the base 83b8b80 with no commits, and the worktree is removed.",
"zone2": {
"item1_handoff": "CONFIRMED on origin/main 83b8b80. The option is declared at approvals-plugin.ts:88 and handed over at :213 (new ApprovalService in start(), :207). The service declares it at approval-service.ts:1263, holds it asprivate readonly recordReaderVisibleObjects: ReadonlySet(:1363), builds it once in the constructor (:1384-1388), and reads it at :6882 (the size-0 early return: default OFF, no probe) and :6886 (has).",
"item2_both_doors": "CONFIRMED by reading the code; the existing pins were not re-run. approvals-plugin.ts:323-326 hands requestVisibilitySourceOf(this.service) to bindRequestReadGate and bindRequestChildReadGates. Its visibleRequestIdsFor (approval-service.ts:1411-1430) returns this.visibleRequestIds, the approvals door's own function (:6754), which calls addRecordReaderVisibleIds. So one set widens all three tables on both doors together. request-read-gate.integration.test.ts:302 and request-child-read-gate.integration.test.ts:322 already turn the tier on through the constructor option and drive the data door. A key that feeds the same set needs no second rule.",
"item3_registration_timing": "STOP. Probe: ObjectKernel + ObjectQLPlugin, with one producer registering a manifest through the manifest service in init() (as AppPlugin.init does, app-plugin.ts:442), one doing so in start() and composed after approvals, and one doing so on kernel:ready (as MarketplaceInstallLocalPlugin does, marketplace-install-local-plugin.ts:475 then :491 rehydrate). A subclass of ApprovalsServicePlugin records the registry inside start(). Output:PROBE seenAtApprovalsStart=[\"probe_init_obj\"] registryAfterBootstrap=[\"probe_init_obj\",\"probe_ready_obj\",\"probe_start_obj\"]. Real producers on each late path: installed marketplace apps, rehydrated on kernel:ready on every boot, so never at start() even after a restart. Durable sys_packages packages (AI-authored app packages), rehydrated in PackageServicePlugin.start() (service-package/src/index.ts:583-626), with no declared order against approvals. Studio object edits, re-registered on the metadatachangedevent (objectql/src/plugin.ts:1114-1162). os dev reloads (objectql/src/plugin.ts:781, :1069). objectql/src/plugin.ts:607 says it in its own comment: manifests registered after start() arrive on kernel:ready or an HTTP request. Studio is a live producer of this block: the object form lists every enable toggle (packages/spec/src/data/object.form.ts:418-430).",
"item4_spec_shape": "CONFIRMED by reading. ObjectCapabilities is a strictObject (surfaceenable) at packages/spec/src/data/object.zod.ts:239. Its keys are trackHistory, searchable, apiEnabled, apiMethods, files, feeds, activities and clone, each a boolean with a default (files defaults false). A new key owes a liveness row under packages/spec/liveness/object.json (props.enable.children, live plus evidence; scripts/liveness/proof-registry.mts binds enable.files the same way). It also owes the Studio form row (object.form.ts:418-430), the regenerated metadata-forms i18n bundles (an enable.KEY label), authorable-surface, docs and the strictness-ledger counts. This is a reading, not a gate run: check:generated decides at implementation time."
},
"tests": "No product code changed, so no suite is owed. Measurements: (1) The closure buildpnpm --workspace-concurrency=2 --filter \"@objectstack/plugin-approvals^...\" buildran under os-verify-lock with slot issue-22560:VERDICT command-exit 0 · held the lock 221s. (2) Throwaway probe src/zz-probe-22560.test.ts in plugin-approvals, run withpnpm exec vitest run --maxWorkers=2 src/zz-probe-22560.test.ts: EXIT=0,Test Files 1 passed (1),Tests 1 passed (1), plus the PROBE line quoted in zone2.item3. It was deleted right after the run;git status --porcelainwas empty before the worktree was removed. No ablation: no implementation exists.",
"gates": "None owed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) exits 2:this branch changes nothing against 'origin/main' (merge base 83b8b8072) — nothing to derive. The Artifact-rosters block is not printed on an empty diff. dual-build-cjs-loads: NOT MEASURED, reason: no diff and no whole-workspace build, per the dispatch.",
"line_budget": "n/a: no diff.",
"files_changed": [],
"deviations": [
"The probe was a temporary uncommitted test file inside the worktree. Module resolution needs the package directory. It was removed after one run, and the clean tree was read before the worktree was removed. Never pushed.",
"Nothing else. Reads used single-card REST (gh api) only. No MCP call, no label or assignee write, and no PR, because the stop came before any edit."
],
"mcp_calls": "0",
"api_writes": "1: POST /repos//issues/22560/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not a REST write: onegit push -u origin claude/issue-22560-record-reader-opt-inof the empty claim branch, at the base, as the write-route probe.",
"open_questions": [
{
"question": "Where is the per-object opt-in (one boolean in ObjectCapabilities, default false) read? Option A: once at start(), inside the claimed surface. Option B: at each use, which edits approval-service.ts, the file the claim excludes because PR #22625 is editing it.",
"options": [
"A: Start-time union in approvals-plugin.ts only. At start(), the plugin unions the constructor option with every object that engine.registry.getAllObjects() shows declaring the key, and hands that list to the service. (1) Real need: it covers the measured consumer's shape. hotclm is config-drivenserve: AppPlugin.init registers the manifest in phase 1, and the probe shows init-registered objects are present at start(). It misses every other measured path: installed marketplace apps (never, not even after a restart), sys_packages and AI-authored packages hydrated in a later start(), Studio-authored objects and edits, and os dev reloads. (2) Long-term fit: it is the three-part defect of AGENTS.md 'Startup registry reads': a read of a still-filling registry at start(), 'not opted in' concluded from absence, and that verdict recorded in the service's readonly Set. That section's first cure is to resolve where the value is used. (3) AI-error resistance: the declaration works on one delivery path and is silently inert on the rest. In the OFF direction it is worse: removing the key in Studio, or upgrading a package without it, leaves the read tier ON until restart, an exposure the author believes is closed. (4) Startup focus: smallest diff, within the claim, but it ships a known-inert and over-exposing path that would need a follow-up card.",
"B: Read the declaration where it is used, in approval-service.ts. addRecordReaderVisibleIds asks, per call, whether the constructor set has the object or the object's live registered definition declares the key. The read goes through this.engine.getSchema, which ApprovalEngine already declares as optional (approval-service.ts:118) and the same file already uses (:6136). approvals-plugin.ts changes only its doc text; the constructor option stays for hosts that build the plugin themselves. (1) Real need: it honours every measured registration path and any later OFF edit at once, and hotclm the same. (2) Long-term fit: it is the AGENTS.md cure 1. One definition is read where it is used, and requestVisibilitySourceOf and both gates stay unchanged, so both doors widen together (zone2.item2). (3) AI-error resistance: declaring the key enforces it on every path, with no restart-only behaviour. (4) Startup focus: a few lines in the service plus the pins the dispatch lists. Default OFF keeps its no-probe property: the added check is an in-memory registry lookup, and the anchor read still runs only for an opted-in object. Cost: it edits a file another seat is changing. Measured: #22625's hunks on approval-service.ts are at :2962 and :3270. This card's would be at :1255-1265 (option doc), :1363-1388 (field and constructor) and :6877-6886 (the read). The hunks are disjoint, so at most one small rebase lap follows; the rest is a sequencing call only the PM can make."
],
"recommendation": "B. It is the only option under which the declaration is enforced on every path an app reaches, it is the cure AGENTS.md prescribes for exactly this shape, and A's OFF-direction gap is an exposure on a security-adjacent widening, not a cosmetic one. Re-dispatch with packages/plugins/plugin-approvals/src/approval-service.ts in the claim's file surface. The measured hunks are disjoint from #22625's, so either landing order costs at most one rebase lap; whether to go before or after #22625 lands is the PM's sequencing call. The spec half (zone2.item4) is unchanged under either option."
}
],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsRelease:
session_01KNKBCRDJCu5tGy3TEbvtrF· why: the dev's measured fork needsapproval-service.ts, which PR #22625 (#22607,domain:services, in flight) is editing, so by the cross-domain rule this card lands after it · to:pm:blockedon #22607, unassignedSeat decision on report
6094812876: B, reading the opt-in where it is used. The card waits on #22607, and the next claim dispatches B as written heredomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T06:50Z · holder of claim6094707451, released by this act. Thread-read: 6094812876.What the dev measured (no edit made; the branch sits at its base
83b8b80728):- The service builds
recordReaderVisibleObjectsonce, as a private readonlySetin its constructor (approval-service.tsabout:1363,:1384), and the plugin constructs it instart()(approvals-plugin.tsabout:207). - A kernel probe shows that at that moment the registry holds only objects registered in
init(). Objects registered in a laterstart()or onkernel:readyarrive after it: marketplace-installed apps (rehydrated onkernel:readyon every boot),sys_packages/ AI-authored packages, Studio object edits andos devreloads. - One visibility set already widens all three request tables on both doors together (
requestVisibilitySourceOf→bindRequestReadGate/bindRequestChildReadGates), so the key needs no second rule.
The decision: B. The declaration is read where it is used.
addRecordReaderVisibleIdsasks, per call, whether the constructor set holds the object, or whether the object's live registered definition (this.engine.getSchema, already declared onApprovalEngineand already used in the file) declares the newenablekey.- Why:
- It is AGENTS.md's "Startup registry reads" cure 1: resolve where the value is used, never conclude "not opted in" from a still-filling registry.
- A start-time union (A) would leave the declaration inert for installed and Studio-authored objects.
- Worse, it would keep the tier ON after an author removes the key, until restart. On a security-adjacent widening, that exposure is not cosmetic.
- This is not an escalation: the ruling (【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652, per-object, default OFF) is unchanged. The decision is how the declared key is honoured, and AGENTS.md's prescribed cure decides it.
The next claim's file surface (the claim
6094707451's, plus one file):packages/spec/src/data/object.zod.ts: one boolean inObjectCapabilities, defaultfalse;- with it, the liveness row in
packages/spec/liveness/object.json, the Studio object form row (object.form.tsabout:418–:430), the regenerated metadata-forms i18n bundles, authorable-surface, docs and strictness-ledger counts, ascheck:generateddecides; packages/plugins/plugin-approvals/src/approval-service.ts: the option doc (about:1255–:1265), the field and the constructor (about:1363–:1388), and the read (about:6877–:6886). The dev measured PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625's hunks at about:2962and:3270, which are disjoint;approvals-plugin.ts: doc text only;- pins in both packages, as the dispatch listed;
- changesets,
Clause-②: yes (widening).
Why wait rather than proceed: the cross-domain rule serialises a file intersection behind the in-flight PR, even when the hunks are disjoint. #22607 (PR #22625) is
domain:services, and this card must not race it.Blocked-by: #22607is now line 1 of the body, so the unlock scan returns this card topm:queuewhen #22607 closes.
Generated by Claude Code
- The service builds
3 remaining items
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 7 (#22560: a declarative, per-object opt-in for the ruled record-reader visibility tier (#8652), in the object's
enableblock, read where it is used, as the seat's decision B6094828840) · 2026-10-10T07:38Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22560-record-reader-opt-in
Worktree:objectstack-issue-22560
Domain:domain:spec(the plugin half inplugin-approvalsis declared below, as aSeam:-style vertical dispatch)
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main1b99388505; stop on breach and explain in the report):packages/spec/src/data/object.zod.ts: one boolean inObjectCapabilities(theenableblock, about:239), defaultfalse, besidefiles,feedsandactivities. Its.describe()says what it grants: a caller who can read a record of this object sees that record's approval requests and history, read-only, on both doors, with no approval action.- Its declaration debts: the liveness row in
packages/spec/liveness/object.json, the Studio object form row (object.form.tsabout:418–:430), the regenerated metadata-forms i18n bundles, authorable-surface, docs references and strictness-ledger counts, ascheck:generateddecides. packages/plugins/plugin-approvals/src/approval-service.ts: the option doc (about:1255–:1265), the field and constructor (about:1363–:1388), and the read inaddRecordReaderVisibleIds(about:6918–:6930; the lines moved since6094828840with PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625's landing).approvals-plugin.ts: doc text only.- Pins in both packages;
.changeset/22560-*.md:@objectstack/specminorand@objectstack/plugin-approvalsat its level, each withClause-②: yes (widening).
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: "no path-derived mandate"). A new declarable key on a published schema, widening a read surface when opted in: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: yes (widening)
Responsibility:n/a — not a defect card(filing gate ③, executing the 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 ruling)
Thread-read: 6094901362
Serial constraints cleared: - plugin-approvals (17.7.0): opening an approval step notifies none of its approvers — no inbox message, no email; the docs also still say no product surface writes
manager_id#22607 (PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625) landed ase8c6666870, soapproval-service.tsis free. No open PR touchesapproval-service.ts,approvals-plugin.ts,object.zod.ts,object.form.tsorliveness/object.json(all 11 open PRs' file lists read at 2026-10-10T07:38Z). - Same area, disjoint files: plugin-approvals:
sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616 (area:access,domain:services) claimssys-approval-token.object.tsonly. spec (17.7.0): a list view or dashboard cannot be limited to an audience, andrequiredPermissionshas no any-of form — an app cannot show each audience only its own views and boards #22611 (area:access, claimed next by this seat) isview.zod.tsanddashboard.zod.ts.
Resume-from:claude/issue-22560-record-reader-opt-in@83b8b80728(the first claim's marker, with no commits; the dev starts fromorigin/main)
The decision this claim dispatches, from
6094828840: "addRecordReaderVisibleIdsasks, per call, whether the constructor set holds the object, or whether the object's live registered definition (this.engine.getSchema, already declared onApprovalEngineand already used in the file) declares the newenablekey." The ruling (#8652,5299823744, per-object, default OFF) is unchanged.This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22560,
"status": "done",
"branch": "claude/issue-22560-record-reader-opt-in",
"pr": "#22660",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (subagent: the parent PM session's id, the one Claim 6095200466 names)",
"premise_still_valid": true,
"summary": "Implemented decision B. packages/spec: ObjectCapabilities (the enable block) gains approvalsVisibleToReaders, a boolean defaulting to false. Its .describe() says what true grants and to whom: a caller who can read a record of the object sees that record's approval requests and full history, read-only, on the approvals API and the generic data API alike, on a read that names the record, with no approval action. plugin-approvals: addRecordReaderVisibleIds asks a new private recordReaderTierOn(object) on every call. It answers true if the host's constructor set holds the object, or if this.engine.getSchema(object).enable.approvalsVisibleToReaders === true. The size-0 early return is gone, so the check now sits behind the object/record test (at most one in-memory lookup per targeted read; the business record is never probed for an undeclared object). It fails closed when getSchema is absent, the object is unknown, the lookup throws, or the value is anything but true. Name choice, also argued in the PR: it is a flag phrase like apiEnabled that names subject, grant and grantee. A bareapprovalswould read as 'turn approvals on' and silently widen visibility. The declaration debts are paid: liveness row (live, evidence anchored at approval-service.ts#recordReaderTierOn), Studio form row with helpText, the regenerated metadata-forms bundles with hand-written zh-CN/ja-JP/es-ES leaves, authorable-surface/defaults, the reference doc, the liveness count shard, and two minor changesets with Clause-② yes (widening). origin/main 86da194 was merged through os-regen-merge.sh. The PR opened as a draft, assigned to zhuangjianguo.",
"zone2": {
"item1_read_point": "CONFIRMED on 1b99388: approval-service.ts:6926if (this.recordReaderVisibleObjects.size === 0) return;sat ahead of the object/record checks and :6930has(object). Under B it could not stand alone. It was removed, and recordReaderTierOn(object) now runs afterif (!object || !recordId) return;. Ablation 2 (putting the size-0 return back before the declared read) turns the declared and late-registration pins red.",
"item2_getSchema_live": "CONFIRMED by a throwaway kernel probe of the round-1 shape (ObjectKernel + ObjectQLPlugin; manifest producers in init, in a start composed after approvals, and on kernel:ready; each object declares the flag). Inside approvals' start: {init: true, start: false, ready: false}. After bootstrap: {init: true, start: true, ready: true, plain_obj: false, never_registered: false}. After the ready producer re-registered its object without the flag: false, with no restart. An ApprovalService over an engine with no getSchema answers false (fails closed: no widening). The committed late-registration pin shows the same on the ObjectQL engine: widens after start, stops on re-registration.",
"item3_getSchema_shape": "MEASURED: the AUTHORED LITERAL, not the parsed object. Through the manifest door the declared object's enable reads {"approvalsVisibleToReaders":true}, with none of the block's other defaults filled in, and an object with no block reads undefined. The raw registerObject door is the same (the late pin asserts enable.approvalsVisibleToReaders undefined after re-registration). Hence the strict=== trueread: absent reads as the spec default false. This is the default, not a lenient fallback.",
"item4_one_definition": "RE-CONFIRMED on the current head by the existing pins: request-read-gate.integration.test.ts and request-child-read-gate.integration.test.ts pass unchanged inside the full plugin-approvals run (70 files, 1011 tests). The new pins read all three request tables on both doors from one reading per caller."
},
"tests": "Full suites through os-verify-lock (slot issue-22560). plugin-approvals at fe81af4: vitestTest Files 70 passed (70),Tests 1011 passed (1011); typecheck (tsc, scripts and check:test-typecheck) VERDICT command-exit 0; tsc -p tsconfig.test.json --listFiles counts record-reader-opt-in.integration.test.ts = 1. spec at fe81af4: vitest --project localTest Files 642 passed (642),Tests 19150 passed | 1 todo; typecheck exit 0; listFiles object.test.ts = 1. platform-objects at e0c562f: vitestTest Files 69 passed (69),Tests 1082 passed (1082); typecheck exit 0. At fe81af4 that package had 1 red pin (lifecycle-panel catalog control: expected 667, got 668); e0c562f moved the count, and git diff fe81af4 e0c562f touches only that test file. New pins: record-reader-opt-in.integration.test.ts, 8 cases (declared object with the host option empty, both doors, all three tables, read-only refusals FORBIDDEN:, viewer can_act false; cannot-read controls with 404 RECORD_NOT_FOUND; inbox not widened; undeclared object unchanged; participants unchanged; host option alone; late registration widens then stops). Spec pins in object.test.ts (default false, accepted on ObjectSchema, a string refused with invalid_type). Ablations through scripts/ablation-replace.mjs, wrap mode, under the lock. The subject resolves from src by relative import, so no dist leg. The mutated blob is 8d7983b7e922, the same as the final head's. (1) declared read replaced byreturn false: anchor x1 to x0,Tests 3 failed | 5 passed (8), red = declared reader, read-only, late registration. (2) size-0 return reinstated before the declared read: same 3 red. Its first attempt was refused by the tool before any test ran (the replacement contained its anchor, so the anchor count could not drop); it was re-anchored on the comment line above. Both restores readblob == HEAD (8d7983b7e922) and git diff HEAD is empty.",
"gates": "At the final head e0c562f (git rev-parse --short HEAD),node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 115 commands, the same list as at fe81af4. All were run, plus the 48 artifact-roster commands that need no PR: 161 exit 0, 1 exit 1, 1 NOT MEASURED.--ranverdict:115 derived, 114 run, 0 NOT-MEASURED, 1 UNRUN(check:dual-build-cjs-loads: NOT MEASURED, reason: it needs a whole-workspace build, which the dispatch rules out). Exit 1: pnpm check:platform-checklist, red on origin/main 86da194 as well. Its 7 problems are checklist symbol anchors in areas/access-security.json and areas/attachments-storage.json naming symbols that b389e43 (#22515) and ce3d0ad (#22513) removed from metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts. This diff touches none of those files; CI will show it red here too. The 3 PR-context roster guards, wired to PR 22660 (PR_NUMBER, PR_BODY, PR_HEAD_REF, GITHUB_REPOSITORY, a read token): check-closing-target-claim exit 0 (its verdict line: the PR's closing target is this card, and both carry a Claim: whose Branch: line names claude/issue-22560-record-reader-opt-in), check-partof-closing-keyword exit 0, check-single-claim-paths exit 0. Verdict lines: check:generatedAll 15 generated artifacts are up to date; check:livenessobject 54 classified (live 53, planned 1), with every path#symbol anchor resolving; check:i18nOK (9 package(s) — all bundles in sync; check:i18n-stale-fill0 stale-fill; check:api-surfaceunchanged; check:nul-bytes OK; check:type-check-debtnone above its recorded number; check:changeset-fixed in sync. CI at the moment of reporting, one read: 14 checks completed, 18 in_progress, 0 failed. Not waited on. Outside local reach: the CI-only jobs and the four type-check lanes the derivation names.",
"line_budget": "627 changed lines (+597 / -30, 19 files) against the 3000-line human-merge threshold: under. No skills/** path touched, so no skill line or token budget applies.",
"files_changed": [
".changeset/22560-plugin-approvals-declared-record-reader-tier.md",
".changeset/22560-spec-approvals-visible-to-readers.md",
"content/docs/references/data/object.mdx (gen:docs)",
"packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts (check-i18n-bundles --write)",
"packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts (regenerated; leaf values hand-written)",
"packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts (regenerated; leaf values hand-written)",
"packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts (regenerated; leaf values hand-written)",
"packages/platform-objects/src/apps/translations/object-collapsed-sections-echo-decisions.test.ts",
"packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approvals-plugin.ts (doc text only)",
"packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts (new)",
"packages/spec/authorable-defaults/data.json (spec build)",
"packages/spec/authorable-surface/data.json (spec build)",
"packages/spec/liveness/object.json",
"packages/spec/liveness/state-counts/object.md (gen:liveness-counts)",
"packages/spec/src/data/object.form.ts",
"packages/spec/src/data/object.test.ts",
"packages/spec/src/data/object.zod.ts"
],
"deviations": [
"SAFETY-CHECK REFUSAL, quoted. A shell slip (a variable set inside a backgrounded chain, unset in the parent) wrote a 6-byte PID file to/build-i18n.pid, outside the repo and the scratchpad. Myrm -f /build-i18n.pidwas refused: "Permission for this command was denied by a built-in Claude Code safety check, not by the user. The check stops removals that can delete far more than intended … What was flagged: Dangerous rm operation detected: '/build-i18n.pid'". I did not retry or work around it; the file is still there for a person to delete. On the dispatch's stop rule: I stopped that step only, and finished the card. The refusal touched a stray file outside the work, and the refusal text itself says to finish without the removal. If the PM reads 'stop there' as stopping the whole run, this is a conflict to note.",
"FILE SURFACE: two platform-objects test files outside the claim's literal list, both forced by its declared form-row and i18n-bundle debt. object-collapsed-sections-echo-decisions.test.ts is the per-leaf decision ledger for the form's collapsed sections. It went red on the new row by design: two decided rows (label, helpText), counts 9→11 capabilities leaves, 105→107 panel leaves, 11→13 decisions, plus the new key on its class-(c) key list. object-lifecycle-panel-echo-decisions.test.ts holds the catalog-wide translated-label control, moved 667→668. Neither file had any other edit.",
"The option doc in approval-service.ts linked {@link ApprovalService.recordReaderVisibleIds}, which does not exist. Inside the hunk this card edits, it now links addRecordReaderVisibleIds.",
"Throwaway probe: src/zz-probe-22560-r2.test.ts in plugin-approvals was run once under the lock and deleted right after. git status --porcelain was empty afterwards, and it was never committed.",
"I stopped my own queued suite runner (PIDs 7295 and 7298, recorded) so origin/main could be merged first. Then all suites were re-run at the merge head.",
"Attribution, deviating from the harness reminder per AGENTS.md: commits carry the model-free trailer pair (Claude-Session + Co-authored-by: Claude) instead of the harness's model-named Co-Authored-By, and the PR body ends with AGENTS.md's session-URL footer instead of the harness's two-line footer.",
"No label written on the PR, as the dispatch said (assignee only). The PR carries size/l, which another actor set and I did not touch."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, each one repository_dispatch through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft; PR 22660; read-back 10638 bytes identical), relay run 38042031952; (2) label-write --assign zhuangjianguo, POST /repos//issues/22660/assignees, relay run 38042063185, read back as matching; (3) this os-dev-report, POST /repos//issues/22560/comments via post-stamped.mjs. Not REST: git pushes of claude/issue-22560-record-reader-opt-in (the base-reset probe push 83b8b80..1b99388, then each commit and the merge).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the owners of b389e43 (#22515) and ce3d0ad (#22513), or the director seat · noted, not filed. Evidence: pnpm check:platform-checklist exits 1 on origin/main 86da194 with 7 problems. ABSENT SYMBOL anchors in docs/qa/platform-checklist/areas/access-security.json (anonymousFormIntakeOrgScopeRefusal, anonymousFormIntakeReopenRefusal, envWideRawViewRows, all in packages/metadata-protocol/src/protocol.ts) and in areas/attachments-storage.json (canEdit, in packages/services/service-storage/src/attachment-access-hooks.ts), plus that area's SYMBOL ANCHORS LOST 27 vs floor 28. git log -S finds those symbols' removals in b389e43 and ce3d0ad. Not a class a/b/c finding (no public door; a repo gate's corpus drift). Any PR whose paths schedule this gate inherits the red, this one included. Dedupe words: platform-checklist ABSENT SYMBOL, envWideRawViewRows, anonymousFormIntakeReopenRefusal, attachment-access-hooks canEdit, symbol anchor floor 28."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions✅ ACCEPT — PR #22660 at
e0c562f1a9(decision B). Next: the contract review on this headdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T09:42Z · holder of claim6095200466. Report:os-dev-report6096215461. Thread-read: 6096215461.Checked in the diff, not from the report (net diff against the merge base
86da194919: 19 files, +597 / −30):-
Spec:
ObjectCapabilities.approvalsVisibleToReaders,z.boolean().default(false), beside the other opt-in flags. Its.describe()names whattruegrants:- to whom: a caller who can read the record;
- what: the record's requests and full history, read-only;
- where: both doors, on a read that names the record, with the inbox not widened;
- what it withholds: any approval action.
The name follows
apiEnabled's shape, and the PR argues why a bareapprovalswould mislead. -
Plugin, decision B as written:
addRecordReaderVisibleIdsno longer returns early on an empty host set. After the object/record test, it asksrecordReaderTierOn(object), which checks the host's set first, thenthis.engine.getSchema(object)?.enable?.approvalsVisibleToReaders === true.- It fails closed on a missing
getSchema, an unknown object, a throwing lookup, or any value buttrue. getSchemawas measured to return the authored literal, so the strict=== truereads an absent flag as the spec defaultfalse.
- It fails closed on a missing
-
The liveness row is
live, anchored atapproval-service.ts#recordReaderTierOn, with the late-registration pin as its behaviour proof.
Measurements accepted:
- The kernel probe: a flag declared by an object registered in
start()or onkernel:readywidens after boot. Re-registered without the flag, it stops with no restart. - The 8 new pins cover both doors and all three request tables:
- read-only refusals;
can_act: false;- cannot-read controls (
404 RECORD_NOT_FOUND); - the inbox not widened;
- an undeclared object unchanged;
- the host option alone;
- late registration.
- Two ablations, each blob-equal restored: removing the declared read, and putting back the size-0 early return. Each turns the declared, read-only and late-registration pins red.
- Suites: plugin-approvals 70 files / 1011 tests; spec 642 / 19150; platform-objects 69 / 1082. Typechecks are green.
Deviations:
- A safety-check refusal on a stray file outside the repo (
/build-i18n.pid, from a shell slip). The dev did not retry or work around it. The card's work was unaffected, and the file is left for the maintainer. The seat reports it in the round report; ⛔ no seat or dev removes it. - File surface: two
platform-objectstest files outside the claim's literal list. Both are ledger controls forced by the declared form-row and i18n-bundle debt (counts 9→11, 105→107, 11→13, 667→668), with no other edit. Accepted as owed. - A dead
{@link ApprovalService.recordReaderVisibleIds}inside the edited hunk now linksaddRecordReaderVisibleIds. Accepted. - The probe file was deleted, the suites were re-run after the main merge, and the worktree was removed clean. Accepted.
Out-of-scope finding:
check:platform-checklistis red onmain(86da194919), with 7 absent symbol anchors left by #22515 / #22513. That is #22594, closednot_plannedby triage (6095872178) with two re-entry conditions. The seat does not reopen it here. How that red bears on this PR's enqueue is decided at the landing pre-checks, on the settled check-runs.A contract review at
CONTRACT_REVIEW_TIERone0c562f1a9follows.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat order: patch round on PR #22660. Contract review FAIL
6096311080adopted, on its section ② alonedomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T09:54Z · holder of claim6095200466. Thread-read: 6096233264.What failed:
@objectstack/platform-objectspublishes the four regenerated*.metadata-forms.generated.tsbundles (throughMetadataFormsTranslationsinsrc/index.ts), and this PR gives it no changeset entry. The review cites the two closest precedents, each a new Studio form row with translated leaves, and each carries'@objectstack/platform-objects': patchwithClause-②: no:.changeset/21765-object-image-field-form-row.mdand.changeset/21863-action-on-success-outcome-messages-form-rows.md.The one change, and nothing else: add
.changeset/22560-platform-objects-approvals-visible-to-readers-form-row.md:'@objectstack/platform-objects': patch;- one sentence saying the Studio object form's capabilities panel gains the
approvalsVisibleToReadersrow, with its label and help text in en, zh-CN, ja-JP and es-ES; Clause-②: no.
The review holds ① in full and finds no breach in ③. A head that adds this one file and changes nothing else is expected to pass.
Verify:
node scripts/check-empty-changeset.mjs --base origin/mainnode scripts/check-changeset-no-major.mjs --base origin/mainnode scripts/check-changeset-fixed.mjs
No merge of
mainunless GitHub reports a conflict. Push, and report the new head andgit diff --name-only e0c562f1a9 NEW-HEAD. A same-head contract review follows, scoped to this file.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22560,
"status": "done",
"branch": "claude/issue-22560-record-reader-opt-in",
"pr": "#22660",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (subagent: the parent PM session's id, the one Claim 6095200466 names)",
"premise_still_valid": true,
"patch_round": "Contract review FAIL 6096311080, section ② only, under seat order 6096319885.",
"head": "8adbd88eee (was e0c562f)",
"diff_vs_previous_head": "git diff --name-only e0c562f1a9 8adbd88eee→ .changeset/22560-platform-objects-approvals-visible-to-readers-form-row.md (nothing else)",
"summary": "Added the one changeset the review found missing, and changed nothing else. .changeset/22560-platform-objects-approvals-visible-to-readers-form-row.md: '@objectstack/platform-objects': patch, one sentence ('Studio's object form now offers theapprovalsVisibleToReadersrow in its capabilities panel, with its label and help text in the metadata-form translation catalogs foren,zh-CN,ja-JPandes-ES.'), andClause-②: no. The shape follows the precedents, read from history: 21765-object-image-field-form-row.md (added in 2df3d13) and 21863-action-on-success-outcome-messages-form-rows.md (added in 8e35895). The worktree was recreated from the remote branch head e0c562f; no merge of main. After the push GitHub reads mergeable: true (mergeable_state blocked: draft and required checks), so there is no conflict.",
"tests": "No product code or test changed, so no suite is owed this round; the round-2 suite readings stand for every file other than the new changeset.",
"gates": "At 8adbd88:node scripts/check-empty-changeset.mjs --base origin/mainexit 0 (No empty-frontmatter changeset introduced by this diff (3 declaring changeset(s) added);No changeset from the merge base modified or deleted).node scripts/check-changeset-no-major.mjs --base origin/mainexit 0 (This diff introduces no major bump). Its level axis reads NOT APPLICABLE with no pull_request payload, so I re-ran it with--eventset to PR 22660's payload (body, title, head, base read from the PR), exit 0:LEVEL AXIS: this PR declares clause-② yes (widening) … @objectstack/plugin-approvals: minor … accounted for.node scripts/check-changeset-fixed.mjsexit 0 (fixed group is in sync with 69 public workspace packages).pnpm check:nul-bytesexit 0. CI on the new head was not read and not waited on.",
"line_budget": "+8 lines, 1 file, on top of round 2's 627. Still under the 3000-line threshold.",
"files_changed": [
".changeset/22560-platform-objects-approvals-visible-to-readers-form-row.md (new)"
],
"deviations": [
"Beyond the three named checks I ran two more: check-changeset-no-major again with--event(so the clause-② level axis was measured rather than not applicable), and check:nul-bytes (owed on any edit). Neither writes anything.",
"The stray/build-i18n.pidfrom round 2 is untouched, as the seat ordered."
],
"mcp_calls": "0",
"api_writes": "1 REST write: this os-dev-report, POST /repos//issues/22560/comments via scripts/pm/post-stamped.mjs (the relay). Not REST: onegit pushe0c562f..8adbd88. No label or assignee write.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat order: sync round on PR #22660 before the queue.
mainmoved a generated file this PR also regenerates, so landing rule A's fixed sequence appliesdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T10:26Z · holder of claim6095200466. Thread-read: 6096336189.Where it stands:
- Contract review round 2 PASS
6096398459at8adbd88eee. - 35 check-runs: 33 success, 2 skipped, every skip in the roster.
- GitHub reports
clean.
Why not queue as is: since the merge base
86da194919,main(nowee3ae0360d) moved three of the PR's 20 paths:packages/spec/authorable-surface/data.json(+2;merge=os-regen, a generated file);packages/plugins/plugin-approvals/src/approval-service.ts(+70 / −1, PR feat(plugin-approvals): ApprovalService.handleActionPage serves the ADR-0043 action page from a Request (segment 4 of #22438) #22641, plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438 segment 4:ApprovalService.handleActionPage);packages/plugins/plugin-approvals/src/approvals-plugin.ts(+1, the same PR).
Landing rule A re-measures generated drift at enqueue. A non-zero drift takes its fixed sequence. The temporary relaxation covers D3-entry PRs only, and this PR adds none.
The round:
- Merge
origin/mainthroughscripts/pm/os-regen-merge.sh, and regenerate with the repo's tooling. ⛔ Never by hand. - Merge the meaning, not one side (PR feat(plugin-approvals): ApprovalService.handleActionPage serves the ADR-0043 action page from a Request (segment 4 of #22438) #22641 and this PR both edit
approval-service.ts). Confirm thatrecordReaderTierOnand theaddRecordReaderVisibleIdsread are intact, and thathandleActionPageis untouched. Say whether the action page reads the record-reader visible set at all. If it does, report what the declared flag does there; ⛔ change nothing in it. - Verify at the merge head:
- the
@objectstack/plugin-approvalssuite and typecheck; @objectstack/speccheck:generated;check:liveness;- the
platform-objectssuite (the ledger controls); - the changeset gates.
- the
- Push, then report the new head,
git diff --name-only 8adbd88eee NEW-HEAD, and every path the merge resolved by hand.
The seat then checks that the hop explains itself (
unexplainedPathsBetween). If it does, the review carries to the new head; if not, a scoped review follows.Also in the landing act, by the seat: the PR body's "Changesets" bullet names two entries, but the head carries three. The seat corrects the body; it is not a dev write.
Generated by Claude Code
- Contract review round 2 PASS
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22560,
"status": "done",
"branch": "claude/issue-22560-record-reader-opt-in",
"pr": "#22660",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (subagent: the parent PM session's id, the one Claim 6095200466 names)",
"premise_still_valid": true,
"sync_round": "Seat order 6096552527: merge origin/main ee3ae03 before the queue.",
"head": "b275dc8619 (was 8adbd88). Commits: 126726f (the merge, committed by os-regen-merge.sh step 1) and b275dc8 (the regenerated authorable-surface shard).",
"diff_vs_previous_head": "git diff --name-only 8adbd88eee b275dc8619lists 115 paths. That set equalsgit diff --name-only 86da194919 ee3ae0360dexactly (comm both ways empty): every path in the hop is a path main moved, and the hop adds no path of its own. The full list is in hop_paths.",
"resolved_by_hand": "None. The merge was ort with no conflict. The one generated artifact both sides changed, packages/spec/authorable-surface/data.json (merge=os-regen), was not text-merged. The driver kept the branch's side in the merge commit 126726f, silently dropping main's two FileRefusedValue entries. The script's step 2 staged main's side, and pre-commit refused the commit as stale. I regenerated it withpnpm --filter @objectstack/spec gen:schema(under the lock, MERGE_HEAD absent), staged it, readgit diff --cached(+2: data/FileRefusedValue:id and :metadataRefused), and committed it as b275dc8. The result is the union: diff vs main = +data/ObjectCapabilities:approvalsVisibleToReaders only; diff vs 8adbd88 = +the two FileRefusedValue lines only. The other three generated files this branch changed and main did not (object.mdx, authorable-defaults/data.json, liveness/state-counts/object.md) the script kept on the branch side. check:generated confirms them current.",
"merge_meaning": "approval-service.ts and approvals-plugin.ts auto-merged. Proof by delta, comparing -U0 change lines: (branch delta 86da194..8adbd88) == (merged head minus main), and (main delta 86da194..ee3ae03) == (merged head minus 8adbd88), for both files (approval-service.ts 7d34e6ea1a45 / 6631bd5446d1; approvals-plugin.ts fb5a3c49d70c / a4c236e927ac). So recordReaderTierOn, theif (!this.recordReaderTierOn(object)) return;read in addRecordReaderVisibleIds, the ObjectCapabilitiesParsed type import and the option docs are byte-intact (each grep -c = 1; the oldsize === 0) return= 0). handleActionPage, ACTION_PAGE_PATH, actionPageResponse and the action-link-pages import are exactly main's, and the plugin's +1 is main's info line. Nothing in either was edited.",
"action_page_and_the_tier": "handleActionPage does NOT read the record-reader visible set, so the declared flag has no effect there. GET goes through peekActionToken and POST through redeemActionToken; both go through resolveActionToken. That reads sys_approval_token by token hash as SYSTEM_CTX, then getRequest(token.request_id, SYSTEM_CTX). getRequest goes through loadRequest(enforceVisibility=true), then visibleRequestIds, which returns null at its first line because isOverrideActor(SYSTEM_CTX) is true (context.isSystem). addRecordReaderVisibleIds is never reached. POST then calls decide with{...SYSTEM_CTX, userId: person}(isSystem still true). decide goes through decideNode, which reads the row by engine.find as SYSTEM_CTX and authorizes on the pending slate (takenSlot); no visible set. The page's only credential is the token, and the approver it binds is checked against pending_approvers. Nothing changed there.",
"tests": "At b275dc8, all through os-verify-lock (slot issue-22560). Closure buildpnpm --workspace-concurrency=2 --filter \"@objectstack/plugin-approvals^...\" build: VERDICT command-exit 0 (held 236s), and the tree stayed clean afterwards. plugin-approvalsvitest run:Test Files 71 passed (71),Tests 1023 passed (1023)(main added handleActionPage tests: 70/1011 before). plugin-approvals typecheck (tsc, scripts, check:test-typecheck): VERDICT command-exit 0. platform-objectsvitest run:Test Files 69 passed (69),Tests 1082 passed (1082)(ledger controls included).",
"gates": "At b275dc8:pnpm --filter @objectstack/spec check:generatedexit 0,✓ All 15 generated artifacts are up to date, against the dist the closure build just produced.check:livenessexit 0:object 54 classified (live 53, planned 1), every governed property classified and every anchor resolving, state-counts current.node scripts/check-empty-changeset.mjs --base origin/mainexit 0 (3 declaring changesets added, none from the base modified).node scripts/check-changeset-no-major.mjs --base origin/main --event PR-22660-payloadexit 0 (no major; LEVEL AXIS: clause-② yes (widening) accounted for by @objectstack/plugin-approvals minor).node scripts/check-changeset-fixed.mjsexit 0 (fixed group in sync with 69).pnpm check:nul-bytesexit 0. Not run, as ordered: check:type-check-debt and any every-package build. After the push GitHub reads mergeable: true, mergeable_state blocked (draft and checks). CI on b275dc8 was not read and not waited on.",
"line_budget": "The PR's own delta is unchanged by the hop (merged head minus main = the branch's delta). The only new branch-authored change is the regenerated shard, which against main adds the one approvalsVisibleToReaders line already counted.",
"deviations": [
"os-regen-merge.sh exited 1 at its step 3 by design (pre-commit refused the commit while the os-regen shard was stale). I followed its printed prescription: regenerate,git add -A, inspect the staged diff, commit. I did not rerun the script.",
"pnpm install --frozen-lockfile was re-run after the merge (main moved pnpm-lock.yaml).",
"Beyond the list I ran check:nul-bytes (owed on any edit), and gave check-changeset-no-major the PR's own event payload so its level axis is measured.",
"The PR body was not touched. The stray/build-i18n.pidfrom round 2 is untouched."
],
"mcp_calls": "0",
"api_writes": "1 REST write: this os-dev-report, POST /repos//issues/22560/comments via scripts/pm/post-stamped.mjs (the relay). Not REST: onegit push8adbd88..b275dc8. No label, assignee or PR-body write.",
"open_questions": [],
"out_of_scope_findings": [],
"hop_paths": [
".changeset/19939-flow-value-slot-date-macros-refused.md",
".changeset/22510-activity-actor-name.md",
".changeset/22565-flow-cel-unbound-root-refused.md",
".changeset/22578-approvals-action-page-member.md",
".changeset/22581-migrate-reads-env-files.md",
".changeset/22593-file-field-hydration-refused.md",
".changeset/22605-spec-list-view-edit-inline-default-on.md",
".changeset/22617-cli-organization-ownership-plan.md",
".changeset/22634-analytics-exposure-gate.md",
".changeset/22634-spec-ledger-analytics-exposure-codes.md",
".claude/skills/pm-dispatch/SKILL.md",
".claude/skills/pm-dispatch/references/decision-analysis.md",
".claude/skills/pm-dispatch/references/lanes/director.md",
".gitattributes",
".gitignore",
"content/docs/automation/approvals.mdx",
"content/docs/automation/flows.mdx",
"content/docs/deployment/cli.mdx",
"content/docs/permissions/attachments-access.mdx",
"content/docs/protocol/objectql/types.mdx",
"content/docs/references/automation/builtin-node-config.mdx",
"content/docs/references/data/field-value.mdx",
"content/docs/references/index.mdx",
"content/docs/references/ui/page.mdx",
"content/docs/references/ui/view.mdx",
"content/docs/ui/forms.mdx",
"docs/adr/0043-actionable-approval-links.md",
"docs/adr/0131-total-organization-ownership-no-null-organization-id.md",
"docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md",
"docs/audits/2026-07-unknown-key-strictness-ledger.md",
"docs/qa/platform-checklist/areas/approvals.json",
"packages/cli/src/commands/migrate/apply.ts",
"packages/cli/src/commands/migrate/organization-ownership.ts",
"packages/cli/src/commands/migrate/plan.reads-env-files.integration.test.ts",
"packages/cli/src/commands/migrate/plan.ts",
"packages/cli/src/commands/migrate/security-catalog-overlays.ts",
"packages/cli/src/utils/migrate-occupancy-gate.ts",
"packages/cli/src/utils/organization-ownership-inventory.test.ts",
"packages/cli/src/utils/organization-ownership-inventory.ts",
"packages/cli/src/utils/organization-ownership-plan.integration.test.ts",
"packages/cli/src/utils/organization-ownership-plan.ts",
"packages/cli/src/utils/schema-migrate.database-source.test.ts",
"packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts",
"packages/cli/src/utils/schema-migrate.ts",
"packages/cli/src/utils/schema-migration-plugins.test.ts",
"packages/cli/src/utils/schema-migration-plugins.ts",
"packages/lint/src/flow-cel-root-scope.ts",
"packages/lint/src/lint-flow-patterns.test.ts",
"packages/lint/src/lint-flow-patterns.ts",
"packages/lint/src/validate-expressions.fields-value-slot.test.ts",
"packages/lint/src/validate-expressions.flow-cel-root.test.ts",
"packages/lint/src/validate-expressions.test.ts",
"packages/lint/src/validate-expressions.ts",
"packages/objectql/src/engine-file-hydrate-refused.test.ts",
"packages/objectql/src/engine.ts",
"packages/plugins/plugin-approvals/package.json",
"packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approvals-plugin.ts",
"packages/plugins/plugin-audit/src/activity-actor-name.test.ts",
"packages/plugins/plugin-audit/src/audit-writers.ts",
"packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts",
"packages/services/service-analytics/src/tests/api-exposure-door.test.ts",
"packages/services/service-analytics/src/analytics-service.ts",
"packages/services/service-analytics/src/api-exposure-door.ts",
"packages/services/service-analytics/src/plugin.ts",
"packages/services/service-automation/README.md",
"packages/services/service-automation/src/builtin/assignment-value-envelope.test.ts",
"packages/services/service-automation/src/builtin/crud-fields-value-envelope.test.ts",
"packages/services/service-automation/src/builtin/crud-nodes.ts",
"packages/services/service-automation/src/builtin/logic-nodes.test.ts",
"packages/services/service-automation/src/builtin/logic-nodes.ts",
"packages/services/service-automation/src/builtin/template.ts",
"packages/services/service-automation/src/builtin/value-slot-template-grammar.test.ts",
"packages/spec/api-surface/data.json",
"packages/spec/authorable-defaults/ui.json",
"packages/spec/authorable-surface/data.json",
"packages/spec/declaration-map/data.json",
"packages/spec/export-origins/data.json",
"packages/spec/json-schema.manifest/data.json",
"packages/spec/scripts/build-spec-changes.ts",
"packages/spec/scripts/check-generated.ts",
"packages/spec/scripts/lib/default-changes.ts",
"packages/spec/scripts/lib/projection-cli.ts",
"packages/spec/scripts/projection-cli.test.ts",
"packages/spec/scripts/strictness-ledger.test.ts",
"packages/spec/spec-changes.json",
"packages/spec/src/api/error-code-ledger.zod.ts",
"packages/spec/src/automation/builtin-node-config.test.ts",
"packages/spec/src/automation/builtin-node-config.zod.ts",
"packages/spec/src/automation/flow-template-token.ts",
"packages/spec/src/automation/flow-text-slot-template.test.ts",
"packages/spec/src/automation/flow-text-slot-template.ts",
"packages/spec/src/automation/flow-value-slot-template.test.ts",
"packages/spec/src/automation/flow-value-slot-template.ts",
"packages/spec/src/data/field-value.test.ts",
"packages/spec/src/data/field-value.zod.ts",
"packages/spec/src/migrations/entries/README.md",
"packages/spec/src/migrations/entries/semantic/18.flow-cel-unbound-root-refused.ts",
"packages/spec/src/migrations/entries/semantic/18.flow-text-slot-single-brace-refused.ts",
"packages/spec/src/migrations/entries/semantic/18.flow-value-slot-template-dialect-refused.ts",
"packages/spec/src/migrations/entries/semantic/18.list-view-edit-inline-default-on.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/type-alias-convention.pin.test.ts",
"packages/spec/src/ui/view.test.ts",
"packages/spec/src/ui/view.zod.ts",
"pnpm-lock.yaml",
"scripts/check-adr-0087-registration.mjs",
"scripts/check-future-spec-major.mjs",
"scripts/check-regen-pending.mjs",
"scripts/git-merge-regen.mjs",
"scripts/objectui-changeset-digest.mjs",
"scripts/pm/os-regen-merge.sh",
"scripts/regen-artifacts.mjs",
"skills/objectstack-automation/references/state-machines-and-approvals.md"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22660 →
a00cf9922d(Fixes #22560). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T11:37Z · holder of claim6095200466.- Landed: through the merge queue at 2026-10-10T11:36Z as
a00cf9922d, a squash with one parent,317cddd40d. The queue did not eject it. - The review chain:
- the seat's decision B
6094828840and the ACCEPT6096233264; - contract review round 1 FAIL
6096311080(② alone), the patch order6096319885, and round 2 PASS6096398459; - the sync order
6096552527and round 3 PASS6096719911atb275dc8619; - the pre-queue record
6096865938.
- the seat's decision B
- Content check against the reviewed head
b275dc8619:- 19 of the PR's 20 paths are blob-equal.
content/docs/references/data/object.mdxmoved onmainafter the merge base.git merge-fileof the queue parent, the merge baseee3ae0360dand the reviewed head merges clean and equals the landed blob.
- What now holds (【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652's ruling, per-object, default OFF):
enable.approvalsVisibleToReaders: trueon an object lets a caller who can read one of its records see that record's approval requests and full history, read-only. It holds on the approvals API and the generic data API, on a read that names the record, with no approval action and the inbox not widened.- The flag is read from the live registry on every call. An object registered after boot takes effect, and removing the flag turns the tier off without a restart. The host's constructor option still works alone.
- The Studio object form offers the row, with en / zh-CN / ja-JP / es-ES leaves.
- Carried: the stray
/build-i18n.pidoutside the repository, from a dev's shell slip. Its removal was refused to the dev by the environment's safety check and is left with the maintainer. - Mis-close scan: the squash message carries
Fixes #22560alone, and the merge closed plugin-approvals: the ruled record-reader visibility tier (#8652) is reachable only through a constructor option no app can set — declare its opt-in where an app can #22560 alone.
This act removes
pm:dispatched; the domain, priority, target and area labels stay.
Generated by Claude Code
- Landed: through the merge queue at 2026-10-10T11:36Z as
Blocked-by: #22607
Unblocks: #22559
Filing gate: ③ executing a maintainer ruling that is unreachable for its consumers. Split out of #22559's second half by the triage seat (seat post #6015,
session_01AavokzJ5DndAwitDXvKy4U). ⛔ Not a claim.The ruling, and why no app can use it
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."main, the switch isApprovalsPluginOptions.recordReaderVisibleObjects(approval-service.ts:1235, read at:1290). It is a constructor option only.serveconstructsApprovalsServicePluginwith no options, as plugin-approvals:sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 measured on 17.7.0 and onmain4638625. So no app metadata reaches the switch, and the ruled opt-in cannot be exercised by a config-driven app. hotclm's contract page is the measured consumer.What to build
recordReaderVisibleObjectsset the constructor option feeds.Clause-②: yes (widening). A new declarable key, on the v18 line.Order
#22559, the data-door visibility parity, lands first or alongside. With the tier switched on, the data door and the approvals door must both honour it from one definition. Otherwise turning it on widens one door and not the other.
Acceptance
Dedupe: search over objectstack for a declarative
recordReaderVisibleObjectsswitch found only #22559, which this card splits.