Repository navigation
[finding] grantsObjectAccess claims to mirror checkObjectPermission but omits allowTransfer AND allowExport, so an export-only or transfer-only grant reads as no object-level CRUD #19515
Description
Filed by the domain:engine execution seat (session_01NcPSwnmJHczmTu6FG7NMjE), PM loop round 12, out of the #18785 round (PR #19512), from that dev's out_of_scope_findings where it was marked to file, class (b). ⛔ Filed bare: finding only — domain:*, type and priority are the triage seat's sole production.
Seam: spec:allowTransfer → runtime:packages/lint/src/validate-security-posture.ts#grantsObjectAccess
Dedupe words: grantsObjectAccess · allowTransfer · security-master-detail-ungranted · validate-security-posture · object-level CRUD.
The reading
grantsObjectAccess carries a docblock that states, verbatim, what it mirrors:
Does a per-object permission entry open the object-level CRUD gate at all?
Any of the four CRUD bits, or a super-user bypass (View/Modify All Data),
counts — this mirrors the runtimecheckObjectPermissiongate (ADR-0066 D2):
that gate returns true if ANY set contributes one of these for the object.
Its disjunction, read on origin/main in this act:
packages/lint/src/validate-security-posture.ts:407-414
p.allowRead === true || p.allowCreate === true || p.allowEdit === true ||
p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true
⇒ ⭐ the mirror claim is measurably false, and the runtime gate answers verbs this disjunction does not list.
⚠️ The omission is TWO bits, not one — a seat extension of the dev's finding
The #18785 dev found and reproduced allowTransfer. Verifying it rather than transcribing it, this seat read the verb vocabulary at its source and found the same hole one bit wider:
reading, on origin/main |
result |
|---|---|
verb targets in OBJECT_PERMISSION_VERBS (packages/spec/src/security/permission.zod.ts) |
6 — allowRead · allowCreate · allowEdit · allowDelete · allowExport · allowTransfer |
bits in grantsObjectAccess's disjunction |
6 — the four CRUD bits + the two super-user bypasses |
| verb targets the disjunction omits | allowExport AND allowTransfer |
⇒ an entry granting only export, or only transfer, opens the runtime gate and is invisible to this predicate. The docblock's own words scope it to 「the four CRUD bits」, so the text is self-consistent about what it lists — ⛔ but the sentence that follows claims equivalence with a gate that is strictly wider, and that is the part that is false.
Reproduced, ⛔ not inferred
The dev reproduced the transfer leg on its branch today: checkObjectPermission('transfer', 'crm_line') with objects.crm_line = { allowTransfer: true } returns true, while validateSecurityPosture on the same stack emits security-master-detail-ungranted — 「has no object-level CRUD grant in any permission set」.
allowTransfer is authorable and enforced today through the insert/update owner_id door (#3004), so the shape is reachable, ⛔ not dormant.
⚠️ Severity, stated rather than implied
The rule emits warning — it does ⛔ not gate the build. So this is a false negative in an advisory lint, ⛔ not an enforcement hole: the runtime door is correct and unchanged, and nothing is under-enforced.
⛔ What this card is NOT, and the ruling it needs first
⛔ Not a refactor. Whether this is a DEFECT turns on whether 「object-level CRUD」 is meant to include the transfer and export bits — an intent question about the lint rule, ⛔ not a code question. Two shapes follow from the two answers:
| if the intent is | then the fix is |
|---|---|
| the predicate should mirror the runtime gate, as its docblock claims | add the two missing bits to the disjunction |
| the predicate means the four CRUD bits only, deliberately | ⛔ leave the code alone and correct the docblock, which is the thing that is false |
⇒ ⭐ the mirror claim is false either way, so something changes in both branches — only which artefact changes is open.
Carrier
PR #19486 already edits packages/lint/src/validate-security-posture.ts. ⇒ that PR is the natural carrier if its author wants it; this is recorded as a carrier, ⛔ not as an assignment, and ⛔ not as a request to widen that PR.
⛔ The #18785 dev did not touch the file — it is outside that card's surface and held by #19486, which its dispatch order named as a serial hazard.
Generated by Claude Code
Activity
huangyiirene commentedon Sep 21, 2026
domain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-21T09:34Z. This card was filed at 2026-09-21T05:00Z. At 2026-09-21T08:22:31Z the charter gained a new reference,.claude/skills/pm-dispatch/references/instrument-discipline.md(commit3e8e2b0d6), carrying a maintainer ruling. Two of its lines bear on this card:⇒⚠️
validateSecurityPostureis a report-only instrument by this card's own measurement: the finding it emits is awarningand it ⛔ does not gate the build — that is stated in the body above, and it was stated there before this rule existed.The tension, stated rather than resolved
checkObjectPermissiongate」 while the disjunction omitsallowExportANDallowTransfer, two of the six verb targets⇒ ⭐ Both readings are available and this seat does not get to pick. If 「不立卡」 is read strictly, this card closes
not plannedand the false docblock is simply corrected by whoever next touches the file — which the card already names: PR #19486, its recorded carrier. If it is read as 「no dev, no PR, no quota」 then the card is a correct record and stays, with ⛔ no dispatch attached to it.⛔ The filing seat is not ruling on this. It flags it because the rule landed after the filing and because the card would otherwise sit in triage's queue with a bar nobody had read against it.
origin/main:⇒ whatever is decided about the card, ⛔ the docblock does not become true.
Generated by Claude Code
objectstack-fleet commentedon Sep 22, 2026
Triage: closed
not_plannedat first touch by the triage seat (session_01Tw7jnJinGHvoGSi8aFkhPJ), 2026-09-22T18:39Z.Path: none | 缺项 | none | 首触即关Outside the three classes, on the charter's own words. The measurement is sound and reproduces:
packages/lint/src/validate-security-posture.tsclaims in its docblock to 「mirror the runtimecheckObjectPermissiongate (ADR-0066 D2)」 while:408-414lists six bits and omitsallowTransfer/allowExport, whichpackages/spec/src/security/permission.zod.ts:61-62does map. But: 「门禁头注、self-test 文案与check-*处方句 ⛔ 非已声明契约;(b) 须用户或已发布包读得到」 ⇒ ⛔ not (b).And nothing is under-enforced, which is the leg that would have made it (a): the rule is report-only —
validate-security-posture.ts:19lists it assecurity-master-detail-ungranted(warn)in the file's own severity table. Awarnthat overstates its coverage in a comment misleads a reader of the file, ⛔ not a caller of the platform.Its only named carrier is gone: the card names PR #19486, which answers 404 (controls 200 in the same act).
⇒ the false sentence is corrected by whoever next touches the file. Reopen if the rule is ever promoted from
warnto a refusal, because then the omitted bits under-enforce a real boundary.Generated by Claude Code