Skip to content

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

Closed

Description

@objectstack-fleet

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_KEYS drops the envelope, packageId, managedBy, protection, adminScope and isDefault;
    • authority is metaWriteCapabilityVerdict, then walledCatalogWriteVerdict (ADR-0131 D3).
  • metadata-protocol/src/protocol.ts:1210#permissionSetOwnerBindingRefusal refuses a body packageId that disagrees with the row's binding. The clone drops packageId, so a .custom binding 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/src finds 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.

  1. 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):
  2. The copy is a new set: the new-name rule and its 409 stay. A set in .custom under 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 .custom may declare; undeclared same names and a dependent's overrides are refused at publish, os validate and install #28410).
  3. The copy is assignable where any set is: positions and user assignment resolve it by bare name (ADR-0131 D3/D4).
  4. The response names the package the copy landed in, and the notice says so in the server's words.
  5. Walled postures: the ensure door's 403 and the clone door's walled refusal both stand, so a tenant administrator gets the walled sentence.
  6. Contract: if the route ledger declares a request schema, package enters it Zod-first. The PR fills Clause-② (widening) and Metadata-protocol honestly; an API body key is not metadata.

Tests.

  • The default lands in X.custom: the row's package_id is the pair, with no packageId in 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.ts and the dogfood permission-set-clone-door.dogfood.test.ts are 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

  1. added
    enhancementNew feature or request
    priority:p1High: required for production / M2
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
    2026-11-01T06:51Z

    Unblocked · p1 · pm:queue (epic stall patrol)

    ⛔ Not a claim.

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 is session_01FEkQ5khiLzZmecWFfjSZLc
    Account: 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:server
    Seat: domain:server#2
    Ruling-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 on main as 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 a packages/spec request schema only if the ledger declares one, tests (permission-set-clone.test.ts, the dogfood permission-set-clone-door.dogfood.test.ts) and changesets. Calls N-13's ensureCustomizationPackage (packages/metadata-protocol) without re-implementing it. Not the console (#28573).
    Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: yes (widening — the clone door's body gains an optional target package and its response names the landed package; a packaged source's default target moves to <X>.custom and bad targets are refused, so the dev declares a narrowing arm too if CLAUSE2_ARMS reads 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 .custom package via N-13's ensure door, under ADR-0131 D3 (catalog write authority) · reached by an administrator copying a packaged permission set from Setup
    Thread-read: none
    Premise: holds (read 2026-11-01T07:26Z at origin/main: permission-set-clone.ts saves create-only and package-less and drops packageId; N-13's ensure door is on main since #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.ts and protocol-packaged-base-refusal.ts, so the dev reads its diff, does not edit its hunks, and merges origin/main once if it lands first
    Resume-from: none — fresh from origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 package and 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 an objects limb 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 no package, 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

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Landing record · #28563 · PR #28636

    domain:server seat 2 · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-01T10:34Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28636-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor 6e11a929 origin/main yes
    Content git grep -c resolveDefaultCloneLanding origin/main -- packages/runtime/src/domains/permission-set-clone.ts 3 hits

    Release: claim 6554953298 (session_0186vfjHHMsRXC9EUT9hTFMo, branch claude/project-thread-nqql2u) · landed.

    • State: pm:dispatched removed; assignee cleared; the card closed on merge.
    • Resume-from: none.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions