Repository navigation
feat(app-shell): the locked permission matrix offers Clone to customize through the published clone_permission_set action (objectui#5987) - #10754
Conversation
…ize" through the published clone_permission_set action (objectui#5987) A permission set a code package ships is locked by the artifact tier (`isArtifactBackedLayer`, unchanged). Its guidance offered only the pre-ruling remedies; the ruled path — lock the base, clone to customize — was never on the screen, although the server's 403 refusal names it. - The locked editor renders "Clone to customize" in the slot Save would occupy and names cloning first in its guidance (en-US and zh-CN rows). - The button runs the `clone_permission_set` record action published on `sys_permission_set`: resolved by name off the object definition the console holds, the set's row stashed under `params._rowRecord`, the declared params surfaced to the runner's dialog, dispatched through the shared console action runner. No clone is assembled from create/update calls; a missing action or row is a named refusal. - The returned clone is opened (`../name`) on the routed metadata admin, where it loads with no code layer and is writable; an embedded host is told the clone's name. - A save that still reaches the environment door and is refused 403 NOT_OVERRIDABLE shows the same guidance and action, keyed on the code, beside the server's sentence. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…itor surface (objectui#5987)
CI Test (shard 1/8) failed `permission-slice.authoredKeys.test.ts` on the
first commit: `expected [ 'objectName', 'params' ] to deeply equal []`.
The scanner reads `{ ...spread, key }` literals inside `setDraft(` regions of
`PermissionMatrixEditor.tsx` as facets written into the permission-set draft.
Its `setDraft(` regex also matches the prose `funnels through setDraft (matrix
checkboxes, …` in a `//` comment, and the `)` that would close that region is
on the next comment line, which the walker skips wholesale — so that region
runs to end of file (on main too) and every leading-spread literal below it is
counted. The clone dispatch literal (`{ ...localized, objectName, params }`)
is the action runner's call shape, not a permission-set facet, and it landed
inside that region.
The construction now lives in `permission-set-clone-dispatch.ts`
(`findCloneAction`, `buildCloneDispatch`), which the scan does not read. No
behaviour change; the slice's carried-key list is untouched. The scanner's
comment-matching is reported separately, not changed here.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Head moved mid-review from ① Derived judgments
② Semver level
Changeset body, sentence by sentence: lock predicate, clone-through-published-action, refusals-not-fallbacks, item 3 and "no second predicate" are all true. One sentence is wrong for the door this editor hits: "The ruled path … was never on the screen, although the server's own Replacement body
③ Boundary flags
Implemented-by: VERDICT: FAIL Code and pins pass at |
…se refusal lacks the clone remedy (objectui#5987) Contract review at 48aaf0f (published text only): - `'@object-ui/app-shell': minor` — a new user-facing affordance is a feature under .changeset/README.md, matching the `feat(` headline; the dispatch order's `patch` was the seat's own correction. - The CHANGELOG-bound sentence no longer says the server's 403 refusal names the clone: only the data door's PackagedPermissionSetLockedError does; the metadata-door refusal a Studio save receives names the pre-ruling remedies. Prose and frontmatter only; no source change. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Round 2, narrow: the delta, the changeset, the r3 body ( ① Derived judgments
② Semver level
Replacements 1 to 5 from round 1: all applied, verified by R1, required. H1 code-layer bullet still states the superseded mechanism: "a DB-hydrated runtime shadow carries R2, required. Gates: "editor 21 pre-existing warnings on unchanged lines" is false in its qualifier. The head editor adds R3, required. "What changed": "the seat corrected its own order in the contract review at Optional, not a stop: the Serial paragraph may add "main has since moved to ③ Boundary flags
Implemented-by: VERDICT: FAIL Text only, again: the delta, the changeset (level and sentence), CI, merge-tree, governed check and the pending changesets all pass. The stop is the r3 body: apply R1 (the code-layer bullet still states the sentinel mechanism and contradicts Case B), R2 (one editor warning is on a new line, not "pre-existing on unchanged lines") and R3 (the |
Contract reviewServed-tier: Round 3, body-only: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS The landing body describes the head that lands: the five edits are round 2's text verbatim and true at source, nothing else in the body contradicts Case B, the eslint reading or the ruling's order, and CI, merge-tree against |
Fixes #5987
Clause-②: no
Placeholders in this body are written as capitalised words (NAME, PR_NUMBER) rather than in angle brackets, because the body sanitizer strips angle-bracket shapes at rest.
What changed
packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx,i18n.ts(new rows only, plus one rewritten hint), a small helperpermission-set-clone-dispatch.ts(resolves the published action off the object definitions and shapes the runner dispatch), one new pin file, one changeset (@object-ui/app-shell: minor; the dispatch order saidpatch, the contract review at48aaf0f068flagged the conflict with.changeset/README.md's "minor (new feature)" and thefeat(headline as the seat's call, and the seat then ruledminor, correcting its own order; sibling objectui#10449 isminor). The lock predicateisArtifactBackedLayeris byte-identical tomain; no second predicate.Why the helper (second commit):
permission-slice.authoredKeys.test.tsscansPermissionMatrixEditor.tsxfor{ ...spread, key }literals insidesetDraft(regions and reads their keys as permission-set facets. ItssetDraft(regex also matches the prosefunnels through setDraft (matrix checkboxes, …in a//comment, and the)that would close that region sits on the next comment line, which the walker skips wholesale — so onmaintoo that region runs to end of file and every leading-spread literal below it is counted. My dispatch literal ({ ...localized, objectName, params }, the action call's argument shape, not a facet) landed inside it and CI readobjectName/paramsas dropped facets. Moving the dispatch construction into the helper, which the scan does not read, is the honest fix: the keys were never facets, and the slice's carried-key list is untouched. The scanner defect itself is reported in the dev report, not changed here.lockedByArtifactTier) renders Clone to customize in the header slot Save would occupy, and a visible guidance line (role="note") that names cloning first. The badge hintperm.readOnly.artifact.hintis rewritten so cloning comes first and the former remedies (edit the source artifact and redeploy, a new runtime set,OS_METADATA_WRITABLE) follow as secondary routes; the server's reason and code stay in it.clone_permission_setrecord action onsys_permission_setthrough the console's shared action runner (useAction()), resolved by name off the object definition the console metadata store holds (useMetadata().ensureType('object')thenfindCloneAction), with the set'ssys_permission_setrow (found byname, the same lookupAssignedUsersSectionuses) stashed underparams._rowRecordand the declaredparamssurfaced asactionParams(buildCloneDispatch) — the dispatch shape ofDeclaredActionsBar. No clone is assembled from create/update calls: an object that publishes no such action, or a set with no row, is a named refusal (perm.clone.actionMissing,perm.clone.rowMissing).../NAME(relative: 'path', the spellingResourceEditPageuses after create), where the clone loads with no code layer and is writable. An embedded host has no route and gets arole="status"notice naming the clone.403with codeNOT_OVERRIDABLE(matched witherrorCodeIs, never on prose) rendersperm.save.lockedwith the same Clone button and keeps the server's sentence underneath, instead of the transport's message alone. Env door only (!packageId).H1 — the agreement measurement
Booted showcase: NOT MEASURED. Blocker, measured in this container:
/home/user/objectstackhasnode_moduleswith 0 entries and nodistunderpackages/spec,packages/plugins/plugin-securityorpackages/cli; objectui'snode_modules/@objectstackholds onlyspec(17.4.0), no runtime plugin. A showcase boot needs a full objectstack install plus a workspace build under the shared verify lock, which is outside this card's objectui-only file surface and the foreground cap. Not guessed.Source-level reading instead (objectstack
origin/mainat2bbebf5321;sys-permission-set.object.tsis byte-identical at tag@objectstack/plugin-security@17.4.0,git diffempty), on the same two inputs:isArtifactBacked(type, name)inmetadata-protocol/src/protocol.tsis true whenlookupArtifactItem(type, name)answers — artifact items only, thesys_metadatarehydration sentinel excluded, plain-key shadows ignored — or whenisNestedArtifactFielddoes (declaredfield-only, irrelevant topermission).codelayer is the MetadataService copy when one exists, elselookupArtifactItem(...) ?? registry.getItem(...); asys_metadatarow reaches thatgetItemfallback ashydrateOverlayIntoRegistryregistered it,_provenance: 'org'stamped bystateTenantAuthorshipand_packageIdabsent (stripDerivedProvenancestrips it at write) or, on the listing path, re-stamped bygetMetaItemsfrom the row'spackage_idcolumn, where thesys_metadatasentinel appears only on a legacy package-less row. Itsprovenanceis the item's own_provenancestamp (resolveLockState→extractProtection,@objectstack/speckernelmetadata-protection.zod.ts).isArtifactBackedLayer(lay)iscodenon-null ANDcode._packageId !== 'sys_metadata'ANDprovenance !== 'org'.saveMetaItemrefuses the env-door PUT with403 NOT_OVERRIDABLE(allowOrgOverride: false, whenenvironmentIdis defined); clientcodecarries the package id → locked. Agree.sys_metadata-backed package set (managed_by: 'package', definition only insys_metadata): serverlookupArtifactItem(getArtifactItem, artifact-only) misses a DB-hydrated row → false → theallowRuntimeCreatetier accepts; clientcodeis the hydrated item, registered with no_packageIdand stamped_provenance: 'org'(hydrateOverlayIntoRegistrythenstateTenantAuthorship), soprovenance !== 'org'is false and the editor does not lock. Agree.classifyPackagedPermissionSet(packaged-permission-set-lock.ts) decides from the same artifact rule (declaredPackageIdOfexcludes_envProjectionechoes and the sentinel), so all three spellings key on one fact.environmentIdundefined) the server disengages its artifact tier and accepts; the client still locks. Conservative, previously accepted.H2 — how the console invokes a published record action
clone_permission_setistype: 'api',method: 'POST',target: '/api/v1/data/sys_permission_set',bodyExtra: { active: true },params:labelandname(required text),descriptionand the five facets withdefaultFromRow/carryOver. It is not ascriptaction, soserverActionHandleris not its path: the console dispatchestype: 'api'throughuseConsoleActionRuntime.apiHandler, registered on theActionProviderthatGlobalActionRuntimeProvider(ConsoleShell) wraps every route in, withonParamCollectionseedingdefaultFromRowparams fromparams._rowRecord. The editor uses that same provider viauseAction().What the action returns: NOT MEASURED on a live server. By source, the data door answers the spec's
CreateDataResponse—{ object, id, record }— with status 201 (runtime/src/domains/data.ts,CreateDataResponseSchemain the installed@objectstack/spec@17.4.0), andapiHandlerunwraps the{ success, data }envelope toresult.data. The editor readsresult.data.record.name; when that is absent it says so (perm.clone.noName) rather than guessing.H3 — strings
All through
metadata-admin/i18n.ts, en-US and zh-CN rows. Rewritten:perm.readOnly.artifact.hint. New:perm.clone.action,perm.clone.hint,perm.clone.guidance,perm.clone.actionMissing,perm.clone.rowMissing,perm.clone.noName,perm.clone.done,perm.save.locked.check:i18n-designer-parityandcheck:i18n-drift(designer population) both green.H4 — item 3
Measured with the 403 mocked at the data-source boundary (
client.saverejecting withstatus: 403, code: 'NOT_OVERRIDABLE'). Before: the red strip rendered the transport's message. For the metadata door that message (saveMetaItem's artifact-tier refusal) says to edit the source artifact and redeploy or setOS_METADATA_WRITABLE— the pre-ruling remedies, never the clone — so it was the generic form. Changed as described above; the pin and its controls are below.Pins and ablations
New file
PermissionMatrixEditor.cloneToCustomize.test.tsx, 17 tests, realActionProviderwith the runtime's three seams faked (api handler, param dialog, toast); adapter and metadata store faked through@object-ui/reactwithimportOriginalspread.Red on the base editor plus base i18n (
a2f361b8c1, the test file kept):Tests 11 failed | 6 passed (17)— the 6 that pass on base are the controls (org-owned / sentinel / type-locked / host-locked show no clone prompt; other refusals stay generic), which hold on base by construction. Head:17 passed.Per-hunk ablations, each landed on disk (anchor 1 → 0, replacement present), run, and restored with
git checkout HEAD -- ABSOLUTE_PATH, proven bygit diff HEADempty and blob hash equal to HEAD's:perm.clone.actionrow deleted → 1 failed (zh), andcheck-i18n-designer-table-parityexit 1 namingperm.clone.action.Gates (local; the farm is CI's)
All under the shared verify lock unless noted; verdict lines quoted from the tools.
pnpm exec vitest run --maxWorkers=2overPermissionMatrixEditor*, the metadata-admin i18n pins andPackageOwdOverviewPanel.readDecorationStrip:Test Files 22 passed (22) · Tests 119 passed (119)after the one selector fix (first run 118/119; the zh pin matched two elements).packages/app-shell/src/views/metadata-admin/directory, every test file, at48aaf0f068:Test Files 314 passed (314) · Tests 3629 passed | 1 skipped (3630)(VERDICT command-exit 0).pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2:Tasks: 28 successful, 28 total.pnpm --filter @object-ui/app-shell type-check(script name echoed):VERDICT command-exit 0, noerror TS.--format json, re-measured at2f47d40422):0 errors; editor 21 warnings, 20 pre-existing on unchanged lines plus oneno-explicit-anyon the new clone handler'scatch (err: any), the spellingdoSavealready uses; helper 0. Population =eslint .frompackages/app-shellunder the root flat config, which sets noparserOptions.project/projectService, so type-aware linting is off and this diff cannot move any untouched file's verdict.pnpm lint(repo scan) is CI's.check-i18n-designer-table-parity,check-i18n-en-drift,check-i18n-call-site-keys,check-changeset-presence(4 source file(s) of 1 released package(s) changed ... declares 1 changeset(s)),check-changeset-no-major,check-changeset-fixed,check-changeset-overwrite,check-changeset-claims,check-pending-changeset-literals,check-control-bytes,check-new-cross-file-line-citations(0 new citation(s)),check-vi-mock-specifiers,check-vi-mock-inherit,check-vi-mock-override-shape,check-test-path-roots,check-unreferenced-sources,check-handler-key-read-sites,check-object-metadata-write-doors.pnpm lint, the four/eight test shards,pnpm check(needs the CLI build).Serial
origin/mainfetched into a private ref before each push. At the final push (2f47d40422)origin/mainis704e05b09d: 6 commits since the basea2f361b8c1bygit rev-list --count(no merges; the review record counted 7 — the six are listed in the dev report), none touching the changed paths;git merge-tree --write-treeagainst HEAD exits 0. No merge commit needed.Round 2 (published text only)
Head
2f47d40422, commitdocs(changeset): grade Clone to customize minor and name the door whose refusal lacks the clone remedy (objectui#5987), one file:.changeset/5987-permset-clone-to-customize.md(md5796364a5879f561ed0748f29b4eee335before,31bb4262ee708eab72e78455936b16a9after). No source change, so the pins, ablations, type-check and directory run above hold as measured at48aaf0f068.'@object-ui/app-shell': patch→minor(the seat's ruling after the review; see "What changed").check-changeset-fixed,check-changeset-no-major(togetherchangeset:check),check-changeset-presence(4 source file(s) of 1 released package(s) ... declares 1 changeset(s)),check-changeset-overwrite(No pre-existing changeset was modified or deleted— this changeset is new on this branch),check-changeset-claims,check-control-bytes,check-new-cross-file-line-citations(0 new citation(s)); eslint and Serial as re-measured above.getArtifactItemmisses a DB-hydrated row) alongside the verbatim client-half replacement.Acceptance notes
saveMetaItemstill names edit-and-redeploy andOS_METADATA_WRITABLE, while the data door'sPackagedPermissionSetLockedErrornames the Clone action. The Studio editor hits the metadata door, which is why item 3 had to key on the code. Reported in the dev report for the seat, not filed here.useAction()falls back to a bareActionRunnerwhen noActionProvideris mounted (a host that embeds the editor outsideConsoleShell); that fallback is the platform's existing shape for everyuseAction()consumer and is not changed here.namecollides with a package-declared set, the data door's insert leg refuses it; that refusal is the published action's own and reaches the user through the runner's error toast, unchanged.Generated by Claude Code