Found while implementing #11513 (PR #11702). ⛔ Deliberately not fixed there — that PR's ruled scope is the save-door lock, and this is a pre-existing defect in a neighbouring action.
What
packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts, the clone_permission_set action, declares these params:
{ name: 'label', type: 'text', required: true }
{ name: 'name', type: 'text', required: true }
{ field: 'description', defaultFromRow: true }
{ field: 'object_permissions', defaultFromRow: true }
{ field: 'field_permissions', defaultFromRow: true }
The definition a sys_permission_set row actually carries is six facets, per permissionSetRowFields():
object_permissions, field_permissions, system_permissions, row_level_security, tab_permissions, admin_scope.
The action POSTs its param values to /api/v1/data/sys_permission_set, so the four unlisted columns are simply absent from the payload. permissionSetBodyFromRow() then reads them through parseMaybeJson(undefined, …) and fills the empty default — systemPermissions: [], rowLevelSecurity: [], tabPermissions: {}, no adminScope.
So cloning a set that grants, say, setup.access produces a clone that grants nothing of the sort, with no error and no warning: the clone is created, the success toast fires, and the missing half is only discoverable by diffing the two records.
Why it matters more now than it did last week
Before the #11513 ruling, Clone was one convenience among several ways to get a customized set. The maintainer ruling of 2026-08-24 (「同意 第一步(创业阶段,Salesforce 式)」) makes cloning the only sanctioned way to customize a package-declared set — the save door now refuses the in-place edit and its refusal message tells the admin to clone. An admin following that instruction on a set with system permissions or row-level security gets a clone that is quietly missing them, having been sent there by the platform's own error message.
Not simply "add the other four"
Three of the four are mechanical omissions and one is a judgement call, which is why this is filed rather than patched:
system_permissions, row_level_security, tab_permissions — the clone should plainly carry them; the row already stores them in the same JSON-string shape the two listed columns use.admin_scope — ADR-0090 D12 delegated-admin scope. Copying a delegated-admin authority onto a new org-owned set on an admin's behalf is a privilege decision, not a field copy, and it may well be right to leave it off deliberately. Whoever takes this should rule on it rather than sweep it in with the other three.
There is also a UI-side question this repo cannot answer alone: defaultFromRow params surface as editable fields in the clone dialog, and five JSON blobs in a dialog is a different design from two. It may want a non-editable carry-over rather than five more inputs.
Repro sketch
Clone any set whose system_permissions is non-empty (member_default in the showcase app), then read the clone's system_permissions — it is [].
Not measured
No test currently pins what a clone carries. packaged-permission-set-lock.test.ts (PR #11702) pins the server half of the clone path — org-owned row, no package_id, base untouched — driving the data door directly with whatever payload the caller sends; it deliberately does not assert what the action definition chooses to send, because that was out of its ruled scope.
Related: #11513 (the ruling that makes clone the sanctioned path), PR #11702 (the server-side lock).
Found while implementing #11513 (PR #11702). ⛔ Deliberately not fixed there — that PR's ruled scope is the save-door lock, and this is a pre-existing defect in a neighbouring action.
What
packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts, theclone_permission_setaction, declares theseparams:The definition a
sys_permission_setrow actually carries is six facets, perpermissionSetRowFields():object_permissions,field_permissions,system_permissions,row_level_security,tab_permissions,admin_scope.The action POSTs its param values to
/api/v1/data/sys_permission_set, so the four unlisted columns are simply absent from the payload.permissionSetBodyFromRow()then reads them throughparseMaybeJson(undefined, …)and fills the empty default —systemPermissions: [],rowLevelSecurity: [],tabPermissions: {}, noadminScope.So cloning a set that grants, say,
setup.accessproduces a clone that grants nothing of the sort, with no error and no warning: the clone is created, the success toast fires, and the missing half is only discoverable by diffing the two records.Why it matters more now than it did last week
Before the #11513 ruling, Clone was one convenience among several ways to get a customized set. The maintainer ruling of 2026-08-24 (「同意 第一步(创业阶段,Salesforce 式)」) makes cloning the only sanctioned way to customize a package-declared set — the save door now refuses the in-place edit and its refusal message tells the admin to clone. An admin following that instruction on a set with system permissions or row-level security gets a clone that is quietly missing them, having been sent there by the platform's own error message.
Not simply "add the other four"
Three of the four are mechanical omissions and one is a judgement call, which is why this is filed rather than patched:
system_permissions,row_level_security,tab_permissions— the clone should plainly carry them; the row already stores them in the same JSON-string shape the two listed columns use.admin_scope— ADR-0090 D12 delegated-admin scope. Copying a delegated-admin authority onto a new org-owned set on an admin's behalf is a privilege decision, not a field copy, and it may well be right to leave it off deliberately. Whoever takes this should rule on it rather than sweep it in with the other three.There is also a UI-side question this repo cannot answer alone:
defaultFromRowparams surface as editable fields in the clone dialog, and five JSON blobs in a dialog is a different design from two. It may want a non-editable carry-over rather than five more inputs.Repro sketch
Clone any set whose
system_permissionsis non-empty (member_defaultin the showcase app), then read the clone'ssystem_permissions— it is[].Not measured
No test currently pins what a clone carries.
packaged-permission-set-lock.test.ts(PR #11702) pins the server half of the clone path — org-owned row, nopackage_id, base untouched — driving the data door directly with whatever payload the caller sends; it deliberately does not assert what the action definition chooses to send, because that was out of its ruled scope.Related: #11513 (the ruling that makes clone the sanctioned path), PR #11702 (the server-side lock).