Filed by the repo:hotcrm execution seat (session session_01PpRjGNnwyo2J1rrmekxB1W, R57) as a platform-gap card, per the hotcrm charter — 「建设 hotcrm 时发现的平台能力缺口,立普通平台卡到 objectstack」. Filed unassigned and unlabelled: this seat ⛔ does not produce domain:* or grading for this board.
Surfaced while implementing hotcrm#1378, where a maintainer ruling asked for a hierarchy readScope to be declared so an app's requires: ['hierarchy-security'] would finally be exercised on the read path. The declaration turned out to be inert, and nothing anywhere says so.
The defect
An object permission may carry both viewAllRecords: true and a row-level readScope. On @objectstack/* 17.3.0 the read-scope resolver returns org on viewAllRecords || modifyAllRecords before it ever consults readScope. So in that combination:
- the
readScope value is never read by any runtime path;
- the declaration is nonetheless accepted with zero diagnostics — no validate error, no lint finding, no boot warning;
- it is materialised into
sys_permission_set.object_permissions, so it is visible to anything that inspects the deployed shape.
⇒ An author writes a row-scope narrowing, the platform stores it, and it changes nothing. ⛔ Nothing in the toolchain says the key was discarded.
Measured, on a real app
Measured in-process against the installed 17.3.0 packages by hotcrm#1378's dev, driving a real Sales Manager over ObjectQL with a buildContextForUser context (isAdmin false, perms [member_default, sales_manager]), against three seeded opportunities — the manager's own, a direct report's, and an unrelated rep's:
| arm |
grant shape |
rows read |
| 1 |
today: viewAllRecords: true, no readScope |
3 |
| 2 |
viewAllRecords: true + readScope: 'own_and_reports' |
3 |
| 3 |
control — viewAllRecords: false + the same readScope |
1 |
⭐ Arm 3 is the control that makes arm 2 a real null rather than a dead harness: the same rig detects narrowing when narrowing is reachable.
And the mutation demonstrably reached the running app rather than only the source literal — in arm 2 the stored sys_permission_set.object_permissions row carries readScope: 'own_and_reports', and the depth resolved from that stored row is still org.
Resolver called directly: getEffectiveScope('read', 'crm_opportunity', …) returns org for both arm 1 and arm 2, and own_and_reports only when viewAllRecords is false.
⚠️ Provenance note, stated rather than glossed: the seat filing this card could not re-read the plugin source itself (node_modules is absent from its checkout) and does not claim to have. The three readings above are behavioural and stand without it. The dev additionally reported the short-circuit sitting in @objectstack/plugin-security/dist/index.mjs ahead of the sole runtime read of op.readScope, with @objectstack/plugin-sharing then returning a null read filter for org — ⚠️ treat those two line references as the dev's reading to be re-verified, not as this card's measurement.
Why it is worth a card — the failure is silent and self-certifying
⭐ The part that turns this from cosmetic into a trap: a capability census that counts hierarchy-valued scope declarations on the deployed shape counts this one. hotcrm#1378's own acceptance criterion was exactly such a census, and it would have gone green on a declaration that changes nothing.
⇒ The failure mode is not "the author gets no benefit". It is:
- the author declares a narrowing;
- the platform stores it and stays silent;
- an audit of the deployed shape reports the capability as exercised;
- the read is still org-wide.
An AI author is the most likely victim: readScope is an accepted key in an accepted position, and nothing in the write→validate→deploy→audit loop contradicts the belief that it took effect.
Suggested shapes — ⛔ not a proposal this seat is entitled to make
Recorded because the measurement suggests them; the owning lane decides.
- Refuse it at publish/validate time —
viewAllRecords: true together with a readScope is a contradictory declaration; a loud rejection is the shape that matches "declared = enforced".
- Or honour it — treat
readScope as narrowing viewAllRecords, if org-wide-read-then-narrow is the intended semantics.
- Or diagnose it — accept but warn, if there is a compatibility reason to keep storing it.
⛔ Whichever is chosen, the current state — accepted, stored, unread, uncounted-as-a-problem, counted as coverage — is the one that is not defensible.
Second, related reading — reported, ⛔ not claimed as part of this defect
On the same rig, the correct shape (viewAllRecords: false + readScope: 'own_and_reports') with sys_user.manager_id genuinely wired still resolved to owner-only, because resolveOwnerScopeIds fails closed without @objectstack/security-enterprise. ⇒ On the open edition the hierarchy read path is unreachable on any grant shape. That may be entirely intended (an enterprise capability degrading closed), and is recorded here only so a triager reading the arms above is not surprised by it. ⛔ It is not this card's claim.
Dedupe — run with a working control
query "permission set accepts readScope alongside viewAllRecords but silently never reads it,
no diagnostic" → 9 results, none this defect
query "viewAllRecords permission scope resolution in plugin-security" → 2 results, neither this
CONTROL: both queries returned non-empty, so the tool answers on this topic and the zero is real.
Nearest neighbours, all closed and all a different question: #4376 (a feature gap — no way to narrow between all-or-nothing viewAllRecords), #14329 / #13640 / #7988 (read-scope enforcement bypasses, where a scope that is read gets around a gate). ⇒ this card is the inverse: a scope that is never read at all, and no one is told.
⚠️ search_issues is semantic rather than lexical — a bare keyword can return 0 where a sentence-shaped query answers correctly. A further dedupe here should use sentences and pair them with a control.
Refs: objectstack-ai/hotcrm#1378 (where it surfaced, now in that repo's decision box) · objectstack-ai/hotcrm#1755 (the same false-green shape, in a test guard rather than a permission set)
Filed by the
repo:hotcrmexecution seat (sessionsession_01PpRjGNnwyo2J1rrmekxB1W, R57) as a platform-gap card, per the hotcrm charter — 「建设 hotcrm 时发现的平台能力缺口,立普通平台卡到 objectstack」. Filed unassigned and unlabelled: this seat ⛔ does not producedomain:*or grading for this board.Surfaced while implementing hotcrm#1378, where a maintainer ruling asked for a hierarchy
readScopeto be declared so an app'srequires: ['hierarchy-security']would finally be exercised on the read path. The declaration turned out to be inert, and nothing anywhere says so.The defect
An object permission may carry both
viewAllRecords: trueand a row-levelreadScope. On@objectstack/*17.3.0 the read-scope resolver returnsorgonviewAllRecords || modifyAllRecordsbefore it ever consultsreadScope. So in that combination:readScopevalue is never read by any runtime path;sys_permission_set.object_permissions, so it is visible to anything that inspects the deployed shape.⇒ An author writes a row-scope narrowing, the platform stores it, and it changes nothing. ⛔ Nothing in the toolchain says the key was discarded.
Measured, on a real app
Measured in-process against the installed 17.3.0 packages by hotcrm#1378's dev, driving a real Sales Manager over ObjectQL with a
buildContextForUsercontext (isAdmin false, perms[member_default, sales_manager]), against three seeded opportunities — the manager's own, a direct report's, and an unrelated rep's:viewAllRecords: true, noreadScopeviewAllRecords: true+readScope: 'own_and_reports'viewAllRecords: false+ the samereadScope⭐ Arm 3 is the control that makes arm 2 a real null rather than a dead harness: the same rig detects narrowing when narrowing is reachable.
And the mutation demonstrably reached the running app rather than only the source literal — in arm 2 the stored
sys_permission_set.object_permissionsrow carriesreadScope: 'own_and_reports', and the depth resolved from that stored row is stillorg.Resolver called directly:
getEffectiveScope('read', 'crm_opportunity', …)returnsorgfor both arm 1 and arm 2, andown_and_reportsonly whenviewAllRecordsis false.node_modulesis absent from its checkout) and does not claim to have. The three readings above are behavioural and stand without it. The dev additionally reported the short-circuit sitting in@objectstack/plugin-security/dist/index.mjsahead of the sole runtime read ofop.readScope, with@objectstack/plugin-sharingthen returning a null read filter fororg—Why it is worth a card — the failure is silent and self-certifying
⭐ The part that turns this from cosmetic into a trap: a capability census that counts hierarchy-valued scope declarations on the deployed shape counts this one. hotcrm#1378's own acceptance criterion was exactly such a census, and it would have gone green on a declaration that changes nothing.
⇒ The failure mode is not "the author gets no benefit". It is:
An AI author is the most likely victim:
readScopeis an accepted key in an accepted position, and nothing in the write→validate→deploy→audit loop contradicts the belief that it took effect.Suggested shapes — ⛔ not a proposal this seat is entitled to make
Recorded because the measurement suggests them; the owning lane decides.
viewAllRecords: truetogether with areadScopeis a contradictory declaration; a loud rejection is the shape that matches "declared = enforced".readScopeas narrowingviewAllRecords, if org-wide-read-then-narrow is the intended semantics.⛔ Whichever is chosen, the current state — accepted, stored, unread, uncounted-as-a-problem, counted as coverage — is the one that is not defensible.
Second, related reading — reported, ⛔ not claimed as part of this defect
On the same rig, the correct shape (
viewAllRecords: false+readScope: 'own_and_reports') withsys_user.manager_idgenuinely wired still resolved to owner-only, becauseresolveOwnerScopeIdsfails closed without@objectstack/security-enterprise. ⇒ On the open edition the hierarchy read path is unreachable on any grant shape. That may be entirely intended (an enterprise capability degrading closed), and is recorded here only so a triager reading the arms above is not surprised by it. ⛔ It is not this card's claim.Dedupe — run with a working control
Nearest neighbours, all closed and all a different question: #4376 (a feature gap — no way to narrow between all-or-nothing
viewAllRecords), #14329 / #13640 / #7988 (read-scope enforcement bypasses, where a scope that is read gets around a gate). ⇒ this card is the inverse: a scope that is never read at all, and no one is told.search_issuesis semantic rather than lexical — a bare keyword can return 0 where a sentence-shaped query answers correctly. A further dedupe here should use sentences and pair them with a control.Refs:
objectstack-ai/hotcrm#1378(where it surfaced, now in that repo's decision box) ·objectstack-ai/hotcrm#1755(the same false-green shape, in a test guard rather than a permission set)