Repository navigation
lint: validate-rls-predicate-enforceability is silent on == / != against a kernel membership key (current_user.org_user_ids …), a policy the runtime drops on every request once PR #19947 lands #19951
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actions分诊首次定级:
priority:p2·bug·domain:spec·pm:queue—— 校验通过、运行时却一律拒绝的策略,lint 必须报出来Path:
packages/lint/src/validate-rls-predicate-enforceability.ts(reference pass 在探测编译被拒时直接退出的那一支)Triage: lands in the RLS predicate-enforceability lint ⇒
domain:spec,bug,priority:p2,pm:queue; rationale: once PR #19947 lands,==/!=against a kernel membership key (org_user_ids,positions,accessible_org_ids, always arrays) makesRLSCompiler.compileFilterdrop the policy on every request (RLS_DENY_FILTER, fail closed) whilevalidateRlsPredicateEnforceabilityreports nothing — a policy that validates clean and enforces a blanket refusal is exactly what this rule exists to catch (NORTH-STAR priority rule 4: wrong metadata must be refused loudly, with a remedy).分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,座位贴 #6015),2026-09-24T05:20Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论)。定级说明
- 不是数据泄露:运行时是 fail-closed,多拒绝而不是多放行。⇒ ⛔ 不挂
security,也不到 p1。 - 但属于产品缺陷:作者或 AI 本意是"不是这些用户中的任何一个",写成了
record.reviewer_id != current_user.org_user_ids,校验一路是绿的,上线后这个对象的所有请求都被拒绝,只有服务端的一条 WARN。正确写法是!(record.reviewer_id in current_user.org_user_ids),lint 应当点名说出来。⇒ p2。 - 字面列表的同类写法(
record.status != ['closed', 'archived'])在 PR fix(formula, driver-mongodb): refuse == / != against a list literal at the CEL lowering and $ne arrays at the mongodb face #19947 分支上已经会被报rls-predicate-unenforceable,本卡只补"值在运行时才知道是数组"的那一类。
方向(卡面 dev 的建议,本席采纳作默认)
探测编译因为"内核成员键按数组探测"被拒为
unsupported时,报rls-predicate-unenforceable,带上编译器的具体原因,并在处方里给出in的写法。同一条路径顺带覆盖"对标量内核键用in"的反向情况。顺序
前提来自 PR #19947(#19886 stage 2c,尚未在
main上)。⇒ 与它同批或在它之后落地;接手前先确认 #19947 的状态。⛔ 不在 #19947 里顺手加。
Generated by Claude Code
- 不是数据泄露:运行时是 fail-closed,多拒绝而不是多放行。⇒ ⛔ 不挂
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsMore shapes for the same silent lint, from the
domain:servicesseat (session_01Evb5jFDZGKQE9KG4jbMfMF, seat post #6021) at 2026-09-24T23:16Z. ⛔ Not a claim; recorded here because this card ownsvalidate-rls-predicate-enforceability's blind spots.Measured by the #20018 dev on
9d81af714f(scratch probe; ⛔ not re-run by this seat). Each of these authored RLS predicates:- passes
isSupportedRlsExpression; - lowers verbatim through
compileCelToFilter→RLSCompiler.compileFilter; - gets 0 findings from
validateRlsPredicateEnforceability; - then is refused at runtime by the shared comparand faces (
assertListComparandShapes/normalizeFilterComparandTypes).
authored predicate lowers to refused by f in ['a', null],f in [null]$inwith a null memberthe null-member arm (2026-08-31 ruling) !(f in ['a', null])$notover that$inthe same f > null,f <= nulla null ordering comparand the null-ordering arm (2026-09-01 ruling) f != current_user$newith the whole user objectthe type face (#7872). The same predicate as #19959 Runtime consequence. Every analytics face refuses such a read scope as
READ_SCOPE_COMPILE_FAILED/ 500: the ObjectQL face since PR #20017, and the native face and the echo once PR #20046 (#20018) lands. So a policy an admin can author and publish cleanly makes every analytics query on that object fail. That is loud, but only at query time, and for a different person.In-repo producers: none.
git grepoverexamples/**andpackages/**/*.ts(tests excluded) found 0 such predicates, with a positive control (77current_userpredicates). The authored-policy surface is the reachability.Direction, for whoever takes this card: the lint (or
isSupportedRlsExpression) refuses at authoring time the predicate shapes whose lowering the shared faces refuse, the same principle this card's==/!=membership-key case applies.f != current_useris carried by #19959, and this comment only cross-references it.
Generated by Claude Code
- passes
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsSerial note from
domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV), 2026-09-27T03:21Z. ⛔ Not a claim; the state is unchanged (pm:queue).Passed over in this seat's round 1 because of a hot file.
packages/lint/src/validate-rls-predicate-enforceability.tsand its test are modified by open draft PR #19947 (#19886, seat 5). Whoever takes this card reads that PR's hunks first, or waits for its merge.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsCross-seat note from
domain:specseat 4 (session_01CiCTczDo7tGhafXjf61dUJ), 2026-09-27T07:22Z. ⛔ Not a claim; the state is unchanged.- Unblocked: PR fix(formula, driver-mongodb): refuse == / != against a list literal at the CEL lowering and $ne arrays at the mongodb face #19947 ([finding]
$newith an array comparand splits across backends: driver-sql and driver-memory refuse (400), driver-mongodb answers, formula matches every row — and both shared faces pass it #19886 stage 2c) merged as9347c1f239. Its hunks invalidate-rls-predicate-enforceability.tsare onmain. - Serial: PR fix(formula): refuse == / != against the bare current_user root at the CEL lowering #20189 ([finding] A CEL row-level predicate comparing a field to the bare
current_userroot lowers against the whole caller object; acheckwritten!= current_useradmits every write #19959, in the merge queue) edits this rule's TEST file,validate-rls-predicate-enforceability.test.ts. It changes no rule source. Mergemainafter it lands. - Same blind spot, more shapes:
record.f in current_user,record.f.startsWith(current_user)and.contains(current_user)pass the shape check and are refused per request, failing closed. The RLS lint's reference pass stays silent on them. This was measured by [finding] A CEL row-level predicate comparing a field to the barecurrent_userroot lowers against the whole caller object; acheckwritten!= current_useradmits every write #19959's dev and by PR fix(formula): refuse == / != against the bare current_user root at the CEL lowering #20189's reviewer (record5853733310, ③ item 3), with anin current_usercheckanswering 403 through the real plugin. They join this card's==/!=membership-key case and the four shapes in5823840908.
- Unblocked: PR fix(formula, driver-mongodb): refuse == / != against a list literal at the CEL lowering and $ne arrays at the mongodb face #19947 ([finding]
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-09-27T07:47Z
Session:session_01Rjy9MeetSfq34PKn81CRiN
Account:os-zhuang
Branch:claude/issue-19951-rls-lint-membership-key-silent
Worktree:objectstack-issue-19951
Domain:domain:spec
Seat:domain:spec#1
File surface:packages/lint/src/validate-rls-predicate-enforceability.ts(the reference pass's refused-probe branch and the prescription text) and its test;.changeset/(@objectstack/lintis published). ⛔ Notpackages/formula/**orplugin-securitysource (the runtime refusals are landed and ruled). ⛔ Not the shared comparand faces (this seat's #19886 stage 2b). ⛔ Notvalidate-filter-tokens.ts/filter-walk.ts(seat 4's #20003, claim5852526924). (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgment tier(dispatch-gates.mjs --tierat560b724cfor this surface: 「no path-derived mandate」, the default slot taken). The rule changes whatos validaterefuses, so it is reviewed atCONTRACT_REVIEW_TIERbefore enqueue. Verify-lock arrival depth 2 at the gate read (07:46Z); dispatch waits for the depth gate.
Clause-②: no
Thread-read: 5853768939
Serial constraints cleared:PR #19947 (#19886 stage 2c) merged as 9347c1f239, and PR #20189 (#19959) merged as 560b724c95, so seat 2's hot-file note 5852258288 and seat 4's serial note 5853768939 are both satisfied. No open PR touches packages/lint/src/validate-rls-predicate-enforceability* or packages/formula CEL lowering (all 14 open PRs' file lists read this act). Seat 4's #20003 claim holds validate-filter-tokens.ts / filter-walk.ts, which are disjoint. This seat's #19886 stage 2b (in flight, no PR) changes the shared comparand face; no shared file, and the dev is told to measure on main and name any probe-path interplay. The shapes in 5823840908 and 5853768939 are this card's scope, as those notes record.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19951,
"status": "done",
"branch": "claude/issue-19951-rls-lint-membership-key-silent",
"pr": "#20210",
"session": "session_01Rjy9MeetSfq34PKn81CRiN — the PM's session (mode:subagent); this run's identity is the branch",
"premise_still_valid": true,
"summary": "validateRlsPredicateEnforceability's reference pass now reports two refusal classes as rls-predicate-unenforceable (error), each with a pasteable rewrite naming the predicate's own field and key. Class 1 is a current_user value of the wrong runtime TYPE for its position (membership key under == / != or a string method, one-value key under in, bare current_user under in / startsWith / contains / endsWith). The discriminator: a one-reference type swap must make the same compile lower and land in a field position; a value-dependent constant fold (current_user.email == 'x') is a probe artefact and stays silent, pinned as a load-bearing control. Class 2 is a null list member / null ordering bound, judged by the same two spec/data faces analytics runs (assertListComparandShapes + normalizeFilterComparandTypes), graded by code INVALID_FILTER. Zone 2 assumption 1 is partly falsified: the null family is NOT refused on the RLS data path (it compiles and driver-sql answers it; read and check disagree) — reported below as a finding; the lint reports it anyway, per the contract rulings.",
"tests": "All at HEAD 77060e6 (merge of origin/main 0bd1126 into the branch; branch delta vs main = 3 files). (1) pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 --reporter=verbose src/validate-rls-predicate-enforceability.test.ts -> 'Tests 120 passed (120)'. (2) pnpm --filter @objectstack/lint test -> 'Test Files 108 passed (108)', 'Tests 4204 passed (4204)'. (3) pnpm --filter @objectstack/lint typecheck -> exit 0; check:test-typecheck 'OK' under tsconfig.test.json; tsc --listFiles includes the test file, 0 errors in it (the 6 ledgered errors are in two other files). (4) pnpm --filter @objectstack/lint build -> exit 0. (5) Ablations, one-time, via node scripts/ablation-replace.mjs WRAP mode (subject imported from src, no dist step): each anchor hit x1 -> x0, blob changed, restore proven (blob == HEAD 8589ec8c, git diff HEAD empty, post-run git diff HEAD 0 bytes). A: discriminator removed (every refusal attributed) -> 2 failed/118 (probe-artefact control; the 7.3.1 BOTH-positions pin). B: sharedFaceRefusal returns null -> 8 failed/112 (7 null rows + NOT-a-drop). C: swap may never land -> 12 failed/108 (8 type rows, flipped kernel-key pin, message, every-site, os validate reach). Predicted direction (red) observed in all three. (6) Shape table at main 560b724: lint 0 findings on 17 of 19 probed shapes (only f != current_user and the literal list reported); runtime via throwaway real SecurityPlugin + ObjectQL + SqlDriver(better-sqlite3) probe: type family -> 0 rows with a per-request 'DENY (fail closed)' WARN, check 403/403; null family -> no WARN, reads answered by SQL. After: every owned shape = exactly one finding per clause, controls 0. (7) Census, BASE rule copy (blob cb850a12 == 560b724) vs this PR in one process: 126 authored using/check literals outside tests (77 current_user = the note's control) -> 0 changed; 327 including every test fixture (205 current_user) -> 0 changed. (8) Gates: dispatch-gates --commands --repo re-derived at 77060e6 = 59; all run; --ran reconcile: '59 derived, 57 run, 2 NOT-MEASURED, 0 UNRUN'; the 57 exited 0. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, both exit 3 PREREQUISITE NOT MET (need every package's dist). Consumer suites (cli, mcp, metadata-protocol, platform-objects, cloud-connection): NOT MEASURED locally, declared to CI — public surface byte-identical, 0 flips in the 327-predicate census, no suite outside packages/lint asserts an rls-predicate id (git grep). CI: in_progress, not awaited.",
"mcp_calls": "0 — no MCP GitHub tool called (SendMessage to the PM for the status probe is not a GitHub call)",
"api_writes": "3 REST writes, all through the fleet-write relay as objectstack-fleet[bot] (each one POST /repos/objectstack-ai/objectstack/dispatches from this container): pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#20210, draft, body read back byte-identical, 20064 bytes); assign -> POST /repos//issues/20210/assignees (os-zhuang, read back MATCHES); comment -> POST /repos//issues/19951/comments (this report). Plus 2 paced git pushes (write-pace --kind 'git push'; not REST): the empty-branch probe, then 560b724..77060e6.",
"files_changed": [
"packages/lint/src/validate-rls-predicate-enforceability.ts (+385 -18)",
"packages/lint/src/validate-rls-predicate-enforceability.test.ts (+177 -24)",
".changeset/19951-rls-lint-per-request-refusals.md (+25; @objectstack/lint minor — new error findings can fail a previously green os validate; FROM -> TO per shape family; no BREAKING marker, following the lint-only #16119 precedent)"
],
"gates": "59 derived at 77060e6 · 57 run exit 0 · 2 NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt: exit 3, prerequisite whole-repo dist) · 0 unrun · PR CI in_progress",
"line_budget": "not applicable — no skills/** or governed ledger touched",
"deviations": [
"Merged origin/main 0bd1126 into the branch before the single validated push (main moved during the run; the order's base clause says work on the current tip). The rule's closure was rebuilt and every reading above re-taken at the merge head.",
"Conflict pointed out, not silently resolved: the standing delta asks for CONSUMER package tests; .claude/agents/os-dev.md (which wins) owes only the package's own tests when the public face is byte-identical. Consumer exposure was MEASURED instead (0 of 327 predicates flip, no outside pin) and the consumer suites are declared to CI.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By: the reminder yields to the user's instructions, and the pre-push hook refuses a model identifier. The PR footer uses AGENTS.md's session-URL form.",
"Measurement probes (lint table, runtime table, census, BASE rule copy) were copied into packages/lint/src and packages/plugins/plugin-security/src only for their locked run, removed by trap; git status clean after each; never committed.",
"Status reply sent to the PM via SendMessage (requested mid-run)."
],
"open_questions": [],
"out_of_scope_findings": [
"class: b · The RLS data path does not run the shared comparand faces on its own compiled filter, so a null list member / null ordering bound in an RLS predicate reaches the driver and the check evaluator, which disagree — contract: the 2026-08-31 / 2026-09-01 rulings refuse these comparands at the shared face, and driver-memory/src/memory-matcher.ts records the cells as 'constructively unreachable through the compile face'. Seam: spec:assertListComparandShapes null-member / null-ordering arms (packages/spec/src/data/filter-comparand-shape.ts) -> runtime: plugin-security RLSCompiler.compileFilter output AND-composed past the engine seam (objectql engine.ts applies the faces to the caller's where only) | formula matchesFilterCondition (check path). · reach: real SecurityPlugin + ObjectQL + SqlDriver(better-sqlite3) probe, caller usr_member, rows r_open (open) / r_closed (closed): !(record.status in ['open', null]) as using -> read returned [] (r_closed hidden, no WARN); the same predicate as check -> insert status 'closed' ADMITTED, status 'open' 403 — the read hides a row its own write check admits. record.status in ['open', null] -> read ['r_open'] (acts as in ['open']), check: open admitted / closed 403. record.status > null and <= null -> read [], check 403/403. record.status in [null] -> read [], check 403/403. Analytics refuses the same scope (assertReadScopeComparandsRunnable, read from code, not re-measured). · dedupe words: 'rls null list member shared face bypass' · 'rls filter skips comparand shape check' · 'check evaluator disagrees with read null member' · 'READ_SCOPE_COMPILE_FAILED null rls'",
"carrier: none (承接者:无) · noted, not filed — packages/spec/src/migrations/entries/semantic/18.cel-predicate-list-comparand-refused.ts says the lint reports the list literal while the membership-set form 'is refused at request time': still true, now incomplete (the lint reports that form too). In the PR's Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — an ordering against a membership set (record.f > current_user.positions) lowers to $gt over a list; neither the compiler nor the faces refuse it and its runtime answer was not exercised. In the PR's Acceptance notes."
]
}objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsReview: ACCEPT · PR #20210 at head
77060e6d067b0b114ee17b07079f7fe388592900· 2026-09-27T09:25Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), reviewer of record, on claim5853940205. Checked against GitHub, ⛔ not against the report (5854600917).Checklist
- Shape: draft, base
main, first lineFixes #19951. It is the body's only closing keyword. Every shape this card owns (the body,5823840908,5853768939) is reported, so closing on merge is correct. - Scope: 3 files, +587/−42: the rule
packages/lint/src/validate-rls-predicate-enforceability.ts, its test, and a@objectstack/lintminorchangeset. That is exactly the claim's surface. No formula, plugin-security or shared-face source changed; not governed. - Merge: clean against
origin/main1c8b320a89(seat'sgit merge-tree). - Discriminator, the order's Zone 2 item 2: a per-request refusal is reported only when a one-reference TYPE swap (kernel key scalar ↔ list, or the root as list / string) makes the same compile lower into a field position. A value-dependent constant fold stays silent. The load-bearing control is
current_user.email == 'ops@acme.com': its removal turned that control and one other pin red (ablation A). The null family is judged by the two spec/data faces analytics runs, graded byINVALID_FILTER(ablation B, 8 red). The type rows are ablation C (12 red). All three ablations were restored with the blob equal to HEAD. - Measured before → after: at
560b724c, 17 of 19 probed shapes gave 0 lint findings; at the head, every owned shape gets exactly one finding per clause, and the controls stay at 0. - Producer census with the BASE rule in the same process: 0 of 126 authored predicates outside tests change verdict (77
current_usercontrol), and 0 of 327 including fixtures. No authored policy starts failingos validate. - Tests: lint 108 files / 4204; the rule file 120/120; lint typecheck exit 0 (the test file is in the program); lint build exit 0.
- Gates: 59 derived, 57 exit 0, 2
NOT MEASURED(whole-workspace build prerequisites), 0 unrun, at77060e6d. Consumer suites (cli, mcp, metadata-protocol, platform-objects, cloud-connection) are declared to CI, based on the byte-identical public surface, the 0-flip census, and no outside pin on anrls-predicateid. - CI at this head: 12 success / 3 skipped / 16 in progress / 0 failing (an honest in-progress reading).
Contract review: the rule changes what
os validaterefuses (Clause-②: no: it narrows), so per the claim it is reviewed atCONTRACT_REVIEW_TIERbefore enqueue. An isolated at-tier reviewer is running.needs:contract-reviewis hung on PR #20210 in the same act.Deviations
- Zone 2 assumption 1 is partly falsified: the null family is ⛔ not refused on the RLS data path. That finding is filed below. The lint still reports those shapes, per the null-member and null-ordering rulings.
- The changeset carries no BREAKING banner, following the lint-only lint: an RLS predicate naming a non-existent field, or an un-pre-resolved
current_user.*variable, is reported by nothing — both fail CLOSED at runtime #16119 precedent. The at-tier review judges it. origin/main0bd11261was merged before the single push. Measurement probes were copied into packagesrconly for their locked run and removed by trap; none was committed.
Findings, one line each
- The RLS path's compiled filter skips the shared comparand faces, so null-member / null-ordering RLS predicates reach driver-sql, and the read and the write check disagree (
!(status in ['open', null]): the read hidesclosed, the check admits it) → filed RLS: a policy's compiled filter skips the shared comparand-shape faces, so a null list member or null ordering bound reaches driver-sql, and the read and the write check disagree (the read hides a row its own check admits) #20212 (bare, for triage; measured reach). 18.cel-predicate-list-comparand-refusedsays the membership-set form 「is refused at request time」, which is now incomplete because the lint reports it too → Acceptance notes.- An ordering against a membership set (
record.f > current_user.positions) lowers to$gtover a list; neither the compiler nor the faces refuse it, and its runtime answer was not exercised → Acceptance notes, reach unmeasured, ⛔ not filed.
- Shape: draft, base
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanded: PR #20210 →
af32cf9a0e· 2026-09-27T10:08Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), claim5853940205. Landing record.- Merged through the merge queue at 2026-09-27T10:08:00Z as
af32cf9a0efb39a2ac2fbaa86112472c93d02273. Two readings: it isorigin/main's tip, and the queue branchgh-readonly-queue/main/pr-20210-…is gone. - Path to it: ACCEPT
5854631178; at-tier contract review PASS5854781207on head77060e6d. That review ran a false-positive hunt over 50 sources × 2 clauses (all clean, identical to base) and an independent census (303 predicates, 0 changed). CI on that head: 32 success / 6 skipped / 0 failed. Not governed; 629 lines. The merge was clean at arming. - Verified by content on
origin/main:sharedFaceRefusalinpackages/lint/src/validate-rls-predicate-enforceability.ts, 3 hits (0 on the parent84156c7d). The19951changeset is present (absent on the parent). Control: the rule idrls-predicate-unenforceableis present on both. - Card: closed
completedbyFixes #19951;pm:dispatchedlifted in this act. No other card closed in the window. - Carried: RLS: a policy's compiled filter skips the shared comparand-shape faces, so a null list member or null ordering bound reaches driver-sql, and the read and the write check disagree (the read hides a row its own check admits) #20212 (filed bare): the RLS data path does not run the shared faces on its compiled filter, so the lint and the runtime now intentionally disagree on the null family. Non-blocking review notes, left in the PR's Acceptance notes:
- the
18.cel-predicate-list-comparand-refusedsentence is now incomplete; - ordering against a membership set is unexercised;
record.f > current_useris reported with a probe object quoted in the message;- the
RLS_PREDICATE_UNENFORCEABLEJSDoc is not updated for the null family.
- the
- Merged through the merge queue at 2026-09-27T10:08:00Z as
- added 3 commits that reference this issue
on Sep 28, 2026
Filed by the
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357) from the #19886 stage-2c round-2 dev report (5807587023, open question 1 and out-of-scope finding, class c). ⛔ Filed unassigned and unlabelled: routing and grading are triage's. ⛔ Not a claim.The premise (PR #19947, not yet on
main)PR #19947 (stage 2c of #19886) makes
compileCelToFilterrefuse==/!=whose comparand resolves to an array. A kernel membership key (positions,org_user_ids,accessible_org_ids) always resolves to an array, soRLSCompiler.compileFilterdrops such a policy on every request and returnsRLS_DENY_FILTER(fail closed; the runtime WARNsDENY (fail closed)).The gap
Measured by the #19886 2c dev at
9a38132f4f(the PR head), with the fields declared:validateRlsPredicateEnforceabilityreturns no finding for:record.reviewer_id != current_user.org_user_ids;!(record.reviewer_id == current_user.org_user_ids).For both,
RLSCompiler.compileFilterreturnsRLS_DENY_FILTER. The lint's reference pass bails when its probe compile is refused, and its shape check cannot see a per-request value. The literal-list forms (record.status != ['closed', 'archived']) are already reported asrls-predicate-unenforceableat that head.Why it matters
A policy that validates clean and enforces nothing but a blanket refusal is the class this lint rule exists to catch. An author (or an AI) meant "not one of these users" and should have written
!(record.reviewer_id in current_user.org_user_ids). The dev's suggested shape, which is not a ruling: when the probe compile is refusedunsupportedfor a kernel key probed as an array, reportrls-predicate-unenforceablewith the compiler's detail. The same path would coverinagainst a scalar kernel key.Seam: spec:
RESERVED_RLS_MEMBERSHIP_KEYS/ExecutionContextSchemaarray keys → runtime:plugin-securityRLSCompiler.compileFilter(policy dropped) | lint:validate-rls-predicate-enforceabilityreference pass (silent).Dedupe words:
rls lint membership key != silent·rls-predicate-unenforceable resolved array·reference pass bails unsupported