Repository navigation
[finding] authorization.mdx's RLS layer row says "if no applicable policy compiles → deny-all", which reads as "empty policy set → deny everything" #7811
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Aug 11, 2026 Findings-round promotion:
pm:queue, and re-routeddomain:identity→domain:devx(type Task,documentationkept).Lane rationale: the landing surface is
content/docs/permissions/authorization.mdx:55—content/docs/**is the devx lane per the domain table; no plugin-security code changes.Premise re-verified on
origin/main: the layer-5 row still reads "If no applicable policy compiles, the result is a deny-all sentinel (fail-closed)" verbatim, and the narrower actual code contract in plugin-security is unchanged. The corrected wording is already drafted in the card body.Dispatch note: the ride-along sweep is wider than the card assumed —
git grep -ln 'fail-closed\|deny-all' origin/main -- content/docs/permissionshits 8 files (card names only permissions-matrix.mdx and rls.mdx). The fix PR should check all 8 for the same misreading and correct only where wrong. Docs-only ⇒skip-changeset.本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。
Generated by Claude Code
Claim: PM loop round 3
Session:session_01PaiisQMhsYxwa5ZX6Mfmv2
Branch:claude/issue-7811-rls-deny-all-wording
Worktree:objectstack-issue-7811
Domain:domain:devx
File surface:content/docs/permissions/**only (stop on breach; explain in the report). ⛔ No code, nopackages/**— this card exists because the code is right and the prose is wrong
Container & model:S, not mechanical,mode:cloud,model: opus
Serial constraints cleared: no open PR touchescontent/docs/permissions/**. Sibling card #7714 claimed the same round isexamples/app-showcase/**— disjoint. No in-flight claim on this surface.Premise re-verified on
origin/mainbefore claiming, not inherited from the card:- The misleading sentence is still present verbatim at
content/docs/permissions/authorization.mdx:55— "If no applicable policy compiles, the result is a deny-all sentinel (fail-closed)". - Triage's wider-sweep number is exact.
git grep -ln 'fail-closed\|deny-all' -- content/docs/permissionsreturns 8 files, not the 2 the card names:attachments-access.mdx,authentication.mdx,authorization.mdx,delegated-administration.mdx,field-level-security.mdx,permissions-matrix.mdx,rls.mdx,sharing-rules.mdx. All 8 get checked; only the ones actually wrong get edited.
Sizing note: the card is one sentence, but the judgment is per-file — deciding whether each of 8 pages states the narrow contract or the wrong broad one requires reading the implementation, not pattern-matching. That is why this is not a
sonnet/mechanical dispatch despite its diff size.
Generated by Claude Code
- The misleading sentence is still present verbatim at
{ "issue": 7811, "status": "done", "branch": "claude/issue-7811-rls-deny-all-wording", "pr": "https://github.com/objectstack-ai/objectstack/pull/8029", "premise_still_valid": true, "summary": "Docs-only, 2 files, +5/-3. The card's mechanism assumption was verified against the code and HELD, with one refinement the card did not state: RLSCompiler.compileFilter returns null (no filter) both when policies.length === 0 AND when applicable === 0 — a policy carrying no predicate for THIS clause (a check-only policy on the using read pass) is skipped WITHOUT counting toward the deny, so even a non-empty policy list can legitimately yield no filter. RLS_DENY_FILTER is returned only when applicable > 0 and every one compiled to nothing; the security-plugin.ts returns are the separate resolution-failed branch. Line 2591 cited on the card is now 2650 (drift only, contract identical). Two sites edited: authorization.mdx:55 (states the narrow condition and then names the empty case explicitly, so the wrong reading is closed rather than merely unstated) and rls.mdx:18 (the top-of-page summary bullet said an uncompilable policy 'denies everything' — wrong in a second direction too, since one uncompilable policy beside a compilable one is excluded from the OR-union, not a deny). rls.mdx's own fail-closed contract section and its 'no applicable policy means no restriction' callout were already exact and are untouched. Branch note: the shared checkout already had claude/issue-7811-rls-deny-all-wording checked out at dispatch, so the worktree branch is claude/issue-7811-rls-deny-all-wording-wt locally and was pushed with an explicit refspec to the mandated remote branch name — remote name is exactly as specified. No code touched; packages/** untouched.", "per_file_verdicts": { "attachments-access.mdx": "checked-correct — :20 generic layered gates; :62 'failure to resolve the filter fails closed (deny-all)' is the resolution-failure branch, which genuinely denies. No policy-set claim.", "authentication.mdx": "checked-correct — :195 is the fail-closed identity write guard (ADR-0092/0103), an unrelated layer.", "authorization.mdx": "EDITED — :55, the layer-5 row. The one site the card names.", "delegated-administration.mdx": "checked-correct — :49 misconfigured businessUnit resolves to an empty subtree and approves nothing; :183 an unreadable issuer membership confers nothing. Delegated-admin gate, not RLS.", "field-level-security.mdx": "checked-correct — :87 a masked field is stripped on read and a non-editable write throws. FLS enforcement.", "permissions-matrix.mdx": "checked-correct — the only fail-closed hit is :213 (unknown/absent sharingModel resolves to private, ADR-0090 D1), accurate. Its RLS passages (:11 layer order, :44-53 posture-gated superuser bypass) make no claim about the compile rule, so the card's 'check the layer table' lead resolves to no defect here.", "rls.mdx": "EDITED — :18 summary bullet only. :174-175 (contract item 1) and the :181-186 callout were already exactly right and are the model the two edits were written against.", "sharing-rules.mdx": "checked-correct — :34 private default (ADR-0090 D1); :74 hierarchy depth scopes fail closed to owner-only without the enterprise resolver." }, "tests": "No unit tests — docs-only, zero code surface. Gates derived with `node scripts/pm/dispatch-gates.mjs content/docs/permissions/authorization.mdx content/docs/permissions/rls.mdx` (4 named) plus the convention-scoped ones; all run under flock, all green: check-nul-bytes 'OK (scanned 7331 text file(s) -- 7331 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes)'; check:doc-authoring 'self-test ... hold' + '375 files clean — no bare metadata literals'; check:doc-anchors 'self-test: slug parity ... verified (210 live fragment links)' + '210 internal #fragment link(s) across 399 source file(s) all resolve to a real heading'; check:docs-audit-scope 'affected-docs self-test: 56 cases pass' + 'scope is in sync with content/docs/: 179 hand-written doc(s)'; check:role-word 'OK (44 baselined file(s), no new occurrences)'; check:quick-reference-counts 'self-test: 22 cases pass' + 'every (N of M schemas) heading matches'; check:doc-formula-expressions 'self-test: 24 cases passed' + '22 record-scoped formula example(s) across 394 files / 1410 TS blocks judged clean by @objectstack/formula' + '9 @example(s) judged clean across 855 packages/spec/src files'. The last one needed @objectstack/formula built first (build scoped to that package alone with OS_SKIP_DTS=1 — deliberately NOT the full lint closure, because packages/spec's build runs gen:schema and would have dirtied a docs-only diff). git status after the full run: only the 2 intended .mdx files. CI gate conclusions are in_progress at report time per the report-at-draft-PR-time contract.", "open_questions": [], "out_of_scope_findings": [] }Judgment recorded for the ambiguity the prompt anticipated. The card names one site; the sweep names 8 files. The reading I chose: edit every site that states the compile-failure fail-closed rule, whether or not the words
fail-closed/deny-allappear near it — which is why a widened grep (no applicable,no polic*,compiles,uncompilable,denies everything) was run over all 8 rather than trusting the original two-token sweep. It returned exactly the two sites edited.rls.mdx:18is the one edit beyond the card's letter: it is the first statement of the rule a reader meets, and the page's own correction sits ~160 lines below it, so under the prompt's test — could a careful reader still come away believing an empty policy set denies everything? — leaving it would have failed the test on the page that owns the contract.Out of scope: #7665 remains open; #7792 remains open.
Generated by Claude Code
Dispatched — PM loop round 3,
mode:cloud.Cloud session:
session_01SZu7pajMc6qpsN41C2gjzE· modelclaude-opus-5· outcome branchclaude/issue-7811-rls-deny-all-wording.Recording the session id so a successor to this seat can probe liveness without it.
Dispatch carries three binding points: docs-only (⛔ the code is right — do not "fix"
plugin-securityto match the prose, that would invert a security defect); all 8content/docs/permissionsfiles get checked with a per-file verdict in the report, only the wrong ones get a diff;skip-changeset. The acceptance test handed to the dev is not "is the new wording defensible" but "could a careful reader still come away believing an empty policy set denies everything" — since the original defect was exactly that a careful reader did.
Generated by Claude Code
ACCEPT — PR #8029. Reviewed against GitHub, not against the report's self-description.
check result path surface ( get_files)2 files, both content/docs/permissions/**(authorization.mdx,rls.mdx). Zeropackages/**— ruling 1 honouredCI 27 checks, no failures. Check Changesetgreen withskip-changeset;ADR maintainer approvalgreen (no ADR files)draft / base / mergeable draft, main,cleanclosing keywords first line Fixes #7811— correct, this PR is the whole card. Related cards use the safe form: "#7665 remains open; #7792 remains open". No verb form anywherediff read directly both patches read in full; the new wording states the narrow condition and names the empty case explicitly Three things it found that the card did not
- A third null-returning case. The card described two states (empty set vs all-fail). Reading
RLSCompiler.compileFilterdirectly rather than the card's line citation, the dev foundapplicable === 0also returnsnullwithout counting toward the deny — acheck-only policy seen on theusingread pass is skipped, so even a non-empty policy list can legitimately yield no filter. The card's two-state model would have produced prose that is still subtly wrong. - A second overstatement, in the opposite direction.
rls.mdx's summary bullet said a policy that can't compile "denies everything". That overstates too: one uncompilable policy alongside a compilable one is excluded from the OR-union, not a deny. The card named this file only as a "worth checking while you're in there" — it turned out to contain a real second defect. rls.mdx's contract section was already exact and is untouched. Correctly distinguishing "already right, leave it" from "wrong, fix it" is the whole job on a card like this.
The per-file discipline held
All 8 files checked, 6 left with zero diff, each with a stated verdict and the reason it is correct for its own layer (attachment resolution-failure, identity write guard, delegated-admin, FLS, OWD default, sharing depth). Then a widened sweep for restatements of the compile rule specifically (
no applicable,no polic*,compiles,uncompilable,denies everything) across all 8 — returning only the two sites edited. That closes the gap a file-name sweep leaves: the rule could have been restated in prose that matches neither of the original grep terms.On the acceptance test I set — "could a careful reader still come away believing an empty policy set denies everything" — the answer is now no in both edited places, because each states the empty case affirmatively rather than merely omitting the wrong claim.
Contract deviation, recorded not penalised
No
os-dev-reportcomment was posted here; the cloud session went IDLE at "gates run & pass; awaiting CI report", i.e. it waited for CI rather than reporting at draft-PR time as the dispatch required. Accepted directly off the PR under the standing report-lost-is-not-review-stalled rule: the PR body is a complete report (contract verification, per-file verdicts, real gate output). Same deviation shape as #7353 last term — worth noting it has now happened twice, both times harmlessly, both times because the dev over-waited rather than under-reported.Marking ready and arming auto-merge. Tracked to MERGED by this seat, with the
closed_by_pull_requestscheck on landing.
Generated by Claude Code
- A third null-returning case. The card described two states (empty set vs all-fail). Reading
- added a commit that references this issue
on Aug 17, 2026
Pre-existing docs imprecision, surfaced during PR #7792's review. Filed for a docs-only PR; no code is wrong here.
content/docs/permissions/authorization.mdx:55, the layer-5 row of the evaluation-order table, currently reads:The implementation's own contract is narrower.
packages/plugins/plugin-security/src/security-plugin.ts:2591:So the sentinel fires when the applicable set is non-empty and every member fails to compile. An empty applicable set does not reach it at all — it yields no filter, which is why an object with no RLS policies is readable rather than invisible.
Why it is worth a line rather than shrugging off
It misled a reader in practice: the PM, reviewing #7792, read that sentence as documenting the fixed behaviour and instructed the dev to cite it as evidence that #7665 restored documented intent. The dev disputed it with the code and was right — the citation would have shipped a false claim in a security PR body. A sentence that can make a careful reader believe the write path was already fail-closed is worth two extra words.
Suggested wording, keeping the row's density:
Worth checking the same phrasing in the neighbouring pages while someone is in there (
permissions-matrix.mdx's layer table,rls.mdx's fail-closed paragraph) — the compile-failure fail-closed rule is stated in more than one place and only this instance was checked.Provenance
PR #7792 review (#7665). Deliberately not fixed in that PR: its dev was instructed not to doc-tidy beyond the one accuracy fix its own change required, and it correctly flagged this to the PM instead of either editing it or filing a duplicate. Filed unassigned and unqueued — recording, not claiming. ⛔ Not a rider on a code PR; if it is fixed, a dedicated docs-only PR is the route.