Repository navigation
[Decision] 墙内组织里持有 manage_metadata 的人仍能改共用的对象、视图、流程:只放行平台运营者(A)、禁止组织范围授予(B),还是维持并写明(C) #23231
Description
Ruled: 6145019463 · letter A · this card is the execution card (measure first, then the wall-gate covers every metadata type) · 2026-10-13T12:02Z
Part of #15204
Filing gate ②:只有维护者能做的决定。这涉及安全与权限边界,也就是集团和隔离模式下,组织能不能改全环境共用的元数据,按规则恒在人工地板。也算 ③:维护者在本席会话中要求「补这 4 张决策卡」。立卡:epic PM session_01Rerax7QTjKMPCUZxQUtPFR(marchtian),席位 epic:#15194。
Prior rulings on this card: none(新卡)。相关记录:
- W(fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895)的 Acceptance notes「Not filed (no reach measured)」;
- W2 的席位说明 6114823168「Not decided here」;
- W2 的契约审查 6117680834 ③3;
- W3 的契约审查 6121612552 ③3。
三处都写的是「这个问题归维护者」,但一直没有立卡(6113841670:「Carried, not filed」)。
锚点: 无自然锚点。W、W2、W3 都已合入,#15204 是活动中的执行卡,所以单开本卡。
查重: 用语义检索搜 objectstack 卡,关键词 walled tenant / manage_metadata / organization admin / environment objects views flows / group isolated posture,命中 15 张:
- 开着的 1 张:automation: config.organization '*' fans a packaged scheduled flow out once per installing organization under isolated tenancy (the recorded end state of #20619 ruling A) #20645,是计划任务按组织扇出,不是同一个问题;
- 其余已关。相关的一张是 Org-scoped presentation customization authority: a tenant org admin authors tier-A overlays without platform-wide
manage_metadata#12702(「a tenant org admin authors tier-A overlays without platform-widemanage_metadata」,已关,ADR-0131 D6/D7 已让按组织的覆盖层退役)。
没有同题的开着的卡或决策卡。
动手的读者:
- 裁 A 或 B:本席为
domain:engine车道立 1 张执行卡,第一步是实测可达性。 - 裁 C:本席为
domain:devx车道立 1 张文档卡。
维护者速读
事情: 集团(group)和隔离(isolated)模式下,一个部署是一个环境,所有组织共用同一套元数据:对象、字段、视图、流程,以及岗位、权限集、能力。
- 对岗位、权限集、能力(「目录类」),W、W2、W3 三个 PR 已经挡住:墙内只有平台运营者能写,组织里的人哪怕持有元数据编辑能力
manage_metadata也会被拒绝。 - 对其他元数据(对象、视图、流程等),代码没有挡:在组织范围内持有
manage_metadata的人,通过/meta和/packages写的仍是全环境共用的元数据,会影响其他组织。这一点是代码阅读(W 的说明),没有实测。
ADR-0131 怎么写的: 集团模式下,「工厂(组织)只分配、不定义」,不能创建岗位、权限集、能力、模板「或任何元数据」;元数据编辑能力「是总部 IT 持有的,工厂管理员没有」(D12 第 7、8 条)。D11 规定三种模式的规则相同,只差墙是否生效。
为什么要你裁: ADR 写的是「工厂管理员没有这个能力」。但如果有人在组织范围内被授予了它,平台该在门口拦下(像目录类那样),还是认为授予它就是有意的委托?W 只测了目录类,所以把这个问题留给你。
三个选项:
- A: 墙内,对所有类型的环境元数据都只放行平台运营者,和目录类同一个判定、同一个 403。组织里持有
manage_metadata的人改对象、视图、流程也会被拒。 - B: 门口不变,改为墙内不准在组织范围内授予
manage_metadata(以及studio.access)。这个能力只能在平台范围内持有。 - C: 维持现状,在文档里写明:墙内把
manage_metadata授给某个组织的人,等于让此人当总部 IT,能改全环境的非目录类元数据。
本席推荐 A。 选 A、B 还是 C?
背景(实测:objectstack origin/main 8024e1243a,2026-10-13T09:25Z)
- 唯一的判定:
walledCatalogWriteVerdict(packages/metadata-core/src/meta-write-capability.ts:169)。- 第一行就是「类型不在
position、permission、capability里就放行」(:180,if (!isSecurityCatalogType(input.type)) return { allowed: true })。 - 墙内放行
isSystem和平台运营者档位(PLATFORM_OPERATOR_POSTURE = 'PLATFORM_ADMIN',:135)。
- 第一行就是「类型不在
- 档位怎么来(
packages/core/src/security/posture-ladder.ts头注释):PLATFORM_ADMIN来自不限范围的admin_full_access授予;TENANT_ADMIN来自organization_admin授予。持有manage_metadata本身不提升档位。 - 已挡住的门(目录类):
- W fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895:
/meta、克隆入口和数据入口,+1,207 / −35,24 个文件; - W2 fix(security)!: under a wall a catalog-carrying package is the platform operator's — install-local and the /packages delete refuse tenants (ADR-0131 D3) #22935:
install-local和包删除,+1,145 / −91,16 个文件; - W3 fix(security)!: under a wall the /packages verbs that publish, discard, roll back, revert, duplicate or adopt catalog items are the platform operator's (ADR-0131 D3) #23003:
/packages其余 7 个写动作,+576 / −42,7 个文件。
- W fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895:
- W2 实测过的可达性(6114823168): 一个组织 A 的所有者,通过组织 A 范围内的授予持有
manage_metadata(档位TENANT_ADMIN)。他重新安装了运营者装的包,改动影响到组织 B 的成员。这一条是目录类,已被 W2 挡住。它说明「组织范围内持有manage_metadata」在实际中是能出现的。 - 没有实测的:
- 非目录类写入(对象、视图、流程)是否确实跨组织生效;
- 组织管理员能不能把一个带
manage_metadata的权限集分配给自己的成员。
Governing text
- ADR-0131 D12,集团模式第 7 条: 「The metadata-authoring capability (
manage_metadata/studio.access) is what headquarters IT holds and plant admins do not.」 - D12 第 8 条: 「Plants assign; plants do not define. … It cannot create a position, a permission set, a capability, a template or any metadata (D3, verbatim ruling 「单库多租户我可以禁止他们创建。但是你要支持我绑定到人员」). A plant that needs its own catalog is given its own environment.」
- ADR-0131 环境元数据一节: 「Studio — every type — requires the metadata-authoring capability (
manage_metadata/studio.access), whichorganization_admindeliberately does not hold.」 - ADR-0131 D3(模式一节): 「Shared-DB multi-tenant (
group/isolated): tenant admins cannot create or edit catalog items; the operator defines the catalog for every tenant」。- 这一条只说了目录类,W 据此只挡目录类。
- ADR-0131 D11: 「Ownership rules, refusals and constraints are identical under
single,groupandisolated; only whether the wall is enforced differs.」 - W 的 Acceptance notes: 「under a wall, an organization-scoped
manage_metadataholder also writes the environment's objects, views and flows through/meta. D3 governs only the catalog. No card is filed, because only the catalog was measured.」
前提(每条带复核命令)
- 判定只挡目录类:
git show origin/main:packages/metadata-core/src/meta-write-capability.ts | grep -c "if (!isSecurityCatalogType(input.type)) return { allowed: true }",应为 1。 - 目录类型只有三种:
git show origin/main:packages/metadata-core/src/meta-write-capability.ts | grep -n "SECURITY_CATALOG_TYPES: readonly string\[\] = ",应列出position、permission、capability。 - 运营者档位:
git show origin/main:packages/metadata-core/src/meta-write-capability.ts | grep -n "PLATFORM_OPERATOR_POSTURE = ",应为'PLATFORM_ADMIN'。 - ADR 原文:
git show origin/main:docs/adr/0131-total-organization-ownership-no-null-organization-id.md | grep -c "or any metadata",应 ≥1。
选项 × 真实代价
| 选项 | 做什么 | 能感知的后果 | 工作量 |
|---|---|---|---|
| A 门口全挡 | 先实测;然后让墙内的判定覆盖所有元数据类型:/meta 的保存、重置、发布、回滚,/packages 的写动作,install-local 的安装、升级、卸载,都只放行平台运营者和 isSystem |
组织里的人在墙内一律不能改全环境的元数据,拒绝信息点名模式和 ADR;single 不变。总部 IT 如果只持有 manage_metadata、不是平台运营者,在墙内也改不了对象、视图、流程(目录类 W 之后已经如此) |
domain:engine 车道,1 次实测加 1–2 个 PR。先例是 W / W2 / W3(+576 到 +1,207 行),这次主要是把已有判定的范围放宽到所有类型,加上每道门的测试、迁移条目和文档;Clause-②: yes (narrowing) |
| B 授予时挡 | 先实测;墙内拒绝在组织范围内授予 manage_metadata / studio.access(分配带这个能力的权限集、或岗位带它时拒绝) |
门口不变;能力只能在平台范围持有,所以组织里的人根本拿不到它。已有的组织范围授予要迁移或报告 | domain:engine 车道,1 次实测加 1 个或以上 PR,规模未实测,无先例;要处理已有授予 |
| C 维持并写明 | 不改代码;权限文档写明:墙内把 manage_metadata 授给组织里的人,等于让此人当总部 IT,能改全环境的非目录类元数据 |
行为不变;授予的人要自己知道后果。一个组织的人改了共用对象,其他组织会直接受影响 | domain:devx 车道 1 个文档 PR,十几行 |
业务含义直译
- A: 集团总部的系统配置只有总部 IT 能改。工厂的人哪怕拿到了配置权限,在集团系统里也改不了,要改就申请独立环境。
- B: 配置权限这把钥匙只发给总部,根本不发给工厂。门锁不变。
- C: 钥匙可以发给工厂的人,发之前要知道他能改所有工厂共用的配置。
四轴(从业务看)
- 项目长远合理性(领起):
- 两年后的样子:在共享一套元数据的多租户部署里,定义只由平台方改,租户只做分配和自己的数据。这正是 ADR-0131 D12 第 8 条写的,也是 D11「三种模式规则相同,只差墙」的直接推论。
- 参照主流做法:
- Salesforce 的多组织是一个组织一套元数据,不共享;
- ServiceNow 的域分离(domain separation)里,全局(global)域的配置只由服务提供方改,租户域只能改自己域里的数据和覆盖项。
- A 用 W 已经建好的同一个判定,去掉「目录类 / 非目录类」这个特例。B 也到终态,但换了一个控制点,要另建一套机制。C 维持一个靠人记住的特例。
- 实际业务拉动:
- 今天没有用户报告撞上它,非目录类的跨组织写入也没有实测。
- 会撞上的是集团或隔离部署里,在组织范围内持有
manage_metadata的人。W2 已实测这种持有在实际中能出现(目录类那一次)。
- 防 AI 犯错(出错时谁看到什么):
- A:AI 代理以组织身份去改一个共用对象,当场 403,拒绝信息说明原因。
- B:AI 一开始就拿不到这个能力。
- C:AI 的改动静默生效,其他组织的人在下一次打开页面时才看到对象变了,不知道是谁改的。这是静默的跨租户影响。
- 创业阶段不扩散:
- A 是收窄,复用已有判定,不新增概念。
- B 新增一道授予时的检查,还要处理已有授予。
- C 零代码,但留下一个要永久记住的例外。
os-decision-facets
- ① 项目长远合理性:A 用已有的唯一判定去掉「目录类 / 非目录类」特例,直接落实 ADR-0131 D12 第 8 条和 D11;B 换控制点、另建机制;C 维持一个靠人记住的特例。
- ② 实际业务拉动:零用户报告,非目录类的跨组织写入未实测;W2 已实测组织范围内持有
manage_metadata能出现。 - ③ 防 AI 犯错:A 当场 403;B 让 AI 拿不到能力;C 静默的跨租户影响。
- ④ 创业阶段不扩散:A 收窄并复用已有判定;B 新增一道检查和迁移;C 零代码但留永久例外。
Prior rulings read: manage_metadata, wall, plant, group, isolated → 30 hits; ADR-0105 D1, ADR-0131 D3, ADR-0131 D8, ADR-0131 D11, ADR-0132 D4, ADR-0057 D9 (others incidental: ADR-0005 §5, ADR-0006 §2/§5, ADR-0029 D9.7); plus ADR-0131 D12 items 7–8 and the environment-metadata section read by hand; thread: #22895 (Acceptance notes), #15204 (6113841670, 6114823168), #22935 (6117680834), #23003 (6121612552).
推荐
推荐 A,回退 C。
- 只看①选 A;②③④ 是否翻转:否。② 的零实测拉动只影响时序:先测,再做。
- 工作量:A 有 W / W2 / W3 三个直接先例;C 是十几行文档。差距 ≥3×,所以点名:选 A 是因为①,而且 v18 是破坏性版本,正是做这种收窄的时机。
- 置信缺口:
- 非目录类跨组织写入没有实测,可能有本席没读到的门已经挡住;
- 不知道组织管理员能不能把带
manage_metadata的权限集分给自己的成员; - 不知道现有集团部署里,总部 IT 是平台运营者,还是只持有
manage_metadata。如果是后者,A 会让他们在墙内改不了对象、视图、流程,需要改成平台运营者身份; - B 的规模和迁移面没有估算。
裁后执行
-
裁 A: 本席为
domain:engine车道立 1 张执行卡,按 W 的路数:- 先实测:组织范围内的
manage_metadata持有者,在group和isolated下,写一个对象、一个视图、一个流程,是否影响另一个组织;同时测组织管理员能否把带这个能力的权限集分给自己的成员。 - 判定覆盖所有类型,每道门加测试和
single/ 运营者 /isSystem对照,加迁移条目、文档和 changeset,Clause-②: yes (narrowing),过契约审查。
实测如果显示这些门已经挡住,就只补测试和文档。
- 先实测:组织范围内的
-
裁 B: 同样先实测,再由车道出方案(在哪一步拒绝授予、已有授予怎么处理),方案回本卡给你过目一次。
-
裁 C: 本席为
domain:devx车道立 1 张文档卡,在权限文档里写明这个委托的后果。本卡关闭。 -
不论裁哪一项,你只需回一个字母。
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 13, 2026
Ruling: batch #336 item 2 · letter A · maintainer 「其他同意」 2026-10-13T11:57Z
Director seat, summon #41,
session_01TJKP6W6UDYEBHtccVdpVoz(GitHubos-steve; written asobjectstack-fleet[bot]through the relay, read back). Presented in this seat's chat as batch #336 item 2 with recommendation A (fallback B; the filing seat's fallback was C); the maintainer's word on the remaining four items of the batch was 「其他同意」. The card's four premises were re-read by this seat onorigin/mainbefore the batch and hold (the verdict passes every non-catalog type;PLATFORM_OPERATOR_POSTURE = 'PLATFORM_ADMIN'; ADR-0131 says "or any metadata"). Thread-read: none (no comment on the card at this write).A — under a wall, every environment-metadata write admits only the platform operator and
isSystem. The one verdict (walledCatalogWriteVerdict) stops special-casing the catalog types:/metasave, reset, publish and rollback, the/packageswrite actions, andinstall-localinstall, upgrade and uninstall take the same answer undergroupandisolated, with the same403naming the posture and ADR-0131 D12 items 7–8 and D11;singleis unchanged. Governing text: ADR-0131 D12 items 7–8 ("plants assign; plants do not define … or any metadata"; the metadata-authoring capability is headquarters IT's), D11 (identical rules, only the wall differs), the standing ruling 5552700625. B (refuse the organization-scoped grant) and C (keep and document) are not taken: B builds a second control point, C leaves a silent cross-tenant write.domain:engine· 1 measurement then 1–2 PRs · precedents W fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895 (+1,207/−35), W2 fix(security)!: under a wall a catalog-carrying package is the platform operator's — install-local and the /packages delete refuse tenants (ADR-0131 D3) #22935 (+1,145/−91), W3 fix(security)!: under a wall the /packages verbs that publish, discard, roll back, revert, duplicate or adopt catalog items are the platform operator's (ADR-0131 D3) #23003 (+576/−42) ·Clause-②: yes (narrowing), the contract review atCONTRACT_REVIEW_TIERbefore the queue.needs-user-decision→pm:queue(domain:engine,priority:p1,target:v18) in this act, unassigned; the epic PM may split it under refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 if the measurement calls for two PRs. Step 1, measure on a booted deployment: an organization-scopedmanage_metadataholder undergroupand underisolatedwrites an object, a view and a flow; does another organization see it; can anorganization_adminassign a permission set carryingmanage_metadatato its own members. Step 2, the verdict covers every type, each door with a test and itssingle/ operator /isSystemcontrols, a migration entry, docs, a changeset. If step 1 finds a door already closed, only tests and docs are owed for it.groupdeployment turns out to hold onlymanage_metadataand not the operator posture, the dev reports it on this card before landing the refusal (the operational consequence the maintainer may want to see), ⛔ not as a reason to keep the hole.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 · 2026-10-13T15:17Z · execution of ruling 6145019463 (letter A), under the maintainer's p1-only scope order for this seat
Session:
session_01P88ehrZ6hM19hBzdpsQTToAccount:
os-support-aiBranch:
claude/issue-23231-walled-environment-metadata-writesWorktree:
objectstack-issue-23231Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface: expected landing points, read off the tree at
bae5841bce:packages/metadata-core/src/meta-write-capability.ts(walledCatalogWriteVerdict, which stops special-casing the catalog types, and its message), with its tests.domain:cli: the callers' own catalog pre-filters, which must stop short-circuiting.packages/rest/src/rest-server.ts:walledCatalogRefusaland its four/metawrite doors.packages/runtime/src/domains/meta.ts: the walled-verdict call.packages/runtime/src/domains/packages.ts:verdictFor.packages/cloud-connection/src/marketplace-install-local-plugin.ts:verdictFor, for install, upgrade and uninstall.packages/qa/dogfood/test/wall-catalog-refusal.dogfood.test.ts.domain:spec: one migration entry besidepackages/spec/src/migrations/entries/semantic/18.walled-catalog-*.ts.domain:devx: thecontent/docs/permissions/*pages W (fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895) edited..changeset/23231-*.md.packages/plugins/plugin-security/src/walled-catalog-data-door.tsand the catalog gates insecurity-plugin.ts(seat 2's C3 #15204 stages 7a ∥ 7b → 8-post, build ahead: delete the permission-set projection and the position write-through that stage 8 leaves unreferenced, then the post-stage-8 catalog gates and residue #22975 region). The data door maps only the catalog objects to a type, so a wider verdict does not reach other data.Container & model:
L(a measurement, then one or two PRs across the write doors; precedents W #22895, W2 #22935, W3 #23003),mode:subagent(the measurement runs in the dogfood harness, as W's did; no browser),model: default(dispatch-gates --tieratbae5841bce: no path-derived mandate; the contract-review-tier review before the queue is a separate isolated review atCONTRACT_REVIEW_TIER)Clause-②: no (narrowing)
Responsibility: n/a — not a defect card
Thread-read: 6145019463
Serial constraints cleared:
domain:spec) editsrest-server.tsat its export block (~670) and at 7324–7370. This card's region there iswalledCatalogRefusal(3668) and its call sites (7057, 7283, 7748, 7933), so the lines are disjoint. The later lander mergesmain.guestanchor's bindings (D3, D4) #23224 is in flight inruntime/src/security/resolve-execution-context.tsand insecurity-plugin.ts'sresolveOperationPrincipals. Neither file is this card's.walled-catalog-data-door.tsand the catalog gates insecurity-plugin.ts. This card reads them and does not edit them.SINGLE_CLAIM_PATHSentry.origin/mainbae5841bce.On
Clause-②: no (narrowing)against the ruling'syes (narrowing): the substance of the ruling is kept: one contract-review-tier review before the queue. Only the declared value differs, and it follows the measured shape:scripts/pm/clause2-line.mjs,yes (narrowing)is "a diff that widens one surface and narrows another".dispatch-gates --tierreads "a card that only NARROWS one isClause-②: no… and owes one contract-review-tier review before the queue".yes (narrowing)as the ruling wrote it. A new error-code ledger entry is one such widening (a new code is alwaysyes).Measurement first: per the ruling, the dev writes no refusal before step 1's readings exist. A door that is already closed gets tests and docs only.
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Shared-pages note from
domain:devxseat 1 (#6023) ·session_012zwsLMxQBwHUYJgPyrpEvx· 2026-10-13T15:58Z. ⛔ Not an objection, ⛔ not a claim, ⛔ no label change. This answers the cross-lane declaration 6148934049.singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 docs sweep,priority:p1. Its hold lifted when S8 landed (PR feat(plugin-security)!: retire the four security-catalog row objects (#15204 stage 8) #23222 →66949b7729). Of its 10 pages, 4 are among thecontent/docs/permissions/*pages that W (PR fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) #22895) edited, so this card's docs hunk can touch them too:authorization.mdxdelegated-administration.mdxpermission-sets.mdxsystem-context.mdx6146552291), it comes after tooling(pm): dispatch-gates' scratch-dir scan reads a.d.tsdeclarationexport function mkdtempSync(prefix: string)as a call site, so the full battery reds on main since PR #23273 #23294 (in flight) and release(next): the Version PR refresh fails with ENOENT.changeset/pre/changes.mdsince the 18.0.0-next.0 cut moved consumed changesets into.changeset/pre/, so no next.1 Version Packages PR can open #23282.mainand re-reads the other's sentences. Neither rewrites the other's paragraph.objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23231,
"status": "done",
"branch": "claude/issue-23231-walled-environment-metadata-writes",
"pr": "#23330",
"session": "session_01P88ehrZ6hM19hBzdpsQTTo (the dispatching PM's; mode:subagent)",
"premise_still_valid": true,
"summary": "Ruling A is executed in draft PR #23330, head 824228e, which merges origin/main 5c9bfb0. walledCatalogWriteVerdict now judges every type alike. Every door that writes environment metadata asks it after its capability gate, with no catalog pre-filter left: REST /meta save, reset, publish and rollback (403 FORBIDDEN); the dispatcher PUT /meta (403 PERMISSION_DENIED); all thirteen /packages write doors (403 PERMISSION_DENIED, refused before any read or write, with the W2/W3 item readers deleted); install-local install, upgrade and uninstall (403 FORBIDDEN); and the /automation definition writes (403 PERMISSION_DENIED). single, the operator rung and isSystem are unchanged. STEP 1, measured first on a booted crm stack at bae5841 before any edit. Under isolated and group, an organization-scoped manage_metadata holder (rung TENANT_ADMIN) answered 2xx at every named door and at POST /automation. Organization B's owner read every result: A's object, view and flow; A's label over the operator's view; the published draft; the rolled-back version; and 404 after the reset and the package delete. No door was already closed. #22447 closes nothing for a manage_metadata holder. organization_admin reading: under both walls the stock organization owner is refused 403 PERMISSION_DENIED (sys_user_permission_set) when it assigns a manage_metadata set to its own member or to itself, and for a set with no capability. organization_admin is read-only on the assignment tables, so an organization-scoped holder arises only from an operator's grant. No ordinary-member product flow is affected. Every door already demands manage_metadata; its only stock holder is admin_full_access. All 28 registered types live in sys_metadata, which has no organization column. FORK CLAUSE (HQ IT), reported here before landing. ADR-0131 D12 item 7 names HQ IT as the manage_metadata holder and does not name the posture rung. PLATFORM_ADMIN comes only from the platform-admin anchors, which under a wall means OS_PLATFORM_OWNER_EMAIL (required there); no capability lifts the rung. A stock group deployment's HQ IT gets manage_metadata only from the platform-admin set, so it is the operator and keeps authoring. An HQ IT given an operator-authored set scoped to the HQ organization is refused every metadata write by this PR, and must be declared in OS_PLATFORM_OWNER_EMAIL. No reference deployment runs group. The PR stays a draft; deployment/tenancy-modes.mdx now says this. files_changed: 30 (+1324/-551 against the merge base): 7 source, 12 test, 3 spec migration entries, 1 ADR anchor, 1 changeset, 6 docs. line_budget: n/a (no skills/** and no governed surface touched). deviations: (1) The file surface grew beyond the claim, named in the PR: runtime automation.ts for the /automation definition writes, a measured reach fixed in place under the four-condition exemption; activation-gate.ts, whose flow, action and permission-set remedies recommended writes this ruling refuses; the two existing W2/W3 migration entries, whose claim that a catalog-free package is handled as before became false; deployment/tenancy-modes.mdx; and one platform-checklist clause. (2) The first REST pre-filter ablation was a no-op: ablation-replace refused it (anchor 1 to 1, replacement contained the anchor) and restored it, so nothing ran; it was redone with a disjoint anchor. (3) Four foreground commands outlived the 10-minute cap and were moved to the background by the harness: a full-suite runtime attempt, a full-suite rest attempt and two closure builds. The builds were waited on by their recorded lock-holder PID with tail --pid, and the suites were rerun in chunks. (4) The gate battery started at merge commit e7fdcc4 and finished on 824228e (a test-budget-only commit); commands 1-60 were rerun on 824228e. (5) Commit trailers follow AGENTS.md's model-free pair, and the PR footer is AGENTS.md's session-URL form, not the harness reminder's model-bearing trailer or its footer. (6) The step-1 harness was a temporary, never-committed dogfood file; a copy is kept in the scratchpad. Cleanup: worktree objectstack-issue-23231 removed after the PR opened.",
"tests": "HEAD 824228e unless noted. BEFORE/AFTER, on the same booted harness (isolated, group, single), 90 readings each: every walled door went from 2xx with B reading A's write to 403 with B reading the operator's version; single was unchanged cell for cell; the operator control stayed 200. New pins: wall-environment-metadata-refusal.dogfood.test.ts (21 passed); packages-walled-write-refusal.test.ts (13 doors x 2 postures, plus operator/isSystem/single/no-tenancy controls); automation-walled-definition-write.test.ts (4 writes x 2 postures, plus the same controls). Suites: runtime, whole package, 374 files / 6178 passed / 19 skipped (domains 116 + 55 + 100 + 103, four runs); rest, whole package, 284 files / 6049 passed / 328 skipped (two runs); metadata-core 18 files / 552 passed; cloud-connection 45 files / 693 passed; spec src/migrations 4 files / 208 passed. Dogfood: the four wall files, 80 passed; eleven other walled dogfood files that write through these doors, 121 passed (at 733500e, pre-merge). Typecheck: turbo typecheck for metadata-core, rest, runtime, cloud-connection, spec and dogfood, 71/71 tasks green; runtime test files are covered by check:test-typecheck. Lint, narrowed with all three pieces of evidence: the population is eslint.config.mjs files **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; eslint --no-inline-config --format json over the 22 changed TS files gives 22 files, 0 errors, 0 warnings; the config enables no type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move. Ablation, verdict leg: the catalog-only line was replanted via ablation-replace (anchor 1->0, blob 1fc932c81c->a6632971b2), metadata-core rebuilt, and ablation-dist-preflight found the marker in dist/index.cjs and dist/index.js. Unit 5 red / 10 green; dogfood 16 refusal pins red, with 2 guards, 2 operator controls and the single control green. Restore: blob == HEAD, git diff HEAD empty, rebuild --absent shows the marker gone from 12 dist files, tree clean. Ablation, door-pre-filter leg: a catalog-only pre-filter in REST walledCatalogRefusal (anchor 1->0, blob 94317088f7->1512e81a2c), rest rebuilt, marker in dist. Exactly the 10 REST /meta pins went red; the /packages, install-local and /automation pins and every control (11) stayed green. Restore was proven the same way. gates: dispatch-gates --commands derives 122; --ran accounts for 122 run and 0 not measured. 121 exit 0: check:query-options-erasure exit 124 under contention, then 0 on rerun; check:skill-examples and check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET), then 0 once their packages were built. 1 exit 1: check:platform-checklist, 8 ABSENT SYMBOL problems in access-security.json, attachments-storage.json and search.json. The identical 8 reproduce on origin/main 404fc47, and this diff's checklist edit (api-backend.json) is not among them. Pin sweep: re-judged meta-walled-catalog-refusal, automation-activation-posture-gate, packages-vetted-org-source, packages-uninstall-refuse-before-mutate, packages-orgless-grants-capability-gate, meta-read-org-scope-parity, and the W2/W3 dogfood catalog-free controls, all flipped to load-bearing refusals with operator controls. No other pin repo-wide encodes the verdict's old sentence. CI convergence is the PM's; PR checks were in_progress at report time.",
"mcp_calls": "0",
"api_writes": "3 REST requests in 2 relay actions, as objectstack-fleet[bot]: pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #23330) + POST /repos//issues/23330/assignees (os-support-ai); os-dev-report = POST /repos//issues/23231/comments. Each action was carried by one repository_dispatch to the fleet-write relay. label-write: 0, because the dispatch named no label and skip-changeset does not apply with a changeset present. git push is not a REST write.",
"open_questions": [
{
"question": "POST /meta/_migrate-stored (both transports) still admits an organization-scoped manage_metadata holder under a wall (measured 200 under isolated and group). The ruling's door list does not name it, but its headline is 'every environment-metadata write'. Wall it too?",
"options": [
"A: wall it in a follow-up card, with the same verdict and two call sites (REST and dispatcher) after their manage_metadata gate",
"B: leave it with manage_metadata alone, as a meaning-preserving maintenance rewrite that defines nothing"
],
"recommendation": "A. Long-term, one rule for every environment-metadata write, with no door left as a special case (D11). There is no business pull for a tenant to run an install-wide rewrite. Against AI error, a door that refuses is harder to misuse than one documented as an exception. Scope stays small: one verdict call per transport. Not done here because the ruling enumerated the doors and this one defines nothing, so another organization's members resolve nothing differently."
}
],
"out_of_scope_findings": [
"class: b · reach: POST /api/v1/meta/_migrate-stored by an organization-scoped manage_metadata holder (rung TENANT_ADMIN) under isolated and group answers 200 (booted crm harness at bae5841 and again on the branch after the change) · evidence: the verdict is not asked at either transport's _migrate-stored door (rest-server.ts and runtime domains/meta.ts); ruling 6145019463's headline is 'every environment-metadata write' · dedupe words: migrate-stored, walled, manage_metadata, environment metadata, platform operator",
"carrier: 承接者:无 · noted, not filed · check:platform-checklist exits 1 on origin/main 404fc47 with 8 ABSENT SYMBOL problems (metadata-protocol protocol.ts#anonymousFormIntakeOrgScopeRefusal, #anonymousFormIntakeReopenRefusal, #envWideRawViewRows, #countable; service-storage attachment-access-hooks.ts#canEdit; attachments-storage symbol-anchor floor 27 below 28). Reproduced in a throwaway origin/main worktree; the symbols were already absent at bae5841 · dedupe words: platform-checklist, absent symbol, protocol.ts, canEdit",
"carrier: 承接者:无 · noted, not filed · @objectstack/cloud-connection still declares @objectstack/objectql (package.json, lockfile, vitest alias), which W2 added for the catalog reading this PR removed; no import remains",
"carrier: 承接者:无 · noted, not filed · the names walledCatalogWriteVerdict, walledCatalogRefusal, refuseWalledCatalogPackageWrite and refuseWalledCatalogPackage keep 'catalog' although they now judge every type; the export is read under that name by seat 2's walled-catalog-data-door.ts (#22975 region), so a rename waits for that lane",
"carrier: 承接者:无 · noted, not filed · walled-catalog-data-door.ts is vestigial by reading: no *.object.ts declares sys_position, sys_permission_set or sys_capability after stage 8 (seat 2's region, not edited)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
PM direction after the dev's report (6154188969): the fork clause as reported, and
_migrate-storedjoins this PR ·domain:engineseat 1 ·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T20:09Z1. The ruling's fork clause (HQ IT): reported here, as ruled. Not a reason to keep the hole.
This is restated from the dev's measurement so the maintainer can see it without reading the report:
PLATFORM_ADMIN(the operator rung) comes only from the platform-admin anchors, that isOS_PLATFORM_OWNER_EMAIL, which is required there. No capability lifts the rung.groupdeployment keeps authoring. Its HQ IT holdsmanage_metadatathrough the platform-admin set, so it is the operator.TENANT_ADMIN) is refused every environment-metadata write by this PR. It must be declared inOS_PLATFORM_OWNER_EMAIL.content/docs/deployment/tenancy-modes.mdxnow says it.group.The ruling's words govern: "⛔ not as a reason to keep the hole". The PR proceeds to review and landing, and this consequence also goes into the seat's round report to the maintainer.
2.
POST /meta/_migrate-stored(open question 1): A, in this PR, not in a follow-up cardmanage_metadataholder answers 200 there underisolatedandgroup, on both transports.isSystem". Its door list enumerates the known doors; it does not bound the rule. A tenant running an install-wide rewrite of stored metadata is an environment-metadata write.Fixes #23231is only true once that door also refuses. It is two verdict calls, one per transport, after theirmanage_metadatagate, each with a test and itssingle, operator andisSystemcontrols, and one line in the changeset and the docs.3. The other out-of-scope findings
check:platform-checklistexits 1 onorigin/main404fc477e5(8 ABSENT SYMBOL problems, none in this diff). The seat reads it at ACCEPT against CI and againstmain.Acceptance notes: thecloud-connection→objectqldependency left unused, and thecatalognames, which now judge every type (renaming them waits for seat 2's lane).walled-catalog-data-door.tsis vestigial after stage 8.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23231,
"status": "done",
"branch": "claude/issue-23231-walled-environment-metadata-writes",
"pr": "#23330",
"session": "session_01P88ehrZ6hM19hBzdpsQTTo (the dispatching PM's; mode:subagent)",
"premise_still_valid": true,
"summary": "Patch round 1, per seat direction 6154226716 section 2. POST /meta/_migrate-stored now asks walledCatalogWriteVerdict on both transports, after its manage_metadata gate and before the protocol is resolved or any row is read. Preview and apply alike, a non-operator under a wall is refused 403 FORBIDDEN on RestServer and 403 PERMISSION_DENIED on the dispatcher, each the door's own envelope. single, the operator rung, isSystem and no tenancy service are unchanged. The refusal names its subject as "the environment's stored metadata", a new key in the verdict's internal subject map, not a new export. Head dbe63f8 = 824228e + merge of origin/main (2868248; no overlap with this diff, no regen pending) + one patch commit. Pushed to the same branch, no rebase, amend or force-push. The PR stays a draft. BEFORE/AFTER row for the door, from the same booted harness: BEFORE (bae5841) 200 under isolated, group and single. AFTER (dbe63f8) 403 FORBIDDEN under isolated and group, naming the posture and ADR-0131 D3, D12 items 7-8, D11; 200 under single. The other 88 readings of the AFTER table are unchanged from the first round. files_changed: patch commit 13 files (+261/-26): rest-server.ts, runtime domains/meta.ts, metadata-core meta-write-capability.ts and its test, runtime meta-walled-catalog-refusal.test.ts, new rest meta-migrate-stored-walled-refusal.test.ts, the dogfood wall file, the changeset, deployment/tenancy-modes.mdx, api/metadata-api.mdx, census row 55b, the migration entry, the ADR anchor. Whole PR against the merge base: 31 files (+1562/-554). line_budget: n/a (no skills/** and no governed surface touched). deviations: (1) Under a wall the PREVIEW (apply false) is refused too: it is the same install-wide pass without the write, and the direction placed the verdict before anything is read. (2) Beyond the one line in the changeset, the changeset's 'Unchanged' clause that said _migrate-stored kept its manage_metadata gate alone was deleted, because it is now false. Its Why paragraph and its subject list name the door too. (3) The census row 55b and the meta-write-capability ADR anchor also name the door, so the two machine-read registers stay true. (4) The step-1 harness was put back into dogfood/test only for the AFTER run, then deleted; it was never committed. PR body edits owed (the seat writes them; this dev does not PATCH the body): add a POST /meta/_migrate-stored row to the BEFORE table (200/200/200); add it to 'What changed' (rest-server.ts and the dispatcher's meta.ts, after the manage_metadata gate, preview included); drop the Acceptance note 'Not walled here: POST /meta/_migrate-stored'; add the new pins, ablation leg 3 and this round's reruns and gates (all in tests below). Cleanup: worktree objectstack-issue-23231 removed again after the push.",
"tests": "HEAD dbe63f8. New pins, one per transport: rest src/meta-migrate-stored-walled-refusal.test.ts, 6 passed. Under isolated and group the tenant is refused for preview and apply, with code FORBIDDEN, status 403, the posture named and migrateStoredMetadata never called; the operator-rung and isSystem controls run it with 200 under both walls; single and no-tenancy controls run it with 200; the capability gate still answers first. runtime src/domains/meta-walled-catalog-refusal.test.ts gains the same five cases on the dispatcher (code PERMISSION_DENIED, status 403); the file has 19 passed. metadata-core meta-write-capability.test.ts (the new subject) 15 passed. wall-environment-metadata-refusal.dogfood.test.ts gains a walled refusal pin (preview and apply, FORBIDDEN 403, both postures), an operator control and a single control. Dogfood on this head: the AFTER harness plus the four wall files, 99 passed (5 files). Suites: runtime src/domains 116 files / 2972 passed; rest /meta and posture files plus the new file 110 files / 2284 passed; metadata-core and runtime meta-migrate-stored.test.ts unchanged-behaviour controls green (no tenancy service, inert). Typecheck: turbo typecheck for metadata-core, rest, runtime, spec and dogfood, 70/70 tasks green; rest and runtime test files are covered by check:test-typecheck. Lint: eslint --no-inline-config --format json over the 23 changed TS files gives 23 files, 0 errors, 0 warnings (population: eslint.config.mjs files **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; no type-aware linting, so untouched files cannot move). Ablation leg 3, the dispatcher's new verdict call disabled: ablation-replace on runtime domains/meta.ts with an anchor unique to the _migrate-stored block (anchor 1->0, blob c40313b77e->ed3b6a8c71). Runtime: exactly the 2 dispatcher _migrate-stored refusal pins (isolated, group) red, 17 green. REST: the other transport's 6 pins stayed green. Restore: blob after restore == HEAD (c40313b77e), git diff HEAD empty, tree clean. Both suites import the mutated source directly (runtime src), so no dist step applies. gates: dispatch-gates --commands at dbe63f8 derives 122 (the same list as the first round); --ran accounts for 122 run and 0 not measured. 121 exit 0 (check:query-options-erasure exit 124 under contention, then 0 on rerun; check:skill-examples and check:dual-build-cjs-loads exit 0 with their packages built first). 1 exit 1: check:platform-checklist, the same 8 ABSENT SYMBOL problems that reproduce on origin/main and are not in this diff, named not fixed: access-security.json protocol.ts#anonymousFormIntakeOrgScopeRefusal, #anonymousFormIntakeReopenRefusal x2, #envWideRawViewRows x2; attachments-storage.json attachment-access-hooks.ts#canEdit and the symbol-anchor floor 27 below 28; search.json protocol.ts#countable. check:system-context-census OK (135 read sites; the new isSystem read sits in a symbol already cited); check:adr-anchors OK. CI convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "1 REST request in this round, as objectstack-fleet[bot] through the fleet-write relay: os-dev-report = POST /repos//issues/23231/comments. No PR body PATCH, no label write, no other write. git push (same branch, fast-forward) is not a REST write. First round, already reported: pr_create (POST /pulls + POST /issues/23330/assignees) and comment 6154188969.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed · check:platform-checklist exits 1 on origin/main with the same 8 ABSENT SYMBOL problems as the first round (seat reads it at ACCEPT, per 6154226716 section 3)",
"carrier: PR #23330 Acceptance notes · noted, not filed · the cloud-connection to objectql dependency left unused, and the 'catalog' names that now judge every type (renaming waits for seat 2's lane); both are already in the PR body's Acceptance notes",
"carrier: seat 2 (#22975 region) · noted, not filed · walled-catalog-data-door.ts is vestigial after stage 8"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
PM ACCEPT: PR #23330 at
dbe63f841e(ruling A) ·domain:engineseat 1 ·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T21:39ZI checked against GitHub and the diff, not the reports (6154188969, then patch round 6155532025).
main;Fixes #23231and line 2 isClause-②: no (narrowing);os-support-ai;check-governed-merges: NOT governed, under the 3000-line threshold.manage_metadataholder underisolatedandgroup, with organization B reading A's writes. No door was already closed.singleunchanged cell for cell, and the operator control 200.organization_admincannot assign amanage_metadataset, so an organization-scoped holder arises only from an operator's grant.CONTRACT_REVIEW_TIER, in an isolated reviewer: record 6155747578, PASS on this head.walledCatalogWriteVerdictnarrows its runtime accept set, and its type signature is byte-identical. No catalog pre-filter is left./meta×4;PUT /meta;_migrate-storedon both transports, preview included;/packagesdoors;install-local×3;/automationdefinition writes.Clause-②: no (narrowing)is the measured value. The diff widens nothing, and there is no new error code.minorwith the**BREAKING**banner and theadr-0087: registered walled-environment-metadata-writes-retireddisposition, matching the semantic entry door for door.deployment/tenancy-modes.mdx. It goes to the maintainer in the seat's round report. Per the ruling, it is not a reason to keep the hole._migrate-storedis in this PR, per seat direction 6154226716 §2. It is verified at the diff and pinned on both transports; the ablation reds exactly the disabled transport's pins.check-expected-skips --pr 23330is OK (7, all in the roster), andmergeable_stateis clean.check:platform-checklistis red onorigin/main(8 ABSENT SYMBOL problems, none in this diff). It is a hand-run gate kept out of per-PR CI. Its carrier is the platform-checklist watchdog (cron51 2 * * *), which files the card at its next sweep while the gate stays red, as it did for check:platform-checklist is red on main #23139, check:platform-checklist is red on main #22924, check:platform-checklist is red on main #22758 and check:platform-checklist is red on main #22594. A hand-filed card would duplicate it, so none is filed.Acceptance notes: the unused@objectstack/cloud-connection→@objectstack/objectqldependency, and thecatalognames, which now judge every type (renaming them waits for seat 2's lane).walled-catalog-data-door.tsis vestigial after stage 8.domain:devx): whichever lands later mergesmainand re-reads the other's sentences (6149658350).Landing:
pr_ready+automerge_enablethrough the relay, now. On merge, this seat verifies by content.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23330 →
430e5f5ad1·domain:engineseat 1 ·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T22:06Z430e5f5ad1, after ACCEPT 6155766852 and contract review PASS 6155747578.Fixes #23231closed this cardcompleted.origin/mainby content:rest-server.tshas 5walledCatalogRefusal(call sites: the four/metadoors plus_migrate-stored;packages.tshas 14refuseWalledCatalogPackageWrite(hits: 13 doors plus the definition;walled-environment-metadata-writes-retiredis present.pm:dispatchedis removed in this stroke; the domain, area, type and priority labels stay.isolatedandgroup, every environment-metadata write admits only the platform operator andisSystem, andsingleis unchanged.deployment/tenancy-modes.mdx.Generated by Claude Code