fix(app-shell): package-door permission save carries every editor-authorable facet (#4302) - #4534
Merged
Merged
Conversation
…horable facet (#4302) An RLS policy authored in Studio's package door was silently discarded: Save returned 200 with a success toast and the PUT body carried no `rowLevelSecurity` key at all, so no row filter was ever persisted while the surface still showed the permission set as configured. Tab visibility and the delegated-admin scope were reverted the same way. `mergePermissionSlice` returned `{...base, name, label, isDefault, objects, fields}` — five keys from the edited draft and every other facet from a fresh server read. That whitelist had drifted behind the editor. It is now inverted: a facet the editor can author comes from `edited`, and `base` supplies only what the editor cannot author. The package scoping ADR-0086 P0 actually needs is `objects` / `fields`, which still go through the same row-level merge, so other packages' contributed rows are preserved byte-for-byte. A structural guard scans the editor sources for the keys their `setDraft(...)` updaters write and fails when the slice does not carry one, so the next facet added to the editor cannot silently drop on this door. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)
Auto-merge armed (squash). Generated by Claude Code Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 13, 2026 05:36
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4302
An RLS policy authored in Studio's package door was silently discarded: Save returned 200 with a success toast, and the PUT body carried no
rowLevelSecuritykey at all. Tab visibility and delegated-admin scope were reverted the same way.What was measured, before writing any code
(a) Why the slice exists — is the hold-back deliberate?
mergePermissionSlicearrived in 4f77044 (#2222, ADR-0086 P0) for one stated reason:objectsandfieldsaccumulate authorization rows contributed by many packages, so a package-door Save must not clobber another package's rows. The load path applies that scoping to exactly two maps —resetDraftBaseline({ ...full, objects: sliced.objects, fields: sliced.fields })— and hands every other facet to the editors unscoped. ADR-0086 agrees (§P0 "this package's own object slice"; the facet table liststabPermissionsandadminScopeas set-level metadata, not per-package slices). So holding the advanced facets back protected no one: the author edits what the panel showed them in full, and the merge discarded it. The deliberate part — other packages' rows — is preserved by the row-level merge loops, which this PR does not touch.(b) Does the server accept the dropped facets on the package door? Read-only in the objectstack sibling.
client.save(..., { mode: 'draft', packageId })reachessaveMetaItem(metadata-protocol/src/protocol.ts:8777), whose spec-conformance gate runs for draft and publish alike — it is not conditioned onmode— and resolvespermissiontoPermissionSetSchema(spec/src/kernel/metadata-type-schemas.ts:156). That schema declaresrowLevelSecurity,tabPermissionsandadminScopeas first-class optional keys (spec/src/security/permission.zod.ts:462/487/508). InsidesaveMetaItemthepackageIddecides only writability and row attribution (:9239, :9288) — there is no per-package narrowing of the body. The server accepts all three on this door, so widening the client cannot convert a silent drop into a 422. Premise holds; no server card.(c) Companion seed defect (ruling item 3) — measured NOT necessary.
PermissionAdvancedFacets.tsx:368no longer seeds the retiredprioritykey: commit 5419f55 (#4057) removed it and addedstripRetiredRlsKeysfor already-poisoned stored policies, closing objectstack#7130 (closed). The current seed is{ name: '', object: '*', operation: 'all', using: '', enabled: true }. Nothing folded in;PermissionAdvancedFacets.retiredKeys.test.tsxstays green.The fix — the drift is inverted, not extended
A facet the editor can author comes from
edited; the freshly-readbasesupplies only what the editor cannot author.objects/fieldsare authored too, but per-row — they keep going through the same scope merge, so other packages' contributions survive byte-for-byte.A key absent from
editedstill comes frombase: absence means "this caller does not model the facet", never "the author cleared it". Clearing still persists as clearing, because the facet editors always write a value — an emptied policy list is[], a present key.Structural guard is a test, not UI copy (ruling item 2):
permission-slice.authoredKeys.test.tsscans the two draft-owning sources for the keys theirsetDraft(...)updaters write, and fails when one is not carried by the slice. It carries its own anti-empty-green assertions — a scanner self-test on a fixture, plus a floor of keys the scan must find — so a scanner that goes blind reds instead of passing on an empty set. No runtime message, no i18n rows (i18n.ts untouched).Red-first evidence
Prediction written before running. Against unfixed code:
That last one is the card's own wire evidence reproduced: the key is not merely wrong, it is absent.
Two prediction deviations, declared rather than smoothed over:
EDITOR_AUTHORED_KEYSasundefined,new Set(undefined)is empty, and the failure came out as an assertion naming all 8 dropped keys. Sharper than predicted, same direction.labelassertion was red: the old code didlabel: edited.labelunconditionally, so a partialeditednulled a stored label. The presence rule fixes that too.Reverse verification (
git diffto a patch file +git checkout --, nevergit stash— shared stack): removing only the source fix reproduced exactly those 9 failures, with the environment-door guard and the ADR-0086 P0 preservation case staying green. Restored withgit apply; sha256 verified byte-identical (f66c8497...OK).Pins
rowLevelSecurity+tabPermissions+adminScope, authored through the real facet editors; a second pin covers the D6 reopen path, where the pending draft carries the policy and the published base does not.Verification
pnpm exec vitest run --maxWorkers=2over the permission family: 18 files, 137 tests passedpnpm --filter @object-ui/app-shell run type-check(both passes:tsc --noEmitandtsc -p tsconfig.test.json): greenno-explicit-anywarnings, the same pattern in the same roles as the siblingPermissionMatrixEditor.scope.test.tsxon main (6)check-control-bytesOK (plus a direct self-scan of the new files),changeset:checkOK,check-phantom-dependenciesOKdist/+tsconfig.tsbuildinfobetween builds): 415 files each way, exactly one differs —views/metadata-admin/permission-slice.d.ts.index.d.tsis byte-identical and the packageexportsmap publishes only., so the new symbol is not entry-reachable ⇒ patch (precedent fix(app-shell): organization & invitation UI translates its six English holdouts (#4474) #4496)Out of scope, filed
idis optional and three call sites omit it #4533 —CelPredicateField'sidprop is optional and three call sites omit it, so the RLS USING/CHECK editors render aLabel htmlForbound to nothing. Found while writing these pins (testing-library refusedgetByLabelText); the test selects by placeholder with a comment pointing at the card.Generated by Claude Code