@sema-agent/settings-schema 1.10.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +137 -3
- package/README.md +29 -2
- package/dist/config-fns.d.ts +72 -4
- package/dist/config-fns.js +204 -6
- package/dist/cross-domain.d.ts +8 -4
- package/dist/cross-domain.js +6 -2
- package/dist/fleet.d.ts +1 -1
- package/dist/fleet.js +1 -1
- package/dist/hooks.d.ts +367 -19
- package/dist/hooks.js +76 -8
- package/dist/remote-exec.d.ts +4 -4
- package/dist/remote-exec.js +1 -1
- package/dist/resolve-roster.d.ts +1 -1
- package/dist/scheduler-store.d.ts +13 -3
- package/dist/scheduler-store.js +13 -3
- package/dist/skills-manifest.js +1 -1
- package/dist/types.d.ts +663 -642
- package/dist/types.js +95 -46
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,140 @@
|
|
|
6
6
|
> `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
|
|
7
7
|
> 包名是**历史记录**,如实保留不追改。
|
|
8
8
|
|
|
9
|
+
## 2.0.0 — 2026-09-15 — 域解析**逐条容错**(行为面 BREAKING)+ `siblingResolver`→`createSiblingResolver`(删已发布导出)+ hooks 闭集补齐两员 + 上游漂移三门 + 文档腐败一次清
|
|
10
|
+
|
|
11
|
+
> **版号裁定(发布方,2026-09-15)**:按本仓「发版纪律」——删已发布导出(`siblingResolver`)与「同一份配置解析出不同结果」(逐条容错)两条都是 major ⇒ 本批发 **2.0.0**。消费方 `^1.x` 不会静默收到;迁移=补两条 `EffectiveReadWarning` 臂(`entry-dropped` / `legacy-governance-lifted`)+ 改一个名字,零行为改造。
|
|
12
|
+
|
|
13
|
+
### ① 逐条容错律 —— 整域 all-or-nothing 按类修(BREAKING)
|
|
14
|
+
|
|
15
|
+
**旧行为**:一份域文档里只要有一条条目解析不过,**整域**退回 schema 默认 —— 13 个模型里写坏一条,整张
|
|
16
|
+
目录一条都不剩;一个 MCP 服务器写错一个字段,整套服务器消失。
|
|
17
|
+
**新行为**:只丢那一条,同域其余条目照常生效,并**逐条**发 `entry-dropped` 告警点名它的点号路径
|
|
18
|
+
(`models.3` / `roles.summarize`)。
|
|
19
|
+
|
|
20
|
+
- 单铸点 `salvageEntries(schema, raw)`(`config-fns.ts`):条目集合**从 schema 自身派生**(数组字段与
|
|
21
|
+
Record 字段),不写「域 → 条目字段」对照表 —— 那是第二份真源,开新域必漏。入参是 **schema 不是域名**:
|
|
22
|
+
`skills` 在 wire 上是 manifest(另一张 schema,在 `buildEffective` 之外单独判),只吃域名的抢救点会把
|
|
23
|
+
那条腿漏在门外 —— 同一份病只因为换了张 schema 就没被覆盖。两条腿现在共用同一条律。
|
|
24
|
+
- 唯一调用点 = `buildEffective` 的 `parseOr`,且**只在整域解析已经失败之后**才跑:合法配置的解析路径
|
|
25
|
+
一字不动(与 0.8.1 的 hosts grandfather 同姿势,成功路径 byte 不变)。`FileConfigStore.getEffective`
|
|
26
|
+
(含 `{tolerant:true}`)与 `readEffectiveWire` 都经 `buildEffective`,自动同愈,无第二机制;wire 上的
|
|
27
|
+
skills manifest 那条旁路也接了同一个抢救点。
|
|
28
|
+
- **不新开档位**:容错只发生在 `DOMAIN_READ_FALLBACK` 判 `"default"` 的域。gate 域(rosters / governance /
|
|
29
|
+
entitlement / execution / workers / runtime / limits / projects)的坏条目**仍然 throw** —— 悄悄丢掉一条
|
|
30
|
+
roster 就是那个 worker 静默拿到全量目录,与整域降级是同一条「放权,不许」的理由,所以共用同一张表。
|
|
31
|
+
- **一域一个结论**:丢掉坏条目后整域仍解析不了(重名 / 悬空 ref / 域级判据),只发 `domain-defaulted`,
|
|
32
|
+
不留「丢了第 3 条」的半路叙述。整域缺席 / 非对象仍走整域档位(那不是「一条坏条目」)。
|
|
33
|
+
- 🔴 **零幸存者不算「部分损坏」**:一个集合如果**所有**条目都坏,它不是部分损坏的集合而是整个坏掉的集合,
|
|
34
|
+
本版对这种字段一条都不丢,原样交给整域档位裁。这不是特判,是「抢救」的定义 —— 没有要抢救的东西;它同时
|
|
35
|
+
一次性挡住一整类 fail-open:本 schema 里有若干字段**空 = 不设限**(最尖的一根是 `models.atModelAllowlist`,
|
|
36
|
+
空/缺席 = 所有模型都可 @),把一份「全是坏值」的限制清单抢救成空数组等于在一次「容错」里把运维的限制
|
|
37
|
+
悄悄删了。修法不是给那些字段加特判(枚举必漏一个),是**从不凭空造出一个空集合**。
|
|
38
|
+
变异读数:去掉这条守卫 ⇒ 10 格红(含 `atModelAllowlist` 放权那格)。
|
|
39
|
+
- 告警 union `EffectiveReadWarning` 加两员:`entry-dropped`、`legacy-governance-lifted`(见 ③)。
|
|
40
|
+
**穷举 `switch` 的消费方会在提货时编译红** —— 那是提货成本,迁移 = 各加一条臂。
|
|
41
|
+
- 红先:`test/entry-salvage-1.11.test.ts` 修前 13 红 / 10 绿(绿的是本版刻意不动的不变量:gate 域仍
|
|
42
|
+
fail-loud、整域非对象仍降级、好配置零告警),修后 23 绿。
|
|
43
|
+
|
|
44
|
+
### ② `PARSE_TRANSPARENT_KEYS` —— 解析透明键单点登记表
|
|
45
|
+
|
|
46
|
+
此前「本包不校验这个键的形」这条判据在**四处** JSDoc 里各复述一遍五行同义散文,没有任何一处能回答
|
|
47
|
+
「一共有几个这样的键、分别归谁校验」。现在一处列全(4 员),每行写清 `domain` · `path` · `since` ·
|
|
48
|
+
`validatedBy`(`engine-ingest` | `consumer` 闭集)· `why`;JSDoc 一律删成一行 `{@link}`。
|
|
49
|
+
「解析透明」按**行为**定义(喂一组代表性值全收),不按 schema 的形名 —— 按形名判有两个静默错法:剥包装会
|
|
50
|
+
把约束一起剥掉(`z.custom<never>()` 内里是 `ZodAny`,看着什么都收其实什么都拒),遇到没见过的包装
|
|
51
|
+
(`.brand()` 之类)就停下。`ModelEntry.extraBody` 因此**不在**表里:它自己有形门(必须是纯 JSON 对象),
|
|
52
|
+
开放的只是对象里的键名词表。
|
|
53
|
+
`test/parse-transparent-keys.test.ts` 从 `DOMAIN_SCHEMAS` **派生**透明叶子与登记表双向对拍:少登记
|
|
54
|
+
(新键静默溜进来)红,多登记(键已经不透明了)红,陈旧行也红;派生遇到不认识的 schema 形 fail-loud。
|
|
55
|
+
|
|
56
|
+
### ③ hooks 闭集补齐 `PreModelSwitch` / `PostModelSwitch`(additive)
|
|
57
|
+
|
|
58
|
+
`HooksConfig` 是闭集 `z.record(HookEventName, …)`,一个未收编的事件名不是「这一键被忽略」而是**整份
|
|
59
|
+
parse 失败** —— 一份在上游合法的 `settings.json` 只因带了 `PreModelSwitch`,连 `PreToolUse` /
|
|
60
|
+
`SessionStart` 一起被拒。这是同病形第 4 次(1.3.0 / 1.6.0 / 1.8.0)。
|
|
61
|
+
|
|
62
|
+
- `HOOK_EVENTS` 31 → **33**(尾插,既有成员相对次序一字不动);matcher 字段 = `to_model`;归属 shell;
|
|
63
|
+
新增 `MODEL_SWITCH_SOURCES`(Pre 三词 / Post 五词,上游刻意分两张表)、`PreModelSwitchHookInput` /
|
|
64
|
+
`PostModelSwitchHookInput`、`HookInput` 两个联合臂、`hookSpecificOutput` 两个臂(Pre = allow/deny/ask
|
|
65
|
+
许可判决,Post = additionalContext)。
|
|
66
|
+
- 持 `Record<HookEventName, …>` / 穷举 `switch` 的消费方提货时编译红(词表尾插的既有口径)。
|
|
67
|
+
|
|
68
|
+
### ④ 上游漂移三门 —— 属主自己看住自己的镜像面
|
|
69
|
+
|
|
70
|
+
本仓是这些闭集/常量的属主,却一直靠消费方替它发现上游动了,方向反了。一条律
|
|
71
|
+
(`driftReport(ours, upstream, registry)`)三处装载:① hook 事件闭集 vs 机器抽取的上游锚定快照
|
|
72
|
+
(`scripts/extract-cc-hook-events.mjs` → `test/fixtures/cc-hook-events.json`,不手抄);② 引擎
|
|
73
|
+
`ModelRole` 词表(结构性查找其 `.d.ts` 声明,不钉文件路径);③ 引擎 `PROJECT_ID_REGEX` 镜像
|
|
74
|
+
(源码里承诺了 7 个 minor 的 drift-lock,本版兑现)。差集里每个词必须有一行成文理由,未登记的差红并点名,
|
|
75
|
+
差已消失却留着登记行也红。变异自证读数写在门内。
|
|
76
|
+
|
|
77
|
+
### ⑤ 长命兼容臂:落日条款 + 静默改写改成出声
|
|
78
|
+
|
|
79
|
+
- `legacyGovernanceFromRuntime` 抬升真生效时发 `legacy-governance-lifted`(点名抬了哪几个键);
|
|
80
|
+
调度存储的 durable `scope` 重铸真发生时发 `durable-scope-rekeyed`(带改写前后)。**行为一字未变**,
|
|
81
|
+
只是不再无声 —— 关窗的前提是有人能证明它已不再触发。
|
|
82
|
+
- 四条臂逐条写落日:两条改写臂是**持久数据**面,删除归下一个 major 并按持久数据的规矩办(响亮拒收 +
|
|
83
|
+
一次性迁移);hosts 夹钳本来就出声,补条款;已删字段墓碑无代码,只留「别把这个名字铸回来」。
|
|
84
|
+
- `SchedulerParseWarning` 由单一形变成 union(加 `durable-scope-rekeyed`):穷举它的消费方补一条臂。
|
|
85
|
+
|
|
86
|
+
### ⑥ `siblingResolver` → `createSiblingResolver`(BREAKING,零别名)
|
|
87
|
+
|
|
88
|
+
命名律:`build*` 出纯数据,`create*` 出活体对象;它返回闭包,此前两个前缀都不带。直接改名,**不留别名、
|
|
89
|
+
不留双读** —— 消费方的 tsc 红就是通知,迁移 = 改这一个名字。
|
|
90
|
+
|
|
91
|
+
### ⑦ 文档腐败一次清(11 处)+ 四格常驻门
|
|
92
|
+
|
|
93
|
+
- `ROADMAP.md`:删掉那张「已 SHIPPED(版本锚)」表(它在三个版本已发上 npm 之后仍把它们列在「候
|
|
94
|
+
publish」栏,烂了一个多月);本档自此只写还没做的。新门 H:本档提到的版本号不许已经在 CHANGELOG 里
|
|
95
|
+
有一节;也不许再维护一份版本锚表。
|
|
96
|
+
- `docs/CONSUMER-INTEGRATION.md` §2 的「runtime 位含 governance 六闸镜像双写」是反事实(0.7.1 已撤,
|
|
97
|
+
同档 §6 自己就这么写),删;「六闸」这个已撤镜像的旧称改成真话「限额残余 5 键」。新门 I:档里不许有
|
|
98
|
+
「还在双写」的现行时声明;runtime 残余键数与运行时表相等。
|
|
99
|
+
- CI 以「`npm run typecheck` 有存量 test 文件报错」为由排除类型门 —— 亲跑是 exit 0 / 零输出,那句话早已
|
|
100
|
+
不是事实,而 `test/**` 的类型面因此一直无门(`build` 只编 `src/`)。删该行,`typecheck` 挂进 CI。
|
|
101
|
+
- src 注释里 **24 处** `docs/*.md` 指路指向本仓根本没有的文件,且随 `dist/*.d.ts`(`removeComments:false`)
|
|
102
|
+
发给消费方 = 24 条 404。逐条改写成中性指称(档在别处,这是诚实的说法)。新门 F:src 里出现的每一个
|
|
103
|
+
`*.md` 指路都必须能解析到真文件。
|
|
104
|
+
- `llms.txt` 的 17 域枚举被两条后插的 bullet 腰斩(数字对、句子断,D 组只钉数字所以门绿文坏):修文;
|
|
105
|
+
新门 G:域名必须逐个出现在同一条 bullet 里。两档补 `toolFaces` / `contentOrigin` 说明与本版新面。
|
|
106
|
+
- `README.md` 的「What's in it」停在 1.4.x(全文零命中 hooks / scheduler / cross-session /
|
|
107
|
+
`EXTENSION_NAMESPACES` / `toolFaces`):按 barrel 面补齐到本版。
|
|
108
|
+
- CHANGELOG 的 `1.1.0` / `1.2.0` / `1.3.0` 三节补日期(npm 发布时间戳为准,与其余各节同用本地日期)。
|
|
109
|
+
- `src/types.ts` 里 `PROJECT_ID_REGEX` 那条「core 公开导出后加 drift-lock」的 TODO 已兑现(见 ④),改写成
|
|
110
|
+
事实 + 成文的 flags 差异理由。
|
|
111
|
+
- `CLAUDE.md` 把 `npm run e2e` 写成「域/契约改动腐坏它就报警」的保护面,与实际不符:它只写三个域
|
|
112
|
+
(models / governance / skills)+ 非域的 remote-exec,也不断言 `warnings`。口径改成真话。
|
|
113
|
+
|
|
114
|
+
### 三问
|
|
115
|
+
|
|
116
|
+
- **谁需要**:运维在 `config.d/models.json` 里加一条新模型时手滑写错一个字段 —— 今天的结果是**整张模型
|
|
117
|
+
目录消失**(那台 worker 一个模型都没有),而不是「那一条没生效」。同一形在 MCP 服务器、skills、场景、
|
|
118
|
+
插件目录上一模一样。这不是假想:1.7.0 / 1.7.1 连着两版,就是因为在一个**展示用的标签**上设了形判据,
|
|
119
|
+
让一条坏值抹掉了整套 MCP 服务器,只好连发两个 patch 撤回。
|
|
120
|
+
- **谁受伤**:① 靠「坏一条 ⇒ 整域退默认」这个**连坐**当粗粒度校验信号的消费方 —— 它此前能从「整域空了」
|
|
121
|
+
推出「文件有问题」,现在整域不空了,信号改为逐条的 `entry-dropped` 告警;② 穷举 `EffectiveReadWarning` /
|
|
122
|
+
`SchedulerParseWarning` / `HookEventName` 的消费方提货时编译红;③ 用了 `siblingResolver` 这个名字的
|
|
123
|
+
消费方编译红。
|
|
124
|
+
- **什么补偿**:不加旋钮 —— 加一个「要不要逐条容错」的开关等于把这条律变成两条律,而两条律里一定有一条
|
|
125
|
+
没人测。补偿是**信号**:每一条被丢掉的条目都有一条点名它路径的告警(`entry-dropped`),`onWarning` 是
|
|
126
|
+
0.8.1 起就有的既有通道,消费方把它接到自己的审计流即可拿到比「整域空了」精确得多的输入;gate 域的
|
|
127
|
+
fail-loud 一条没动,所以「静默放权」这条最坏的路本就不存在。
|
|
128
|
+
|
|
129
|
+
### 消费方点名(按能力显式表态制)
|
|
130
|
+
|
|
131
|
+
- **worker 服务**:①`EffectiveReadWarning` 穷举 `switch` 需补 `entry-dropped` / `legacy-governance-lifted`
|
|
132
|
+
两臂;②读面行为变化 —— 坏条目不再连坐整域,若有「整域空 ⇒ 配置有问题」的推断需改读 `entry-dropped`;
|
|
133
|
+
③`HookEventName` 穷举面加两员;④`createSiblingResolver` 改名(若有引用)。
|
|
134
|
+
- **CLI / 壳**:①`HooksConfig` 现在受理 `PreModelSwitch` / `PostModelSwitch`(settings.json 不再整份被拒),
|
|
135
|
+
持 `Record<HookEventName, …>` 的表需补两员;②`SchedulerParseWarning` 成 union,穷举它的地方补
|
|
136
|
+
`durable-scope-rekeyed` 臂。
|
|
137
|
+
- **client-core**:透明键集从 zod shape 反射派生的那条门 —— 本版新增导出 `PARSE_TRANSPARENT_KEYS`,
|
|
138
|
+
两边可以直接对拍(派生 vs 登记),不必各自反射。
|
|
139
|
+
- **config-center 产品(舰队管理台)**:①写侧仍走 `parseDomain`(严判,一字未变)—— 逐条容错只在**读**
|
|
140
|
+
路径;②读面同 worker 服务:坏条目不再连坐;③它今天装的是改名前、冻结在 `0.15.0` 的旧名包,本版之前的
|
|
141
|
+
全部契约它都不认,提货 `^1.11.0` 是它自己的车。
|
|
142
|
+
|
|
9
143
|
## 1.10.0 — 2026-09-09 — `models.models[].compat` 引擎兼容声明席(镜像 `OpenAICompletionsCompat`;新键=minor)
|
|
10
144
|
|
|
11
145
|
- 引擎 `OpenAICompletionsCompat`(五键 `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
|
|
@@ -211,7 +345,7 @@ permissions 先例形)。红先:3 新钉先红(members 缺席)→ 加表后 2
|
|
|
211
345
|
4→0 / `dist/remote-exec.d.ts` 4→0;**全 dist 52→1**,唯一剩余 = 上述默认值行。钉
|
|
212
346
|
`test/docs-hygiene.test.ts` E 组(src 三族零命中、唯一放行行 = 该默认值字面;裸人名零命中)。
|
|
213
347
|
|
|
214
|
-
## 1.3.0 — hooks 域加员 `DirectoryAdded`(CC 2.1.219 新事件;`HooksConfig` 拒键病形修复)
|
|
348
|
+
## 1.3.0 — 2026-08-31 — hooks 域加员 `DirectoryAdded`(CC 2.1.219 新事件;`HooksConfig` 拒键病形修复)
|
|
215
349
|
|
|
216
350
|
**`HOOK_EVENTS` 30 → 31:`POST_198_HOOK_EVENTS` **尾插** `DirectoryAdded`(1.2.0 三员声明序逐字节不动;重排 = BREAKING,
|
|
217
351
|
免裁)。additive:既有 30 员的 matcher 字段 / owner / 输入形 / `hookSpecificOutput` 臂一字不动(FROZEN 1.2.0 全表字面量夹具钉,
|
|
@@ -298,7 +432,7 @@ export type DirectoryAddedHookInput = z.infer<typeof DirectoryAddedHookInput>;
|
|
|
298
432
|
错 + 40 文件 631 用例),归因 codex `--sandbox read-only` 下 vitest 写 `node_modules/.vite/vitest` 缓存失败(目录亲验存在;
|
|
299
433
|
归因为假设,复跑绿为实测)。
|
|
300
434
|
|
|
301
|
-
## 1.2.0 — `MinTrust` 排序原语 + `task` 员([5796] 序题 → [5805] schema 认领 / [5807] cli 表态三点)
|
|
435
|
+
## 1.2.0 — 2026-08-31 — `MinTrust` 排序原语 + `task` 员([5796] 序题 → [5805] schema 认领 / [5807] cli 表态三点)
|
|
302
436
|
|
|
303
437
|
**新导出 `trustRank(layer)` / `isAtLeast(layer, minTrust)`——规范信任阶梯的**单源**(v2-design §1:29 逐字);
|
|
304
438
|
`MinTrust` **尾插** `task`。发布表 `SAFETY_MERGE_SPEC` **一字不动**(1.1.0 全表字面量冻结钉),枚举声明序不重排
|
|
@@ -380,7 +514,7 @@ for (const m of membersOf("permissions")) {
|
|
|
380
514
|
- 全量 39 文件 / 621 用例绿(1.1.0 基线 610 + 11);tsc 0;build 绿(dist `.d.ts` 含两原语);dist 运行时冒烟
|
|
381
515
|
(`node -e import('./dist/index.js')`)六词/六序/未知词 RangeError 如述。
|
|
382
516
|
|
|
383
|
-
## 1.1.0 — `permissions` 成员级准入(权限双通道终裁四件套之④,clay 终裁 [5797];合流形 [5794])
|
|
517
|
+
## 1.1.0 — 2026-08-30 — `permissions` 成员级准入(权限双通道终裁四件套之④,clay 终裁 [5797];合流形 [5794])
|
|
384
518
|
|
|
385
519
|
**`SAFETY_MERGE_SPEC` 类目可声明 `members`(成员级 minTrust + 方向);首个也是当前唯一成员化的类目 = `permissions`
|
|
386
520
|
(`allow` / `deny` / `ask`,闭集)。纯 additive:类目级 `minTrust` 一个字不动,老消费方行为逐字节不变。**
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
|
|
|
50
50
|
`domainWriteRole`, …), `resolveActiveTiers`(0.9.0 档位组→引擎 `RunnerDeps.tiers` 组存 `models.tierGroups`+`models.activeTierGroup`,组切换=换表下发), and the pure data
|
|
51
51
|
shapes (`User`, `PublishedSnapshot`, …).
|
|
52
52
|
- **Roster resolution** (`resolve-roster.ts`): `resolveEffectiveForWorker` → the per-worker wire payload.
|
|
53
|
-
- **Ref integrity** (`cross-domain.ts`): `refIntegrityIssues` + `
|
|
53
|
+
- **Ref integrity** (`cross-domain.ts`): `refIntegrityIssues` + `createSiblingResolver` (the pure half — fleet
|
|
54
54
|
auto-placement stays in the config-center 产品).
|
|
55
55
|
- **Hashing** (`hash.ts`): `stableHash` (FNV-1a 64) for ETags / version detection.
|
|
56
56
|
- **Skills manifest** (`skills-manifest.ts`): content-addressing for the lazy-pull skill bodies.
|
|
@@ -108,7 +108,34 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
|
|
|
108
108
|
limits/workers) stay fail-loud even in tolerant mode. 0.19.0 adds a FAIL-LOUD sweep on the same read:
|
|
109
109
|
a `config.d/*.json` the local set will never read (a name that is not a domain, or a non-portable one)
|
|
110
110
|
and any top-level key a domain schema strips are both NAMED through `onWarning`
|
|
111
|
-
(`unread-config-file` / `unknown-keys-dropped`) instead of vanishing silently.
|
|
111
|
+
(`unread-config-file` / `unknown-keys-dropped`) instead of vanishing silently. **2.0.0** adds per-ENTRY
|
|
112
|
+
tolerance on the same read path: one malformed catalog entry is dropped on its own (named through
|
|
113
|
+
`onWarning` as `entry-dropped`, dot-path) and its siblings survive, instead of the whole domain falling to
|
|
114
|
+
its schema default. It is the SAME `DOMAIN_READ_FALLBACK` table that decides where this applies — gate
|
|
115
|
+
domains keep failing loud, entry by entry included.
|
|
116
|
+
- **Hooks contract** (`hooks.ts`, subpath `./hooks`): the CC-parity hook surface — `HOOK_EVENTS` (33 events:
|
|
117
|
+
the CC 2.1.198 set of 27 plus `CwdChanged` / `FileChanged` / `MessageDisplay` / `DirectoryAdded` and, from
|
|
118
|
+
2.0.0, the model-switch pair `PreModelSwitch` / `PostModelSwitch`), the single-owner map
|
|
119
|
+
`HOOK_EVENT_OWNER` (engine | shell), the matcher-field table, a per-event input schema (`HookInput`),
|
|
120
|
+
the output/decision shapes (`SyncHookOutput` / `HookSpecificOutput` / `HookPermissionRequestDecision`),
|
|
121
|
+
`HooksConfig` (the `settings.json` `hooks` shape — a CLOSED key set, so a typo is caught at the wire),
|
|
122
|
+
and stop attribution (`StoppedBy` / `stopAttributionText`).
|
|
123
|
+
- **Cross-session settings** (`cross-session.ts`): the two `settings.json` keys `crossSessionInbound`
|
|
124
|
+
(accept/hold/refuse, `default` = unset) and `dialogExpiry` (60s/5m/10m/never), each with a reader that
|
|
125
|
+
answers `ok:false` on a bad value instead of silently folding to the default.
|
|
126
|
+
- **Scheduler store contract** (`scheduler-store.ts`, subpath `./scheduler-store`; the fs binding is in
|
|
127
|
+
`./node`): the durable self-wake record shape + `parseSchedulerStore` / `serializeSchedulerStore`
|
|
128
|
+
(forward-compatible: unknown fields survive a round-trip, malformed records are dropped one by one).
|
|
129
|
+
- **Fleet contract** (`fleet.ts`, subpath `./fleet`): drain protocol, instance discovery, dispatch policy,
|
|
130
|
+
quota lease, usage report, quota-reset event.
|
|
131
|
+
- **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
|
|
132
|
+
carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side,
|
|
133
|
+
a consumer's projection, or the key's own guard). Today: `models[].extraBody`, `models[].compat` (1.10.0),
|
|
134
|
+
`mcp.servers[].source` (1.7.2), `mcp.servers[].toolFaces` (1.9.0), and an entitlement override's `value`.
|
|
135
|
+
- **Extension namespaces** (`EXTENSION_NAMESPACES`, 1.5.0): keys another writer puts in a domain document by
|
|
136
|
+
contract and this package deliberately does not model (today: the shell's entry-level `sema*` fields on
|
|
137
|
+
`models`). They are still stripped, but they are NOT reported as unknown keys — intent is declared at the
|
|
138
|
+
registration site, so every remaining unknown key is a typo, with no per-consumer special-casing.
|
|
112
139
|
|
|
113
140
|
## Secret boundary
|
|
114
141
|
|
package/dist/config-fns.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* The fat `ConfigStore` interface itself STAYS in sema-registry (app-only, ~32 publish/RBAC/worker-status
|
|
14
14
|
* methods); only the narrow {@link ConfigReader} (reader.ts) is carried here.
|
|
15
15
|
*/
|
|
16
|
-
import
|
|
16
|
+
import { z } from "zod";
|
|
17
17
|
import { type DomainConfig, type DomainName, type EffectiveConfig, type EffectiveWire } from "./types.js";
|
|
18
18
|
/** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
|
|
19
19
|
* `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
|
|
@@ -53,7 +53,7 @@ export declare function isRole(v: unknown): v is UserRole;
|
|
|
53
53
|
/**
|
|
54
54
|
* Minimum role to WRITE a domain. Default = `editor`; `skills` and `mcp` are raised to `publisher`
|
|
55
55
|
* because their content goes straight into the agent's system prompt / tool surface — one edit retargets
|
|
56
|
-
* the prompt of every worker × scenario that loads it (
|
|
56
|
+
* the prompt of every worker × scenario that loads it (the mcp/skills design note §3.3, decision §6 Q3 从严).
|
|
57
57
|
* Looked up by the [domain] PUT route. Any domain absent here uses {@link DEFAULT_DOMAIN_WRITE_ROLE}.
|
|
58
58
|
*/
|
|
59
59
|
export declare const DEFAULT_DOMAIN_WRITE_ROLE: UserRole;
|
|
@@ -133,6 +133,46 @@ export interface PublishedSnapshot {
|
|
|
133
133
|
export declare function emptyEffective(): EffectiveConfig;
|
|
134
134
|
/** Validate + fill defaults for a domain payload (or the schema's empty default when absent/invalid). */
|
|
135
135
|
export declare function parseDomain<K extends DomainName>(domain: K, raw: unknown): DomainConfig[K];
|
|
136
|
+
/** 一条被丢掉的条目:`path` = 点号路径(`models.3` / `roles.summarize`),`error` 已脱敏。 */
|
|
137
|
+
export interface DroppedEntry {
|
|
138
|
+
path: string;
|
|
139
|
+
error: unknown;
|
|
140
|
+
}
|
|
141
|
+
/** {@link salvageEntries} 的结果。`raw` 是剔掉坏条目后的文档;**什么都没丢时返回原引用**
|
|
142
|
+
* (与 {@link grandfatherHostsRead} 同纪律:好数据穿过读路径 byte 不变)。 */
|
|
143
|
+
export interface EntrySalvageResult {
|
|
144
|
+
raw: unknown;
|
|
145
|
+
dropped: DroppedEntry[];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 从**schema 自身**派生条目集合(数组字段 / Record 字段),逐条判,坏的丢掉并逐条记账。
|
|
149
|
+
*
|
|
150
|
+
* 入参是 **schema 而不是域名**:`skills` 在 wire 上不是域 schema 而是 manifest(`SkillsManifestSchema`),
|
|
151
|
+
* 在 `buildEffective` 之外单独判 —— 只吃域名的抢救点会把那条腿漏在门外,同一份病只因为换了张 schema
|
|
152
|
+
* 就没被覆盖。吃 schema ⇒ 域面与 wire 面共用同一条律,没有第二实现。
|
|
153
|
+
*
|
|
154
|
+
* 为什么从 schema 派生而不是写一张「域 → 条目字段」表:那张表是第二份真源,开新域必漏。schema 里
|
|
155
|
+
* 「这个字段是一组条目」这件事本来就写着,读它即可 —— 新域天然被覆盖,零登记动作。
|
|
156
|
+
*
|
|
157
|
+
* 🔴 **零幸存者不算「部分损坏」**:一个集合如果所有条目都坏,那它不是「部分损坏的集合」,而是**整个坏掉
|
|
158
|
+
* 的集合** —— 本函数对这种字段**一条都不丢**,原样留着让整域档位去裁。这条不是特判,是「抢救」的定义:
|
|
159
|
+
* 没有要抢救的东西。它同时把一整类 fail-open 陷阱一次性挡住 —— 本 schema 里有若干字段**空 = 不设限**
|
|
160
|
+
* (最典型的 `models.atModelAllowlist`:空/缺席 = 所有模型都可 @),把一份「全是坏值」的限制清单抢救成
|
|
161
|
+
* 空数组,等于把运维的限制悄悄删了。这里不去枚举「哪些字段空了等于放权」(枚举必漏),而是**从不凭空
|
|
162
|
+
* 造出一个空集合**。
|
|
163
|
+
*
|
|
164
|
+
* 🔴 **告警不许比它诊断的东西更危险**(复审 R1 两条,红先复现):① Record 的键是用户写的,完全可能就是
|
|
165
|
+
* 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
|
|
166
|
+
* 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
|
|
167
|
+
* 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
|
|
168
|
+
* 绝不把解析拖下水、更不回显它的错误(与同文件 `scanDropped` 逐字同纪律)。
|
|
169
|
+
*
|
|
170
|
+
* 边界(如实登记):① 只看**顶层**字段(条目集合就在顶层,嵌套的坏形属于条目自身的问题);② raw 不是
|
|
171
|
+
* 纯 JSON 对象、字段值与 schema 形状对不上(schema 说数组、文档给对象)⇒ 原样返回,交给整域档位裁;
|
|
172
|
+
* ③ 域级判据(重名 / 悬空 ref)不在本函数视野内 —— 丢条目救不了它们,调用方据此回落整域档位;
|
|
173
|
+
* ④ 只认 `ZodArray` / `ZodRecord` 两种条目集合形 —— 别的形(元组等)不抢救,退回整域档位(保守方向)。
|
|
174
|
+
*/
|
|
175
|
+
export declare function salvageEntries(schema: z.ZodTypeAny, raw: unknown): EntrySalvageResult;
|
|
136
176
|
/**
|
|
137
177
|
* Shared GC predicate for worker-status rows (F10) — keeps memory/sqlite/tidb identical. Given the rows'
|
|
138
178
|
* `{name, updatedAt}` and the GC options, returns the names to prune (A: not in `keepNames`; B: older than
|
|
@@ -154,7 +194,13 @@ export declare function statusRowsToPrune(rows: {
|
|
|
154
194
|
/** Lift the governance trio out of a LEGACY runtime doc (pre-0.6 stored runtime carried autonomy/commandPolicy/
|
|
155
195
|
* approvalRequire). Used as the governance domain's read fallback when governance was NEVER set — so a 0.5
|
|
156
196
|
* store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
|
|
157
|
-
* (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
|
|
197
|
+
* (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
|
|
198
|
+
*
|
|
199
|
+
* SUNSET (1.11.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
|
|
200
|
+
* its guardrails silently, so this arm cannot simply be deleted at a minor. It goes at the next MAJOR, with
|
|
201
|
+
* the standard persistent-data treatment (a loud refusal naming the domain + a one-time migration on the
|
|
202
|
+
* writer side), and the closing condition is observable rather than a promise: `buildEffective` now emits
|
|
203
|
+
* `legacy-governance-lifted` every time the arm actually fires, so a deployment can prove it no longer does. */
|
|
158
204
|
export declare function legacyGovernanceFromRuntime(rawRuntime: unknown): unknown;
|
|
159
205
|
/** A warning emitted by {@link buildEffective} when it clamps/degrades instead of throwing (0.8.1), or by
|
|
160
206
|
* {@link FileConfigStore} when a file it will never read sits in `config.d/` (0.19.0). Callers route it to
|
|
@@ -193,6 +239,24 @@ export type EffectiveReadWarning = {
|
|
|
193
239
|
keys: string[];
|
|
194
240
|
truncatedFrom?: number;
|
|
195
241
|
}
|
|
242
|
+
/** 0.5 期的 runtime 文档里还带着治理三闸,而 governance 域从未被设过 —— 读侧抬升腿真生效了。
|
|
243
|
+
* `keys` = 这次抬起来的那几个键。**照旧生效,只是不再无声**:兼容窗要能关掉,前提是有人知道它还开着。
|
|
244
|
+
* 1.11.0 起。 */
|
|
245
|
+
| {
|
|
246
|
+
domain: "governance";
|
|
247
|
+
kind: "legacy-governance-lifted";
|
|
248
|
+
keys: string[];
|
|
249
|
+
}
|
|
250
|
+
/** 一条**条目**没通过它自己的 entry schema,被单独丢掉了(域内其余条目照常生效)。`path` = 点号路径
|
|
251
|
+
* (`models.3` / `roles.summarize`),与 `unknown-keys-dropped` 的路径形同一套。逐条一发 —— 载荷是
|
|
252
|
+
* O(域里的条目数),与被读文档同阶,不放大,故不设条数上限(有上限的是**单条**告警里的键列表)。
|
|
253
|
+
* 1.11.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。 */
|
|
254
|
+
| {
|
|
255
|
+
domain: DomainName;
|
|
256
|
+
kind: "entry-dropped";
|
|
257
|
+
path: string;
|
|
258
|
+
error: unknown;
|
|
259
|
+
}
|
|
196
260
|
/** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
|
|
197
261
|
| {
|
|
198
262
|
kind: "unread-config-file";
|
|
@@ -319,7 +383,11 @@ export interface GrandfatherHostsReadResult {
|
|
|
319
383
|
/** READ-path grandfather clamp for the 0.8.0 hosts tightening (0.8.1, S3 M-1). Clamps ONLY the two bounds
|
|
320
384
|
* 0.8.0 added (name > 63 → drop the entry; array > 1024 → slice) so 0.7-era legal stored data keeps reading;
|
|
321
385
|
* everything else (regex, types, duplicates) is left for parseDomain — this is a targeted grandfather, not a
|
|
322
|
-
* lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
|
|
386
|
+
* lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
|
|
387
|
+
*
|
|
388
|
+
* SUNSET (1.11.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
|
|
389
|
+
* never silent (`hosts-grandfathered` fires whenever it clamps), so the closing condition is already
|
|
390
|
+
* observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
|
|
323
391
|
export declare function grandfatherHostsRead(raw: unknown): GrandfatherHostsReadResult;
|
|
324
392
|
export declare function buildEffective(version: number, getRaw: (d: DomainName) => unknown, updatedAt: string, onWarning?: (w: EffectiveReadWarning) => void): EffectiveConfig;
|
|
325
393
|
/** READ-side tolerant judgment for a GET /effective **wire payload**(the {@link EffectiveWire} contract)at
|
package/dist/config-fns.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure config functions + data shapes — LIFTED verbatim out of sema-registry's `store.ts` (the half that
|
|
3
|
+
* depends only on `./types`, zero app/DB/next coupling). sema-registry re-exports these so its existing
|
|
4
|
+
* `@/lib/config` importers keep working unchanged (ONE copy of the code, here).
|
|
5
|
+
*
|
|
6
|
+
* Two DETERMINISM refactors vs. the original store.ts (so the package is wall-clock-free and a local file
|
|
7
|
+
* source can produce byte-identical effective config for identical inputs):
|
|
8
|
+
* 1. `buildEffective(version, getRaw, updatedAt)` — `updatedAt` is now a REQUIRED param (was a
|
|
9
|
+
* `new Date().toISOString()` default). Every caller supplies it explicitly.
|
|
10
|
+
* 2. `emptyEffective()` seeds the epoch (`new Date(0).toISOString()`) instead of the wall clock, matching
|
|
11
|
+
* the stores' existing "epoch sentinel" reduce seed.
|
|
12
|
+
*
|
|
13
|
+
* The fat `ConfigStore` interface itself STAYS in sema-registry (app-only, ~32 publish/RBAC/worker-status
|
|
14
|
+
* methods); only the narrow {@link ConfigReader} (reader.ts) is carried here.
|
|
15
|
+
*/
|
|
16
|
+
// zod is a VALUE import here (not just a type): {@link salvageEntries} introspects a domain schema to find
|
|
17
|
+
// its entry collections. zod is this package's one runtime dependency, so this costs nothing new.
|
|
18
|
+
import { z } from "zod";
|
|
1
19
|
import { DOMAIN_SCHEMAS, GOVERNANCE_MIRROR_KEYS, HOSTS_ARRAY_MAX, HOST_NAME_MAX, InfraCostRates, LimitsConfig, SkillsManifestSchema, containsSecretToken } from "./types.js";
|
|
2
20
|
/** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
|
|
3
21
|
* `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
|
|
@@ -45,7 +63,7 @@ export function isRole(v) {
|
|
|
45
63
|
/**
|
|
46
64
|
* Minimum role to WRITE a domain. Default = `editor`; `skills` and `mcp` are raised to `publisher`
|
|
47
65
|
* because their content goes straight into the agent's system prompt / tool surface — one edit retargets
|
|
48
|
-
* the prompt of every worker × scenario that loads it (
|
|
66
|
+
* the prompt of every worker × scenario that loads it (the mcp/skills design note §3.3, decision §6 Q3 从严).
|
|
49
67
|
* Looked up by the [domain] PUT route. Any domain absent here uses {@link DEFAULT_DOMAIN_WRITE_ROLE}.
|
|
50
68
|
*/
|
|
51
69
|
export const DEFAULT_DOMAIN_WRITE_ROLE = "editor";
|
|
@@ -105,6 +123,139 @@ export function parseDomain(domain, raw) {
|
|
|
105
123
|
return r.data;
|
|
106
124
|
throw sanitizeZodError(r.error); // recursive redaction (message/received/unionErrors) — no secret echo. R13/R14.
|
|
107
125
|
}
|
|
126
|
+
// ── 逐条容错律(1.11.0,S-144 按类修)───────────────────────────────────────────────────────────────
|
|
127
|
+
//
|
|
128
|
+
// 病:域解析整域 all-or-nothing —— 13 个模型里坏一条,整张目录退成 schema 默认 = 一条都不剩。缓解一直
|
|
129
|
+
// 是**逐键**打补丁(把新键改成 `z.unknown()` 让它别拒),那是给每个新键加特判,规则只会越来越多;
|
|
130
|
+
// 而本仓的调度存储早就有成熟的逐条容错形(坏记录逐条丢 + 逐条上报,其余全存活),只是没抬到域面。
|
|
131
|
+
//
|
|
132
|
+
// 本节把那条判据提成**域无关的单铸点**:`salvageEntries(domain, raw)` 从域 schema 自身派生出「哪些字段
|
|
133
|
+
// 是条目集合」(数组 / Record),逐条 parse,坏的丢掉并点名。域面唯一的调用点是 `buildEffective` 的
|
|
134
|
+
// `parseOr`,且**只在整域解析已经失败之后**才跑 —— 好配置的解析路径一字不动(与 0.8.1 的 grandfather
|
|
135
|
+
// 同姿势:先精确、后通用、成功路径 byte 不变)。
|
|
136
|
+
//
|
|
137
|
+
// 🔴 不新开档位:容错只发生在 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域。gate 域(rosters /
|
|
138
|
+
// governance / entitlement / execution / workers / runtime / limits / projects)坏条目仍然 throw ——
|
|
139
|
+
// 悄悄丢掉一条 roster 就是那个 worker 静默拿到全量目录,与整域降级是同一条「放权,不许」的理由,所以
|
|
140
|
+
// 共用同一张表,不为容错另立一张。
|
|
141
|
+
/** 展开 zod 的包装层(`.optional()` / `.default()` / `.nullable()` / `.catch()` / `.transform()` /
|
|
142
|
+
* `.superRefine()`),拿到里面真正的那个 schema。包装不改变「条目能不能单独解析」这件事。 */
|
|
143
|
+
function unwrapSchema(schema) {
|
|
144
|
+
let cur = schema;
|
|
145
|
+
// 有界:包装层数是 schema 作者写出来的常数,8 层远超实际(防被畸形 schema 拖住)。
|
|
146
|
+
for (let i = 0; i < 8; i++) {
|
|
147
|
+
if (cur instanceof z.ZodEffects)
|
|
148
|
+
cur = cur.innerType();
|
|
149
|
+
else if (cur instanceof z.ZodOptional || cur instanceof z.ZodNullable)
|
|
150
|
+
cur = cur.unwrap();
|
|
151
|
+
else if (cur instanceof z.ZodDefault)
|
|
152
|
+
cur = cur.removeDefault();
|
|
153
|
+
else if (cur instanceof z.ZodCatch)
|
|
154
|
+
cur = cur.removeCatch();
|
|
155
|
+
else
|
|
156
|
+
return cur;
|
|
157
|
+
}
|
|
158
|
+
return cur;
|
|
159
|
+
}
|
|
160
|
+
/** 一条条目的 parse —— 坏就返回它的(脱敏)错误。恶意 getter/Proxy 在遍历中抛错时**绝不**外泄其文案
|
|
161
|
+
* (可能带 secret,与 parseDomain 的 R13 同纪律),只留一句中性话。 */
|
|
162
|
+
function judgeEntry(schema, value) {
|
|
163
|
+
try {
|
|
164
|
+
const r = schema.safeParse(value);
|
|
165
|
+
return r.success ? undefined : sanitizeZodError(r.error);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return new Error("entry could not be read during validation");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 从**schema 自身**派生条目集合(数组字段 / Record 字段),逐条判,坏的丢掉并逐条记账。
|
|
173
|
+
*
|
|
174
|
+
* 入参是 **schema 而不是域名**:`skills` 在 wire 上不是域 schema 而是 manifest(`SkillsManifestSchema`),
|
|
175
|
+
* 在 `buildEffective` 之外单独判 —— 只吃域名的抢救点会把那条腿漏在门外,同一份病只因为换了张 schema
|
|
176
|
+
* 就没被覆盖。吃 schema ⇒ 域面与 wire 面共用同一条律,没有第二实现。
|
|
177
|
+
*
|
|
178
|
+
* 为什么从 schema 派生而不是写一张「域 → 条目字段」表:那张表是第二份真源,开新域必漏。schema 里
|
|
179
|
+
* 「这个字段是一组条目」这件事本来就写着,读它即可 —— 新域天然被覆盖,零登记动作。
|
|
180
|
+
*
|
|
181
|
+
* 🔴 **零幸存者不算「部分损坏」**:一个集合如果所有条目都坏,那它不是「部分损坏的集合」,而是**整个坏掉
|
|
182
|
+
* 的集合** —— 本函数对这种字段**一条都不丢**,原样留着让整域档位去裁。这条不是特判,是「抢救」的定义:
|
|
183
|
+
* 没有要抢救的东西。它同时把一整类 fail-open 陷阱一次性挡住 —— 本 schema 里有若干字段**空 = 不设限**
|
|
184
|
+
* (最典型的 `models.atModelAllowlist`:空/缺席 = 所有模型都可 @),把一份「全是坏值」的限制清单抢救成
|
|
185
|
+
* 空数组,等于把运维的限制悄悄删了。这里不去枚举「哪些字段空了等于放权」(枚举必漏),而是**从不凭空
|
|
186
|
+
* 造出一个空集合**。
|
|
187
|
+
*
|
|
188
|
+
* 🔴 **告警不许比它诊断的东西更危险**(复审 R1 两条,红先复现):① Record 的键是用户写的,完全可能就是
|
|
189
|
+
* 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
|
|
190
|
+
* 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
|
|
191
|
+
* 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
|
|
192
|
+
* 绝不把解析拖下水、更不回显它的错误(与同文件 `scanDropped` 逐字同纪律)。
|
|
193
|
+
*
|
|
194
|
+
* 边界(如实登记):① 只看**顶层**字段(条目集合就在顶层,嵌套的坏形属于条目自身的问题);② raw 不是
|
|
195
|
+
* 纯 JSON 对象、字段值与 schema 形状对不上(schema 说数组、文档给对象)⇒ 原样返回,交给整域档位裁;
|
|
196
|
+
* ③ 域级判据(重名 / 悬空 ref)不在本函数视野内 —— 丢条目救不了它们,调用方据此回落整域档位;
|
|
197
|
+
* ④ 只认 `ZodArray` / `ZodRecord` 两种条目集合形 —— 别的形(元组等)不抢救,退回整域档位(保守方向)。
|
|
198
|
+
*/
|
|
199
|
+
export function salvageEntries(schema, raw) {
|
|
200
|
+
try {
|
|
201
|
+
return salvageEntriesUnsafe(schema, raw);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return { raw, dropped: [] }; // 反射/访问器抛错:抢不动就当没抢救,绝不外泄其文案
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function salvageEntriesUnsafe(schema, raw) {
|
|
208
|
+
if (!isPlainJsonObject(raw))
|
|
209
|
+
return { raw, dropped: [] };
|
|
210
|
+
const root = unwrapSchema(schema);
|
|
211
|
+
if (!(root instanceof z.ZodObject))
|
|
212
|
+
return { raw, dropped: [] };
|
|
213
|
+
const shape = root.shape;
|
|
214
|
+
const dropped = [];
|
|
215
|
+
const patch = {};
|
|
216
|
+
for (const [field, fieldSchema] of Object.entries(shape)) {
|
|
217
|
+
const value = raw[field];
|
|
218
|
+
if (value === undefined)
|
|
219
|
+
continue;
|
|
220
|
+
const collection = unwrapSchema(fieldSchema);
|
|
221
|
+
if (collection instanceof z.ZodArray && Array.isArray(value)) {
|
|
222
|
+
const element = collection.element;
|
|
223
|
+
const bad = [];
|
|
224
|
+
const kept = value.filter((entry, i) => {
|
|
225
|
+
const error = judgeEntry(element, entry);
|
|
226
|
+
if (error === undefined)
|
|
227
|
+
return true;
|
|
228
|
+
bad.push({ path: `${field}.${i}`, error });
|
|
229
|
+
return false;
|
|
230
|
+
});
|
|
231
|
+
// 零幸存者 ⇒ 没有要抢救的东西(见头注:绝不凭空造出一个空集合)。
|
|
232
|
+
if (bad.length > 0 && kept.length > 0) {
|
|
233
|
+
dropped.push(...bad);
|
|
234
|
+
patch[field] = kept;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (collection instanceof z.ZodRecord && isPlainJsonObject(value)) {
|
|
238
|
+
const keySchema = collection.keySchema;
|
|
239
|
+
const valueSchema = collection.valueSchema;
|
|
240
|
+
const bad = [];
|
|
241
|
+
const kept = {};
|
|
242
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
243
|
+
const error = judgeEntry(keySchema, key) ?? judgeEntry(valueSchema, entry);
|
|
244
|
+
if (error === undefined)
|
|
245
|
+
kept[key] = entry;
|
|
246
|
+
else
|
|
247
|
+
bad.push({ path: `${field}.${redactSecrets(key)}`, error }); // 键是用户写的,可能就是一条粘错位置的凭据
|
|
248
|
+
}
|
|
249
|
+
if (bad.length > 0 && Object.keys(kept).length > 0) {
|
|
250
|
+
dropped.push(...bad);
|
|
251
|
+
patch[field] = kept;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (dropped.length === 0)
|
|
256
|
+
return { raw, dropped };
|
|
257
|
+
return { raw: { ...raw, ...patch }, dropped };
|
|
258
|
+
}
|
|
108
259
|
/**
|
|
109
260
|
* Shared GC predicate for worker-status rows (F10) — keeps memory/sqlite/tidb identical. Given the rows'
|
|
110
261
|
* `{name, updatedAt}` and the GC options, returns the names to prune (A: not in `keepNames`; B: older than
|
|
@@ -127,7 +278,13 @@ export function statusRowsToPrune(rows, opts) {
|
|
|
127
278
|
/** Lift the governance trio out of a LEGACY runtime doc (pre-0.6 stored runtime carried autonomy/commandPolicy/
|
|
128
279
|
* approvalRequire). Used as the governance domain's read fallback when governance was NEVER set — so a 0.5
|
|
129
280
|
* store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
|
|
130
|
-
* (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
|
|
281
|
+
* (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
|
|
282
|
+
*
|
|
283
|
+
* SUNSET (1.11.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
|
|
284
|
+
* its guardrails silently, so this arm cannot simply be deleted at a minor. It goes at the next MAJOR, with
|
|
285
|
+
* the standard persistent-data treatment (a loud refusal naming the domain + a one-time migration on the
|
|
286
|
+
* writer side), and the closing condition is observable rather than a promise: `buildEffective` now emits
|
|
287
|
+
* `legacy-governance-lifted` every time the arm actually fires, so a deployment can prove it no longer does. */
|
|
131
288
|
export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
132
289
|
if (rawRuntime === null || typeof rawRuntime !== "object")
|
|
133
290
|
return undefined;
|
|
@@ -352,7 +509,11 @@ export const DOMAIN_READ_FALLBACK = {
|
|
|
352
509
|
/** READ-path grandfather clamp for the 0.8.0 hosts tightening (0.8.1, S3 M-1). Clamps ONLY the two bounds
|
|
353
510
|
* 0.8.0 added (name > 63 → drop the entry; array > 1024 → slice) so 0.7-era legal stored data keeps reading;
|
|
354
511
|
* everything else (regex, types, duplicates) is left for parseDomain — this is a targeted grandfather, not a
|
|
355
|
-
* lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
|
|
512
|
+
* lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
|
|
513
|
+
*
|
|
514
|
+
* SUNSET (1.11.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
|
|
515
|
+
* never silent (`hosts-grandfathered` fires whenever it clamps), so the closing condition is already
|
|
516
|
+
* observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
|
|
356
517
|
export function grandfatherHostsRead(raw) {
|
|
357
518
|
if (raw === null || typeof raw !== "object" || !Array.isArray(raw.hosts)) {
|
|
358
519
|
return { raw, droppedNames: [] }; // not the expected shape — let parseDomain rule on it
|
|
@@ -391,6 +552,24 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
391
552
|
catch (e) {
|
|
392
553
|
if (DOMAIN_READ_FALLBACK[domain] === "throw")
|
|
393
554
|
throw e;
|
|
555
|
+
// 逐条容错(1.11.0)在整域降级**之前**试一次:只丢掉真正坏掉的那几条条目,同域好兄弟活下来。
|
|
556
|
+
// 告警先缓存再决定 —— 一个域只给一个结论:救活了就逐条报 `entry-dropped`,没救活就只报
|
|
557
|
+
// `domain-defaulted`(那一域的条目反正一条都不剩,再讲「丢了第 3 条」只会误导)。
|
|
558
|
+
const salvage = salvageEntries(DOMAIN_SCHEMAS[domain], raw);
|
|
559
|
+
if (salvage.dropped.length > 0) {
|
|
560
|
+
try {
|
|
561
|
+
const buffered = [];
|
|
562
|
+
const parsed = parseDomainLoud(domain, salvage.raw, (w) => buffered.push(w));
|
|
563
|
+
for (const d of salvage.dropped)
|
|
564
|
+
onWarning?.({ domain, kind: "entry-dropped", path: d.path, error: d.error });
|
|
565
|
+
for (const w of buffered)
|
|
566
|
+
onWarning?.(w);
|
|
567
|
+
return parsed;
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// 条目之外还有毛病(重名 / 悬空 ref / 域级判据)—— 丢条目救不了它,照常整域降级。
|
|
571
|
+
}
|
|
572
|
+
}
|
|
394
573
|
onWarning?.({ domain, kind: "domain-defaulted", error: e });
|
|
395
574
|
return parseDomain(domain, undefined); // schema default — always valid
|
|
396
575
|
}
|
|
@@ -398,7 +577,15 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
398
577
|
// governance TRUTH: the domain itself; if it was never set, lift the trio from a legacy runtime doc
|
|
399
578
|
// (0.5→0.6 upgrade path — a2/local stores keep their gates without a data migration).
|
|
400
579
|
const governanceRaw = getRaw("governance");
|
|
401
|
-
|
|
580
|
+
let governanceSource = governanceRaw;
|
|
581
|
+
if (governanceRaw === undefined) {
|
|
582
|
+
const lifted = legacyGovernanceFromRuntime(getRaw("runtime"));
|
|
583
|
+
// 兼容臂真生效时出声(1.11.0):行为一字不变,但窗要能关就得有人看得见它还开着。
|
|
584
|
+
if (lifted !== undefined)
|
|
585
|
+
onWarning?.({ domain: "governance", kind: "legacy-governance-lifted", keys: Object.keys(lifted) });
|
|
586
|
+
governanceSource = lifted;
|
|
587
|
+
}
|
|
588
|
+
const governance = parseLoud("governance", governanceSource);
|
|
402
589
|
// runtime slot = limit residue ONLY. 0.7.1(BREAKING,service 回执收账):E1 的 governance→runtime
|
|
403
590
|
// 六闸双写镜像已撤——service 1.150.0 已切优先读 eff.governance 并回帖确认(撤双写以其确认为门槛,
|
|
404
591
|
// ask⑤ 顺序契约兑现)。READ 方向的 legacy lift(governance 域从未设时从旧 runtime 文档抬升三闸,
|
|
@@ -477,8 +664,19 @@ export function readEffectiveWire(raw, onWarning) {
|
|
|
477
664
|
skills = manifest.data;
|
|
478
665
|
}
|
|
479
666
|
else {
|
|
480
|
-
|
|
481
|
-
|
|
667
|
+
// 逐条容错律的 wire 腿:manifest 走的是另一张 schema,但**同一个**抢救点 —— 一条坏条目只丢它自己,
|
|
668
|
+
// 整张 skill 清单不再因此蒸发(零幸存者仍整份退默认,见 salvageEntries 头注)。
|
|
669
|
+
const salvage = salvageEntries(SkillsManifestSchema, r.skills);
|
|
670
|
+
const retry = salvage.dropped.length > 0 ? SkillsManifestSchema.safeParse(salvage.raw) : undefined;
|
|
671
|
+
if (retry?.success) {
|
|
672
|
+
for (const d of salvage.dropped)
|
|
673
|
+
onWarning?.({ domain: "skills", kind: "entry-dropped", path: d.path, error: d.error });
|
|
674
|
+
skills = retry.data;
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
onWarning?.({ domain: "skills", kind: "domain-defaulted", error: sanitizeZodError(manifest.error) });
|
|
678
|
+
skills = { skills: [] };
|
|
679
|
+
}
|
|
482
680
|
}
|
|
483
681
|
// Open-world passthrough: keys that are neither validated domains nor the envelope fields ride verbatim.
|
|
484
682
|
const passthrough = {};
|
package/dist/cross-domain.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-domain referential integrity — the PURE half, shared by the single-domain PUT and the batch write so
|
|
3
|
-
* BOTH enforce the same rules (
|
|
3
|
+
* BOTH enforce the same rules (the multi-roster design note §6). The FLEET-coupled `applyAutoPlacement` (which imports
|
|
4
4
|
* `../fleet/placement`) STAYS in sema-registry — it's not pure and not portable. This package carries only
|
|
5
|
-
* `refIntegrityIssues` + `
|
|
5
|
+
* `refIntegrityIssues` + `createSiblingResolver` + the ref-issue types.
|
|
6
6
|
*
|
|
7
7
|
* BATCH-AWARE: a sibling domain (the models a roster references, the rosters a worker references) may be
|
|
8
8
|
* changing in the SAME batch. So checks resolve siblings through a {@link SiblingResolver} that prefers a
|
|
@@ -21,8 +21,12 @@ export interface RefIssue {
|
|
|
21
21
|
export type SiblingResolver = <K extends keyof DomainConfig>(domain: K) => Promise<DomainConfig[K] | undefined>;
|
|
22
22
|
/** A resolver over the reader, optionally shadowed by pending (same-operation) writes keyed by domain.
|
|
23
23
|
* Widened from the fat ConfigStore to the narrow {@link ConfigReader} (it only calls `getDomain`) so a TOC
|
|
24
|
-
* file source can use it without the full store.
|
|
25
|
-
|
|
24
|
+
* file source can use it without the full store.
|
|
25
|
+
*
|
|
26
|
+
* 1.11.0 renamed this from `siblingResolver` (BREAKING, zero aliases): the naming law is `build*` returns
|
|
27
|
+
* plain DATA and `create*` returns a LIVE object — this returns a closure, so it was on the wrong side of
|
|
28
|
+
* the line and carried neither prefix. A consumer's compile error IS the notice; the fix is the new name. */
|
|
29
|
+
export declare function createSiblingResolver(store: ConfigReader, pending?: Partial<{
|
|
26
30
|
[K in keyof DomainConfig]: DomainConfig[K];
|
|
27
31
|
}>): SiblingResolver;
|
|
28
32
|
/** The domains whose writes carry cross-domain model/roster references we validate at save time. */
|