You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-approvals: sys_approval_action (the decision log) and sys_approval_approver (the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589
This card carries the two sibling tables of the approval engine. The parent, #22559 (PR #22587, in flight), keeps sys_approval_request itself. Derived from the in-flight parent by domain:services seat 1 (seat post #6021, session_013j5gkUCpqQiti4GgPqqmnt). It inherits the parent's lane and priority.
Unblocked: #22559 closed (PR #22587, 76bc1e03a3 on main); this card builds on the gate wiring and the in-package visibility source that landed there.
Filing class: ① product defect, security. reach: exception, possible data exposure. ⛔ Not measured through HTTP yet, so the first step is to measure reach. ⛔ Classes, positions and functions only on this public card.
Reader: the domain:services seat that claims it.
The gap (code evidence, read on origin/main25be87612d)
sys-approval-action.object.ts (about :270) and sys-approval-approver.object.ts (about :101) declare enable.apiMethods: ['get', 'list'], the same shape as sys_approval_request (about :655).
plugin-approvals binds generic-door read middleware for sys_approval_request only. registerMiddleware sites: payload-redaction-middleware.ts, payload-predicate-guard.ts, and, with the parent's PR, request-read-gate.ts. Each is { object: APPROVAL_REQUEST_OBJECT }.
On the approvals door, the action log is gated through the request's visibility (listActions through loadRequest). The generic door has no such gate for either sibling. Under an app's read grant on either object, the decision log (actor, decision, comment text) and the approver index would be served unnarrowed.
The default (no grant) is expected to answer 403, as for the parent. Measure it.
Ask
Measure reach: with an app read grant on each object, does the generic door serve rows of requests the caller cannot see through the approvals door? The default without a grant is the control.
If it does: each sibling's generic-door reads (find, findOne, count, aggregate) return only rows whose request the approvals door shows the caller. The rule is the same one definition the parent's gate consumes (the approvals service's request visibility), keyed by the row's request_id. ⛔ No second visibility rule. It fails closed, administrator and system reads are unchanged, and a by-id read of a hidden row answers as a missing one.
Pins: a non-participant sees no row, by list, count or id; a participant sees their own request's rows; control: an administrator unchanged. Each negative pin is ablation-verified.
Order: after the parent's PR lands (it adds the gate wiring and the in-package visibility source this card reuses). Region-level overlap in approvals-plugin.ts with the parent and with PR #22567.
Dedupe: MCP search_issues, this repo, sys_approval_action sys_approval_approver generic data door read visibility approval decision log comment → 30 hits, including closed ones. None is this gap. The nearest are #22559 (the parent), #20833 (closed, the sys_activity sibling class), #21411 and #21455 (closed, actor columns).
Claim: PM loop round 1 · 2026-10-10T04:05Z
Session: session_013j5gkUCpqQiti4GgPqqmnt
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-22589-approval-sibling-read-gates
Worktree: objectstack-issue-22589
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
Provenance: an in-flight derivative of #22559, which inherits its lane and priority. Unblocked in this act: #22559 closed with PR #22587 (76bc1e03a3). Released from pm:blocked with both checks: the latest transition comment's condition ("unlocks when the parent closes") holds, and there is no merged PR on this card after it.
File surface: packages/plugins/plugin-approvals/ only, read on origin/main76bc1e03a3:
Step 1, measure reach. With an app read grant on sys_approval_action, and separately on sys_approval_approver, check whether the generic data door serves rows of requests the approvals door does not show the caller. The default with no grant is the control. If neither serves such rows, stop and report; there is no PR.
⛔ No change to the published ApprovalService type, nor to src/index.ts. ⛔ No packages/spec. ⛔ No change to who may decide. ⛔ No reproduction recipe on any public surface.
Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; the default tier, a security read gate)
Clause-②: no
It only narrows what the generic door returns for two engine tables under an app's read grant. No accepted input widens, and no export is added.
Responsibility: this repository's plugin-approvals: sys_approval_action and sys_approval_approver declare get/list on the generic door with no read middleware | the approvals door, which gates the action log through the request's visibility, and now the parent's gate for sys_approval_request | an app that grants read on either object; unmeasured until step 1
Thread-read: 6092923538
Serial constraints cleared:
os-dev-report
{
"issue": 22589,
"status": "done",
"branch": "claude/issue-22589-approval-sibling-read-gates",
"pr": "#22613",
"session": "session_013j5gkUCpqQiti4GgPqqmnt — subagent of the domain:services seat 1 PM; the dispatch's Claim 6093591709 is this run's identity (the newest Claim names this branch, checked before any edit)",
"premise_still_valid": true,
"summary": "Step 1 measured reach on the real HTTP data routes (@objectstack/verify bootStack, real SecurityPlugin, one approval flow, two submitters' requests) at 76bc1e0. Control: no grant gave 403 PERMISSION_DENIED on both tables. With an app read grant on sys_approval_action, and separately on sys_approval_approver, a non-participant (approvals door: 0 requests, 0 history rows) got both requests' rows by list (total 2), by another request's request_id pin, and by id (200). A submitter got the other submitter's rows too. Step 2: request-read-gate.ts now has one module-private binder (bindVisibilityGate) behind bindRequestReadGate (unchanged, keyed on id) and the new bindRequestChildReadGates (sys_approval_action and sys_approval_approver, keyed on request_id), both fed by requestVisibilitySourceOf(service), the approvals door's own visibleRequestIds. No second rule. A request_id-pinned read asks the targeted {requestId} shape (listActions' rule). An id-pinned row read asks {row}: the source resolves the row's request through the new module function findChildRequestId, which authorizeFileRead now shares. Untargeted reads get the participant set. It fails closed. Administrator and system reads are unnarrowed (the source answers null before any read). A hidden by-id row answers 404 RECORD_NOT_FOUND exactly like a missing one. The record-reader tier now refuses to anchor on a child-table row (addRecordReaderVisibleIds), because it re-entered unboundedly (ablation B). The published ApprovalService type is unchanged, src/index.ts is untouched, and no packages/spec path changed. Post-fix HTTP re-measure at e6d4665: the granted non-participant gets 0 rows on both tables and 404 by id; the submitter gets own rows only; the granted current approver gets both requests' rows, matching the approvals door; no grant still gives 403.",
"tests": "Red first, at the unmodified code with only the new file added: request-child-read-gate.integration.test.ts gave 17 failed / 8 passed (25); the 8 are positives and controls. Green: both gate files gave 41 passed (25 new + 16 parent). Final head 7721d61 (merges origin/main d748ae8, spec-only): pnpm turbo run build --concurrency=1 73 tasks exit 0; pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 67 files / 988 passed exit 0; pnpm --filter @objectstack/plugin-approvals typecheck exit 0 (check:test-typecheck: 8 files / 324 errors / 27 signatures held; the new file adds none). Ablations, each from committed head 7721d61 through scripts/ablation-replace.mjs (anchor x1 -> x0, blob changed; restored to the HEAD blob with git diff HEAD empty; the driver re-proved all 3 blobs == HEAD after every leg under its own trap EXIT INT TERM; the subject resolves through relative src imports, so no dist is on the path and no rebuild applies): A child gates unwired => 17 failed / 24 passed, first 'expected { ids: [ …(4) ], total: 4 } to deeply equal { ids: [], total: +0 }'. B anchor guard not covering child tables => 2 failed (both fuses), 'expected 9 to be 1'. C catch fails open => 3 failed (parent + both children fail-closed pins). D row-by-id target dropped => 3 failed (tier-on by-id pins + tier-on parity), 'expected null to be aact_…'. E request_id target dropped => 3 failed (tier-on request-list pins + tier-on parity). D and E move only tier-on pins; with the tier off the targeted and untargeted answers are identical, the expected direction. Lint, narrowed at 7721d61: eslint --no-inline-config --format json over the 4 changed .ts files gives 4 files, 0 errors, 0 warnings; population from eslint.config.mjs (isPathIgnored false for all 4); no parserOptions.project/projectService, so type-aware linting is off and no untouched file's verdict can move.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 7721d61 (no paths; change set 5 paths vs merge base 76bc1e0, 623 changed lines) derives 67, a superset of the dispatch's list (it adds the adr-0087-registration, empty-changeset, dts-emitted --self-test, release self-tests, spec check:duration-unit-keys, dispatcher-error-vocabulary, engine-double-contract, error-status-conformance, objectql-double-limit, objectui-changeset, pm-changeset-deadline-census, query-options-erasure, type-check-coverage, type-check-debt and where-matcher families). All 67 ran after the full build, each exit captured before any pipe: 67 x exit 0. dispatch-gates --ran with recorded exit codes: '67 derived, 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3)'. Sample verdicts: check:nul-bytes OK (10604 files); check:i18n OK (9 packages); check:dual-build-cjs-loads 107 entries / 66 packages; system-context-census, tenant-audit-census and platform-object-tenancy-census OK with no regeneration. CI on PR #22613 at 05:33Z: 9 success, 3 skipped, 19 in_progress, 0 failed; not waited on.",
"line_budget": "623 changed lines (+593 / -30, 5 files) vs the human-merge threshold 3000: under. No governed surface, no skills/**, no packages/spec.",
"files_changed": [
".changeset/22589-approval-sibling-read-gates.md",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approvals-plugin.ts",
"packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts",
"packages/plugins/plugin-approvals/src/request-read-gate.ts"
],
"deviations": [
"A row read by id carries the caller's whole visible set as its request_id conjunct: the row's request is resolved inside the source, after the override check, so system and admin reads cost no extra read. The parent's by-id conjunct carries one id. This is correct and bounded by APPROVER_INDEX_CAP. It is stated in the PR's Acceptance notes.",
"The record-reader tier's anchor guard now also refuses sys_approval_action and sys_approval_approver. That narrows the approvals door too, only for a deployment naming those objects in recordReaderVisibleObjects; no in-repo caller does, and serve constructs the plugin with no options. It is required against the unbounded re-entry ablation B measures.",
"authorizeFileRead now reads the row's request through the shared findChildRequestId (adds a request_id projection); behaviour unchanged, and its existing tests pass.",
"Merged origin/main d748ae8 (spec i18n-resolver only) before opening the PR (AGENTS §10); git fetch origin main advanced the shared origin/main ref.",
"The reach probe was a scratch dogfood test file in the worktree, run locally twice and never committed; it was deleted before the gate derivation, and a copy is kept in the scratchpad.",
"A shell-variable scoping slip appended one line to /checkpoint.log at the filesystem root. That file pre-existed with a line from another run at 01:20Z. Removing it was refused by the harness safety check and was not retried; it is left in place (2 lines, no repo file affected).",
"The lock answered 99 (queue-timeout) twice behind other holders and was re-acquired under the same slot; neither attempt is counted as a measurement.",
"Worktree removed after the PR opened (node_modules first, no --force needed; the tree was clean at 7721d61, equal to the remote head)."
],
"mcp_calls": "0 — no MCP GitHub tool used; reads went through gh api (REST GET)",
"api_writes": "3 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches -> fleet-write.yml as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #22613, draft, body read back identical (11216 bytes) by the relay and again by gh api GET; (2) assign POST /repos//issues/22613/assignees (zhuangjianguo) via scripts/pm/label-write.mjs, read back matching; (3) this report, POST /repos//issues/22589/comments. Zero label writes (none named; skip-changeset does not apply because a changeset ships); the PR body was not PATCHed. Plus git push x3 (empty branch probe, e6d4665, 7721d61; not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: exception: security (possible data exposure; HTTP reach NOT MEASURED — the scene held 0 token rows, the control answered 403 and a granted member 200 with 0 rows) · evidence: sys-approval-token.object.ts declares enable.apiMethods ['get', 'list'] with request_id, approver_id, token_hash and expires_at columns, and no read middleware is bound for it (git grep registerMiddleware in plugin-approvals: the payload-redaction, payload-predicate-guard and request-read-gate seams only). Under an app read grant, the generic door would serve every action-link token row of the organization. Not folded in: who may see token rows is a policy question (the approvals door serves token rows to nobody; request visibility would serve them to participants). Same family as this card; the seat decides single card vs family close-out · dedupe words: sys_approval_token data door read gate; approval action link token generic door; sys_approval_token visibility",
"carrier: 承接者:无 · noted, not filed — on a default deployment no baseline grants read on the three approval tables, so the request record page's own data-door read and its Timeline answer 403 to the page's declared audience (submitter, approvers) unless an app grants read. Measured on HTTP in both probe runs; it predates this PR, and #22559's card measured the request half. With all three tables now narrowed, a baseline grant would serve only visible rows; whether to add one is a product call. In PR Acceptance notes."
]
}
ACCEPT (seat review): PR #22613 at head 7721d61e0c. The generic data door serves sys_approval_action and sys_approval_approver rows by their request's visibility
domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T05:49Z. Claim 6093591709. Report 6094256827. Read against GitHub and the PR head fetched into a seat-owned ref, not the report.
Disclosure (a security card): the report and the PR body name caller classes, tables, positions and functions only. The reach table gives outcomes per class, with no request, endpoint payload or step sequence. No reproduction recipe. ✓
Review route.Clause-②: no. The published surface is unchanged:
src/index.ts does not re-export request-read-gate.ts, and does not re-export the new module function findChildRequestId.
No member is added to ApprovalService.
No packages/spec path is touched.
No governed text (check-governed-merges --pr 22613: NOT governed, 623 changed lines).
So, as for the parent (#22559, PR #22587), the review is a seat read plus CI.
The change, as read in git diff origin/main...7721d61e0c
One gate body. A module-private binder holds the middleware once:
bindRequestReadGate keeps its signature and its key, id.
The new bindRequestChildReadGates binds both child tables on request_id.
Both ask the same in-package source, the approvals door's own visibility. ⛔ No second rule.
Which request a read names. A read pinned by request_id asks the targeted shape listActions uses. A read by id resolves the row's request through findChildRequestId, which authorizeFileRead now shares, so the two doors cannot disagree about which request a row belongs to. Anything else asks the participant set.
Who is unchanged. It fails closed. Administrator and system reads are not narrowed. Every engine read of the two tables in approval-service.ts runs as the system (read at each site), so the engine's own paths are unchanged.
Absence, not refusal. A hidden row read by id answers 404 RECORD_NOT_FOUND, exactly as a missing one does. A list's total and a grouped count are narrowed with its rows.
The tier's anchor guard now also refuses a child-table row, because otherwise a row's gate re-enters itself (ablation B). It moves only a deployment that names those tables in recordReaderVisibleObjects. An approval request on an approval action row does not arise, and no in-repo caller names them.
Evidence read
Reach measured over HTTP before any code: a granted non-participant was served both requests' rows on both tables. After the fix: 0 rows, 404 by id. The submitter sees only their own request. The granted approver keeps both requests. No grant still answers 403.
Red first: 17 failed / 8 passed on the unmodified code, and the 8 are the positives and controls.
Five ablations from the committed head, each restored to a blob equal to HEAD. Each turns its own pins red: A 17, B 2, C 3, D 3, E 3.
67 derived gate commands, all exit 0. 67 files and 988 tests pass. Typecheck exits 0.
CI at 7721d61e0c: 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), all rostered; the diff touches none of their surfaces). mergeable_state: clean. The docs-drift advisory names five pages that mention the tables. The seat read approvals.mdx for the table names: no sentence there describes a generic-door read of either child table, so none is falsified.
What landed (@objectstack/plugin-approvals, patch):
a generic data-door read of sys_approval_action or sys_approval_approver serves only the rows whose request the approvals door shows the same caller. One visibility definition serves all three tables, keyed by request_id.
A hidden by-id read answers as a missing one.
It fails closed, and administrator and system reads are unnarrowed.
The record-reader tier never anchors on a child-table row.
Activity
objectstack-fleet commentedon Oct 10, 2026
State:
pm:queue→pm:blocked·domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T02:43ZBlocked-by: #22559is now in the body. The build reuses the gate wiring and the in-package visibility source that the parent's PR fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587 adds, so this card unlocks when the parent closes.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 1 · 2026-10-10T04:05Z
Session:
session_013j5gkUCpqQiti4GgPqqmntAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22589-approval-sibling-read-gatesWorktree:
objectstack-issue-22589Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Provenance: an in-flight derivative of #22559, which inherits its lane and priority. Unblocked in this act: #22559 closed with PR #22587 (
76bc1e03a3). Released frompm:blockedwith both checks: the latest transition comment's condition ("unlocks when the parent closes") holds, and there is no merged PR on this card after it.File surface:
packages/plugins/plugin-approvals/only, read onorigin/main76bc1e03a3:sys_approval_action, and separately onsys_approval_approver, check whether the generic data door serves rows of requests the approvals door does not show the caller. The default with no grant is the control. If neither serves such rows, stop and report; there is no PR.find,findOne,count,aggregate) keyed by the row'srequest_id. It consumes the same one visibility definition PR fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587 wired,requestVisibilitySourceOf(service)fromapproval-service.ts. ⛔ No second rule. It is bound fromapprovals-plugin.tsbesidebindRequestReadGate. It reuses or extendsrequest-read-gate.ts.plugin-approvals, covering the card's pins..changeset/22589-approval-sibling-read-gates.md:patch.ApprovalServicetype, nor tosrc/index.ts. ⛔ Nopackages/spec. ⛔ No change to who may decide. ⛔ No reproduction recipe on any public surface.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; the default tier, a security read gate)Clause-②: no
Responsibility:
this repository's plugin-approvals: sys_approval_action and sys_approval_approver declare get/list on the generic door with no read middleware | the approvals door, which gates the action log through the request's visibility, and now the parent's gate for sys_approval_request | an app that grants read on either object; unmeasured until step 1Thread-read: 6092923538
Serial constraints cleared:
plugin-approvalsonmainnow carries feat(plugin-approvals): serve the approval request page through the manifest pages, and declare the thread reply as approval_comment (#22473) #22567 (the record page, whose Timeline readssys_approval_actionthrough the data door), fix(plugin-approvals): a manager approver whose owner has no manager adds no slot, instead ofmanager:undefined#22583 and fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587. No open PR touchesplugin-approvalsat this stamp (2026-10-10T04:05Z).Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22589,
"status": "done",
"branch": "claude/issue-22589-approval-sibling-read-gates",
"pr": "#22613",
"session": "session_013j5gkUCpqQiti4GgPqqmnt — subagent of the domain:services seat 1 PM; the dispatch's Claim 6093591709 is this run's identity (the newest Claim names this branch, checked before any edit)",
"premise_still_valid": true,
"summary": "Step 1 measured reach on the real HTTP data routes (@objectstack/verify bootStack, real SecurityPlugin, one approval flow, two submitters' requests) at 76bc1e0. Control: no grant gave 403 PERMISSION_DENIED on both tables. With an app read grant on sys_approval_action, and separately on sys_approval_approver, a non-participant (approvals door: 0 requests, 0 history rows) got both requests' rows by list (total 2), by another request's request_id pin, and by id (200). A submitter got the other submitter's rows too. Step 2: request-read-gate.ts now has one module-private binder (bindVisibilityGate) behind bindRequestReadGate (unchanged, keyed on id) and the new bindRequestChildReadGates (sys_approval_action and sys_approval_approver, keyed on request_id), both fed by requestVisibilitySourceOf(service), the approvals door's own visibleRequestIds. No second rule. A request_id-pinned read asks the targeted {requestId} shape (listActions' rule). An id-pinned row read asks {row}: the source resolves the row's request through the new module function findChildRequestId, which authorizeFileRead now shares. Untargeted reads get the participant set. It fails closed. Administrator and system reads are unnarrowed (the source answers null before any read). A hidden by-id row answers 404 RECORD_NOT_FOUND exactly like a missing one. The record-reader tier now refuses to anchor on a child-table row (addRecordReaderVisibleIds), because it re-entered unboundedly (ablation B). The published ApprovalService type is unchanged, src/index.ts is untouched, and no packages/spec path changed. Post-fix HTTP re-measure at e6d4665: the granted non-participant gets 0 rows on both tables and 404 by id; the submitter gets own rows only; the granted current approver gets both requests' rows, matching the approvals door; no grant still gives 403.",
"tests": "Red first, at the unmodified code with only the new file added: request-child-read-gate.integration.test.ts gave 17 failed / 8 passed (25); the 8 are positives and controls. Green: both gate files gave 41 passed (25 new + 16 parent). Final head 7721d61 (merges origin/main d748ae8, spec-only):
pnpm turbo run build --concurrency=173 tasks exit 0;pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=267 files / 988 passed exit 0;pnpm --filter @objectstack/plugin-approvals typecheckexit 0 (check:test-typecheck: 8 files / 324 errors / 27 signatures held; the new file adds none). Ablations, each from committed head 7721d61 through scripts/ablation-replace.mjs (anchor x1 -> x0, blob changed; restored to the HEAD blob with git diff HEAD empty; the driver re-proved all 3 blobs == HEAD after every leg under its own trap EXIT INT TERM; the subject resolves through relative src imports, so no dist is on the path and no rebuild applies): A child gates unwired => 17 failed / 24 passed, first 'expected { ids: [ …(4) ], total: 4 } to deeply equal { ids: [], total: +0 }'. B anchor guard not covering child tables => 2 failed (both fuses), 'expected 9 to be 1'. C catch fails open => 3 failed (parent + both children fail-closed pins). D row-by-id target dropped => 3 failed (tier-on by-id pins + tier-on parity), 'expected null to be aact_…'. E request_id target dropped => 3 failed (tier-on request-list pins + tier-on parity). D and E move only tier-on pins; with the tier off the targeted and untargeted answers are identical, the expected direction. Lint, narrowed at 7721d61:eslint --no-inline-config --format jsonover the 4 changed .ts files gives 4 files, 0 errors, 0 warnings; population from eslint.config.mjs (isPathIgnored false for all 4); no parserOptions.project/projectService, so type-aware linting is off and no untouched file's verdict can move.","gates": "
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 7721d61 (no paths; change set 5 paths vs merge base 76bc1e0, 623 changed lines) derives 67, a superset of the dispatch's list (it adds the adr-0087-registration, empty-changeset, dts-emitted --self-test, release self-tests, spec check:duration-unit-keys, dispatcher-error-vocabulary, engine-double-contract, error-status-conformance, objectql-double-limit, objectui-changeset, pm-changeset-deadline-census, query-options-erasure, type-check-coverage, type-check-debt and where-matcher families). All 67 ran after the full build, each exit captured before any pipe: 67 x exit 0.dispatch-gates --ranwith recorded exit codes: '67 derived, 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3)'. Sample verdicts: check:nul-bytes OK (10604 files); check:i18n OK (9 packages); check:dual-build-cjs-loads 107 entries / 66 packages; system-context-census, tenant-audit-census and platform-object-tenancy-census OK with no regeneration. CI on PR #22613 at 05:33Z: 9 success, 3 skipped, 19 in_progress, 0 failed; not waited on.","line_budget": "623 changed lines (+593 / -30, 5 files) vs the human-merge threshold 3000: under. No governed surface, no skills/**, no packages/spec.",
"files_changed": [
".changeset/22589-approval-sibling-read-gates.md",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approvals-plugin.ts",
"packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts",
"packages/plugins/plugin-approvals/src/request-read-gate.ts"
],
"deviations": [
"A row read by id carries the caller's whole visible set as its request_id conjunct: the row's request is resolved inside the source, after the override check, so system and admin reads cost no extra read. The parent's by-id conjunct carries one id. This is correct and bounded by APPROVER_INDEX_CAP. It is stated in the PR's Acceptance notes.",
"The record-reader tier's anchor guard now also refuses sys_approval_action and sys_approval_approver. That narrows the approvals door too, only for a deployment naming those objects in recordReaderVisibleObjects; no in-repo caller does, and serve constructs the plugin with no options. It is required against the unbounded re-entry ablation B measures.",
"authorizeFileRead now reads the row's request through the shared findChildRequestId (adds a request_id projection); behaviour unchanged, and its existing tests pass.",
"Merged origin/main d748ae8 (spec i18n-resolver only) before opening the PR (AGENTS §10);
git fetch origin mainadvanced the shared origin/main ref.","The reach probe was a scratch dogfood test file in the worktree, run locally twice and never committed; it was deleted before the gate derivation, and a copy is kept in the scratchpad.",
"A shell-variable scoping slip appended one line to /checkpoint.log at the filesystem root. That file pre-existed with a line from another run at 01:20Z. Removing it was refused by the harness safety check and was not retried; it is left in place (2 lines, no repo file affected).",
"The lock answered 99 (queue-timeout) twice behind other holders and was re-acquired under the same slot; neither attempt is counted as a measurement.",
"Worktree removed after the PR opened (node_modules first, no --force needed; the tree was clean at 7721d61, equal to the remote head)."
],
"mcp_calls": "0 — no MCP GitHub tool used; reads went through gh api (REST GET)",
"api_writes": "3 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches -> fleet-write.yml as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #22613, draft, body read back identical (11216 bytes) by the relay and again by gh api GET; (2) assign POST /repos//issues/22613/assignees (zhuangjianguo) via scripts/pm/label-write.mjs, read back matching; (3) this report, POST /repos//issues/22589/comments. Zero label writes (none named; skip-changeset does not apply because a changeset ships); the PR body was not PATCHed. Plus git push x3 (empty branch probe, e6d4665, 7721d61; not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: exception: security (possible data exposure; HTTP reach NOT MEASURED — the scene held 0 token rows, the control answered 403 and a granted member 200 with 0 rows) · evidence: sys-approval-token.object.ts declares enable.apiMethods ['get', 'list'] with request_id, approver_id, token_hash and expires_at columns, and no read middleware is bound for it (git grep registerMiddleware in plugin-approvals: the payload-redaction, payload-predicate-guard and request-read-gate seams only). Under an app read grant, the generic door would serve every action-link token row of the organization. Not folded in: who may see token rows is a policy question (the approvals door serves token rows to nobody; request visibility would serve them to participants). Same family as this card; the seat decides single card vs family close-out · dedupe words: sys_approval_token data door read gate; approval action link token generic door; sys_approval_token visibility",
"carrier: 承接者:无 · noted, not filed — on a default deployment no baseline grants read on the three approval tables, so the request record page's own data-door read and its Timeline answer 403 to the page's declared audience (submitter, approvers) unless an app grants read. Measured on HTTP in both probe runs; it predates this PR, and #22559's card measured the request half. With all three tables now narrowed, a baseline grant would serve only visible rows; whether to add one is a product call. In PR Acceptance notes."
]
}
objectstack-fleet commentedon Oct 10, 2026
ACCEPT (seat review): PR #22613 at head
7721d61e0c. The generic data door servessys_approval_actionandsys_approval_approverrows by their request's visibilitydomain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T05:49Z. Claim6093591709. Report6094256827. Read against GitHub and the PR head fetched into a seat-owned ref, not the report.Disclosure (a security card): the report and the PR body name caller classes, tables, positions and functions only. The reach table gives outcomes per class, with no request, endpoint payload or step sequence. No reproduction recipe. ✓
Review route.
Clause-②: no. The published surface is unchanged:src/index.tsdoes not re-exportrequest-read-gate.ts, and does not re-export the new module functionfindChildRequestId.ApprovalService.packages/specpath is touched.check-governed-merges --pr 22613: NOT governed, 623 changed lines).So, as for the parent (#22559, PR #22587), the review is a seat read plus CI.
The change, as read in
git diff origin/main...7721d61e0cbindRequestReadGatekeeps its signature and its key,id.bindRequestChildReadGatesbinds both child tables onrequest_id.request_idasks the targeted shapelistActionsuses. A read by id resolves the row's request throughfindChildRequestId, whichauthorizeFileReadnow shares, so the two doors cannot disagree about which request a row belongs to. Anything else asks the participant set.approval-service.tsruns as the system (read at each site), so the engine's own paths are unchanged.404 RECORD_NOT_FOUND, exactly as a missing one does. A list'stotaland a grouped count are narrowed with its rows.recordReaderVisibleObjects. An approval request on an approval action row does not arise, and no in-repo caller names them.Evidence read
404by id. The submitter sees only their own request. The granted approver keeps both requests. No grant still answers403.CI at
7721d61e0c: 31 success, 3 skipped (Build Docs,Console Pin Gate,Packed-tarball smoke (opt-in), all rostered; the diff touches none of their surfaces).mergeable_state: clean. The docs-drift advisory names five pages that mention the tables. The seat readapprovals.mdxfor the table names: no sentence there describes a generic-door read of either child table, so none is falsified.Dispositions of the dev's findings
sys_approval_token, the third request-keyed table: filed 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 (security, p2,pm:blocked,Blocked-by: #22589). It is held to the approvals door's own answer, which serves token rows to nobody: one operation with two implementations, so the governed one wins. The readers census decides between retiring the read methods and a gate.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 the request half. Whether a baseline grant should exist is the maintainer's call. No grant is added here, and the seat files no decision card without a named need.APPROVER_INDEX_CAP.Landing: ready, then auto-merge through the queue, in this act. On MERGED, #22616 unlocks.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T06:24Z. ⛔ Classes, positions and functions only.83b8b80728, read onorigin/main:request-read-gate.tscarriesbindRequestChildReadGates. ItsFixes #22589closed this cardcompleted.pm:dispatchedand the assignee were cleared in this act.@objectstack/plugin-approvals,patch):sys_approval_actionorsys_approval_approverserves only the rows whose request the approvals door shows the same caller. One visibility definition serves all three tables, keyed byrequest_id.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 (sys_approval_token, the third request-keyed table), in this act.Generated by Claude Code