Skip to content

Commit 35eb2f0

Browse files
feat(app-shell): the locked permission matrix offers Clone to customize through the published clone_permission_set action (objectui#5987) (#10754)
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 helper `permission-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 said `patch`, the contract review at `48aaf0f068` flagged the conflict with `.changeset/README.md`'s "minor (new feature)" and the `feat(` headline as the seat's call, and the seat then ruled `minor`, correcting its own order; sibling objectui#10449 is `minor`). The lock predicate `isArtifactBackedLayer` is byte-identical to `main`; no second predicate. Why the helper (second commit): `permission-slice.authoredKeys.test.ts` scans `PermissionMatrixEditor.tsx` for `{ ...spread, key }` literals inside `setDraft(` regions and reads their keys as permission-set facets. Its `setDraft(` regex also matches the prose `funnels 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 on `main` too 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 read `objectName` / `params` as 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. - The locked editor (artifact tier is the deciding gate: `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 hint `perm.readOnly.artifact.hint` is 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. - Clicking it runs the server-published `clone_permission_set` record action on `sys_permission_set` through the console's shared action runner (`useAction()`), resolved by name off the object definition the console metadata store holds (`useMetadata().ensureType('object')` then `findCloneAction`), with the set's `sys_permission_set` row (found by `name`, the same lookup `AssignedUsersSection` uses) stashed under `params._rowRecord` and the declared `params` surfaced as `actionParams` (`buildCloneDispatch`) — the dispatch shape of `DeclaredActionsBar`. 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`). - On success the routed metadata admin navigates to `../NAME` (`relative: 'path'`, the spelling `ResourceEditPage` uses after create), where the clone loads with no code layer and is writable. An embedded host has no route and gets a `role="status"` notice naming the clone. - Item 3: a save that still reaches the environment door and is refused `403` with code `NOT_OVERRIDABLE` (matched with `errorCodeIs`, never on prose) renders `perm.save.locked` with 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/objectstack` has `node_modules` with 0 entries and no `dist` under `packages/spec`, `packages/plugins/plugin-security` or `packages/cli`; objectui's `node_modules/@objectstack` holds only `spec` (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/main` at `2bbebf5321`; `sys-permission-set.object.ts` is byte-identical at tag `@objectstack/plugin-security@17.4.0`, `git diff` empty), on the same two inputs: - Server `isArtifactBacked(type, name)` in `metadata-protocol/src/protocol.ts` is true when `lookupArtifactItem(type, name)` answers — artifact items only, the `sys_metadata` rehydration sentinel excluded, plain-key shadows ignored — or when `isNestedArtifactField` does (declared `field`-only, irrelevant to `permission`). - The layered read's `code` layer is the MetadataService copy when one exists, else `lookupArtifactItem(...) ?? registry.getItem(...)`; a `sys_metadata` row reaches that `getItem` fallback as `hydrateOverlayIntoRegistry` registered it, `_provenance: 'org'` stamped by `stateTenantAuthorship` and `_packageId` absent (`stripDerivedProvenance` strips it at write) or, on the listing path, re-stamped by `getMetaItems` from the row's `package_id` column, where the `sys_metadata` sentinel appears only on a legacy package-less row. Its `provenance` is the item's own `_provenance` stamp (`resolveLockState` → `extractProtection`, `@objectstack/spec` kernel `metadata-protection.zod.ts`). - Client `isArtifactBackedLayer(lay)` is `code` non-null AND `code._packageId !== 'sys_metadata'` AND `provenance !== 'org'`. - Case A, a code-package set: server true → `saveMetaItem` refuses the env-door PUT with `403 NOT_OVERRIDABLE` (`allowOrgOverride: false`, when `environmentId` is defined); client `code` carries the package id → locked. Agree. - Case B, a `sys_metadata`-backed package set (`managed_by: 'package'`, definition only in `sys_metadata`): server `lookupArtifactItem` (`getArtifactItem`, artifact-only) misses a DB-hydrated row → false → the `allowRuntimeCreate` tier accepts; client `code` is the hydrated item, registered with no `_packageId` and stamped `_provenance: 'org'` (`hydrateOverlayIntoRegistry` then `stateTenantAuthorship`), so `provenance !== 'org'` is false and the editor does not lock. Agree. - The data door's `classifyPackagedPermissionSet` (`packaged-permission-set-lock.ts`) decides from the same artifact rule (`declaredPackageIdOf` excludes `_envProjection` echoes and the sentinel), so all three spellings key on one fact. - Known residue, unchanged (objectui#4526): on a single-kernel host (`environmentId` undefined) 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_set` is `type: 'api'`, `method: 'POST'`, `target: '/api/v1/data/sys_permission_set'`, `bodyExtra: { active: true }`, `params`: `label` and `name` (required text), `description` and the five facets with `defaultFromRow` / `carryOver`. It is not a `script` action, so `serverActionHandler` is not its path: the console dispatches `type: 'api'` through `useConsoleActionRuntime.apiHandler`, registered on the `ActionProvider` that `GlobalActionRuntimeProvider` (ConsoleShell) wraps every route in, with `onParamCollection` seeding `defaultFromRow` params from `params._rowRecord`. The editor uses that same provider via `useAction()`. 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`, `CreateDataResponseSchema` in the installed `@objectstack/spec@17.4.0`), and `apiHandler` unwraps the `{ success, data }` envelope to `result.data`. The editor reads `result.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-parity` and `check:i18n-drift` (designer population) both green. ## H4 — item 3 Measured with the 403 mocked at the data-source boundary (`client.save` rejecting with `status: 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 set `OS_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, real `ActionProvider` with the runtime's three seams faked (api handler, param dialog, toast); adapter and metadata store faked through `@object-ui/react` with `importOriginal` spread. 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 by `git diff HEAD` empty and blob hash equal to HEAD's: - A header Clone button removed → 8 failed (primary action, zh, dispatch, opens writable, embedded notice, cancel, both no-fallback refusals). - B item-3 branch removed → 2 failed (NOT_OVERRIDABLE guidance, lowercase-code spelling). - C en hint with the secondary routes moved before cloning → 1 failed (ordering pin). - D visible guidance strip removed → 2 failed (primary-action pin, zh). - E zh `perm.clone.action` row deleted → 1 failed (zh), and `check-i18n-designer-table-parity` exit 1 naming `perm.clone.action`. - F declared params not surfaced to the runner → 2 failed (dispatch pin, cancel pin). - G clone not opened after success → 1 failed (opens-writable pin). ## 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=2` over `PermissionMatrixEditor*`, the metadata-admin i18n pins and `PackageOwdOverviewPanel.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). - Whole `packages/app-shell/src/views/metadata-admin/` directory, every test file, at `48aaf0f068`: `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`, no `error TS`. - eslint on the editor and the helper (lock-free, `--format json`, re-measured at `2f47d40422`): `0 errors`; editor 21 warnings, 20 pre-existing on unchanged lines plus one `no-explicit-any` on the new clone handler's `catch (err: any)`, the spelling `doSave` already uses; helper 0. Population = `eslint .` from `packages/app-shell` under the root flat config, which sets no `parserOptions.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. - Lock-free gates, each exit 0: `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`. - Not run locally, CI's: `pnpm lint`, the four/eight test shards, `pnpm check` (needs the CLI build). ## Serial `origin/main` fetched into a private ref before each push. At the final push (`2f47d40422`) `origin/main` is `704e05b09d`: 6 commits since the base `a2f361b8c1` by `git 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-tree` against HEAD exits 0. No merge commit needed. ## Round 2 (published text only) Head `2f47d40422`, commit `docs(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` (md5 `796364a5879f561ed0748f29b4eee335` before, `31bb4262ee708eab72e78455936b16a9` after). No source change, so the pins, ablations, type-check and directory run above hold as measured at `48aaf0f068`. - Frontmatter `'@object-ui/app-shell': patch` → `minor` (the seat's ruling after the review; see "What changed"). - The CHANGELOG-bound sentence now reads: "was never on the screen, and the metadata-door refusal a Studio save receives names only the pre-ruling remedies; the data door's refusal is the one that names the Clone action." It used to say the server's 403 refusal names the clone, which is true of the data door only. - Gates at this head, each exit 0: `check-changeset-fixed`, `check-changeset-no-major` (together `changeset: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. - The H1 Case B sentence's server half was aligned to the record's reading (`getArtifactItem` misses a DB-hydrated row) alongside the verbatim client-half replacement. ## Acceptance notes - Observation (objectstack side, not in this PR): the metadata door's artifact-tier refusal text in `saveMetaItem` still names edit-and-redeploy and `OS_METADATA_WRITABLE`, while the data door's `PackagedPermissionSetLockedError` names 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. - The 6 control pins pass on base by construction (they assert an absence); the 11 affordance pins are the red-then-green evidence. - `useAction()` falls back to a bare `ActionRunner` when no `ActionProvider` is mounted (a host that embeds the editor outside `ConsoleShell`); that fallback is the platform's existing shape for every `useAction()` consumer and is not changed here. - If the typed clone `name` collides 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. - The single-kernel residue of objectui#4526 is unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 63835d9 commit 35eb2f0

5 files changed

Lines changed: 916 additions & 4 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@object-ui/app-shell': minor
3+
---
4+
5+
feat(app-shell): a package-provided permission set offers "Clone to customize" as its primary action, and names it first (objectui#5987)
6+
7+
The permission matrix already locks a set a code package ships (the artifact
8+
tier: `isArtifactBackedLayer`, the client mirror of the server's
9+
`isArtifactBacked`), and its guidance offered only the pre-ruling remedies —
10+
edit the source artifact and redeploy, a new runtime set, the
11+
`OS_METADATA_WRITABLE` hatch. The ruled path (objectstack#11513, 「同意 第一步
12+
(创业阶段,Salesforce 式)」: lock the base, clone to customize) was never on the
13+
screen, and the metadata-door refusal a Studio save receives names only the
14+
pre-ruling remedies; the data door's refusal is the one that names the Clone
15+
action.
16+
17+
The locked editor now renders **Clone to customize** in the slot Save would
18+
occupy. It runs the `clone_permission_set` record action the server publishes
19+
on `sys_permission_set` — resolved by name off the object definition the
20+
console holds, the set's row stashed under `params._rowRecord`, the declared
21+
`params` surfaced to the runner's dialog — through the console's shared action
22+
runner, so the clone's payload, dialog and toasts are the published action's,
23+
not a copy assembled here. On the routed metadata admin the returned clone is
24+
opened (`../<name>`), where it loads with no code layer and is writable; an
25+
embedded host is told the clone's name. A server that publishes no such action,
26+
or a set with no `sys_permission_set` row, gets a refusal naming what is
27+
missing — never a fallback clone. The lock's guidance names cloning first, with
28+
the former remedies kept as secondary routes.
29+
30+
A save that still reaches the server and is refused `403 NOT_OVERRIDABLE` at the
31+
environment door now shows the same guidance beside the refusal, with the same
32+
primary action, keyed on the refusal's code rather than its prose; it used to
33+
surface the transport's sentence alone.
34+
35+
Lock predicate unchanged — no second predicate was added.

0 commit comments

Comments
 (0)