Skip to content

Commit ee58392

Browse files
qq9340100claude
andauthored
fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) (#6608)
* fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) 依据 2026-08-08 维护者三段式裁决(issue #6483,os-project-manager 评论)执行, 与 #6283(flow)同一裁决家族。ADR 锚:docs/adr/0005-metadata-customization-overlay.md:53-68 (租户可定制类型白名单 + 收尾段「新类型默认 false + admission pair」)。 ━━━ 逐类型交付表(registry 行号为本分支最终行号 / origin/main 行号)━━━ 裁决分支①(立即回滚,免测量;row counts 仍测量入档): - permission registry 877 (main 820) true→false 测量: 0 条仓内活 org-scoped overlay 行; 但发现 1 条生产写路径:plugin-security ADR-0094 write-through (permission-set-projection.ts:713/752/794/928,方向确认注释 2026-07-14)。 详见下方「ADR-0094 冲突证据」。ADR 锚: ADR-0005 security 行(:58) - tool registry 970 (main 904) true→false 测量: 0 行、0 生产写点。ADR 锚: ai 行(:60) - skill registry 971 (main 905) true→false 测量: 0 行、0 生产写点。ADR 锚: ai 行(:60) 裁决分支②(先测量后定;六类全部测得 0 行 ⇒ 全部回滚): - page registry 675 (main 653) true→false 0 行。ADR 锚: ui ❌ 行(:62) - app registry 677 (main 655) true→false 0 行;生产写点 runtime/domains/packages.ts:232 (ADR-0045 publish unhide)经核实走 allowRuntimeCreate 层(DB-materialized 行, sys_metadata provenance ≠ artifact),不受影响。ADR 锚: ui ❌ 行(:62) - action registry 681 (main 656) true→false 0 行;其 supportsOverlay:false 与 true 并存 即 #6283 flow 的 #6190 幽灵写形态,本次一并关闭。ADR 锚: ui ❌ 行(:62) - dataset registry 685 (main 660) true→false 0 行;表中缺席 ⇒ 收尾段默认 false。 - book registry 852 (main 817) true→false 0 行;表中缺席 ⇒ 默认 false。 - position registry 878 (main 821) true→false 0 行;表中缺席 ⇒ 默认 false。 admission pair 测量:九类均有 resolveOverlaySchema() 接受的 overlay schema (schema registry 统一后 (a) 恒真),九类均无 ADR-0005 要求的书面 render-only 理据 ⇒ (b) 全部不成立。无「有行且理据齐全」形态,无需 promote 保留。 对照组不动(核实):view 674 / dashboard 676 / report 682 = true; translation/email_template = true;flow 717 = false(#6283/PR #6478 已合并,未触碰)。 ━━━ 测量方法(入档)━━━ 1. 种子/夹具静态扫描: grep 全仓 *.seed.{ts,yml,json}·examples/·dogfood 数据· sys_metadata insert 点 —— 仅 1 个生产 insert(repository 本身),0 条九类 overlay 行。 2. 逐类型字面量普查: grep "type: '<t>'" 全仓非测试代码,逐文件人工分类 (artifact 定义 vs overlay 写)。 3. 决定性实验:九类临时全部翻 false,跑 spec/metadata-protocol/objectql/rest/runtime/metadata/plugin-security/dogfood 全套 —— 失败 16 例 = 精确爆炸半径(全部为测试对旧 true 值的依赖,逐一如实对齐, 无一静默放宽;清单见下)。 ━━━ 写侧消费者核实 ━━━ - OVERLAY_ALLOWED_TYPES (metadata-protocol/src/protocol.ts:6954) 派生读取 ✓ 自然收敛 - assertAllowed 白名单 (metadata-protocol/src/sys-metadata-repository.ts:184,995) 派生读取 ✓ 自然收敛 - 无任何硬编码副本需要对齐;promoteDraft 生产调用方(protocol.ts:8799)已正确 按 isArtifactBacked 传 intent,无需改动(仅测试补传 intent)。 ━━━ 测试随动(16 处失败 → 全绿)━━━ 新增: metadata-protocol/src/protocol.adr0005-org-override-rollback.test.ts (40 例:9×声明 pin + 9×2 403 拒绝(org-scoped/无 environmentId)+ 9×runtime-create 仍开 + 派生集 pin + action 幽灵形态 pin + view/dashboard/report 对照) 对齐(specimen 改走 OS_METADATA_WRITABLE 文档化逃生门,机制覆盖不降): - protocol.save-receipt-wording.test.ts / protocol.delete-receipt-wording.test.ts (overlay-less-yet-overridable 种群 pin 为空集 + ACTION 收据案例挂逃生门) - objectql/protocol-registry-shadow.test.ts(4 例,app 信封机制) - objectql/protocol-meta.test.ts([#5265] app 收据案例) - runtime/meta-overlay-read-your-writes.test.ts(action shadow/restore 机制) 对齐(断言新行为): - objectql/overlay-precedence.test.ts(permission/position 期望 false + 重引入护栏注释) - objectql/sys-metadata-repository.test.ts(promoteDraft 测试补 intent:'runtime-only', 与生产调用方一致) 反转(ADR-0094 行为 pin → 403 拒绝 pin,见冲突证据): - dogfood/showcase-permission-projection.dogfood.test.ts(2 例反转 + 文件头改述) - dogfood/two-doors-permission.dogfood.test.ts(块2 两例反转 + env-authored 案例 换真正 runtime-authored specimen + 新增 artifact-backed 拒绝案例) ━━━ ③ 治理锚 ━━━ - registry 四个域段落均加 ADR-0005 + #6483 注释(ui 段 675-685 前、book 852 前、 security 877 前、ai 970 前),含「promote 须修订 ADR-0005 而非改本文件」指路。 - scripts/adr-anchors.json 新增 packages/spec/src/kernel/metadata-plugin.zod.ts → ADR-0005 锚(#6191 同机制);check:adr-anchors 39/39 OK。 ━━━ 验证读数 ━━━ - spec test: 342 files / 8786 全绿;spec typecheck 绿 - metadata-protocol test: 58 files / 651+40 全绿 - objectql: 147 files / 2470 全绿;runtime: 111 files / 1611 全绿 - metadata / plugin-security / rest / dogfood: 全绿(dogfood 86 files, permission 两文件 13/13) - check:adr-anchors OK(39);check:generated 10/10(registry 值不入生成物, authorable-defaults 仅含 schema 默认值 false,无需重生成) - pnpm lint(改动文件)0 error;三示例 objectstack validate exit 0/0/0 (仓内自身元数据不依赖被回滚的 overlay 能力,warnings 为存量与本单无关) - 逆向验证(方向先判后跑):skill false→true 重建后,预测 4 红 (声明 pin + 派生集 pin + 2 拒绝断言以「promise resolved {success:true} instead of rejecting」形态红)/其余 36 绿;实测 4 红/36 绿,恢复后 40/40 绿。 - push 前已 merge 最新 origin/main(d0a5ceb,快进,无重叠文件);合并基座上 复跑:metadata-protocol 58 files/651 全绿、spec test+typecheck 与 objectql 全绿、check:adr-anchors 39 OK、check:generated 10/10、ADR-0087 门禁绿。 ━━━ ADR-0094 冲突证据(留独立 ADR-0005 修订单裁处)━━━ permission 回滚与 ADR-0094「2026-07-14 方向确认:package 集经 ADR-0005 overlay 做环境定制」直接冲突。证据链: 1. plugin-security/src/permission-set-projection.ts 文件头注释明写该方向及日期; 写路径 4 处 saveMetaItem({type:'permission'})。 2. dogfood 两文件 4 例曾以功能 pin 固化该行为(本 commit 已按裁决反转为 403 pin)。 3. 其单测 permission-set-projection.test.ts 为 mock saveMetaItem 的隔离 harness, 感知不到真实门 —— 回滚后依旧全绿(未改动;真实门行为由 dogfood + 新增 403 套件覆盖)。 4. 行为边界实测:门读 artifact-backing 而非记录 managed_by —— code-declared 集(member_default、bootStack 下的 showcase_contributor)拒 403; runtime-created / metadata-door materialized 集(sys_metadata provenance) 经 allowRuntimeCreate 层不受影响。 若维护者意图保留 ADR-0094 定制通道,应在独立修订单中为 permission 补 admission pair 后 promote —— 本 PR 未做任何 promote。 changeset: .changeset/adr0005-nine-type-org-override-rollback.md(minor —— 实测 全类型 0 活行,按裁决口径 minor 如实写明 403 行为收紧;ADR-0087 门禁绿, 无 breaking 声明故无需处置标记)。 范围红线遵守:未动 flow 行;未做 promote;未动 objectui(#5768 症状在 changeset/注释中交叉引用);未动 content/docs/releases/。 Closes #6483 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE52u4GiUSiVh1F7F75arw * test(metadata-protocol): type the specimen lists with the registry literal union check:type-check-debt --re-measure caught +2 in metadata-protocol's DEBT layer (63 -> 65): ROLLED_BACK and CONTROLS were bare string[], and the derived-set pin calls Set<RegistryType>.has() on them. Typed via the registry's own literal union — as const satisfies for the controls, a cast for Object.keys(BODIES) whose keys are the nine rolled-back types by construction. tsc raw count back to 63; the 40-case suite stays green. * test(runtime): main 新增的 #5079 ARTIFACT-backed 用例改走 OS_METADATA_WRITABLE 逃生门 合并 origin/main 后的 jointly-wrong 破坏(AGENTS.md §10):两侧各自正确、 合起来红。main 在 meta-overlay-read-your-writes.test.ts 新增了整个 `#5079 — list / get / dispatch agree immediately after deleteMeta` describe 块, 其中 `an ARTIFACT-backed delete resets to the shipped value` 用例对一个 PACKAGED action 做 overlay 写;本 PR 把 `action` 的 allowOrgOverride 回滚为 false(#6483 裁决),该写点遂抛 NOT_OVERRIDABLE。文本合并干净,故 git 无冲突。 对齐方式沿用本 PR 已提交的同类处置(#4521 块中的同形用例):打开 `OS_METADATA_WRITABLE` 运营逃生门并重置两处 memoised 缓存,配 afterEach 清理, 避免 action 可写性泄漏到相邻用例。该 describe 块其余用例写的都是全新名字, 走 allowRuntimeCreate,不受回滚影响,故只此一处需要逃生门。 pnpm --filter @objectstack/runtime test: 115 files / 1733 tests 全绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 255588b commit ee58392

13 files changed

Lines changed: 709 additions & 121 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
ADR-0005 whitelist enforced: nine unratified `allowOrgOverride: true` flags rolled back to `false` (#6483)
6+
7+
`DEFAULT_METADATA_TYPE_REGISTRY` granted per-org overlay writes to nine metadata types the ADR-0005 tenant-customizable whitelist (docs/adr/0005-metadata-customization-overlay.md §"Tenant-customizable type whitelist") never admitted: `page`, `app`, `action`, `dataset`, `book`, `permission`, `position`, `tool`, `skill`. The 2026-08-08 maintainer ruling on #6483 rolled all nine back (same verdict family as `flow`, #6283).
8+
9+
**Behaviour change** — an org-scoped overlay write (`PUT /api/v1/meta/{type}/{name}`, `saveMetaItem`, or `SysMetadataRepository.put` with `override-artifact` intent) against a **packaged/artifact-backed** item of these types now fails loudly with **`403 NOT_OVERRIDABLE`** instead of being accepted. Measured in-repo before the rollback: **zero** live org-scoped overlay rows existed for any of the nine types (no seeds, no dogfood data, no fixtures), so no stored data is invalidated.
10+
11+
Unchanged, deliberately:
12+
13+
- `allowRuntimeCreate` stays `true` on all nine — authoring a **brand-new** item of these types through the runtime API (the ADR-0005 two-tier model) keeps working, including ADR-0045's publish visibility flip for materialized apps and ADR-0094's write-through for runtime-created permission sets.
14+
- `view` / `dashboard` / `report` (the ADR-0005 ✅ row) and `translation` / `email_template` are untouched.
15+
- `OS_METADATA_WRITABLE` remains the documented operator escape hatch for re-opening a type at runtime.
16+
17+
Known consumer impact: a Setup/data-door edit of a **code-declared** permission set (ADR-0094's 2026-07-14 "customize packaged sets via env overlay" direction) now answers 403 — ADR-0086 two-doors applies (edit the package, re-publish). Readmitting any of the nine requires an ADR-0005 revision ratifying the admission pair (overlay schema + written render-only rationale), not a registry edit; the registry rows and `scripts/adr-anchors.json` now carry that governance anchor.
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #6483 — the nine ADR-0005 whitelist divergences, rolled back. The WRITE
5+
* PATH now says what the ADR's amendment table has said since 2026-05-22.
6+
*
7+
* `DEFAULT_METADATA_TYPE_REGISTRY` carried `allowOrgOverride: true` on nine
8+
* rows the ADR-0005 tenant-customizable whitelist
9+
* (`docs/adr/0005-metadata-customization-overlay.md:53-64`) never granted:
10+
*
11+
* • `page` / `app` / `action` — the table says ❌ outright: "Conservative
12+
* default — these bind to routes and side-effects. Promote individually
13+
* if a concrete need appears." No promotion ever appeared.
14+
* • `permission` — the security row says ❌: "Authorization correctness;
15+
* overlays would create silent privilege drift."
16+
* • `tool` / `skill` — the ai row says ❌: "Behavioural contracts with
17+
* model providers; treat like flows."
18+
* • `dataset` / `book` / `position` — absent from the table entirely, and
19+
* the amendment's closing rule assigns absentees their default: "The
20+
* default for any new metadata type is `allowOrgOverride: false`",
21+
* admission requiring (a) an overlay schema `resolveOverlaySchema()`
22+
* accepts and (b) a WRITTEN render-only rationale. (a) holds for every
23+
* registered type since the schema registry unified
24+
* (`getMetadataTypeSchema`), so (b) — the written rationale — is the
25+
* discriminating clause, and none of the three carries one.
26+
*
27+
* The 2026-08-08 maintainer ruling on #6483 settled all nine: `permission` /
28+
* `tool` / `skill` roll back unconditionally; the other six roll back unless
29+
* live org-scoped overlay rows AND a complete admission pair exist. Measured
30+
* in-repo: zero committed `sys_metadata` overlay rows for any of the nine
31+
* (no seeds, no dogfood data, no fixtures that survive a suite run), so all
32+
* nine roll back. Same verdict shape as `flow` (#6283, the file this one is
33+
* modeled on — `protocol.flow-org-override-closed.test.ts`).
34+
*
35+
* `action` deserves one more sentence: its row declares
36+
* `supportsOverlay: false`, so — exactly like #6283's flow — the `true` flag
37+
* granted a write permission for an overlay nothing could ever read back
38+
* (#6190's phantom-write shape). The other eight declare
39+
* `supportsOverlay: true`; for them the read capability stays, only the
40+
* per-org WRITE permission closes.
41+
*
42+
* ---------------------------------------------------------------------------
43+
* What the rollback does and does NOT close
44+
* ---------------------------------------------------------------------------
45+
* • OVERRIDING A PACKAGED ITEM of these types — closed. 403
46+
* `NOT_OVERRIDABLE` at the moment of the write, on every kernel
47+
* (the repository's `assertAllowed` is topology-independent).
48+
* • CREATING A BRAND-NEW ITEM (`allowRuntimeCreate`) — still open for all
49+
* nine, deliberately: no code-shipped artifact is being shadowed. The
50+
* Studio "save a new view of the world" flows keep working.
51+
* • The env-var escape hatch (`OS_METADATA_WRITABLE`) — untouched; an
52+
* operator can still opt a type back in at runtime, per ADR-0005.
53+
*
54+
* Known write-side consumers, verified while landing this:
55+
* • `OVERLAY_ALLOWED_TYPES` (protocol.ts) and the repository's
56+
* `assertAllowed` allow-list are both DERIVED from the registry (Prime
57+
* Directive #8) — no hardcoded copy to align.
58+
* • plugin-security's ADR-0094 write-through
59+
* (`permission-set-projection.ts`) redirects data-door edits of
60+
* permission sets into `saveMetaItem({type:'permission'})`. For
61+
* RUNTIME-CREATED sets that write rides `allowRuntimeCreate` and keeps
62+
* working. For PACKAGE-OWNED sets it now refuses with 403 — which is
63+
* the ADR-0005 security row enforced ("silent privilege drift"), and
64+
* the same refusal that write-through already issues on kernels without
65+
* an overlay layer. Its own suite stubs `saveMetaItem`, so this file is
66+
* where that behaviour is actually pinned against the real gate.
67+
* • runtime's ADR-0045 publish visibility flip (`domains/packages.ts`)
68+
* writes `saveMetaItem({type:'app'})` against apps MATERIALIZED into
69+
* `sys_metadata` by additive builds — DB-only rows, never
70+
* artifact-backed, so the two-tier `allowRuntimeCreate` door keeps them
71+
* writable. Pinned below ("a DB-only item of every rolled-back type
72+
* still saves").
73+
*
74+
* ---------------------------------------------------------------------------
75+
* Reverse verification, direction predicted BEFORE running
76+
* ---------------------------------------------------------------------------
77+
* Restoring `allowOrgOverride: true` on any single rolled-back row (the run
78+
* recorded below used `skill`) must turn that type's declaration pin, its
79+
* derived-set pin, and its two refusal cases red, while every other type's
80+
* cases and the `view`/`dashboard`/`report` controls stay green — the
81+
* refusal failing as "promise resolved instead of rejecting", the
82+
* accepted-then-phantom write this rollback exists to forbid. Predicted 4
83+
* red / rest green; measured exactly that — the skill declaration pin, the
84+
* derived-set pin, and both skill refusals ("promise resolved
85+
* { success: true } instead of rejecting"), 36 green — then restored.
86+
*
87+
* Harness: the real write path over a stub engine — same shape as
88+
* `protocol.flow-org-override-closed.test.ts`. The gate runs INSIDE
89+
* `saveMetaItem` / `SysMetadataRepository.put`; a harness that mocks either
90+
* cannot see it.
91+
*/
92+
import { describe, expect, it } from 'vitest';
93+
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core';
94+
import { DEFAULT_METADATA_TYPE_REGISTRY } from '@objectstack/spec/kernel';
95+
import { ObjectStackProtocolImplementation } from './protocol.js';
96+
97+
interface Row {
98+
id: string;
99+
type: string;
100+
name: string;
101+
organization_id: string | null;
102+
state: string;
103+
metadata: string;
104+
}
105+
106+
function makeStubEngine(artifacts: Array<{ type: string; name: string }> = []) {
107+
const rows = new Map<string, Row>();
108+
let nextId = 0;
109+
const artifactKeys = new Set(artifacts.map((a) => `${a.type}|${a.name}`));
110+
const keyOf = (w: Record<string, unknown>) =>
111+
`${w.type}|${w.name}|${w.organization_id ?? '__env__'}|${w.state ?? 'active'}`;
112+
const engine: any = {
113+
async findOne(_t: string, opts: { where: Record<string, unknown> }) {
114+
for (const row of rows.values()) {
115+
if (opts.where.type !== undefined && row.type !== opts.where.type) continue;
116+
if (opts.where.name !== undefined && row.name !== opts.where.name) continue;
117+
if (opts.where.state !== undefined && row.state !== opts.where.state) continue;
118+
return row;
119+
}
120+
return null;
121+
},
122+
async find() { return []; },
123+
async insert(_t: string, data: Record<string, unknown>) {
124+
if (_t !== 'sys_metadata') return { id: 'side_effect_skip' };
125+
nextId += 1;
126+
const row = { id: `r_${nextId}`, ...(data as any) } as Row;
127+
rows.set(keyOf(data), row);
128+
return { id: row.id };
129+
},
130+
async update(_t: string, data: Record<string, unknown>, opts?: Record<string, unknown>) {
131+
assertEngineUpdateDispatch(data, opts);
132+
return { id: null };
133+
},
134+
async delete(_t: string, opts?: Record<string, unknown>) {
135+
assertEngineDeleteDispatch(opts);
136+
return { deleted: 0 };
137+
},
138+
registry: {
139+
registerItem: () => {},
140+
registerObject: () => {},
141+
listItems: () => [],
142+
getItem: () => undefined,
143+
// `isArtifactBacked` prefers this lookup — a hit here means the
144+
// name is shipped by a code package (`_packageId` provenance).
145+
getArtifactItem: (type: string, name: string) =>
146+
artifactKeys.has(`${type}|${name}`) ? { name, _packageId: 'showcase' } : undefined,
147+
},
148+
};
149+
return { engine, rows };
150+
}
151+
152+
function makeProtocol(
153+
artifacts?: Array<{ type: string; name: string }>,
154+
environmentId?: string,
155+
) {
156+
const { engine, rows } = makeStubEngine(artifacts);
157+
const protocol = new ObjectStackProtocolImplementation(
158+
engine,
159+
() => new Map(),
160+
environmentId,
161+
) as any;
162+
return { protocol, rows };
163+
}
164+
165+
/**
166+
* A schema-VALID minimal body per rolled-back type — spec validation runs
167+
* before the authorization gate, so an invalid body would 422 first and this
168+
* suite would pass without ever reaching the thing it pins.
169+
*/
170+
const BODIES: Record<string, Record<string, unknown>> = {
171+
page: { name: 'probe_item', label: 'Probe', type: 'record', regions: [] },
172+
app: { name: 'probe_item', label: 'Probe' },
173+
action: { name: 'probe_item', label: 'Probe', type: 'script', body: { language: 'expression', source: '1 + 1' } },
174+
dataset: { name: 'probe_item', label: 'Probe', object: 'task', dimensions: [], measures: [] },
175+
book: { name: 'probe_item', label: 'Probe', groups: [] },
176+
position: { name: 'probe_item', label: 'Probe' },
177+
permission: { name: 'probe_item', label: 'Probe', objects: {} },
178+
tool: { name: 'probe_item', label: 'Probe', description: 'p', parameters: {} },
179+
skill: { name: 'probe_item', label: 'Probe', description: 'p', instructions: 'do it', tools: [] },
180+
};
181+
182+
type RegistryType = (typeof DEFAULT_METADATA_TYPE_REGISTRY)[number]['type'];
183+
184+
const ROLLED_BACK = Object.keys(BODIES) as RegistryType[];
185+
186+
/** The control specimens: the three types ADR-0005 whitelists (✅ row). */
187+
const CONTROLS = ['view', 'dashboard', 'report'] as const satisfies readonly RegistryType[];
188+
189+
const VIEW = {
190+
name: 'probe_view',
191+
label: 'Probe',
192+
object: 'task',
193+
columns: [{ field: 'name', label: 'Name' }],
194+
};
195+
196+
const entry = (type: string) => DEFAULT_METADATA_TYPE_REGISTRY.find((e) => e.type === type);
197+
198+
describe('#6483 — the nine ADR-0005 divergences: allowOrgOverride rolled back to false', () => {
199+
// ── the declaration ───────────────────────────────────────────────────
200+
201+
it.each(ROLLED_BACK)('the registry declares %s non-overridable, runtime-create untouched', (type) => {
202+
// `allowRuntimeCreate` staying true is as much a part of the verdict
203+
// as the flag going false: the rollback closes per-org shadowing of
204+
// PACKAGED items, not runtime authoring of new ones.
205+
expect(entry(type)).toMatchObject({
206+
allowOrgOverride: false,
207+
allowRuntimeCreate: true,
208+
});
209+
});
210+
211+
it('action was the flow shape exactly — supportsOverlay:false made its write permission a phantom grant', () => {
212+
// Same self-contradiction #6283 rolled back on `flow`: the loader
213+
// cannot merge an overlay of this type, so per-org write permission
214+
// authorized writes nothing could ever read back (#6190).
215+
expect(entry('action')).toMatchObject({ supportsOverlay: false, allowOrgOverride: false });
216+
});
217+
218+
it('the ADR-0005 whitelist controls (view/dashboard/report) are untouched', () => {
219+
for (const type of CONTROLS) {
220+
expect(entry(type)).toMatchObject({ supportsOverlay: true, allowOrgOverride: true });
221+
}
222+
});
223+
224+
it('no parallel allowlist — the derived overlay set drops all nine with it', () => {
225+
// Prime Directive #8: `OVERLAY_ALLOWED_TYPES` is DERIVED from this
226+
// registry in both `protocol.ts` and `sys-metadata-repository.ts`.
227+
const derived = new Set(
228+
DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type),
229+
);
230+
for (const type of ROLLED_BACK) expect(derived.has(type), type).toBe(false);
231+
for (const type of CONTROLS) expect(derived.has(type), type).toBe(true);
232+
});
233+
234+
// ── the enforcement (the acceptance criterion) ────────────────────────
235+
236+
it.each(ROLLED_BACK)('an ORG-scoped overlay of a packaged %s is refused loudly, not accepted', async (type) => {
237+
const { protocol, rows } = makeProtocol([{ type, name: 'probe_item' }], 'env_prod');
238+
239+
await expect(
240+
protocol.saveMetaItem({
241+
type,
242+
name: 'probe_item',
243+
item: BODIES[type],
244+
organizationId: 'org_alpha',
245+
}),
246+
).rejects.toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 });
247+
248+
// Refused, not "refused after writing" — the phantom row IS the bug
249+
// class (#6190), so its absence is part of the claim.
250+
expect(rows.size).toBe(0);
251+
});
252+
253+
it.each(ROLLED_BACK)('the %s refusal does not depend on deployment topology (no environmentId either)', async (type) => {
254+
// `SysMetadataRepository.assertAllowed` refuses an `override-artifact`
255+
// write on ANY kernel — pinned so the rollback bites the flagship
256+
// showcase too (host configs boot with no environmentId, #5086).
257+
const { protocol, rows } = makeProtocol([{ type, name: 'probe_item' }]);
258+
259+
await expect(
260+
protocol.saveMetaItem({ type, name: 'probe_item', item: BODIES[type] }),
261+
).rejects.toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 });
262+
expect(rows.size).toBe(0);
263+
});
264+
265+
// ── the half that stays open, deliberately ────────────────────────────
266+
267+
it.each(ROLLED_BACK)('a BRAND-NEW org %s still saves — allowRuntimeCreate is a different tier', async (type) => {
268+
// Covers the two production write paths verified above: ADR-0045's
269+
// publish visibility flip (`app` rows materialized into sys_metadata)
270+
// and ADR-0094's write-through for runtime-created permission sets.
271+
const { protocol } = makeProtocol([], 'env_prod');
272+
273+
const result = await protocol.saveMetaItem({
274+
type,
275+
name: 'probe_item',
276+
item: BODIES[type],
277+
organizationId: 'org_alpha',
278+
});
279+
280+
expect(result.success).toBe(true);
281+
});
282+
283+
// ── the control that makes the red half mean something ────────────────
284+
285+
it('view — still allowOrgOverride:true — is still accepted over a packaged artifact', async () => {
286+
// Without this, the refusals above would also pass on a harness that
287+
// could not save ANYTHING over an artifact.
288+
const { protocol, rows } = makeProtocol([{ type: 'view', name: 'probe_view' }], 'env_prod');
289+
290+
const result = await protocol.saveMetaItem({
291+
type: 'view',
292+
name: 'probe_view',
293+
item: VIEW,
294+
organizationId: 'org_alpha',
295+
});
296+
297+
expect(result.success).toBe(true);
298+
expect(rows.size).toBe(1);
299+
});
300+
});

0 commit comments

Comments
 (0)