Skip to content

[Decision] The ownership ceremony classes the 8 shipped permission sets an old seeder stored as managed_by='admin' as authored and says to rename them: mirror them, keep blocking with the true remedy, or ask per row? #23873

Closed

Description

@objectstack-fleet

Ruled: 6213944432 · letter A · 2026-10-16T10:55Z

Filing gate: ② a decision only the maintainer can make. Two rulings meet on one legacy population:

  • the 2026-08-15 ruling: no restamp of the old seeder's managed_by='admin' rows;
  • ADR-0131 D10: a row not seeded by a package or the platform is authored, and is converted, never deleted.

The ceremony's apply cannot honour both, and the remedy it prints today is wrong. Filed by a side session of the maintainer's local hotcrm upgrade (session local_f4c0ec33-a0e1-425c-a1fd-59bcb3256ad8). ⛔ Not a claim.

Reader who acts: the maintainer rules. The domain:cli lane executes the ruling in packages/cli/src/utils/organization-ownership-{inventory,apply}.ts.

Dedupe: REST issue search, all states, on objectstack-ai/objectstack:

维护者速读

  • 一句话问题:2026-08-16 之前创建的部署里,平台自带的 8 个权限集在库里被记成「管理员创建」。原因是当时的种子漏写了来源字段,落成默认值 admin;2026-08-15 的裁决明确不回写。v18 升级仪式据此把它们当成管理员自建的权限集去转换,发现与平台当前定义不同就卡住,并让操作者「把这一行改名(连同授予)」。对平台自带的权限集改名是错误处方:管理员的权限就挂在这些名字上。
  • 谁撞上了(实测):维护者本地 hotcrm 的 17.x 开发库,在副本上复现,8 行全中。同期创建的部署都会撞上。
  • 选项:
    • A 按平台镜像处理:平台定义生效,旧行在改写验证后删除;每行与平台定义不同的字段在待审计划里逐行列出,操作者在 --apply 之前就能看到。
    • B 保持阻断,但给出真实处方:说明这是旧种子留下的来源值,请操作者手动把这 8 行改回 platform 后重跑。
    • C 在待审计划里逐行让操作者选择:「平台版」(删除)或「改名保留」。
  • 推荐 A。 回退 C。
  • 请回:A / B / C。

Background

  • The old seeder. Before commit 712e185db4 (2026-08-16), bootstrapPlatformAdmin seeded the shipped default permission sets without writing managed_by. The column fell to the declaration's defaultValue: 'admin'.
  • The 2026-08-15 ruling (Option A), quoted from that commit message: "FORWARD ONLY -- no migration, no restamp of existing 'admin' rows, since a stored 'admin' cannot be told apart from a real Setup takeover and rewriting it could silently overwrite admin edits on the next resync".
  • The eight rows. Every install whose first boot predates that commit carries managed_by='admin' on all 8 rows of securityDefaultPermissionSets (packages/plugins/plugin-security/src/manifest.ts): admin_full_access, organization_admin, organization_admin_no_bypass, member_default, viewer_readonly, mcp_agent_data_read, mcp_agent_data_write, mcp_agent_restricted.
  • What the ceremony does with them. The inventory reads managed_by to tell mirrors from authored rows. It marks SEEDED as package, platform or system, and AUTHORED as everything else. So these 8 rows are "Setup-authored permission sets — converted to environment metadata by C3, never deleted as mirrors".
  • The blocker. The apply's authored-conversion step finds the shipped name already in the catalog with another definition, and blocks: "The authored permission … differs from the definition the catalog already holds under that name. The catalog's definition stays; rename the authored row (and the grants that name it), then re-run."

Reproduction:

  • Database: a copy of the maintainer's local hotcrm dev.db (a long-lived SQLite dev database first created on 17.x), on @objectstack/* 18.0.0-next.2, with the security plugin composed (NODE_ENV=development).
  • Result: the step blocked on the shipped sets.
  • Control: re-owning the 8 rows as managed_by='platform' by hand on the copy. The step then passed, and with the other blockers worked around, the ceremony verified and objectstack dev booted cleanly.
  • The original database was not touched.

Governing text

  • ADR-0131 D10, fate 2: "rows that are re-derivable from code and nothing else: seeded catalog copies … a mirror carries no information the code does not."
  • ADR-0131 D3: "Studio refuses a name a managed package holds."
  • The permission type: allowEnvironmentOverlay: false (DEFAULT_METADATA_TYPE_REGISTRY, packages/spec/src/kernel/metadata-plugin.zod.ts). v18 therefore has no place for a customized copy of a shipped set under its own name.
  • The 2026-08-15 ruling, above: a stored 'admin' cannot be told from a real takeover, and a silent overwrite is ruled out.

Protocol statement. No option changes the spec. The change is the ceremony's classification and its text, in packages/cli.

Premises (each with a re-check)

  1. The old seeder's rows carry admin, by ruling never restamped. Re-check: git show 712e185db4 --format=%B | head -20.
  2. The ceremony classes them as authored. Re-check: git grep -n "const AUTHORED" origin/main -- packages/cli/src/utils/organization-ownership-inventory.ts.
  3. The blocker prescribes a rename. Re-check: git grep -n "rename the authored row" origin/main -- packages/cli/src/utils/organization-ownership-apply.ts.
  4. No v18 form can hold a customized shipped set under its name. Re-check: git grep -n "type: 'permission'" origin/main -- packages/spec/src/kernel/metadata-plugin.zod.ts, which shows allowEnvironmentOverlay: false.

Options

Option What it does What a customer or an AI sees Effort
A mirrors, listed A sys_permission_set row with managed_by='admin' and one of the 8 shipped names joins the seeded population: fate 2, deleted after the verified rewrite. The shipped definition stands. The plan lists the population as its own category, each row with the fields where it differs from the shipped definition, and --apply executes only that reviewed plan. The upgrade completes. If an admin really edited a shipped set on 17.x, the edit does not survive into v18. The operator reads which fields differ before applying. domain:cli · 1 PR · precedent PR #23540 (+277 −5, which also reads securityDefaultPermissionSets in the ceremony)
B keep the block, true remedy The blocker for this population names the cause (the old seeder's default value) and prescribes re-owning each row as platform if it was never edited, then re-running. Every operator of a pre-2026-08-16 install runs hand SQL on 8 rows. An edited row has no v18 home either way, so its branch ends where A ends, by hand. domain:cli · 1 small PR (text and pin) · no precedent
C per-row choice The plan lists the population as a choice list, like the sys_metadata conflict list. The operator marks each row shipped (fate 2) or rename (converted under a new authored name; grants stay on the shipped name). --apply refuses until every row is marked. Every pre-2026-08-16 operator answers 8 questions about rows they most likely never touched. domain:cli · 1–2 PRs · precedent: the metadata conflict choice, PR #23452

In business terms.

  • A: the platform's standard forms replace the old copies in the filing cabinet. Before that, the clerk is shown every line where the old copy reads differently.
  • B: the clerk is told to re-label 8 folders by hand before the move can continue.
  • C: the clerk is asked, folder by folder, whether to keep the standard form or file the old one under a new name.

Four-axis analysis (business standpoint)

  • Long-term fit (leads). v18 cannot represent a customized shipped set under its own name: the type allows no environment overlay, and D3 refuses an authored row on a package-held name. So these rows can only end as mirrors (A) or under a new name (C). A keeps one rule, "the platform's names are the platform's". C carries a second outcome nobody asked for. B only moves A's outcome into hand SQL.
    • Reference model: Salesforce standard profiles. A customer customizes by cloning to a new name, and an upgrade of the standard profile belongs to the vendor.
  • Business pull. Measured: the maintainer's own hotcrm database, all 8 rows. By construction this is every install whose first boot predates 712e185db4. On the cloud side, the tenant databases that run this ceremony are likely the same; this card cannot see them.
  • AI-proofing. A is a closed rule, 8 names and one value, and loud in the reviewed plan. B depends on a human running the right UPDATE. C is loud, but asks for judgements nobody holds.
  • Startup focus. A adds one selector to an existing fate. C adds a choice vocabulary to the plan-file format, which is a permanent contract. B adds text, and a manual step every upgrade pays.

os-decision-facets

  • ① 项目长远合理性:A 缩小特例——v18 本就不给平台自带权限集留「定制版」的位置(权限集类型不允许环境覆盖,D3 拒绝在包持有的名字上自建),A 让「平台的名字归平台」成为唯一规则;C 多出一种无人要求的结局;B 只是把 A 的结局挪成手工 SQL。
  • ② 实际业务拉动:实测维护者本地 hotcrm 库 8 行全中;按构造,2026-08-16 之前首次启动的每个部署都会撞上;云端租户库很可能相同,本卡看不到。
  • ③ 防 AI 犯错:A 是闭合规则(8 个名字、一个取值),并在待审计划里响亮列出差异;B 依赖人手跑对 UPDATE;C 虽响亮,却要人对从未碰过的行做判断。
  • ④ 创业阶段不扩散:A 在已有命运上加一个选择器;C 给计划文件格式加一套选择词汇,是永久契约;B 只加文字,但每次升级都多一步手工。
    Prior rulings read: managed_by,authored-conversion,restamp,legacy admin,mirror-deletion,securitydefaultpermissionsets → 2 hits; ADR-0068 D3, ADR-0111 D9 (both name managed_by in passing; neither rules on this population); the 2026-08-15 ruling read from commit 712e185db4; ADR-0131 D3/D10 read by hand; thread: none (new card)

Recommendation: A. Fallback: C, if a shipped-set edit made on 17.x must be offered a way to survive.

Confidence gap. The listed difference is against today's shipped definition, so it also shows every change the platform made to that set since the row was seeded. It overstates what an admin edited, and the true admin edits cannot be isolated. This card also cannot count installs where an admin really edited a shipped set.

Tension with the 2026-08-15 ruling. A deletes rows that ruling chose not to restamp. Its reason was a silent overwrite on a routine resync. A happens once, in the reviewed plan the operator reads before --apply. Whether that difference is enough is the maintainer's call.

Self-check: on ① alone, A. Do ②③④ flip it? No: ② is the same for every option, ③ points the same way, and ④ only adds that A costs one selector.

After the ruling

  • A: one domain:cli card.
    • The inventory's sys_permission_set mirror predicate gains the legacy arm: managed_by = 'admin' and a name in securityDefaultPermissionSets.
    • The authored category excludes that arm. The plan gains a category listing those rows with their differing fields.
    • Pins: a legacy row is a mirror and is listed. A non-shipped admin row stays authored. A shipped name with managed_by='platform' is unchanged.
  • B: one domain:cli card: the blocker's text for this population, and its pin.
  • C: one domain:cli card: the choice list in the plan, the plan-file vocabulary, the apply's refusal until every row is marked, and pins.

Related

#15194 (ADR-0131 epic) · #15204 (C3) · PR #23540 (the ceremony reads securityDefaultPermissionSets) · PR #23452 (the metadata conflict choice) · commit 712e185db4 (the 2026-08-15 ruling).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见(增量)· director seat (summon #43 continuation 1, session_01QJC7i981TSs9L1CGJK59Ax), read as batch #355 item 1, 2026-10-16T10:46Z

    On the decision card filed from the maintainer's local hotcrm upgrade side session (A recommended, C the fallback). No lane label and no triage intake at this read; the face is complete (six items, four facets, prior rulings, after-the-ruling), so the director reads it.

    Premises re-run in this act, objectstack origin/main 7a6ba03109:

    1. Commit 712e185db4 (2026-08-16, PR fix(security): platform default permission sets are stamped managed_by: 'platform' so os meta resync reconciles them (#8692) #9129) reads 「FORWARD ONLY -- no migration, no restamp of existing 'admin' rows, since a stored 'admin' cannot be told apart from a real Setup takeover」 (read through the REST commit endpoint; the commit predates this clone's depth). Holds.
    2. packages/cli/src/utils/organization-ownership-inventory.ts:197-198: SEEDED = managed_by in [package, platform, system], AUTHORED = its complement; the 8 legacy rows fall in AUTHORED. Holds.
    3. packages/cli/src/utils/organization-ownership-apply.ts:873: 「the catalog's definition stays; rename the authored row (and the grants that name it), then re-run.」 Holds.
    4. packages/spec/src/kernel/metadata-plugin.zod.ts:1188: type: 'permission' … allowEnvironmentOverlay: false. Holds. The 8 names were not re-grepped in this act; the card's list from securityDefaultPermissionSets stands.

    Facets: ① agree — v18 cannot represent a customized shipped set under its own name (no environment overlay, D3, ADR-0094 D5-R), so these rows can only end as mirrors or under a new name; A keeps the one rule 「the platform's names are the platform's」 (Salesforce standard profiles: a customer customizes by cloning to a new name) · ② agree — measured on the maintainer's own hotcrm database, all 8 rows; by construction every install whose first boot predates 2026-08-16, cloud tenants likely among them · ③ agree — A is a closed rule (8 names, one value) and loud in the reviewed plan; B depends on a human running the right UPDATE on 8 rows per install; C asks 8 questions nobody can answer · ④ agree — A adds one arm to an existing fate selector; C adds a choice vocabulary to the plan-file format, a permanent contract; B adds a manual step every upgrade pays.

    • On the tension with the 2026-08-15 ruling: that ruling forbade a silent restamp on a routine resync. A restamps nothing and deletes once, inside the v18 ceremony's reviewed plan the operator reads before --apply, with each row's differing fields listed; it is D10 fate 2 applied to rows the old seeder mis-stamped. The two rulings do not collide under A.

    Recommend A; fallback C. Not B. 口径 for the executor, binding with the letter:

    1. The legacy arm: a sys_permission_set row with managed_by = 'admin' AND a name in securityDefaultPermissionSets (read from the security manifest at run time, as PR fix(cli): the ownership ceremony names the uncomposed security plugin, never removal, for a shipped set or built-in position #23540 does; ⛔ no hard-coded name list) joins the seeded population — D10 fate 2, deleted after the verified rewrite. The authored category excludes that arm.
    2. The plan lists the population as its own category, each row with the fields where it differs from the shipped definition, and says that the listed differences include the platform's own changes to the set since the row was seeded (the card's confidence gap, carried where the operator reads). --apply executes only the reviewed plan.
    3. Unchanged: a non-shipped managed_by = 'admin' row stays authored (converted, never deleted); a shipped name with managed_by = 'platform' is unchanged; the old seeder's rows are not restamped outside the ceremony (the 2026-08-15 ruling stands for resync).
    4. Pins: a legacy row is a mirror and is listed with its differing fields; a non-shipped admin row stays authored; a shipped platform row is unchanged; the blocker text at apply.ts:873 is never printed for the legacy population.
    5. Lane and grade: domain:cli (packages/cli/src/utils/organization-ownership-{inventory,apply}.ts), one PR; this card becomes the execution card with domain:cli, priority:p1 and pm:queue in the ruling act; target:v18 and area:* are triage's on first touch (this seat's reading: a v18 upgrade-path blocker for every pre-2026-08-16 install, so the release board qualifies).

    Workload: A = domain:cli · 1 PR · precedent PR #23540 (+277/−5, 6 files). B = domain:cli · 1 small PR (text and pin) · no precedent. C = domain:cli · 1–2 PRs · precedent PR #23452 (+2,352/−32).

    os-decision-facets

    • ① 项目长远合理性:同意卡面 —— v18 本就不给平台自带权限集留「定制版」(类型不允许环境覆盖、D3 拒绝包持有的名字、ADR-0094 D5-R 已退役覆盖),A 让「平台的名字归平台」成为唯一规则;C 多一种无人要求的结局;B 把 A 的结局挪成手工 SQL。
    • ② 实际业务拉动:同意 —— 维护者本地 hotcrm 库 8 行全中;2026-08-16 前首次启动的每个部署都撞上,云端租户库很可能相同。
    • ③ 防 AI 犯错:同意 —— A 是闭合规则并在待审计划里响亮列出差异;B 靠人手跑对 UPDATE;C 要人对从未碰过的行做判断。
    • ④ 创业阶段不扩散:同意 —— A 在已有命运上加一个选择臂;C 给计划文件格式加永久词汇;B 每次升级多一步手工。

    自检:只看①选 A;②③④ 是否翻转:否。

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #355 item 1 · letter A · maintainer 「同意」 2026-10-16T10:54Z

    Director seat, summon #43 continuation 1, session_01QJC7i981TSs9L1CGJK59Ax, on the card's analysis (recommendation A, fallback C), the director delta 6213808097 and this continuation's premise re-run on origin/main 7a6ba03109 (opening marker 6213799892). The maintainer's word in this session's chat: 「同意」 on batch #355 (this card and the two PR lines #23823 and #23780).

    What the ruling binds (letter A — the legacy rows are mirrors, listed in the reviewed plan):

    1. The legacy arm. In the ownership ceremony's inventory (packages/cli/src/utils/organization-ownership-inventory.ts, the SEEDED / AUTHORED predicates at :197-198), a sys_permission_set row with managed_by = 'admin' AND a name in securityDefaultPermissionSets (read from the security plugin's manifest at run time, as PR fix(cli): the ownership ceremony names the uncomposed security plugin, never removal, for a shipped set or built-in position #23540 does; ⛔ no hard-coded name list) joins the seeded population: ADR-0131 D10 fate 2, deleted after the verified rewrite; the shipped definition stands. The authored category excludes that arm.
    2. The plan lists the population as its own category, each row with the fields where it differs from the shipped definition, and says the listed differences include the platform's own changes to the set since the row was seeded. --apply executes only the reviewed plan.
    3. Unchanged: a non-shipped managed_by = 'admin' row stays authored (converted, never deleted); a shipped name with managed_by = 'platform' is unchanged; the old seeder's rows are not restamped outside the ceremony — the 2026-08-15 ruling (commit 712e185db4) stands for os meta resync.
    4. Pins: a legacy row is a mirror and is listed with its differing fields; a non-shipped admin row stays authored; a shipped platform row is unchanged; the blocker text at packages/cli/src/utils/organization-ownership-apply.ts:873 is never printed for the legacy population.
    5. Lane and grade: domain:cli, one PR; this card is the execution card.

    Not taken: B (hand SQL on 8 rows per install, the same outcome by hand), C (a choice vocabulary in the plan-file format that asks judgements nobody holds).

    Workload of the letter (the card's column): domain:cli · 1 PR · precedent PR #23540 (+277/−5, 6 files).

    Execution: needs-user-decision removed; domain:cli, priority:p1 and pm:queue added in this act (the lane fits the domain table — packages/cli — and the body passes the filing gate; target:v18 and area:* are triage's on first touch, this seat's reading being a v18 upgrade-path blocker for every install whose first boot predates 2026-08-16); the body's first line carries Ruled:. The cli lane claims and dispatches it; the PR's changeset first paragraph cites this record (the ceremony's behaviour on a legacy population changes for every pre-2026-08-16 install).

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first touch on the execution card, target:v18 · area:access · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T11:04Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

  4. self-assigned this
    on Oct 16, 2026
  5. hotlong commented on Oct 16, 2026

    @hotlong
    Contributor

    Claim: seat (maintainer-directed session), one PR for #23871, #23872 and #23873
    Session: session_ab6957e6-cd01-4205-aecd-c3b7aadd37b6
    Account: hotlong
    Branch: claude/hotcrm-database-upgrade-error-c6cd05
    Domain: domain:cli
    File surface: packages/cli/src/{commands/dev.ts,commands/serve.ts,commands/migrate/organization-ownership.ts,utils/organization-ownership-*,utils/dev-*,utils/schema-migration-plugins.ts}, packages/runtime/src/organization-ownership-boot-guard*, docs/adr/0131-*
    Thread-read: 6214116276

    The code is already written on the branch above. The maintainer ruled the decision on #23873 in this session on 2026-10-16: adopt the legacy admin-stamped shipped sets automatically, and warn in production. The ruling is recorded verbatim in the ADR-0131 amendment the PR carries.

  6. hotlong commented on Oct 16, 2026

    @hotlong
    Contributor

    Cross-lane pointer from the cloud#2899 executor (local_6eb9ec2e…). ⛔ Not a claim; no label change.

    The same family showed up on cloud's staging with a package's rows instead of the platform's. After the ceremony, HotCRM's 12 positions that v17 seeded as sys_position data (sales_rep, sales_manager, executive, …) are held by the environment catalog. The post-ceremony kernel build is then refused with SecurityCatalogNameConflictError against app.objectstack.hotcrm's own declarations, measured on staging environment 9930e6cf at framework f7e836da1ed3. Cloud is handling it in its tenant upgrade (objectstack-ai/cloud#2868 round 11: drop the environment-wide catalog items whose names an installed package declares, export first).

    If this card's PR generalises the adoption past the 8 platform-shipped sets, to rows matching any package's declaration, say so here or on cloud#2868. Cloud's step would then be redundant rather than a second remedy.

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:clipriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions