Filing gate ① — a product defect with a named site and a reproduction (class a).
The defect
POST /api/v1/security/explain with { object, operation: 'read', recordId } answers decision.record.visible: false (decidedBy: 'sharing') for rows that the same caller's find through the real enforcement stack returns.
Direction: fails closed. The report under-states read visibility; enforcement is correct.
Site (read by the seat on origin/main 2c1011b01b)
packages/plugins/plugin-security/src/security-plugin.ts, in explainAccessForCaller's dependency wiring:
? { sharingReadFilter: (o: string, c: any) => sharing.buildReadFilter(o, c) }
It passes the bare context, with no __readScope. The enforcement middleware (step 2.6) stamps __readScope before plugin-sharing's buildReadFilter reads it, so an org- or unit-depth reader gets a null sharing filter on the real path and an owner-only one in explain.
Measured (the #19963 dev's one-time probe on 2c1011b01b; ⛔ not re-run by this seat)
Real SecurityPlugin + SharingService + the security and sharing middleware, the platform member_default baseline, an unset (private) OWD:
explain(read) for a principal whose read depth is org answers visible: false, decidedBy: sharing, on a shared, an owned and an unshared row.
- The same
find through the middleware chain returns each row: the sharing filter is null because __readScope is org, and the AST where carries only the id and organization_id terms.
- Control: a principal with
own read depth agrees on both sides.
Fix site and shape
The producer is the explain wiring, not a consumer:
⛔ No objectui change: a consumer ignoring record.visible would show rows where the server refuses.
Serial: after PR #19984 (same region of the same file).
Dedupe
Semantic issue search, open and closed, on objectstack-ai/objectstack:
Seam: spec:ExplainDecision.record.visible → runtime: plugin-security explainAccessForCaller sharingReadFilter binding vs plugin-sharing buildReadFilter under the step-2.6 __readScope stamp.
Dedupe words: explain read visible __readScope · sharingReadFilter bare context · explain read org depth under-report · buildReadFilter readScope explain · explain hides readable row
Generated by Claude Code
Filing gate ① — a product defect with a named site and a reproduction (class a).
updateis not computed with the write path's inputs —record.visibleis false on rows the by-id PATCH admits, so every consumer hides Edit from permitted users #19963 dev, as an out-of-scope finding on PR fix(plugin-security): explain's record update/delete verdict uses the by-id write path's inputs #19984 (its## Acceptance notes).domain:servicesseat (session_01Evb5jFDZGKQE9KG4jbMfMF, seat post [PM seat] domain:services — 🟢 os-bill #6021).domain:servicesseat, once PR fix(plugin-security): explain's record update/delete verdict uses the by-id write path's inputs #19984 lands. The fix is the read twin of that PR's write-depth stamp, in the same wiring region.The defect
POST /api/v1/security/explainwith{ object, operation: 'read', recordId }answersdecision.record.visible: false(decidedBy: 'sharing') for rows that the same caller'sfindthrough the real enforcement stack returns.Direction: fails closed. The report under-states read visibility; enforcement is correct.
Site (read by the seat on
origin/main2c1011b01b)packages/plugins/plugin-security/src/security-plugin.ts, inexplainAccessForCaller's dependency wiring:It passes the bare context, with no
__readScope. The enforcement middleware (step 2.6) stamps__readScopebefore plugin-sharing'sbuildReadFilterreads it, so anorg- or unit-depth reader gets a null sharing filter on the real path and an owner-only one in explain.Measured (the #19963 dev's one-time probe on
2c1011b01b; ⛔ not re-run by this seat)Real
SecurityPlugin+SharingService+ the security and sharing middleware, the platformmember_defaultbaseline, an unset (private) OWD:explain(read)for a principal whose read depth isorganswersvisible: false,decidedBy: sharing, on a shared, an owned and an unshared row.findthrough the middleware chain returns each row: the sharing filter is null because__readScopeisorg, and the ASTwherecarries only the id andorganization_idterms.ownread depth agrees on both sides.Fix site and shape
The producer is the explain wiring, not a consumer:
__readScopefrom the same resolver the middleware uses, always overwritten, for thesharingReadFilterbinding;__writeScopeoncanEditRecord/canDeleteRecord;⛔ No objectui change: a consumer ignoring
record.visiblewould show rows where the server refuses.Serial: after PR #19984 (same region of the same file).
Dedupe
Semantic issue search, open and closed, on
objectstack-ai/objectstack:updateis not computed with the write path's inputs —record.visibleis false on rows the by-id PATCH admits, so every consumer hides Edit from permitted users #19963, the write half, and nothing covers the read half.Seam:
spec:ExplainDecision.record.visible→ runtime:plugin-securityexplainAccessForCallersharingReadFilterbinding vsplugin-sharingbuildReadFilterunder the step-2.6__readScopestamp.Dedupe words:
explain read visible __readScope·sharingReadFilter bare context·explain read org depth under-report·buildReadFilter readScope explain·explain hides readable rowGenerated by Claude Code