Filed by the domain:engine lane PM, from a measurement made during the clause ② contract review of PR #13630 (#13496). Recording only — no severity asserted, routing is triage's.
⚠️ PRE-EXISTING and low-urgency. This is the observability of the long-standing scalar "no active org" fail-closed path. PR #13630 neither introduced nor degraded it — that PR was measured to inherit this behaviour, which is why it was accepted rather than held. Ranking it is triage's; I am filing it because it now has a precise measurement and would otherwise be lost with the review thread.
Measured, at 1cdf3c14ef
When compileCelToFilter refuses (a current_user.* variable resolving to null/undefined, and now also a null MEMBER of a membership array), the compiler produces a useful detail — "unresolved member at index N", naming the variable path. Three things then happen to it:
| stage |
what happens |
plugin-security/rls-compiler.ts:343 |
the detail is discarded — only !ok is consumed |
| the drop-warn beside it |
gated on !isSupportedRlsExpression, which is true for membership shapes ⇒ nothing logs |
explain |
reports the sentinel with verdict narrows, not denies — its isDenyAll matches only its own __deny_all__, never __rls_deny__ |
So the denial is real and correct — record attribution excludes properly, and __rls_deny__ is visible in readFilter — but the reason is thrown away at the one place that could surface it, and the one diagnostic tool that exists mislabels the outcome.
Why it matters more than an ordinary missing log
The failure mode this produces is the worst-shaped one for an operator: a user sees zero rows, no error is raised, nothing appears in the log, and the built-in explanation tool says the policy "narrows" rather than "denies". Every signal available points away from the actual cause.
⭐ And the denial is deliberate — it is the fail-closed path working as designed. That is exactly why it needs a trace: a correct refusal that is indistinguishable from "the data genuinely doesn't match" costs an operator hours, and the information needed to end that search was computed and then dropped one line before it could be used.
Scope NOT established
- ⛔ Whether other
!ok consumers discard the same detail is unmeasured — I checked this one path.
- ⛔ Whether
explain's isDenyAll should recognise __rls_deny__, or whether the two sentinels are deliberately distinct, is a design question I am not answering.
- ⛔ No claim that an operator has been bitten. What is measured is that the signal does not exist.
Suggested shape, not a prescription
Two candidates, both small, neither costed here: surface the discarded detail at the drop site (the warn is already there, only mis-gated), and/or teach explain's isDenyAll about __rls_deny__. Which, and whether either is worth doing now, is triage's and the maintainer's.
Related
#13496 / PR #13630 (where the clause ② review measured it) · #13552 (the polarity-blind deny guard on the same seam) · ADR-0056 / ADR-0058
Filed by the
domain:enginelane PM, from a measurement made during the clause ② contract review of PR #13630 (#13496). Recording only — no severity asserted, routing is triage's.Measured, at
1cdf3c14efWhen
compileCelToFilterrefuses (acurrent_user.*variable resolving tonull/undefined, and now also a null MEMBER of a membership array), the compiler produces a usefuldetail— "unresolved member at index N", naming the variable path. Three things then happen to it:plugin-security/rls-compiler.ts:343detailis discarded — only!okis consumed!isSupportedRlsExpression, which is true for membership shapes ⇒ nothing logsexplainnarrows, notdenies— itsisDenyAllmatches only its own__deny_all__, never__rls_deny__So the denial is real and correct — record attribution excludes properly, and
__rls_deny__is visible inreadFilter— but the reason is thrown away at the one place that could surface it, and the one diagnostic tool that exists mislabels the outcome.Why it matters more than an ordinary missing log
The failure mode this produces is the worst-shaped one for an operator: a user sees zero rows, no error is raised, nothing appears in the log, and the built-in explanation tool says the policy "narrows" rather than "denies". Every signal available points away from the actual cause.
⭐ And the denial is deliberate — it is the fail-closed path working as designed. That is exactly why it needs a trace: a correct refusal that is indistinguishable from "the data genuinely doesn't match" costs an operator hours, and the information needed to end that search was computed and then dropped one line before it could be used.
Scope NOT established
!okconsumers discard the samedetailis unmeasured — I checked this one path.explain'sisDenyAllshould recognise__rls_deny__, or whether the two sentinels are deliberately distinct, is a design question I am not answering.Suggested shape, not a prescription
Two candidates, both small, neither costed here: surface the discarded
detailat the drop site (the warn is already there, only mis-gated), and/or teachexplain'sisDenyAllabout__rls_deny__. Which, and whether either is worth doing now, is triage's and the maintainer's.Related
#13496 / PR #13630 (where the clause ② review measured it) · #13552 (the polarity-blind deny guard on the same seam) · ADR-0056 / ADR-0058