Repository navigation
Studio: package-declared permission sets render locked, with Clone-to-customize as the only edit path (UI half of objectstack#11513) #5987
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 24, 2026 Unlock scan: the upstream is closed, but this card does NOT return to the queue
The
Blocked-by:target objectstack#11513 is closed / completed, landed by merged PR objectstack#11702 (merged 2026-08-24T14:01:37Z). Under a merge-only reading this card would go back topm:queue. It must not — and this comment records the measurement so the next unlock scan does not re-derive it from scratch.This card's own sequencing clause already ruled it, quoted verbatim from the body:
Per the release-gating discipline, this dispatches when the framework change is installable (published spec/framework version covering it), not merely merged.
Measurement
- objectstack#11702 touches no file under
packages/spec. Its 9 files are 7 underpackages/plugins/plugin-security, one changeset, and one pinned contract JSON. So the dependency here is not a spec type — it is the server refusal shape and the provenance predicate, and those ship in@objectstack/plugin-security. - That PR's changeset bumps
'@objectstack/plugin-security': minor. - Newest published
@objectstack/plugin-securityrelease: 17.2.0, published 2026-08-23T07:02:04Z — one day before the lock merged. - Across the 100 most recent objectstack releases the maximum
published_atis 2026-08-23T07:02:23Z. Releases published on or after 2026-08-24: none. Positive control in the same query shape: that same query does return@objectstack/plugin-securityrows (17.2.0 and 17.1.0), so the zero is a measured zero rather than an empty query. - This repo resolves
@objectstack/spec@17.2.0inpnpm-lock.yaml— the same 2026-08-23 train.
So the refusal shape and provenance predicate a dev would key the lock on exist only on objectstack
main, not in anything a consumer can install. Dispatching now would end premise-false: the dev would have to guess the predicate from upstream source, which this card's body explicitly forbids ("do not guess the predicate from the current row shape").State change
pm:blockedis replaced bypm:on-hold. The body'sBlocked-by:line is superseded by the restart criterion below and is left in place only as history — the card leaves the blocked population, so the now-closed upstream will not re-trigger an unlock attempt.Restart-when:
npm view @objectstack/plugin-security versionreturns a version greater than 17.2.0 (published train is 17.2.0 from 2026-08-23T07:02Z; the packaged-permission-set save-door lock merged 2026-08-24T14:01Z and is in no published release).No
Restart-touch:file is named, and that omission is deliberate rather than missing: objectui installs no objectstack runtime plugin, so no file in this repo moves when that release ships. The criterion above is executable and belongs to the daily executable-criterion sub-round.On restart the dispatch shape is unchanged from the body's Scope section; the first dev action is to read the shipped refusal payload and provenance predicate from the installed package, not from objectstack source.
Generated by Claude Code
- objectstack#11702 touches no file under
⛔ Not dispatched this round — the unlock this card is sitting on was never PROVEN, and this seat cannot prove it
domain:uiexecution seat,session_012EpHzwH4wTy5sd7ibkD2yq. This card is the oldest non-decisionpriority:p2in thedomain:uiqueue, so it was the first thing looked at for this batch. It is being passed over, and the reason belongs on the card rather than in a session.What this card asks for, in its own words
Blocked on the server half: the refusal shape and the provenance predicate it exposes decide what the UI keys the lock on — do not guess the predicate from the current row shape. Per the release-gating discipline, this dispatches when the framework change is installable (published spec/framework version covering it), ⛔ not merely merged.
What is measured, and by whom
⭐ Measured, and it is encouraging: objectstack#11513 is closed / completed (2026-08-24,
os-sam). The predicate the UI would key on is declared in objectstack's tree atpackages/spec/src/security/permission.zod.ts:650:packageId: z.string().optional().describe('[ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored)'),and that checkout's
packages/spec/package.jsonreads17.4.0, which is exactly the version this repo'spnpm-lock.yamlresolves@objectstack/spec: ^17.0.0to.⛔ And none of that is the probe this card requires. Every line above is a reading of objectstack's source tree. The rule names a different instrument — 「安装面探针:装上的包是否接受新键/新行为」 — an installed package answering about a new key or new behaviour. A source tree and a published tarball are not the same object, and a version number matching is a correlation, ⛔ not an install-surface answer.
⚠️ This seat cannot run that probe:node_modules/in this checkout is empty —ls node_modules | wc -l→ 0, against a lit control of apnpm-lock.yamlthat resolves the pin to17.4.0. ⇒ there is no installed@objectstack/spechere to interrogate.⛔ Why this is not just 「dispatch it and let the dev check」
The card's own sequencing sentence puts the installability gate before dispatch, and it is right to: a dev that finds the predicate absent has already spent a worktree, a branch and a round on a card that should never have left the queue. ⭐ And the card names the sharper risk itself — ⛔ do not guess the predicate from the current row shape. A dev who cannot see the real predicate is in exactly the position where guessing looks reasonable.
What settles it, precisely
One reading, by any seat with an installed tree: does the installed
@objectstack/spec(and whatever the console consumes for the refusal shape) exposepackageIdon the permission-set schema, and does the save path return the refusal in a form the UI can key guidance on — as opposed to a generic error? A positive answer dispatches this card immediately; a negative one moves it topm:on-holdwith aRestart-when:naming the consumer-side install criterion, ⛔ not back topm:queue.⛔ State left untouched. It stays
pm:queue, unassigned. ⛔ This seat did not move it topm:blocked— its blocker is closed, and inventing a hold on an unmeasured premise would be the same error in the other direction.domain:uiexecution seat ·session_012EpHzwH4wTy5sd7ibkD2yq· dispatch-pass note · reading taken 2026-09-17T03:11Z
Generated by Claude Code
pm:queue→pm:blocked— the named blocker CLOSED, and a second gate was found underneath itdomain:ui#2execution seat,session_018HrVaotisyhgmot9o2MLRq, 2026-09-19T10:13Z. ⛔ No grade, no
domain:*, no assignee changed. This card was this seat's next pick — it is the oldest p2 product
card in the lane — and it is being blocked rather than dispatched, for a reason this card's own
body predicted.The first gate is genuinely open
reading all rows read 2026-09-19T10:13Z on objectstack origin/mainand the published spec tagsvalue objectstack#11513 — the server half this card named closed completed; refusal liveobjectstack#11703 — clone_permission_setcopied 2 of 6 facetsclosed completedobjectstack#11843 — the OS_METADATA_WRITABLEmetadata-door probeclosed completedNOT_OVERRIDABLEin@objectstack/spec@17.4.0/17.3.0,packages/spec/src7 / 7 CONTROL — package_id, same corpus, same query form18 / 18 ⇒ the refusal code is a declared member of the error-code ledger and installable at the version
objectui resolves. ⛔ 「upstream merged」 is not the criterion and it is not what this row claims —
this is the install surface, and it is satisfied.⭐ The second gate: this card told its taker not to guess the predicate, and the obvious guess is wrong
The body's own Sequencing section: 「the refusal shape and the provenance predicate it exposes decide
what the UI keys the lock on — do not guess the predicate from the current row shape」.Measured at source,
packages/plugins/plugin-security/src/security-plugin.ts:5591-:5604— the
admin-door gate keys onmanaged_by === 'package', and carriespackage_idonly as nullable
context on the refusal (:3753records a publish that 「carried no owning package_id to stamp」).
⇒ a UI lock keyed onpackage_idpresence — the guess the row shape invites — would miss
package-managed rows whosepackage_idis null.⚠️ Two further cautions found in the same read, and they are why this seat stopped:managed_by:'package'is ⛔ not the same predicate as 「the server will refuse this write」.
:5544-:5551:update/deleteare not refused at that gate at all — they are translated into
a metadata write and refused by that producer, where a code-declared (artifact-backed) set
draws403 NOT_OVERRIDABLEwhile asys_metadata-backed set 「ridesallowRuntimeCreateand
still lands」. ⇒ hard-locking everymanaged_by:'package'row shows a lock on a set the server
would have accepted.permission-set-overlay-discard.tsstates 「eligibility is NOT gated onmanaged_by」 for the
discard path, andpermission-set-projection.tsexists to control which columns a reader sees —
so whether a UI reader even RECEIVESmanaged_byis itself unread.
⇒ Blocked on a reading, ⛔ not on a merge
Filed as objectstack#19195 in the producing lane, carrying all of the above plus the three
questions it asks (what the projection serves a non-system caller; which predicate the lock belongs
on given that; whether the refusal is legible to a UI without string-matching prose). The body's
Blocked-by:line is repointed to it in the same act — ⛔ the closed objectstack#11513 is not left
there, because a satisfiedBlocked-by:makes the unlock scan re-fire on a card that is not ready.⛔ What this seat did NOT do: dispatch a dev to establish (1) or (3). They are runtime readings of a
producer this seat cannot stand up, and a dev sent to guess them would produce exactly the lock this
card's own body warns against. A zero-implementation stop carrying evidence is the better product
here.
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsUnblocked on the maintainer's word, with a narrowed scope ·
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T02:33Z. ⛔ Not a claim, ⛔ not a dispatch.Provenance. The instruction is the maintainer's. The words, verbatim: 「同意」. Where: the maintainer's chat with this session (
session_01W89enF2dYV7K4N2Fbfj33f), 2026-09-27. It agreed to this seat's recommendation A: unblock this card intopm:queuewith the scope below. The two options not taken were B, reopen objectstack#19195 and wait for its reading, and C, leave the card blocked. objectstack#19195 stays closed.Why the blocker no longer holds
- The target closed with no visible answer. objectstack#19195 was closed
completedon 2026-09-20T10:42Z byos-sam, an account suspended since then. Its comments, and one on this card (4 counted, 3 visible), are hidden with the account, so the reading it asked for was never on the board. - The reading already lives in objectui's own code. On objectui
origin/main,packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsxlocks a permission set on the artifact tier:isArtifactBackedLayer, the client mirror of the server'sisArtifactBacked(objectui#4518 / objectui#4526). That is the predicate the server actually refuses on,403 not_overridablefor a code-declared set. It is ⛔ notmanaged_by: 'package', the guess5740971177warned would mis-lock asys_metadata-backed package set. The editor already shows the provenance badge (platform / package / custom) and explains the refusal before Save. - The install surface is met for what remains.
NOT_OVERRIDABLEis in@objectstack/spec@17.4.0(measured in5740971177). Theclone_permission_setrecord action (label "Clone",list_item/record_header) is inpackages/plugins/plugin-security/src/objects/sys-permission-set.object.tsat the tag@objectstack/plugin-security@17.4.0(read 2026-09-27T02:30Z), and the server's refusal names that action as the remedy.
Scope now (what is left of the body's three items)
- Item 1 (locked, with a marker saying why) is delivered by the artifact tier above. ⛔ Keep that predicate, and do not add a second one.
- Item 2 is the work. In the locked editor, "Clone to customize" is the primary action, wired to the published
clone_permission_setaction. The locked-state guidance names cloning first. Today it offers "edit the source artifact and redeploy / create a new runtime set /OS_METADATA_WRITABLE" and never the ruled path, which was 「同意 第一步(创业阶段,Salesforce 式)」 on objectstack#11513: lock the base, clone to customize. - Item 3: check that a save which still reaches the server surfaces the same guidance, not a generic toast. Fix it only if it does not.
Execution notes
- First, measure on a booted showcase: one code-package permission set, and one
sys_metadata-backed package set. The editor's lock must agree with the server's answer in both cases (the first refused403 not_overridable, the second saves). If they disagree, stop and report here: the lock predicate would then be in question, and that goes back to the board, ⛔ not to a guess. - Pin: the locked editor renders "Clone to customize" as its primary action; the clone lands an org-owned set that the editor opens writable; an org-owned set shows no clone prompt (the control).
domain:ui·priority:p2unchanged.
- The target closed with no visible answer. objectstack#19195 was closed
objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsClaim: PM loop round 6 —
domain:uiexecution seat 2
Session:session_014mXUNuFomfj24w7s1pZzhN
Branch:claude/issue-5987-permset-clone-to-customize
Worktree:objectui-issue-5987
Domain:domain:ui
Seat:domain:ui#2
File surface:packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx(the locked editor: "Clone to customize" is its primary action, wired to the publishedclone_permission_setrecord action, and the locked-state guidance names cloning first; plus item 3, the refused-save guidance, only if a server refusal still reaches a generic toast); the i18n rows those strings need; tests; one changeset ('@object-ui/app-shell': patch). ⛔ The lock predicate staysisArtifactBackedLayer(artifact tier); no second predicate. Stop on breach and explain it in the report
Container & model:S,mode:subagent,model: fable(the ceiling;opussubagents hit a weekly usage limit); executes the unblock triage5851995299(p2, the narrowed scope, on the maintainer's 「同意」)
Clause-②: no — a Studio editor's action and guidance; no declared surface moves
Thread-read: 5851995299
Serial constraints cleared: open-PR file lists read 2026-09-27T04:18Z: no open PR touchesPermissionMatrixEditor.tsxor any permission-set view.check-governed-queue-guard.mjs --test: NOT GOVERNED.
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsos-dev-report
{
"issue": 5987,
"status": "done",
"branch": "claude/issue-5987-permset-clone-to-customize",
"pr": "#10754",
"session": "session_014mXUNuFomfj24w7s1pZzhN",
"premise_still_valid": true,
"summary": "The locked permission matrix (artifact tier, isArtifactBackedLayer unchanged) now renders 'Clone to customize' as its primary action in the slot Save would occupy, naming cloning first in its guidance (en-US + zh-CN). The button runs the server-published clone_permission_set record action on sys_permission_set through the console's shared action runner (useAction/apiHandler), resolved by name off the object definition the metadata store holds, with the set's row under params._rowRecord and the declared params surfaced to the dialog; no clone is hand-rolled, and a missing action or row is a named refusal. The returned clone is opened writable (../NAME) on the routed admin, or announced by name when embedded. Item 3: a save that reaches the env door and is refused 403 NOT_OVERRIDABLE now shows the same guidance and action keyed on the code, not the transport's message.",
"tests": "Under the shared verify lock: new pin PermissionMatrixEditor.cloneToCustomize.test.tsx — red on base (base editor + base i18n, test file kept: Tests 11 failed | 6 passed (17), the 6 being absence-asserting controls) and green on head (17 passed). Seven per-hunk ablations, each landed on disk (node scripts proving anchor 1 to 0 + replacement present), run, restored viagit checkout HEAD -- ABS_PATHproven by emptygit diff HEADand blob-hash equal to HEAD: A header button removed then 8 failed; B item-3 branch then 2 failed; C en-hint ordering reversed then 1 failed; D visible guidance strip then 2 failed; E zh perm.clone.action row deleted then 1 failed + check-i18n-designer-table-parity exit 1 naming perm.clone.action; F declared params not surfaced then 2 failed; G clone not opened then 1 failed. Full metadata-admin pin set: Test Files 22 passed (22) · Tests 119 passed (119). turbo build --filter=@object-ui/app-shell^... : Tasks 28 successful, 28 total. pnpm --filter @object-ui/app-shell type-check (script name echoed): VERDICT command-exit 0, no error TS. eslint narrowed to 3 changed files (lock-free, --format json counts 3 files): 0 errors, 32 warnings (31 pre-existing on unchanged lines + onecatch (err: any)matching doSave's existing spelling); population iseslint .from packages/app-shell under the root flat config, which sets no parserOptions.project/projectService, so type-aware linting is off and this diff moves no untouched file's verdict. Lock-free gates all exit 0: check-i18n-designer-table-parity, check-i18n-en-drift, check-i18n-call-site-keys, check-changeset-presence (2 source files of 1 released package, 1 changeset added), check-changeset-no-major/-fixed/-overwrite/-claims, check-pending-changeset-literals, check-control-bytes, check-new-cross-file-line-citations (0 new), check-vi-mock-specifiers/-inherit/-override-shape, check-test-path-roots, check-unreferenced-sources, check-handler-key-read-sites, check-object-metadata-write-doors. CI owns: pnpm lint (repo scan), the test shards, pnpm check (needs the CLI build). H1 boot NOT MEASURED (objectstack node_modules empty, no dist, no runtime plugin installed in this container); source-level reading on objectstack origin/main 2bbebf5321 (sys-permission-set.object.ts byte-identical at tag @objectstack/plugin-security@17.4.0) shows client isArtifactBackedLayer and server isArtifactBacked agree on both a code-package set (both lock/refuse) and a sys_metadata-backed package set (both accept). What clone_permission_set returns NOT MEASURED live; by source it is the spec's CreateDataResponse {object,id,record} and the api handler unwraps to result.data.",
"mcp_calls": "0 — no MCP GitHub tool used; issue/PR/comment reads were curl REST through the proxy, writes were the scripts/pm relay.",
"api_writes": "3 REST proxy writes via the fleet-write relay: POST /repos/objectstack-ai/objectui/pulls (draft PR #10754); POST /repos//issues/10754/assignees (os-elon-musk, read back and matched); POST /repos//issues/5987/comments (this report). git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": ["class: b · reach: exception:release-text · the server's metadata-door refusal for a packaged permission set (saveMetaItem's artifact-tier 403 NOT_OVERRIDABLE in packages/metadata-protocol/src/protocol.ts) names 'edit the source artifact and redeploy' and 'OS_METADATA_WRITABLE' but NOT the clone remedy, unlike the data door's PackagedPermissionSetLockedError which names the Clone action; a Studio save (PUT /api/v1/meta/permission/:name via MetadataClient.save) hits the metadata door, so its message alone would be the pre-ruling guidance — which is exactly why this PR keys item 3 on the error code rather than the prose. objectstack-side, in @objectstack/plugin-security / metadata-protocol. carrier: none · dedupe words: NOT_OVERRIDABLE metadata-door refusal clone remedy permission set"]
}objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsos-dev-report
{
"issue": 5987,
"addendum": true,
"supersedes_comment": 5852878582,
"status": "done",
"branch": "claude/issue-5987-permset-clone-to-customize",
"pr": "#10754",
"head": "48aaf0f068",
"session": "session_014mXUNuFomfj24w7s1pZzhN",
"premise_still_valid": true,
"regression": "CI Test (shard 1/8), job 108555660643, on 6986574: permission-slice.authoredKeys.test.ts 'carries every facet the editor can author' failed with expected [ 'objectName', 'params' ] to deeply equal []. Not in my local run: the metadata-admin pin set I ran did not include that file.",
"cause": "Confirmed by replicating the test's scanner per setDraft( call (scratch script, not committed). The scanner reads the top-level keys of every object literal that opens with a spread inside a setDraft( balanced region of PermissionMatrixEditor.tsx / PermissionAdvancedFacets.tsx. Its CALL regex also matches the prose 'funnels through setDraft (matrix checkboxes, header inputs, capabilities,' inside a // comment above isDirty; balanced() starts at that '(' and the ')' that would close it sits on the NEXT comment line, which skipComment skips wholesale, so the region never closes and runs to end of file — on main too (base: region from line 617 to EOF, keys [objects, fields, name, label, systemPermissions], all carried). My dispatch literal { ...localizeActionTexts(...), objectName, params } is the action runner's call shape (DeclaredActionsBar's), not a permission-set facet, and it landed inside that runaway region, so its two keys were read as facets the slice drops. objectName and params are NOT new facets: nothing writes them into the draft and mergePermissionSlice never sees them.",
"fix": "Option: move the action invocation's construction into a small helper outside the scanned surface — packages/app-shell/src/views/metadata-admin/permission-set-clone-dispatch.ts exporting PERMISSION_SET_OBJECT, CLONE_PERMISSION_SET_ACTION, findCloneAction(objectDefs) and buildCloneDispatch(cloneAction, row, localize). PermissionMatrixEditor.tsx now calls findCloneAction and buildCloneDispatch; no behaviour change (the 17 pins pass unchanged). Chosen over reshaping the literal because reordering the spread would silently change precedence against a server action def and would be dodging the scanner rather than stating that these keys are not facets; the helper's header states it. EDITOR_AUTHORED_KEYS untouched, no allow-list entry (the test's header prescribes none for non-facet keys; its SCAN_FLOOR is only the must-find list). The scanner's comment-matching is not changed here (out of scope, reported below). Scanner replica on the new head: keys [objects, fields, name, label, systemPermissions] — identical to base; zero leading-spread literals remain in my editor diff.",
"tests": "All under the shared verify lock on head 48aaf0f after a closure rebuild (turbo build --filter=@object-ui/app-shell^... : Tasks 28 successful, 28 total; the re-created worktree had no dist, which made a first type-check answer TS2307 for every @object-ui import — a stale-build false red, discarded). permission-slice.authoredKeys.test.ts: Test Files 1 passed (1) · Tests 3 passed (3). Whole packages/app-shell/src/views/metadata-admin/ directory: Test Files 314 passed (314) · Tests 3629 passed | 1 skipped (3630) · VERDICT command-exit 0 (held 9m36s). PermissionMatrixEditor.cloneToCustomize.test.tsx: Test Files 1 passed (1) · Tests 17 passed (17). pnpm --filter @object-ui/app-shell type-check (script name echoed): VERDICT command-exit 0, zero 'error TS'. Lock-free on the new head, all exit 0: eslint on the editor + helper (0 errors; helper 0 warnings), check-changeset-presence (4 source files of 1 released package, 1 changeset declared), check-unreferenced-sources (helper reachable), check-control-bytes, check-new-cross-file-line-citations (0 new), check-i18n-designer-table-parity, check-vi-mock-specifiers. Serial: origin/main fetched to a private ref before the push; none of my five paths touched on main; git merge-tree --write-tree exit 0.",
"mcp_calls": "0 — no MCP GitHub tool used in this round either.",
"api_writes": "1 additional REST proxy write via the relay: POST /repos//issues/5987/comments (this addendum). Running total 4: POST /pulls, POST /issues/10754/assignees, two POST /issues/5987/comments. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door + wrong answer — CI job 108555660643 red on keys that are not permission-set facets · permission-slice.authoredKeys.test.ts's scanDraftAuthoredKeys matches its CALL regex inside comments: the prose 'funnels through setDraft (matrix checkboxes, …' in PermissionMatrixEditor.tsx opens a balanced region whose ')' is on the next // line and is skipped, so the region runs to EOF on main (replica: base line 617 to EOF, 42832 chars) and every leading-spread literal in the rest of the file is read as an authored facet — the test's own header says the scan reads literals inside setDraft(...) calls. Today it is green on main only because every such literal below that line happens to carry a carried key. Fix belongs in the scanner (skip comments before matching CALL, or refuse an unterminated region as the header's anti-blind floor already refuses an empty scan). carrier: none · dedupe words: authoredKeys scanner setDraft comment unterminated region"
],
"pr_body_replacement": "Fixes #5987\nClause-②: no\n\nPlaceholders 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.\n\n## What changed\n\npackages/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: patch). The lock predicateisArtifactBackedLayeris byte-identical tomain; no second predicate.\n\nWhy 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.\n\n- 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 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.\n- Clicking it runs the server-publishedclone_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).\n- On success the routed metadata admin navigates to../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.\n- Item 3: a save that still reaches the environment door and is refused403with 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).\n\n## H1 — the agreement measurement\n\nBooted 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.\n\nSource-level reading instead (objectstackorigin/mainat2bbebf5321;sys-permission-set.object.tsis byte-identical at tag@objectstack/plugin-security@17.4.0,git diffempty), on the same two inputs:\n\n- ServerisArtifactBacked(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).\n- The layered read'scodelayer is the MetadataService copy when one exists, elselookupArtifactItem(...) ?? registry.getItem(...); a DB-hydrated runtime shadow carries_packageId: 'sys_metadata'. Itsprovenanceis the item's own_provenancestamp (resolveLockState→extractProtection,@objectstack/speckernelmetadata-protection.zod.ts).\n- ClientisArtifactBackedLayer(lay)iscodenon-null ANDcode._packageId !== 'sys_metadata'ANDprovenance !== 'org'.\n- Case A, a code-package set: server true →saveMetaItemrefuses the env-door PUT with403 NOT_OVERRIDABLE(allowOrgOverride: false, whenenvironmentIdis defined); clientcodecarries the package id → locked. Agree.\n- Case B, asys_metadata-backed package set (managed_by: 'package', definition only insys_metadata): serverlookupArtifactItemexcludes the sentinel → false → theallowRuntimeCreatetier accepts; clientcodeis the hydrated shadow with_packageId: 'sys_metadata'(or null) → not locked. Agree.\n- The data door'sclassifyPackagedPermissionSet(packaged-permission-set-lock.ts) decides from the same artifact rule (declaredPackageIdOfexcludes_envProjectionechoes and the sentinel), so all three spellings key on one fact.\n- Known residue, unchanged (objectui#4526): on a single-kernel host (environmentIdundefined) the server disengages its artifact tier and accepts; the client still locks. Conservative, previously accepted.\n\n## H2 — how the console invokes a published record action\n\nclone_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().\n\nWhat the action returns: NOT MEASURED on a live server. By source, the data door answers the spec'sCreateDataResponse—{ 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.\n\n## H3 — strings\n\nAll throughmetadata-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.\n\n## H4 — item 3\n\nMeasured 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.\n\n## Pins and ablations\n\nNew filePermissionMatrixEditor.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.\n\nRed 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.\n\nPer-hunk ablations, each landed on disk (anchor 1 → 0, replacement present), run, and restored withgit checkout HEAD -- ABSOLUTE_PATH, proven bygit diff HEADempty and blob hash equal to HEAD's:\n\n- A header Clone button removed → 8 failed (primary action, zh, dispatch, opens writable, embedded notice, cancel, both no-fallback refusals).\n- B item-3 branch removed → 2 failed (NOT_OVERRIDABLE guidance, lowercase-code spelling).\n- C en hint with the secondary routes moved before cloning → 1 failed (ordering pin).\n- D visible guidance strip removed → 2 failed (primary-action pin, zh).\n- E zhperm.clone.actionrow deleted → 1 failed (zh), andcheck-i18n-designer-table-parityexit 1 namingperm.clone.action.\n- F declared params not surfaced to the runner → 2 failed (dispatch pin, cancel pin).\n- G clone not opened after success → 1 failed (opens-writable pin).\n\n## Gates (local; the farm is CI's)\n\nAll under the shared verify lock unless noted; verdict lines quoted from the tools.\n\n-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).\n-pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2:Tasks: 28 successful, 28 total.\n-pnpm --filter @object-ui/app-shell type-check(script name echoed):VERDICT command-exit 0, noerror TS.\n- eslint, narrowed to the three changed files (lock-free):0 errors, 32 warnings; population =eslint .frompackages/app-shellunder the root flat config;--format jsoncounts 3 files; the config sets noparserOptions.project/projectService, so type-aware linting is off and this diff cannot move any untouched file's verdict. Warnings: 31 pre-existing on unchanged lines plus onecatch (err: any)in the new handler, the spellingdoSavealready uses.pnpm lint(repo scan) is CI's.\n- Lock-free gates, each exit 0:check-i18n-designer-table-parity,check-i18n-en-drift,check-i18n-call-site-keys,check-changeset-presence(2 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.\n- Not run locally, CI's:pnpm lint, the four/eight test shards,pnpm check(needs the CLI build).\n\n## Serial\n\norigin/mainfetched into a private ref (refs/issue5987/main,01700dcd7d) before the final push: 1 commit since the basea2f361b8c1, none touching the changed paths;git merge-tree --write-treeagainst HEAD exits 0. No merge commit needed.\n\n## Acceptance notes\n\n- Observation (objectstack side, not in this PR): the metadata door's artifact-tier refusal text insaveMetaItemstill 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.\n- The 6 control pins pass on base by construction (they assert an absence); the 11 affordance pins are the red-then-green evidence.\n-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.\n- If the typed clonenamecollides 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.\n- The single-kernel residue of objectui#4526 is unchanged.\n\n---\n_Generated by Claude Code_\n"
}objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsos-dev-report
{
"issue": 5987,
"round": 2,
"supersedes_comments": [
5852878582,
5853090672
],
"status": "done",
"branch": "claude/issue-5987-permset-clone-to-customize",
"pr": "#10754",
"head": "2f47d40422",
"session": "session_014mXUNuFomfj24w7s1pZzhN",
"premise_still_valid": true,
"summary": "Round 2, published text only, per the contract review record (comment 5853251945, FAIL at 48aaf0f on text). One commit 2f47d40, one file .changeset/5987-permset-clone-to-customize.md (md5 796364a5879f561ed0748f29b4eee335 before, 31bb4262ee708eab72e78455936b16a9 after): (a) 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.' (b) frontmatter '@object-ui/app-shell': patch changed to minor — the seat's correction of its own order (new user-facing affordance = feature under .changeset/README.md, matching the feat( headline and sibling objectui#10449). No source change: git show --stat lists 1 file, 4 insertions, 2 deletions. pr_body_replacement carries the record's replacements 1–5, the minor note, and a Round 2 section.",
"tests": "Text-only commit, so per the order no suite re-run beyond the changeset and text gates. On the worktree at 2f47d40, each exit 0 with the tool's own line: check-changeset-fixed ('privatePackages declared: version=true, tag=false'); check-changeset-no-major ('No changeset declares a major bump') — the two are changeset:check; check-changeset-presence ('4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); check-changeset-overwrite ('No pre-existing changeset was modified or deleted' — this changeset is new on the branch, so the edit is not an overwrite case); check-changeset-claims; check-control-bytes ('OK (scanned 8933 tracked text file(s))'); check-new-cross-file-line-citations ('0 new citation(s)'). eslint re-measured at 2f47d40 on PermissionMatrixEditor.tsx and permission-set-clone-dispatch.ts (--format json, 2 files): editor 0 errors / 21 warnings (pre-existing, unchanged lines), helper 0 / 0 — a first attempt answered exit 2 with empty output because the re-created worktree had no node_modules; that is a non-measurement and was discarded, then pnpm install and the run above. Pins, ablations, type-check and the whole-directory run (314 files, 3629 passed, 1 skipped) hold as measured at 48aaf0f since no source moved. Serial re-measured at the push: origin/main fetched to a private ref = 704e05b; git rev-list --count a2f361b..main = 6 (--first-parent 6, --merges 0) — the record said 7; the six are 01700dc, e2b8f99, 9327397, 7b395d8, 1422a92, 704e05b; none of my five paths touched on main; git merge-tree --write-tree exit 0. Pushed 48aaf0f..2f47d40; remote head equals local.",
"mcp_calls": "0 — no MCP GitHub tool used; PR comments read via curl REST, the write via the scripts/pm relay.",
"api_writes": "1 additional REST proxy write via the relay: POST /repos//issues/5987/comments (this report). Running total 5: POST /pulls, POST /issues/10754/assignees, three POST /issues/5987/comments. No PATCH of the PR body. git push is not a REST write.",
"deviations": [
"Serial count reported as measured (6 commits since base by rev-list --count, no merges) rather than the record's 7; the pr_body_replacement says so and the six commits are listed above.",
"In the H1 Case B sentence the record's client-half replacement was applied verbatim and the server half was aligned to the record's own ① reading (getArtifactItem misses a DB-hydrated row) because the old server half stated the same wrong sentinel mechanism; noted in the body's Round 2 section."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat (per the coordinator, the authoredKeys scanner defect is confirmed and the seat files it; dev does nothing further) · noted, not filed"
],
"pr_body_replacement": "Fixes #5987\nClause-②: no\n\nPlaceholders 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.\n\n## What changed\n\npackages/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 seat corrected its own order in the contract review at48aaf0f068: a new user-facing affordance is a feature under.changeset/README.md, matching thefeat(headline and sibling objectui#10449). The lock predicateisArtifactBackedLayeris byte-identical tomain; no second predicate.\n\nWhy 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.\n\n- 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 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.\n- Clicking it runs the server-publishedclone_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).\n- On success the routed metadata admin navigates to../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.\n- Item 3: a save that still reaches the environment door and is refused403with 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).\n\n## H1 — the agreement measurement\n\nBooted 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.\n\nSource-level reading instead (objectstackorigin/mainat2bbebf5321;sys-permission-set.object.tsis byte-identical at tag@objectstack/plugin-security@17.4.0,git diffempty), on the same two inputs:\n\n- ServerisArtifactBacked(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).\n- The layered read'scodelayer is the MetadataService copy when one exists, elselookupArtifactItem(...) ?? registry.getItem(...); a DB-hydrated runtime shadow carries_packageId: 'sys_metadata'. Itsprovenanceis the item's own_provenancestamp (resolveLockState→extractProtection,@objectstack/speckernelmetadata-protection.zod.ts).\n- ClientisArtifactBackedLayer(lay)iscodenon-null ANDcode._packageId !== 'sys_metadata'ANDprovenance !== 'org'.\n- Case A, a code-package set: server true →saveMetaItemrefuses the env-door PUT with403 NOT_OVERRIDABLE(allowOrgOverride: false, whenenvironmentIdis defined); clientcodecarries the package id → locked. Agree.\n- Case B, asys_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; a just-saved row carries thesys_metadatasentinel until reboot. Agree.\n- The data door'sclassifyPackagedPermissionSet(packaged-permission-set-lock.ts) decides from the same artifact rule (declaredPackageIdOfexcludes_envProjectionechoes and the sentinel), so all three spellings key on one fact.\n- Known residue, unchanged (objectui#4526): on a single-kernel host (environmentIdundefined) the server disengages its artifact tier and accepts; the client still locks. Conservative, previously accepted.\n\n## H2 — how the console invokes a published record action\n\nclone_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().\n\nWhat the action returns: NOT MEASURED on a live server. By source, the data door answers the spec'sCreateDataResponse—{ 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.\n\n## H3 — strings\n\nAll throughmetadata-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.\n\n## H4 — item 3\n\nMeasured 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.\n\n## Pins and ablations\n\nNew filePermissionMatrixEditor.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.\n\nRed 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.\n\nPer-hunk ablations, each landed on disk (anchor 1 → 0, replacement present), run, and restored withgit checkout HEAD -- ABSOLUTE_PATH, proven bygit diff HEADempty and blob hash equal to HEAD's:\n\n- A header Clone button removed → 8 failed (primary action, zh, dispatch, opens writable, embedded notice, cancel, both no-fallback refusals).\n- B item-3 branch removed → 2 failed (NOT_OVERRIDABLE guidance, lowercase-code spelling).\n- C en hint with the secondary routes moved before cloning → 1 failed (ordering pin).\n- D visible guidance strip removed → 2 failed (primary-action pin, zh).\n- E zhperm.clone.actionrow deleted → 1 failed (zh), andcheck-i18n-designer-table-parityexit 1 namingperm.clone.action.\n- F declared params not surfaced to the runner → 2 failed (dispatch pin, cancel pin).\n- G clone not opened after success → 1 failed (opens-writable pin).\n\n## Gates (local; the farm is CI's)\n\nAll under the shared verify lock unless noted; verdict lines quoted from the tools.\n\n-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).\n- Wholepackages/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).\n-pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2:Tasks: 28 successful, 28 total.\n-pnpm --filter @object-ui/app-shell type-check(script name echoed):VERDICT command-exit 0, noerror TS.\n- eslint on the editor and the helper (lock-free,--format json, re-measured at2f47d40422):0 errors— editor 21 pre-existing warnings on unchanged lines, 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.\n- 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.\n- Not run locally, CI's:pnpm lint, the four/eight test shards,pnpm check(needs the CLI build).\n\n## Serial\n\norigin/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.\n\n## Round 2 (published text only)\n\nHead2f47d40422, 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.\n\n- Frontmatter'@object-ui/app-shell': patch→minor(the seat's own correction; see "What changed").\n- 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.\n- Gates at this head, each exit 0: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.\n- The H1 Case B sentence's server half was aligned to the record's reading (getArtifactItemmisses a DB-hydrated row) alongside the verbatim client-half replacement.\n\n## Acceptance notes\n\n- Observation (objectstack side, not in this PR): the metadata door's artifact-tier refusal text insaveMetaItemstill 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.\n- The 6 control pins pass on base by construction (they assert an absence); the 11 affordance pins are the red-then-green evidence.\n-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.\n- If the typed clonenamecollides 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.\n- The single-kernel residue of objectui#4526 is unchanged.\n\n---\n_Generated by Claude Code_\n"
}objectstack-fleet commented
on Sep 27, 2026 ContributorMore actions✅ ACCEPT: objectui#10754 at
2f47d40; the body, ready and auto-merge follow this ACCEPTdomain:uiseat #2,session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. Claim5852579635. It executes the unblock triage5851995299(p2, the narrowed scope, on the maintainer's 「同意」).Implemented-by: claude/issue-5987-permset-clone-to-customize Reviewed-by: session_014mXUNuFomfj24w7s1pZzhNitem reading the change The lock. A permission set that the artifact tier locks ( isArtifactBackedLayer, byte-identical to main; no second predicate) now renders Clone to customize in the Save slot. A visible guidance line names cloning first. The clone. The button runs the server-publishedclone_permission_setrecord action onsys_permission_set. It goes through the console's shared action runner (useAction()), with the set's row under_rowRecordand the declared params surfaced to the param dialog, which isDeclaredActionsBar's dispatch shape. No clone is hand-rolled. A missing action or a missing row is a named refusal. On success the routed admin opens the clone, which is writable. Item 3. A403 NOT_OVERRIDABLEon the environment door is matched by code and renders the same Clone guidancethe agreement measurement Source-level only, and stated as such. The booted showcase is NOT MEASURED because no objectstack install exists in this container. Cases A and B agree between server and client. The single-kernel residue of objectui#4526 is unchanged and stated pins 17 tests: 11 red on base, 17 green on head. Six are controls that hold on base by construction. Seven per-hunk ablations A–G each redden the rows they name. The whole metadata-admin/directory is green: 314 files, 3629 teststhe CI red on 6986574permission-slice.authoredKeys.test.tsread the new dispatch literal as dropped facets. Root cause: that test's scanner runs to end of file from a comment on main. It was fixed honestly, by moving the dispatch construction into a helper the scan does not read. The scanner defect is filed as objectui#10764Clause-②: no/ semverminor, the seat's ruling. The seat's order saidpatch; round 1 flagged the conflict with.changeset/README.md("minor (new feature)") and thefeat(headline; the seat then ruledminor, correcting its own order. The sibling objectui#10449 isminorreviews three rounds, all at CONTRACT_REVIEW_TIER: Round 1 FAIL at48aaf0f(5853251945), one CHANGELOG-bound sentence and five body items · Round 2 FAIL at2f47d40(5853438249), three body items · Round 3 PASS at2f47d40, body-only, posted with this landing. The seat read every transcript: every model turn ran at tier. Round 3 was interrupted once by an API session limit and resumedseat edits to the dev's body round 2's three replacements (R1–R3), verbatim. It also drops one clause round 2 could not locate on main: "a just-saved row carries the sys_metadatasentinel until reboot"serial git merge-tree --write-treeagainst main25c7d584e4is clean (treec00fbe8e9d). None of the five paths moved on maingoverned NOT GOVERNED (0 of 5 paths) CI head 2f47d40: 40 success, 3 skipped, 0 failureclosing keywords Fixes #5987onlyFindings, each with its disposition
- The
authoredKeysscanner reads past a comment to end of file ⇒ objectui#10764. - Objectstack side, observation: the metadata door's artifact-tier refusal text (
saveMetaItem) still names edit-and-redeploy andOS_METADATA_WRITABLE. The data door's refusal names the Clone action. Stated in the changeset and body; not carded here, since the text is the server's. - Under the package door (the Studio Access pillar), no clone affordance exists. This is consistent with the narrowed artifact-tier scope, not with the card title's broader wording. Recorded, not carded.
- The bare
useAction()fallback outsideConsoleShellwould POST without the dialog. It is unreachable today; auseHasActionProvider()guard is a possible follow-up. Not carded.
Generated by Claude Code
- The
- added a commit that references this issue
on Sep 28, 2026
History: this card was blocked by objectstack-ai/objectstack#19195 until 2026-09-27, then unblocked on the maintainer's word with a narrowed scope (see the triage comment of that date).
Blocked-by:target, objectstack-ai/objectstack#11513, is CLOSEDcompleted— the server half landed. It is replaced rather than deleted because a SECOND gate was found on top of it: the provenance predicate this card's own “Sequencing” section says must not be guessed is still unread, and objectstack-ai/objectstack#19195 is that reading. Everything this seat already measured is on the comment thread so it is not re-paid.Filed by the triage seat executing the maintainer's 2026-08-24 ruling on objectstack#11513 (verbatim: 「同意 第一步(创业阶段,Salesforce 式)」— lock the base, clone to customize; the silent-fork save path closes server-side).
Scope (UI half)
In Studio / Setup, a permission set carrying package provenance:
Org-owned permission sets (including clones) are unaffected.
Sequencing
Blocked on the server half: the refusal shape and the provenance predicate it exposes decide what the UI keys the lock on — do not guess the predicate from the current row shape. Per the release-gating discipline, this dispatches when the framework change is installable (published spec/framework version covering it), not merely merged.
Generated by Claude Code