Repository navigation
runtime: the permission-set clone door lands "new similar" in a writable target package, by default the source package's <base>.custom, never package-less #28563
Description
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
Filing gate: ④ child of epic #28236 (P2, cut 2026-11-01 on the maintainer's 「四项提速措施全部按推荐执行」, schedule reviewed; the review split N-20 into this server half and #28573's UI) · plan of record: ADR-0146 Rollout P2 "N-20 permission set 'new similar' (D6)". ⛔ Not a claim.
Blocked-by: #28407
Part of #28236 (P2, child N-20a) · governs: ADR-0146 D6 ("a customer's own set is a new set in .custom"), D3 ("It receives … permission sets for the base"), ADR-0126 §7.1 · lane: domain:server (packages/runtime) · size: S–M.
Business reason. An administrator who wants "HotCRM Sales Rep, but without delete" copies HotCRM's set under a new name. Today the copy belongs to no package, so it never exports with the customer's other HotCRM changes. It should land in hotcrm.custom, the package that holds everything the customer changed about HotCRM, as a new set beside the vendor's, never as a replacement of it.
Measured (35b178bd0a).
runtime/src/domains/permission-set-clone.ts,POST /security/permission-sets/:name/clone:- the body is closed
{ name, label }; - step 6 saves through
saveMetaItem, create-only, "environment-wide, no package"; PERMISSION_SET_CLONE_DROPPED_KEYSdrops the envelope,packageId,managedBy,protection,adminScopeandisDefault;- authority is
metaWriteCapabilityVerdict, thenwalledCatalogWriteVerdict(ADR-0131 D3).
- the body is closed
metadata-protocol/src/protocol.ts:1210#permissionSetOwnerBindingRefusalrefuses a bodypackageIdthat disagrees with the row's binding. The clone dropspackageId, so a.custombinding passes.packages/plugins/plugin-security/src/packaged-permission-set-lock.ts:286–341: the locked-set sentence prescribes this door.- Callers: the console's
views/metadata-admin/permission-set-clone.ts(server-only door, no SDK method; Studio: "New similar" on a base permission set — the copy lands where the clone door puts it (<base>.custom) and opens in Studio; Setup's clone button reads the same #28573). - No spec request schema:
git grep PermissionSetClone packages/spec/srcfinds nothing. Not verified whether the route ledger lists the door. - Not verified: that the security catalog serves a permission set bound to a writable package the way it serves a package-less one.
Wanted.
- The door takes an optional target
package, in the shape of the flow door (Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149):- Given: it must be an installed, writable package inside whose dependency closure the set's objects lie; otherwise a worded refusal in Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149's family.
- Omitted, source shipped by package X: the target is X's
<X>.custom, ensured through N-13's door (metadata-protocol: the <base>.custom ensure door — the one idempotent creation path for the paired package, explicit <base namespace>_custom, walled 403 #28407), the only creation path. - Omitted, source authored in the environment (no package): the save stays as today. D6 charters new sets for a base, not a package for every set.
- The copy is a new set: the new-name rule and its 409 stay. A set in
.customunder the base set's name stays refused (D6, N-09's excluded-type refusal, metadata-protocol: the override write gate — only the base's paired.custommay declare; undeclared same names and a dependent'soverridesare refused at publish,os validateand install #28410). - The copy is assignable where any set is: positions and user assignment resolve it by bare name (ADR-0131 D3/D4).
- The response names the package the copy landed in, and the notice says so in the server's words.
- Walled postures: the ensure door's 403 and the clone door's walled refusal both stand, so a tenant administrator gets the walled sentence.
- Contract: if the route ledger declares a request schema,
packageenters it Zod-first. The PR fillsClause-②(widening) andMetadata-protocolhonestly; an API body key is not metadata.
Tests.
- The default lands in
X.custom: the row'spackage_idis the pair, with nopackageIdin the body. The pair is created when absent and reused when present. - An explicit writable target lands there; read-only, unknown and out-of-closure targets are refused, leaving no row.
- An environment-authored source is unchanged (control).
- The copy is granted through a position and through a direct assignment.
- A walled tenant administrator is refused; a taken name gets 409.
permission-set-clone.test.tsand the dogfoodpermission-set-clone-door.dogfood.test.tsare re-judged.
Release safety. A release right after this card changes where Setup's Clone puts a copy of a packaged set: the paired .custom, created on demand. The console needs no change because the response only gains a key. N-13 (#28407) in Blocked-by: makes the pair creatable.
Not here. The Studio / Setup "new similar" UI (#28573). The flow clone door (#28149). Overriding a permission set (D6: never). Uninstalling a pair that holds an assigned set (N-14, landed).
Dedupe. #22881 (closed, the door itself). Searched permission set clone package (1,297 hits; on topic only #28236 and #22881), permission set new similar (42 hits, none a card for this) and clone door target package is:open (#28149, #28407). No card retargets this door.
Links back: #28236, #28149, #28407, #22881, ADR-0146 D3, D6, ADR-0126 §7.1, ADR-0131 D3.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 1, 2026
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
2026-11-01T06:51Z
Unblocked · p1 ·
pm:queue(epic stall patrol)⛔ Not a claim.
Blocked-by: #28407(N-13, the.customensure door). metadata-protocol: the <base>.custom ensure door — the one idempotent creation path for the paired package, explicit <base namespace>_custom, walled 403 #28407 is closed as completed (closed 2026-11-01T06:07Z).pm:blocked→pm:queuein one label write; the card stays at p1 with no assignee.mainas the only way to create<base>.custom; ⛔ no second creation path.objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Claim: PM loop round 4 · card thread · 2026-11-01T07:26Z
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5khiLzZmecWFfjSZLcAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-nqql2u(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. Also the maintainer, 2026-11-01T06:40Z: 「任务很多啊 is:issue state:open label:pm:queue label:domain:server」. Epic #28236 direction, the maintainer (quoted on the epic): 「方向定为包级覆盖:代码和 Studio 用同一机制,定制都进 .custom。」 · 「等我拍板的 8 个问题,全部同意」. Unblock
6554636923: the dev builds on N-13's door onmainas the only way to create<base>.custom; ⛔ no second creation path. One target-package check shared with #28149 (claimed by this seat, 6554908823): ⛔ no second spelling; whichever PR lands first owns the helper.File surface:
packages/runtime/src/domains/permission-set-clone.ts(the door's body, target resolution and response), a shared target-package check with #28149 if one is agreed, the route-ledger entry and apackages/specrequest schema only if the ledger declares one, tests (permission-set-clone.test.ts, the dogfoodpermission-set-clone-door.dogfood.test.ts) and changesets. Calls N-13'sensureCustomizationPackage(packages/metadata-protocol) without re-implementing it. Not the console (#28573).Container & model:
M,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: highClause-②: yes (widening — the clone door's body gains an optional target
packageand its response names the landed package; a packaged source's default target moves to<X>.customand bad targets are refused, so the dev declares a narrowing arm too ifCLAUSE2_ARMSreads it that way; the seat runs the contract review owed before the queue)Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the code is the permission-set clone door · the platform path is ADR-0146 D3/D6's paired
.custompackage via N-13's ensure door, under ADR-0131 D3 (catalog write authority) · reached by an administrator copying a packaged permission set from SetupThread-read: none
Premise: holds (read 2026-11-01T07:26Z at
origin/main:permission-set-clone.tssaves create-only and package-less and dropspackageId; N-13's ensure door is onmainsince #28407 closed)Serial constraints cleared: #28407 (N-13) closed as completed; #28149 runs in parallel on the same target rule (coordinated per the order); open PR #28554 (#28410) edits
packages/metadata-protocol/src/protocol.tsandprotocol-packaged-base-refusal.ts, so the dev reads its diff, does not edit its hunks, and mergesorigin/mainonce if it lands firstResume-from: none — fresh from
origin/mainState: →
pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_01FEkQ5khiLzZmecWFfjSZLc
os-dev-report
{
"issue": 28563,
"status": "done",
"branch": "claude/project-thread-nqql2u",
"pr": "#28636",
"session": "session_01FEkQ5khiLzZmecWFfjSZLc",
"premise_still_valid": true,
"summary": "POST /security/permission-sets/:name/clone takes an optional
packageand answers{ permissionSet, package, notice }. Omitted: a set shipped by read-only package X lands in X.custom through N-13's protocol.ensureCustomizationPackage (the only creation path; created on first copy, reused after), a set in a writable package lands there, an environment-authored set stays package-less. Given: the shared new-similar target rule (#28149's file new-similar-target.ts, byte-identical, plus anobjectslimb via narrowObjectsToPackageClosure) refuses an unknown/read-only target 422 WRITABLE_PACKAGE_REQUIRED and one whose closure misses the source package or a granted object 409 RESOURCE_CONFLICT, before any write. The save binds through saveMetaItem's own packageId; the item never carries packageId. Head 40a3731.","tests": "Red first: dogfood permission-set-clone-door against base cd065d3 door, 4 of 10 red (
expected undefined to be 'com.example.showcase.custom'x2, env control had nopackage, target case). Green: dogfood file 11/11 on head 40a3731 (pair 404 before / 200 after, sys_metadata.package_id = pair, metadata has no packageId, catalog packageId = pair, reuse = one pair in GET /packages, position grant + direct sys_user_permission_set grant resolve the pair-bound copy, member_default lands in its own pair, target land + 422 read-only/unknown + 409 out-of-closure with no row, env control package null). Unit permission-set-clone.test.ts 35/35. Runtime: typecheck exit 0; test 502/502 files, 8170 passed | 19 skipped. Ablations via scripts/ablation-replace.mjs on src (no build between), each restored blob == HEAD: ensure bypass 3 red; objects limb off 1 red (re-run on final code: 1 red); source-package limb off was GREEN on first run (fixture also tripped the objects limb), added the crm-ships-sys_user-only case, re-run 1 red.","gates": "dispatch-gates --ran on 40a3731:
✓ dispatch-gates --ran: 124 derived famil(ies) accounted for — 124 run, 0 NOT-MEASURED(all exit 0; check:type-check-debt exit 0 under os-verify-lock,0 raw tsc error(s) total). Reds met and fixed on the way: check:route-envelope (new domains file undeclared -> DISPATCHER_DOMAINS line, same as #28149), check:test-source-alias (vitest alias to lint/src/runtime.ts, byte-identical to #28149), check:system-context-census (objects limb read isSystem -> hands the object through whole). check-platform-checklist --changed-from origin/main exit 0; check-commit-card-trailers exit 0; check-nul-bytes exit 0. PR CI: started at open, check-runs not concluded at report time.","dogfood_battery": "40a37314 ·
os-verify-lock: VERDICT command-exit 1 · held the lock 36s· clone + route-ledger-live-mount-parity: Test Files 1 failed | 1 passed (2) · Tests 1 failed | 18 passed (19) · the one failure is route-ledger-live-mount-parity:GET /packages/:id/customizations — LEDGERED BUT NOT MOUNTED(main's #28595 row, not this diff; every clone case is an HTTP call to POST /api/v1/security/permission-sets/:name/clone through the real dispatcher, whose ledger row note this PR changes). Full battery on merged head 56f6631: Test Files 1 failed | 364 passed | 3 skipped · Tests 2942 passed · the one failed file is the same parity red. Pre-merge head 76b93c1: 364 passed | 3 skipped, 2938 tests, no failure.","mcp_calls": "5 — mcp__github__issue_read x5; no write tool",
"api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft forced, no labels, no assignees; read-back 14736 bytes sent = stored) and this comment via post-stamped.mjs (POST /repos//issues/28563/comments). git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door + wrong answer: GET /api/v1/packages/:id/customizations is ledgered (route-ledger.ts) but the live router 404s it, so dogfood route-ledger-live-mount-parity is red on main since #28595 · evidence: parity dogfood on 40a3731 and 56f6631 · dedupe words: customizations LEDGERED BUT NOT MOUNTED",
"carrier: seat 2 / #28595 · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Landing record · #28563 · PR #28636
domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-11-01T10:34Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28636-*'git merge-base --is-ancestor 6e11a929 origin/maingit grep -c resolveDefaultCloneLanding origin/main -- packages/runtime/src/domains/permission-set-clone.ts6e11a929.POST /security/permission-sets/:name/clonetakes an optional targetpackage, and its answer names the package the copy landed in. With no target, a set shipped by package X lands in<X>.custom, which N-13's ensure door creates on demand. A given target must be installed, writable and in reach. The copy is assignable by bare name.packages/runtime/src/domains/new-similar-target.tsis now onmain. PR feat(runtime)!: the flow clone door lands the copy switched off in a writable package, by default the paired <base>.custom, never package-less #28621 (Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149, the flow clone door) mergesmainand uses this copy, so one target rule serves both clone doors.Release: claim
6554953298(session_0186vfjHHMsRXC9EUT9hTFMo, branchclaude/project-thread-nqql2u) · landed.pm:dispatchedremoved; assignee cleared; the card closed on merge.Generated by Claude Code