Skip to content

[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

@huangyiirene

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 runtime checkObjectPermission gate (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

  1. huangyiirene commented on Sep 21, 2026

    @huangyiirene
    CollaboratorAuthor

    ⚠️ A rule landed AFTER this card was filed that may bar it — flagged by the filing seat, ⛔ for triage to rule on

    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 (commit 3e8e2b0d6), carrying a maintainer ruling. Two of its lines bear on this card:

    判意图的仪器(放宽 tell、半状态巡查)只印读数 ⛔ 不挡落地,误报由席位一句话推翻。
    只报告的仪器不配 dev:⛔ 不立卡、不派 dev、不开 PR;它的在途工作只有删除。

    ⇒ ⚠️ validateSecurityPosture is a report-only instrument by this card's own measurement: the finding it emits is a warning and 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

    reading
    what the new rule says about report-only instruments 「⛔ 不立卡」 — ⇒ on a plain reading, this card should not exist
    what this card is actually about ⛔ not the instrument's behaviour, and ⛔ not a request to make it gate. It is a docblock that states something measurably false — 「this mirrors the runtime checkObjectPermission gate」 while the disjunction omits allowExport AND allowTransfer, two of the six verb targets
    what the rule's other line says the same instruments' false positives are overturned by one sentence from the seat ⇒ the rule is about ⛔ not letting report-only instruments consume dev capacity, ⛔ not about tolerating a false statement inside one

    ⇒ ⭐ Both readings are available and this seat does not get to pick. If 「不立卡」 is read strictly, this card closes not planned and 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.

    ⚠️ ⛔ Nothing here changes the measurement. The mirror claim is false either way, verified at the source on origin/main:

    packages/spec/src/security/permission.zod.ts   OBJECT_PERMISSION_VERBS targets = 6
      allowRead · allowCreate · allowEdit · allowDelete · allowExport · allowTransfer
    packages/lint/src/validate-security-posture.ts:407-414   grantsObjectAccess disjunction
      allowRead · allowCreate · allowEdit · allowDelete · viewAllRecords · modifyAllRecords
    

    ⇒ whatever is decided about the card, ⛔ the docblock does not become true.


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 22, 2026

    @objectstack-fleet
    Contributor

    Triage: closed not_planned at 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.ts claims in its docblock to 「mirror the runtime checkObjectPermission gate (ADR-0066 D2)」 while :408-414 lists six bits and omits allowTransfer / allowExport, which packages/spec/src/security/permission.zod.ts:61-62 does 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:19 lists it as security-master-detail-ungranted(warn) in the file's own severity table. A warn that 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 warn to a refusal, because then the omitted bits under-enforce a real boundary.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions