Repository navigation
Studio RLS editor rates a non-lowerable USING predicate a WARNING ("can … WIDEN access") and autosaves it, while publish / os build refuse it as an error (rls-predicate-unenforceable, fail-closed) #13061
Description
Filing gate: ① a product defect with a named landing point and a reproduction (pm:queue class).
Reader: the objectui lane owning packages/app-shell/src/views/metadata-admin/celAuthoring.ts (Studio CEL authoring) — the fix PR there.
Dedupe (open AND closed, semantic search): objectstack-ai/objectui "RLS policy editor non-pushdownable predicate warning widen access lintCelPredicate severity error rls-predicate-unenforceable" → 4 hits, all closed, none on the severity tier (#2413 the original lint feature, #4302 package-door discard, #4533 a11y, #8069 field-rule fallback); objectstack-ai/objectstack "RLS editor warning tier differs from build lint error unenforceable predicate" → 39 hits, all closed, none on the editor tier (objectstack-ai/objectstack#24452 / objectstack-ai/objectstack#19951 are framework lint coverage). First-round wave cards (#13052 #13054 #13055 #13056) checked — none covers it.
reach: public entry measured — Studio's RLS policy editor (package door) on a stock showcase boot with the console built at objectui 4016370a: the editor warns and autosaves a USING predicate that publish refuses 422.
Path: studio-authoring.expression-editors
QA-source: objectstack-ai/objectstack#24604 · studio-authoring.expression-editors · negative[0]
Checklist item priority: P1
type: Bug
Filed on behalf of the QA runner session session_01NvpZQ6sRwAVRNDi4UsF2c7 (its GitHub scope did not include this repository) by the 18.0 QA wave dispatch session session_01CfRDy7SuTfUqaTu2AEH2Hi.
What is wrong
For an RLS USING predicate that cannot be lowered to a row filter — e.g. size(record.asset_code) > 3 — the Studio editor and the engine give different verdicts for the same expression at the same site:
- Editor (objectui
celAuthoring.tslintCelPredicate, at4016370a):severity: 'warning'(engine.celLint.notPushdownable) with the text that the predicate "can be dropped and WIDEN access"; because it is only a warning the Save gate stays open and the policy autosaves (draft PUT 200). - Engine (
@objectstack/lintrls-predicate-unenforceable,packages/lint/src/validate-rls-predicate-enforceability.tsin objectstack-ai/objectstack, the ruleos buildruns): severity error; publish answers 422INVALID_METADATA, "RLS using is not lowerable (unsupported operand "call"), so the policy is dropped and grants no access".
The editor's wording also states the wrong direction: the runtime fails CLOSED (packages/plugins/plugin-security/src/rls-compiler.ts in objectstack-ai/objectstack adds no filter for a dropped policy and returns the deny filter when none compiles), so a dropped policy only removes the access it was written to grant — it never widens. Not an access-control hole; a second grammar in the GUI, which ADR-0032 / celAuthoring exists to prevent.
Reproduction
- Boot the showcase with the console built at the
.objectui-shapin; sign in as admin. - Create a writable scratch package (
POST /api/v1/packages) with an object carrying a text fieldasset_code. - Studio → that package → Access → a permission set → RLS policy on the object, USING
size(record.asset_code) > 3.- Expected (engine verdict): an error that holds Save, worded as fail-closed ("the policy is dropped and grants no access").
- Actual: an advisory warning "…can be dropped and WIDEN access"; autosave issues
PUT /api/v1/meta/permission/NAME?package=PKG&mode=draft→ 200.
- Publish:
POST /api/v1/meta/permission/NAME/publish?package=PKG→ 422INVALID_METADATA, rulerls-predicate-unenforceable, severityerror.
Control: USINGrecord.asset_code == 'A1'→ no warning; publish 200.
Expected fix shape
Give the lowerability verdict at error tier for USING and CHECK, using the same decision function the lint rule uses (isSupportedRlsExpression), so Save is gated; drop the "WIDEN access" wording in favour of the fail-closed sentence the engine prints.
Note for the checklist lane (not this card's work): the item's own acceptance[2] / acceptance[4] still encode the warning tier and will need the matching re-point; it is recorded on the wave anchor objectstack-ai/objectstack#24579.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p2 ·
bug·domain:ui·area:access·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T17:55ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on objectui
origin/mainc2191654a4.packages/app-shell/src/views/metadata-admin/celAuthoring.tscarries both tiers (:119/:120). The card measures the non-lowerable USING case ratedwarningand autosaved, where publish andos buildrefuse it asrls-predicate-unenforceable.domain:ui: rate iterrorand block the save, the same verdict as the server, with the server's wording. ⛔ Do not restate the lint: attach the spec/lint verdict where an export exists (the objectui#7715 rule).Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 1 (
priority:p2,bug)Session:
session_014MXhk1yjWh37d634hLy3p7Account:
huangyiirene(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13061-rls-lowerable-errorWorktree:
objectui-issue-13061Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/app-shell/src/views/metadata-admin/celAuthoring.ts(lintCelPredicate's non-lowerable USING / CHECK verdict, theengine.celLint.notPushdownableadvisory at about :566 of objectuimainfb85637, and theCelLintIssue/CelLintMessageKeytypes), its caller's save gate if it reads only errors,packages/app-shell/src/views/metadata-admin/i18n.ts(the advisory's wording, about :1734 and :5253), tests beside them, and one.changeset/13061-rls-lowerable-error.md. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuiover this surface reads no path-derived mandate)Clause-②: no
Responsibility: the risk is produced by objectui's own Studio CEL lint (
celAuthoring.tslintCelPredicaterates a non-lowerable RLS USING predicatewarning, so Save stays open and the draft autosaves); the platform path that covers it: the server, at publish (rls-predicate-unenforceable, severity error, 422INVALID_METADATA), so nothing unenforceable goes live, and the gap is the editor's second verdict; who reaches it: every Studio author writing an RLS predicate that cannot be lowered (QA sweep objectstack-ai/objectstack#24604, reproduced on a stock showcase).Thread-read: 6273706610
Serial constraints cleared: no open objectui PR touches
celAuthoring.ts(open PRs #13077, #13073, #13072, #13071, #13070, #13050, file lists read at this claim). PR #13050 (objectui#12982) editsviews/metadata-admin/i18n.ts, a shared file and not a single-claim path: whichever PR lands second mergesmain.Why
Clause-②: no:celAuthoring.tsis internal to@object-ui/app-shell; its entry (exports['.']→dist/index) re-exports nothing from it. The server's accept set is unchanged (it already refuses the predicate); only the editor's own gate moves to the server's verdict. A route that would widen a published face (a new export, a new published key or prop) is outside this claim: the dev stops and reports it.Claimed at: 2026-10-19T19:21Z
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 13061,
"status": "done",
"branch": "claude/issue-13061-rls-lowerable-error",
"pr": "#13080",
"session": "session_014MXhk1yjWh37d634hLy3p7 (subagent; parent PM session)",
"premise_still_valid": true,
"summary": "lintCelPredicate (celAuthoring.ts) now asks @objectstack/formula's isSupportedRlsExpression for both USING and CHECK. For a clause it refuses, it carries the finding @objectstack/lint's published validateRlsPredicateEnforceability returns for a one-clause stack, with severity (error) and message as written, e.g. 'RLS using is not lowerable (unsupported operand "call"), so the policy is dropped and grants no access'. The existing gate (PermissionAdvancedFacets counts errors; PermissionMatrixEditor holds Save and useDraftAutoSave on celErrorCount) therefore blocks Save with no host change. The module-worded 'WIDEN access' advisory, its engine.celLint.notPushdownable key and its en/zh rows are retired, and the fail-open claims in comments and the app-shell README are corrected. H1-H4 all held as measured; card assignee huangyiirene unchanged (PM's).",
"tests": "HEAD ae0abf7. Ran from the worktree root under os-verify-lock:
pnpm exec vitest run --maxWorkers=2over 14 files in packages/app-shell/src/views/metadata-admin/ (celAuthoring.test.ts, celAuthoring.fieldRuleVerdict-9318.test.ts, CelPredicateField.{test,i18n-10862-s2.test,labelBinding.test,scopeVerdict-11789.test}.tsx, PermissionAdvancedFacets.{cel,adminScopeAnchor,retiredKeys,rlsLabelDescription,sharedSelect-11865}.test.tsx, ConditionalFormattingEditor.test.tsx, PermissionMatrixEditor.{autosave-11787,packageDoorFacets}.test.tsx). Output: 'Test Files 14 passed (14)' / 'Tests 186 passed (186)' / 'VERDICT command-exit 0'. Full app-shell suite declared to CI (per dispatch). Ablation, one-off: node /home/user/objectstack/scripts/ablation-replace.mjs replaced the severity-mapping line in rlsLowerabilityVerdict with an always-'warning' push (the pre-fix tier). Output: 'anchor x1 -> x0', 'replace x0 -> x1', 'blob 1c4e132a78f3 -> 7a02dbc069a0'. Run over the 4 changed test files: 'Tests 6 failed | 70 passed (76)'. Red: 2 celAuthoring tier pins, the CelPredicateField blocking pin, 3 PermissionAdvancedFacets blocking pins. Green: the 2 i18n word controls and the lowerable control. Restore: 'blob == HEAD (1c4e132a78f3) and git diff HEAD is empty'. No dist rebuild needed: tests import ./celAuthoring from source, and @objectstack/lint/formula were not mutated.","gates": "Local, HEAD ae0abf7, each exit captured before any pipe.
pnpm --filter @object-ui/app-shell type-checkexit 0. The first run exited 2 because the deps closure was unbuilt; afterturbo run build --filter=@object-ui/app-shell^... --concurrency=2(exit 0), a second run exited 2 with a real TS2345:everyinferred-predicate narrowing, fixed in ae0abf7.pnpm --filter @object-ui/app-shell lintexit 0 ('0 errors'; the 1 warning on CelPredicateField.tsx is on untouched code).pnpm check:new-line-citationsexit 0 ('0 new citation(s)').pnpm check:control-bytesexit 0.pnpm check:i18n-keys,check:i18n-dead-keys,check:i18n-designer-parity,check:i18n-driftall exit 0.node scripts/check-changeset-presence.mjsexit 0.pnpm check:changeset-claimsandcheck:pending-changeset-literalsexit 0.node scripts/check-governed-queue-guard.mjs --testover the diff exit 0 ('NOT GOVERNED'). No --ran reconciliation: dispatch-gates.mjs refuses objectui, so this list was hand-derived from objectui package.json, as in the dispatch. CI on PR #13080 at report time: label success; skipped: Test (coverage), coverage shards, dependabot. in_progress: Lint, Type Check, Test shards 1-16/16, Test (dist pins), Build & E2E, Build Docs, Bundle Analysis, Repo Gates, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Governed Surface Queue Guard, Live E2E (informational). Not waited on.","deviations": "1) File surface beyond the claim list: CelPredicateField.tsx (comments only) and packages/app-shell/README.md (two paragraphs). Both stated the retired advisory or the fail-open claim and would be false after this change (AGENTS.md commandment 2, docs-driven); both are named in the PR body. Test edits beyond celAuthoring.test.ts are files beside it in the same directory (CelPredicateField.test.tsx, CelPredicateField.i18n-10862-s2.test.tsx, PermissionAdvancedFacets.cel.test.tsx). 2) Gates added beyond the dispatch list: changeset-presence, changeset-claims, pending-changeset-literals, governed-queue-guard --test, i18n-drift, i18n-designer-parity. 3) Ran the read-only
git stash listonce while filtering lint output; no stash push or pop. 4) Worktree removed after the PR opened, per definition (node_modules then git worktree remove, exit 0).","mcp_calls": "0 — no MCP GitHub tool called (reads via
gh api)","api_writes": "2 — two fleet-write relay dispatches, each POST /repos/objectstack-ai/objectstack/dispatches. (a) pr_create, executed as POST /repos/objectstack-ai/objectui/pulls (draft #13080) plus POST /repos//issues/13080/assignees in the same action; read-back: 8083 bytes sent and stored, identical; assignee huangyiirene. (b) comment, executed as POST /repos//issues/13061/comments (this report). Not REST: git push x3 of the branch (empty probe, 39e07c8, ae0abf7).",
"open_questions": [
{
"question": "Should the RLS editor also show the lint finding's
hint(rewrite guidance, e.g. 'write has(record.x) as record.x != null') under the error? Today only the message ships, as the card asks.","options": [
"A Keep message only (as shipped), the publish door's sentence",
"B Also render the lint's hint as a second line beneath the error"
],
"recommendation": "A, because the card and triage ask for the server's sentence, and adding the hint is a wording/UX call for the lane. It also moves no verdict."
}
],
"out_of_scope_findings": [
"carrier: none · noted in PR #13080 Acceptance notes, not filed · the lint rule's reference-pass verdicts on a lowerable clause (e.g. current_user.nope == owner_id -> rls-predicate-unknown-user-variable, error at publish) get no editor finding: silent, not wrong. Read off the rule via a node probe, not reproduced in Studio · dedupe words: RLS editor, unknown user variable, reference pass, publish 422, lintCelPredicate",
"carrier: none · noted, not filed · legacy SQL-ish
owner_id = current_user.id: the editor's CEL parse error blocks Save while the deprecated server bridge lowers it. Pre-existing, the editor is the stricter side; canonical RLS is CEL (ADR-0058 D1)"]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT → PR #13080 comment 6276033278 · no contract-review face hit (
Clause-②: no, no schema, no governed path) ·domain:ui#1·session_014MXhk1yjWh37d634hLy3p7· 2026-10-19T19:57Z. Out-of-scope finding filed as #13082. The flip to ready and auto-merge wait for green check-runs on headae0abf7.objectstack-fleet commentedon Oct 19, 2026
LANDED ·
domain:uiseat 1 ·session_014MXhk1yjWh37d634hLy3p7· 2026-10-19T20:19Z ·pm:dispatchedcleared2a75c83, whichgit merge-base --is-ancestorplaces onorigin/main(exit 0)gh-readonly-queue/main/pr-13080-*branch remainsorigin/main,celAuthoring.tscarriesasync function rlsLowerabilityVerdict(1 hit).notPushdownableis gone frompackages/app-shell/src(0 files). Positive control:engine.celLint.notTheRowstill reads 2 hits in the same file6275349095, dev report on this card, ACCEPT6276033278(no contract-review face hit)completedat 2026-10-19T20:18Z through the PR'sFixes #13061, the only card closed since the merge;pm:dispatchedis removed in this actOut of scope, carried: the reference-pass parity (a lowerable predicate the rule refuses on what it names) is #13082.