Skip to content

Studio RLS editor: "Add policy" autosaves the blank row it inserts, so a 422 lands as a raw spec-validation banner (rowLevelSecurity.0.name [invalid_format]) before the author types anything, and the row is lost on reload #13113

Closed

Description

@objectstack-fleet

Filing gate: ① user-visible UX defect (Studio write feedback is wrong). Reader: the objectui dev seat that takes this card from the pm-dispatch queue; fix lands in packages/app-shell/src/views/metadata-admin (PermissionAdvancedFacets / PermissionMatrixEditor). Dedupe: objectui issues, open+closed, semantic queries "RLS Add policy autosave empty policy spec validation error rowLevelSecurity name" (4 hits: #13082, #13061, #4302, #2413) and "permission set editor new RLS row saved before name entered 422 error banner" (4 hits: #13082, #13061, #4302, #4533) — none covers an incomplete NEW row being autosaved; this wave's #13061 / #13082 are about CEL predicates that publish refuses, not about the blank row.

reach: Studio (package door) → Access → a writable permission set → Row-level security tab → click "Add policy". Measured on stock showcase (--seed-admin), framework 54159fd742e5, console pin objectui@12dc7b47c340, dist stamp equal to the pin. Within ~1.5 s, with nothing typed, the page shows a red banner: "permission/visit_readers failed spec validation: 2 issues — rowLevelSecurity.0.name [invalid_format]; rowLevelSecurity.0 [custom]". The network shows PUT /api/v1/meta/permission/visit_readers?mode=draft&package=... answered 422. Reloading the page shows "No row-level policies" — the row the author was about to fill in is gone.

Expected: a freshly inserted policy row is local, incomplete state. Nothing is sent until it is complete enough to pass the protocol (a name in the accepted format at least), and while it is held the editor says, next to the row, what is missing ("Give the policy a name to save it"). If the server does refuse a save, the message names the field in the author's words (Policy name), not a schema path.
Actual: the autosave sends the blank row immediately, and the refusal is rendered as the raw protocol string with spec paths and zod codes (rowLevelSecurity.0.name [invalid_format]; rowLevelSecurity.0 [custom]). The author has done nothing wrong yet and sees an error they cannot map to the form; the unsaved row is then lost on reload.

Repro:

  1. Boot stock showcase with --seed-admin, sign in as admin@objectos.ai.
  2. Home → "Build an app" → "New package (writable base)" → any name → Create package.
  3. Access → "New permission set" → any name → create it.
  4. Row-level security tab → "Add policy". Do not type anything. Wait 2 s.
  5. Observe the banner above; reload and observe the policy row is gone.

Source (objectui main df943a2, re-read before filing):

  • packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx — the "Add policy" button's onClick appends { name: '', object: '*', operation: 'all', using: '', enabled: true } straight into the draft via setPolicies.
  • packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx — useDraftAutoSave's blocked is !packageId || !writable || saving || celErrorCount > 0 || destructive !== null; an incomplete row (empty name) does not hold the autosave, only a CEL lint error does.
  • The banner prints the server's refusal string as-is (setError with the raw message).

Generic blocks / contract: no new widget. The fix is a hold condition on the existing shared autosave (an RLS row with no valid name is "incomplete", same as a CEL error) plus a per-row inline hint; the protocol's accepted name format is the gate (PermissionSet rowLevelSecurity[].name in packages/spec).

type: Bug

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · bug · lane domain:ui · area:access · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T03:53Z

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on objectui origin/main 4b67746498.

    Filing gate. All three answers are present: ① measured in the browser, the objectui dev seat, and two searches of 4 hits each, closed included.

    Premise. The "Add policy" flow lives in packages/app-shell/src/views/metadata-admin (PermissionMatrixEditor / its facets). The 422, the raw rowLevelSecurity.0.name [invalid_format] banner and the lost row are the filer's browser measurement at the console pin.

    Why p2. Adding a row-level policy in Studio opens with an error the author did not cause, worded in schema paths, and the row is gone on reload. That is the first step of every RLS policy an author writes.

    Shape.

    • A new row stays local until it passes the protocol's own minimum (a name in the accepted format). While held, it says beside the row what is missing.
    • A server refusal names the field in the author's words.
    • ⛔ The editor does not restate the protocol's rule. It reads the refusal the door gives (objectui#7715's rule).

    Generated by Claude Code

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 20, 2026
  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 16 (priority:p2)
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-13113-rls-add-policy-hold
    Worktree: objectui-issue-13113
    Domain: domain:ui
    Seat: domain:ui#2
    File surface:

    • packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx (useDraftAutoSave's blocked) and PermissionAdvancedFacets.tsx (the "Add policy" row). An RLS row the protocol would refuse (no name in the accepted format) holds the autosave, the same way a CEL lint error does, and the row says beside itself what is missing.
    • Where the save path renders a server refusal, it names the field in the author's words, read from the refusal the door gives and not restating the protocol's rule (objectui#7715).
    • New copy goes in the designer i18n table (en + zh).
    • Tests beside them.
    • One .changeset/13113-rls-add-policy-hold.md (@object-ui/app-shell patch).

    ⛔ No new package export. Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (default tier; no path-derived mandate over this surface)
    Clause-②: no (internal metadata-admin editor; no export, prop or key moves)
    Responsibility: the editor's autosave sends a just-inserted blank RLS row | the shared autosave already holds on a CEL error, and the protocol's name rule is in the spec | every author adding a row-level policy in Studio (the first step of every RLS policy)
    Thread-read: 6282978730
    Serial constraints cleared: objectui#12725's PR #13106 (df943a2, PermissionMatrixEditor save surface) landed. No open objectui PR touches these files (read at 04:27Z).
    Claimed at: 2026-10-20T04:27Z


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 13113,
     "status": "done",
     "branch": "claude/issue-13113-rls-add-policy-hold",
     "pr": "https://github.com/objectstack-ai/objectui/pull/13167",
     "session": "session_01XVp19E1MYUTFM3dnkwz8gB",
     "premise_still_valid": true,
     "summary": "PermissionMatrixEditPage now holds the shared autosave while any RLS row in the draft has an empty or blank name. The test is emptiness only; the spec regex is not restated. The environment Save button is disabled for the same reason. The row shows an inline hint, perm.rls.nameNeeded in en and zh, and the status line repeats it in a neutral tone. A refused save whose MetadataError.issues name rowLevelSecurity.N.name reads 'Not saved. Policy name, on row N+1: PROBLEM', with the server's own message; any other issue path keeps the transport text. The held row stays in the in-memory draft. A reload discards it, because nothing unsaved is persisted (ruling 3 allows this). Branch head 9a6cdafe39 (fix f318298686 + refactor) was checked against all three rulings and needed no further commit.",
     "tests": "All at HEAD 9a6cdafe39, under os-verify-lock. Closure build --filter '@object-ui/app-shell^...' build: VERDICT command-exit 0. New file PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx: 5/5 passed, covering pins (a), (b), (c) and both (d) controls. Family vitest --maxWorkers=1 over metadata-admin/PermissionMatrixEditor* + PermissionAdvancedFacets*: 28 files / 196 tests passed. i18n family (CelPredicateField.i18n*, studio-locale.i18n, metadata-form-i18n): 3 files / 22 tests passed. app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0, and --listFilesOnly includes the new test. Ablation 1 (saveHeld = celErrorCount > 0): (a) and (b) red, 2 failed / 3 passed. Ablation 2 (setError without rlsNameRefusal): (c) red, 1 failed / 4 passed. Both mutations went through objectstack scripts/ablation-replace.mjs (anchor 1 to 0, blob 864d2df7b5b3 changed to 3188f0cad1aa and to 49781175306c). Both restores were proven: blob 864d2df7b5b3 equals HEAD:path and git diff HEAD has 0 lines. A trap restored on EXIT. Tests resolve src through a relative import, so no dist rebuild was needed. Narrowed lint: eslint --no-inline-config --format json over the 4 changed ts/tsx files gave 4 files, 0 errors, 23 warnings, all on pre-existing lines. No type-aware linting is configured, so untouched files' verdicts cannot change. Full pnpm lint and the full packages/app-shell directory run are declared to CI.",
     "mcp_calls": "0",
     "api_writes": "2 relay strokes: (1) fleet-write pr_create on objectstack-ai/objectui, which is POST /repos/objectstack-ai/objectui/pulls (draft) plus POST /repos/objectstack-ai/objectui/issues/13167/assignees, read back as identical with assignee baozhoutao; (2) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/13113/comments via post-stamped. git push: none needed, because the branch was already at 9a6cdafe39 on origin.",
     "gates": {
      "head": "9a6cdafe39",
      "closure-build": 0,
      "type-check": 0,
      "vitest-targeted": 0,
      "vitest-new-file": 0,
      "vitest-i18n-family": 0,
      "ablation-1-red": 1,
      "ablation-2-red": 1,
      "restores-blob-proven": 0,
      "lint-narrowed": 0,
      "check:phantom-deps": 0,
      "check:self-import": 0,
      "check:unreferenced-sources": 0,
      "check:doc-example-readers": 0,
      "check:handler-key-reads": 0,
      "check:side-effects-array": 0,
      "check:element-data-source-declaration": 0,
      "check:spec-symbols": 0,
      "check:action-forward-parity": 0,
      "check:designer-field-key-parity": 0,
      "check:icon-record-names": 0,
      "check:i18n-keys": 0,
      "check:i18n-drift": 0,
      "check:i18n-designer-parity": 0,
      "check:control-bytes": 0,
      "check-hand-rolled-comment-mask": 0,
      "check-changeset-fixed": 0,
      "check:spec-floors": "1 - NOT MEASURED, reason: prerequisite no-artifact (workspace build required), as the order anticipated",
      "app-shell full dir": "declared to CI",
      "pnpm lint full": "declared to CI"
     },
     "line_budget": "+333 / -5 across 5 files (test 215, PermissionMatrixEditor.tsx +83/-5, PermissionAdvancedFacets.tsx +19, i18n.ts +4, changeset 12)",
     "files_changed": [
      ".changeset/13113-rls-add-policy-hold.md",
      "packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx",
      "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx",
      "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx",
      "packages/app-shell/src/views/metadata-admin/i18n.ts"
     ],
     "deviations": [
      "This run resumed from the pushed sha claude/issue-13113-rls-add-policy-hold@9a6cdafe39 after a container restart. It reused the healthy, clean worktree at that sha and added no new commits. Every verification above was re-run in this session; none comes from the lost session's record.",
      "No labels were written: objectui path labels belong to labeler.yml, and the order named none.",
      "The order says to run each touched package's directory under the lock, and also says no whole-package-directory runs. This run followed the second instruction: it ran the named families and declared packages/app-shell to CI.",
      "Commit subjects carry '(#13113)' in prose. There are no card trailers, and the only trailers are Co-authored-by Claude and Claude-Session."
     ],
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: none, so noted only in the PR Acceptance notes. studio-design/metadataError.ts issueRefusal/IssueLocator is the general author-words refusal path. The permission editor's string error strip cannot use it without a MetadataSelection target, so this card's mapping is local. Not filed."
     ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #13167 @ 9a6cdafe39405c586cdb7ea1343c79c8ca68bd87 · domain:ui seat 2 (session_01XVp19E1MYUTFM3dnkwz8gB)

    • Clause-②: no. This is an internal metadata-admin editor; no export, prop or key moves. No contract review is owed, and the seat read the diff.
    • Seat review against the rulings:
      • (1) The autosave holds (saveHeld = celErrorCount > 0 || rlsUnnamedCount > 0) on emptiness only: a name that is not a non-blank string. The spec's name regex is not restated. The env Save button is disabled with the same reason, and the row and the status line say "give the policy a name".
      • (2) A refusal is mapped from the door's own MetadataError.issues. The one regex matches the issue path rowLevelSecurity.N.name, not the name's format. The message is the server's, through translateValidationMessage, and other issues keep the transport text.
      • (3) The held row stays in the in-memory draft. A reload discards it, as ruling 3 allows.
    • Resumed after a container restart from the pushed 9a6cdafe39. Every verification was re-run in the resumed session:
      • 5 new pins (hold, typing saves, a mapped 422, CEL-hold control, complete-row control);
      • families of 28 files / 196 tests and the i18n family;
      • two ablations, each red as intended, with restores proven by blob;
      • the full battery.
    • Changeset: @object-ui/app-shell patch.
    • CI: all 36 check-runs completed on the head, 33 passed and 3 skipped. Mergeable state: clean.
    • Path fork: no governed path, 338 changed lines. Seat landing.
    • Findings: Acceptance notes. studio-design/metadataError.ts's general author-words refusal path needs a MetadataSelection target that this editor's string strip lacks, so the mapping is local here.

    Next: ready + auto-merge; landing is verified by content on main.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB

    • Merged: PR fix(app-shell): hold an unnamed RLS policy instead of autosaving it, and word a refused policy name #13167 through the merge queue as main 427e59c9e0.
    • Verified by content: each of the 5 files the PR changes has the same blob at 427e59c9e0 as at the accepted head 9a6cdafe39. The merge message carries no model identifier.
    • Landed:
      • Studio's permission editor holds the autosave while an RLS row has no name, and says so beside the row;
      • a server refusal of a policy name reads "Policy name, on row N: …" in the author's words, not the raw schema path;
      • nothing is sent before the row is named.

    pm:dispatched is removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions