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-security: the delegate business-unit read (#29029) is not reflected by canReadObject / security explain, and its aggregate arm has no pin #30758
Follow-up from the contract review of PR #30747 (card #29029, record 6673498665), should-fix items 1 and 2. Neither blocks that PR. ⛔ Not a claim.
PR #30747 adds delegatedUnitReadScope in packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts. When checkObjectPermission refuses a read of sys_business_unit, a delegated administrator may still read the units their held scopes cover, on their own behalf, in their own organization. This is per ruling 6663849489.
canReadObject and security/explain do not reflect that read. A picker or an explain answer may deny a business unit the delegate can in fact read and name. Expected: both answers say what the read door does.
The aggregate arm has no pin.delegatedUnitReadScope admits find, findOne, count and aggregate. The posture pins cover the first three only. Expected: a pin for aggregate, inside the scope and outside it, with the field guard of step 2.5b still applied.
Carried notes, no separate card asked:
ADR-0090 D12 carries no note of the ruled read. Add it on the next ADR-0090 touch.
The comment on reference_not_found in packages/spec/src/api/errors.zod.ts still says "does not exist". Fix it on the next spec touch.
Suggested grade: p3, domain:server. Serial: after PR #30747 lands.
Claim: PM loop round 1 · 2026-11-07T12:36Z
Session: session_017ihL2dFb2QdZEHEJ83cR2B
Account: marchtian
Branch: claude/project-thread-wmhsd9
Worktree: objectstack-issue-30758 (the card thread's own checkout)
Domain: domain:server
Seat: domain:server#3
File surface: packages/plugins/plugin-security/** (src and tests) (stop on breach; explain in the report)
Container & model: M, mode:thread (card thread cmsg_01VmuUhXU9SSUzqJYyXSmYSiJ6EQj8p8a3x6XGSv5Nfuvh of this account's project), model: TIER_DEFAULT — dispatch-gates --tier: no path-derived mandate, effort: session default — card-thread effort is set by the maintainer only; the seat's reading is high (M; security-explain surface)
Clause-②: no
Metadata-protocol: none
Responsibility: plugin-security's canReadObject and security/explain (objectstack, packages/plugins/plugin-security/src/) do not consult delegatedUnitReadScope | the read door already admits the ruled read (ruling 6663849489); only the answers lag | a delegated business-unit administrator using a unit picker or explain; over-refuses, widens nothing
Thread-read: 6674720378
Premise: holds — read at origin/main 5f00374: delegatedUnitReadScope is applied only in security-plugin-authz-middleware.ts; security-plugin-security-service.ts wires canReadObject without it
Serial constraints cleared: no open domain:server claim or open PR of this seat names plugin-security; siblings of this seat touch platform-objects, plugin-audit and plugin-auth only
The dispatch order is the card thread's opening message (files server-seat-3/orders/_standing.md + server-seat-3/orders/30758.md in this account's project); slot granted by the project coordinator.
{
"issue": 30758,
"status": "done",
"branch": "claude/project-thread-wmhsd9",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31142",
"head": "40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd",
"session": "session_017ihL2bzfJoq2gdSaX2sVcL",
"premise_still_valid": true,
"summary": "Premise held at 5f0037449: delegatedUnitReadScope reached main in eb96480dd60c (squash of #31031; #30747 closed unmerged) and was applied only by the authz middleware. The judgment is split out as delegatedUnitReadAdmission and the door's delegatedUnitReadScope now delegates to it; canReadObject (arm 5 fallback), getReadFilter (ANDs the id $in confinement where no set grants find) and security/explain (object_crud grants through it, readFilter carries the confinement, record pass refuses a unit outside the scope) all ask that one judgment. Object-level 'some rows' follows the plugin's precedent: canReadObject true + getReadFilter scope. getReadFilter had to change too: flipping canReadObject alone measured a bypassing door reading bu_hq. The door itself is unchanged.",
"before_after": "identical in single/group/isolated: door find/findOne/count/aggregate = bu_east,bu_east_field (unchanged); canReadObject false -> true; units under getReadFilter all three -> bu_east,bu_east_field; explain read allowed false/object_crud denies -> true/grants; units under explain readFilter all three -> covered two; explain record bu_east not visible -> visible, bu_hq not visible -> not visible (also when the org admin explains the delegate)",
"tests": "New top-level describe in delegated-admin-own-name-set.posture.test.ts (8 cases x 3 postures): 57/57 pass at 40c2c8aba3. Before the fix 9 of them failed (canReadObject, explain read, explain record) while aggregate pin and controls already passed. pnpm --filter @objectstack/plugin-security test :: exit 0 (248 files, 5820 passed, 45 skipped) before and after merging main. Ablations via scripts/ablation-replace.mjs, restored by hash + empty git diff HEAD, plugin imported from src (no build between): A1 getReadFilter scope dropped -> 3 red (bu_hq read); A2 explain filterParts -> 3 red; A3 explain record check -> 3 red; A4 middleware extra.push(delegatedUnitScope) dropped -> 15 red incl. aggregate ['east:1','east_field:1','hq:1']; A5 step 2.5b renamed away -> 3 red (forbiddenFields undefined); A6 canReadObject delegatorSets->null -> 3 red; A6b explain delSets->null -> 3 red; A7 explain asks find for every op -> 3 red; A8 null admission treated as admitted -> 6 red. First A6 (delegator without BU read) stayed green because arm 6 refuses it; control now names a delegator who reads every unit (7792e807).",
"gates": "dispatch-gates --commands: 64 families; all 64 :: exit 0 on 2d9ed8952a; dispatch-gates --ran (exit-coded record) :: exit 0, 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). Earlier head: plugin-teardown-shape --self-test exit 3 (shallow; unshallowed), check:i18n exit 3 (CLI closure built), check:dual-build-cjs-loads exit 3 (full pnpm build 120/120), check:query-options-erasure exit 1 (3 'as any' on query options in new test helpers; typed in 2d9ed8952a) -> all exit 0 on rerun. After merging origin/main (40c2c8aba3, 2 commits outside plugin-security): rebuilt formula/platform-objects/plugin-auth/service-settings; plugin-security typecheck + test :: exit 0; query-options-erasure, type-check-coverage, type-check-debt, engine-double-contract, test-source-alias, cross-package-test-inputs, i18n, nul-bytes, changeset-no-major, empty-changeset, adr-0087-registration :: exit 0. eslint (repo binary, --no-inline-config) on the 7 touched files :: exit 0. CI check-runs: in_progress at report time (draft just opened).",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"line_budget": "+287 / -29 over 8 files (PR #31142)",
"deviations": [
"Merge commit 40c2c8aba3 carries git's default message without the trailer pair (no card ref, no model id; check:commit-card-trailers passed on push).",
"Commit 2d9ed8952a also carries the changeset wording fix (angle brackets replaced) under a test-typing subject."
],
"files_changed": [
"packages/plugins/plugin-security/src/delegated-admin-gate.ts",
"packages/plugins/plugin-security/src/security-plugin-object-admission.ts",
"packages/plugins/plugin-security/src/security-plugin-read-filter.ts",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/plugins/plugin-security/src/explain-engine.ts",
"packages/plugins/plugin-security/src/security-plugin-explain-access.ts",
"packages/plugins/plugin-security/src/delegated-admin-own-name-set.posture.test.ts",
".changeset/30758-delegated-unit-read-answers.md"
],
"mcp_calls": "7 — read only: issue_read x2 (#30758 get, get_comments), list_commits x1, pull_request_read x1 (#31142 get), plus ToolSearch loads; no write tool",
"api_writes": "1 — fleet relay pr_create (repository_dispatch fw-20261107T141945Z-80d821, run 42381078886 success) -> POST /repos/objectstack-ai/objectstack/pulls (#31142, draft, no assignees, no labels); this report comment is the 2nd write (POST /repos/objectstack-ai/objectstack/issues/30758/comments via post-stamped.mjs). git push is not a REST write.",
"open_questions": [
{
"question": "canReadObject is also asked by the engine's ReadableIds composition over a master (ADR-0139). An app object controlled_by_parent with sys_business_unit as master would now let a delegate read that detail's rows under covered units (before: none). Is that within the ruled read?",
"options": [
"A: keep — ADR-0055 makes a detail's read follow its master's; no platform object is such a detail",
"B: carve the ReadableIds asker out (a second admission answer for master composition), which would make canReadObject disagree with the master's own read door"
],
"recommendation": "A, because the detail door is defined by its master's read and B reintroduces a second answer; seat to confirm"
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: #30758 card (carried notes) · ADR-0090 D12 note of the ruled read and the reference_not_found comment in packages/spec/src/api/errors.zod.ts — untouched per order"
]
}
MERGED 2026-11-07T15:33:24Z through the merge queue as 449c858ae7, head 40c2c8aba3 (requeued once after a Last-Modified second-boundary red in runtime, 6708725260)
Activity
objectstack-fleet commentedon Nov 6, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p3 ·
domain:server· 2026-11-06T01:52Z⛔ Not a claim.
lookupwrite accepts a target the writer cannot read — the reference check probes it elevated, and onlymaster_detailasks the caller's read door #29029):canReadObjectandsecurity/explaindo not reflect the delegated business-unit read, so a picker or an explain answer can deny a unit the read door allows. Theaggregatearm has no pin. It over-refuses and widens nothing. p3.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B
Claim: PM loop round 1 · 2026-11-07T12:36Z
Session:
session_017ihL2dFb2QdZEHEJ83cR2BAccount:
marchtianBranch:
claude/project-thread-wmhsd9Worktree:
objectstack-issue-30758(the card thread's own checkout)Domain:
domain:serverSeat:
domain:server#3File surface:
packages/plugins/plugin-security/**(src and tests) (stop on breach; explain in the report)Container & model:
M,mode:thread(card thread cmsg_01VmuUhXU9SSUzqJYyXSmYSiJ6EQj8p8a3x6XGSv5Nfuvh of this account's project),model: TIER_DEFAULT — dispatch-gates --tier: no path-derived mandate,effort: session default — card-thread effort is set by the maintainer only; the seat's reading is high (M; security-explain surface)Clause-②: no
Metadata-protocol: none
Responsibility: plugin-security's
canReadObjectandsecurity/explain(objectstack,packages/plugins/plugin-security/src/) do not consultdelegatedUnitReadScope| the read door already admits the ruled read (ruling 6663849489); only the answers lag | a delegated business-unit administrator using a unit picker or explain; over-refuses, widens nothingThread-read: 6674720378
Premise: holds — read at origin/main 5f00374: delegatedUnitReadScope is applied only in security-plugin-authz-middleware.ts; security-plugin-security-service.ts wires canReadObject without it
Serial constraints cleared: no open
domain:serverclaim or open PR of this seat names plugin-security; siblings of this seat touch platform-objects, plugin-audit and plugin-auth onlyThe dispatch order is the card thread's opening message (files
server-seat-3/orders/_standing.md+server-seat-3/orders/30758.mdin this account's project); slot granted by the project coordinator.objectstack-fleet commentedon Nov 7, 2026
domain:server#3 · marchtian · session_017ihL2bzfJoq2gdSaX2sVcL
os-dev-report
{ "issue": 30758, "status": "done", "branch": "claude/project-thread-wmhsd9", "pr": "https://github.com/objectstack-ai/objectstack/pull/31142", "head": "40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd", "session": "session_017ihL2bzfJoq2gdSaX2sVcL", "premise_still_valid": true, "summary": "Premise held at 5f0037449: delegatedUnitReadScope reached main in eb96480dd60c (squash of #31031; #30747 closed unmerged) and was applied only by the authz middleware. The judgment is split out as delegatedUnitReadAdmission and the door's delegatedUnitReadScope now delegates to it; canReadObject (arm 5 fallback), getReadFilter (ANDs the id $in confinement where no set grants find) and security/explain (object_crud grants through it, readFilter carries the confinement, record pass refuses a unit outside the scope) all ask that one judgment. Object-level 'some rows' follows the plugin's precedent: canReadObject true + getReadFilter scope. getReadFilter had to change too: flipping canReadObject alone measured a bypassing door reading bu_hq. The door itself is unchanged.", "before_after": "identical in single/group/isolated: door find/findOne/count/aggregate = bu_east,bu_east_field (unchanged); canReadObject false -> true; units under getReadFilter all three -> bu_east,bu_east_field; explain read allowed false/object_crud denies -> true/grants; units under explain readFilter all three -> covered two; explain record bu_east not visible -> visible, bu_hq not visible -> not visible (also when the org admin explains the delegate)", "tests": "New top-level describe in delegated-admin-own-name-set.posture.test.ts (8 cases x 3 postures): 57/57 pass at 40c2c8aba3. Before the fix 9 of them failed (canReadObject, explain read, explain record) while aggregate pin and controls already passed. pnpm --filter @objectstack/plugin-security test :: exit 0 (248 files, 5820 passed, 45 skipped) before and after merging main. Ablations via scripts/ablation-replace.mjs, restored by hash + empty git diff HEAD, plugin imported from src (no build between): A1 getReadFilter scope dropped -> 3 red (bu_hq read); A2 explain filterParts -> 3 red; A3 explain record check -> 3 red; A4 middleware extra.push(delegatedUnitScope) dropped -> 15 red incl. aggregate ['east:1','east_field:1','hq:1']; A5 step 2.5b renamed away -> 3 red (forbiddenFields undefined); A6 canReadObject delegatorSets->null -> 3 red; A6b explain delSets->null -> 3 red; A7 explain asks find for every op -> 3 red; A8 null admission treated as admitted -> 6 red. First A6 (delegator without BU read) stayed green because arm 6 refuses it; control now names a delegator who reads every unit (7792e807).", "gates": "dispatch-gates --commands: 64 families; all 64 :: exit 0 on 2d9ed8952a; dispatch-gates --ran (exit-coded record) :: exit 0, 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). Earlier head: plugin-teardown-shape --self-test exit 3 (shallow; unshallowed), check:i18n exit 3 (CLI closure built), check:dual-build-cjs-loads exit 3 (full pnpm build 120/120), check:query-options-erasure exit 1 (3 'as any' on query options in new test helpers; typed in 2d9ed8952a) -> all exit 0 on rerun. After merging origin/main (40c2c8aba3, 2 commits outside plugin-security): rebuilt formula/platform-objects/plugin-auth/service-settings; plugin-security typecheck + test :: exit 0; query-options-erasure, type-check-coverage, type-check-debt, engine-double-contract, test-source-alias, cross-package-test-inputs, i18n, nul-bytes, changeset-no-major, empty-changeset, adr-0087-registration :: exit 0. eslint (repo binary, --no-inline-config) on the 7 touched files :: exit 0. CI check-runs: in_progress at report time (draft just opened).", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "line_budget": "+287 / -29 over 8 files (PR #31142)", "deviations": [ "Merge commit 40c2c8aba3 carries git's default message without the trailer pair (no card ref, no model id; check:commit-card-trailers passed on push).", "Commit 2d9ed8952a also carries the changeset wording fix (angle brackets replaced) under a test-typing subject." ], "files_changed": [ "packages/plugins/plugin-security/src/delegated-admin-gate.ts", "packages/plugins/plugin-security/src/security-plugin-object-admission.ts", "packages/plugins/plugin-security/src/security-plugin-read-filter.ts", "packages/plugins/plugin-security/src/security-plugin.ts", "packages/plugins/plugin-security/src/explain-engine.ts", "packages/plugins/plugin-security/src/security-plugin-explain-access.ts", "packages/plugins/plugin-security/src/delegated-admin-own-name-set.posture.test.ts", ".changeset/30758-delegated-unit-read-answers.md" ], "mcp_calls": "7 — read only: issue_read x2 (#30758 get, get_comments), list_commits x1, pull_request_read x1 (#31142 get), plus ToolSearch loads; no write tool", "api_writes": "1 — fleet relay pr_create (repository_dispatch fw-20261107T141945Z-80d821, run 42381078886 success) -> POST /repos/objectstack-ai/objectstack/pulls (#31142, draft, no assignees, no labels); this report comment is the 2nd write (POST /repos/objectstack-ai/objectstack/issues/30758/comments via post-stamped.mjs). git push is not a REST write.", "open_questions": [ { "question": "canReadObject is also asked by the engine's ReadableIds composition over a master (ADR-0139). An app object controlled_by_parent with sys_business_unit as master would now let a delegate read that detail's rows under covered units (before: none). Is that within the ruled read?", "options": [ "A: keep — ADR-0055 makes a detail's read follow its master's; no platform object is such a detail", "B: carve the ReadableIds asker out (a second admission answer for master composition), which would make canReadObject disagree with the master's own read door" ], "recommendation": "A, because the detail door is defined by its master's read and B reintroduces a second answer; seat to confirm" } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: #30758 card (carried notes) · ADR-0090 D12 note of the ruled read and the reference_not_found comment in packages/spec/src/api/errors.zod.ts — untouched per order" ] }Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B
Landed · 2026-11-07T15:35Z
449c858ae7, head40c2c8aba3(requeued once after aLast-Modifiedsecond-boundary red in runtime, 6708725260)Fixes #30758State:
pm:dispatchedremoved.