Found while implementing #7474 (PR splitting the six assertControlledByParentWrite refusal legs). Filed rather than fixed: the maintainer ruling of 2026-08-11 on #7474 ordered the runtime split only, and explicitly left question 2 of that card — lint vs runtime — undecided. This is that question, filed so it is not lost.
What was measured
An object that declares sharingModel: 'controlled_by_parent' and has no relation for the platform to derive access from is now refused at RUNTIME with 422 INVALID_METADATA (the #7474 PR). Nothing reports it before that:
packages/lint/src/validate-security-posture.ts carries the adjacent rules — security-owd-unset, security-owd-alias, security-master-detail-ungranted (a detail object that no authored permission set grants), security-private-no-readscope — and none of them checks this one. Grep for controlled_by_parent in packages/lint/src returns only the OWD vocabulary list, the D11 comment, and the security-master-detail-ungranted docblock.
- The check needs nothing a lint pass does not already hold: the object document carries
sharingModel and fields, and the relation resolution is the same three-step fallback resolveCbpRelation performs at runtime (security-plugin.ts) — a required master_detail, then any master_detail, then a required lookup. An object matching none of the three has nothing to derive from.
Why it is worth a rule
The runtime refusal fires when a user attempts a write on that object — i.e. after the app is published and in someone's hands, and only on the detail objects a caller happens to touch. The defect exists from the moment the metadata is authored. This is the shape security-master-detail-ungranted already exists for, one gate earlier.
It matters most for AI-authored metadata: controlled_by_parent is a plausible thing for an agent to write next to a lookup it forgot to mark required, and nothing in the authoring loop says so.
Suggested shape
A new advisory rule alongside SECURITY_MASTER_DETAIL_UNGRANTED in validate-security-posture.ts — error rather than warning is arguable, since the runtime answer is already a hard refusal, and that severity choice is the one thing this needs a decision on.
Pointers
Found while implementing #7474 (PR splitting the six
assertControlledByParentWriterefusal legs). Filed rather than fixed: the maintainer ruling of 2026-08-11 on #7474 ordered the runtime split only, and explicitly left question 2 of that card — lint vs runtime — undecided. This is that question, filed so it is not lost.What was measured
An object that declares
sharingModel: 'controlled_by_parent'and has no relation for the platform to derive access from is now refused at RUNTIME with422 INVALID_METADATA(the #7474 PR). Nothing reports it before that:packages/lint/src/validate-security-posture.tscarries the adjacent rules —security-owd-unset,security-owd-alias,security-master-detail-ungranted(a detail object that no authored permission set grants),security-private-no-readscope— and none of them checks this one. Grep forcontrolled_by_parentinpackages/lint/srcreturns only the OWD vocabulary list, the D11 comment, and thesecurity-master-detail-ungranteddocblock.sharingModelandfields, and the relation resolution is the same three-step fallbackresolveCbpRelationperforms at runtime (security-plugin.ts) — a requiredmaster_detail, then anymaster_detail, then a requiredlookup. An object matching none of the three has nothing to derive from.Why it is worth a rule
The runtime refusal fires when a user attempts a write on that object — i.e. after the app is published and in someone's hands, and only on the detail objects a caller happens to touch. The defect exists from the moment the metadata is authored. This is the shape
security-master-detail-ungrantedalready exists for, one gate earlier.It matters most for AI-authored metadata:
controlled_by_parentis a plausible thing for an agent to write next to a lookup it forgot to markrequired, and nothing in the authoring loop says so.Suggested shape
A new advisory rule alongside
SECURITY_MASTER_DETAIL_UNGRANTEDinvalidate-security-posture.ts—errorrather thanwarningis arguable, since the runtime answer is already a hard refusal, and that severity choice is the one thing this needs a decision on.Pointers
packages/lint/src/validate-security-posture.ts— the rule family and its existing master-detail rulepackages/plugins/plugin-security/src/security-plugin.ts—resolveCbpRelation(the relation resolution a rule must mirror) andassertControlledByParentWrite(the runtime refusal)assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 — the runtime split; its question 2 is this issue