Skip to content

fix(app-shell): stop authoring the retired rowLevelSecurity[].priority key in the Studio RLS editor (objectstack#7130) - #4057

Merged
os-help merged 1 commit into
mainfrom
claude/issue-7130-rls-priority-seed
Aug 10, 2026
Merged

fix(app-shell): stop authoring the retired rowLevelSecurity[].priority key in the Studio RLS editor (objectstack#7130)#4057
os-help merged 1 commit into
mainfrom
claude/issue-7130-rls-priority-seed

Conversation

@os-help

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#7130

rowLevelSecurity[].priority was removed in @objectstack/spec 17.0.0 (objectstack#3896) and left as a retiredKey tombstone, so an authored value is rejected at parse time with the upgrade prescription rather than ignored. The Studio structured RLS editor was still authoring it: PermissionAdvancedFacets typed the key and seeded priority: 0 on every policy its "Add policy" button created. Per the objectstack#7130 ruling, only the objectui producer changes here — the spec/ledger side is already executed and correct, and no objectstack file is touched.

Premise re-verified at objectui origin/main @ 11c1e71e8

The card's three sites are all present, unchanged (the file's last touch is #3779, long merged):

  • :38 docblock — "Shapes mirror the framework spec (sampled from live data): RLS policies {name,object,operation,using,check,enabled,priority}"
  • :49priority?: number; on the local RlsPolicy interface
  • :368{ name: '', object: '*', operation: 'all', using: '', enabled: true, priority: 0 }

Zero-hit census re-run and falsified in both directions: grep -n priority on the file yields exactly those three lines; the known-present neighbour enabled yields 6 hits on the same file, and grep -n prior adds nothing (no cross-line concatenation hiding a fourth site). Repo-wide, every other priority in objectui belongs to a live key on a different surface (validation rules, hooks, kanban) — no other file types or produces an RLS policy shape.

Spec side re-read at objectstack origin/main: packages/spec/src/security/rls.zod.ts:432 is the retiredKey(...) call, and :219 records the deliberate exclusion of priority from the alias suggestion pool.

The unmeasured half the filer flagged: there is NO save-path scrub

Measured end to end; the answer is that nothing strips the key.

  • PermissionAdvancedFacets.tsx:161 loads policies with asArray(draft.rowLevelSecurity) — verbatim, no normalization.
  • Every field edit spreads the loaded policy (next[i] = { ...pol, name: e.target.value }), so a stored priority is carried back out verbatim on any edit.
  • PermissionMatrixEditor.tsx:546doSave sends payload (the draft) straight to client.save; the only transform is package scope's mergePermissionSlice.
  • permission-slice.ts:94mergePermissionSlice returns { ...base, name, label, isDefault, objects, fields } — it takes only objects/fields from the edit and copies rowLevelSecurity from the freshly-read base.

So at environment scope — the only scope where these facets are persisted (the docblock and PermissionMatrixEditor.tsx:754 both say "persisted by the whole-record Save at env scope") — the seeded priority: 0 goes into the saved permission set unmodified. The severity in the card stands as filed: this is a live producer of a parse-rejected key, not a seeded-then-scrubbed field.

Round-trip behaviour of an already-poisoned stored policy, before this PR: loaded verbatim, spread verbatim on every edit, re-persisted verbatim — the poison stays alive indefinitely.

The fix (editor-side only, no data migration)

  1. priority dropped from the local RlsPolicy interface.
  2. priority: 0 dropped from the Add-policy seed — it now authors exactly {name,object,operation,using,enabled}.
  3. Retired keys are stripped as policies are read out of the draft (RETIRED_RLS_KEYS + stripRetiredRlsKeys). policies is the single value every write path spreads from, so an edit-and-save round-trip of a policy already carrying the key now comes out clean — which is what the dispatch ruling asked for. This is deliberately not a migration: a set nobody opens is untouched, nothing rewrites the draft on mount (that would fake an unsaved-changes state on open), and the strip is keyed to the named tombstone rather than being a blanket unknown-key purge, so live keys the editor does not itself render (e.g. positions, tags) survive a round-trip.
  4. The docblock stops claiming the shapes are "sampled from live data" — that sampling is precisely how a removed key stayed in the editor.

Verification

Build closure first (fresh worktree), then the targeted gates:

pnpm --filter '@object-ui/app-shell^...' build # green
pnpm --filter '@object-ui/app-shell' type-check # tsc --noEmit + typetests, green
pnpm --filter '@object-ui/app-shell' lint # 0 errors
node scripts/check-control-bytes.mjs # OK (3923 files)
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.retiredKeys.test.tsx \
packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.cel.test.tsx
-> Test Files 2 passed (2) / Tests 5 passed (5)

eslint on the two touched files reports 0 errors and 3 warnings, all on pre-existing lines (:164 props any, :233 set-state-in-effect).

Reverse verification — direction predicted, then measured

Prediction: restoring the pre-fix component should turn all three new pins red — the seed pin because the key set gains a member, the two round-trip pins because the strip is gone. Took the fix out with git checkout origin/main -- PermissionAdvancedFacets.tsx (never git stash), re-ran the new file, then reapplied from a patch:

 × the Add-policy seed authors exactly the live spec keys — no `priority`
× an edit-and-save round-trip of a stored policy carrying `priority` comes out clean
× a policy the editor never touches is still emitted without `priority` once any policy is edited
AssertionError: expected [ 'enabled', 'name', 'object', ...(3) ] to deeply equal [ 'enabled', 'name', 'object', ...(2) ]
AssertionError: expected true to be false
Test Files 1 failed (1) / Tests 3 failed (3)

Red in the predicted direction, on all three. The third pin also carries a falsification assertion in the positive direction (check / using / enabled survive the strip), so the pins cannot pass by emitting nothing.

Scope

No objectstack edits. No data migration. Disjoint from objectstack#7129 (packages/plugin-dashboard); at PR time the only other open objectui PR is the release PR #3598, and nothing in flight touches this file.


Generated by Claude Code

…y key (objectstack#7130)
`rowLevelSecurity[].priority` is a retiredKey tombstone in @objectstack/spec
17.0.0 (objectstack#3896) — authored values are REJECTED at parse time with the
upgrade prescription. PermissionAdvancedFacets still typed the key and seeded
`priority: 0` on every policy its Add button created, and nothing on the save
path stripped it, so a permission set saved after using the structured RLS
editor carried a parse-rejected key.
- drop `priority` from the local `RlsPolicy` shape and from the Add-policy seed
- strip the retired key as policies are read out of the draft, so an
edit-and-save round-trip of an already-poisoned policy comes out clean
(editor hygiene, not a data migration)
- re-anchor the docblock, which described the shapes as sampled from live data
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016R9de1FqP7NvwKvqXi92Gh
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 10, 2026 2:27am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-Vl9bEIBE.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)483.72KB106.71KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)228.51KB56.69KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.84KB10.80KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.49KB17.48KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.67KB30.69KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)237.80KB59.48KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)113.37KB27.40KB
plugin-gantt (index.js)162.79KB39.67KB
plugin-grid (index.js)187.97KB49.79KB
plugin-kanban (index.js)48.53KB13.38KB
plugin-list (index.js)109.73KB26.55KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-help
os-help marked this pull request as ready for review August 10, 2026 02:42
@os-help
os-help added this pull request to the merge queueAug 10, 2026
Merged via the queue into main with commit 5419f55Aug 10, 2026
21 checks passed
@os-help
os-help deleted the claude/issue-7130-rls-priority-seed branch August 10, 2026 02:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-help@claude