@sema-agent/settings-schema 6.0.0 → 6.2.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 CHANGED
@@ -6,6 +6,284 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 6.2.0 — 2026-09-26 — additive:`ModelRefusalsWire` / `ModelRefusalsSignal` 一处定义 + `readModelRefusals`(S-768;条目两码 / 组拒四码闭集是读方返回型,wire 开放串、读方收窄,心跳字段形坏降缺席 —— 诊断键不打路由)· `isDisclosableIdentity` / `isReportableRefusalName` 导出 · devDep core 2.1.0 → 7.33.1 精确(漂移门 ② 改读运行期 `MODEL_ROLES`,删陈旧豁免)· README / llms-full zod 两实例真话(server / CLI 已在 zod 4;归一 = 7.0.0,S-777)
10
+
11
+ > 本节全 additive(新导出 / 新可选键),零删零改名。判档口径见 README「发版纪律」:新键 / 新导出 = minor。心跳上的新键
12
+ > 形坏只降为缺席,6.1.0 收下的任何心跳体 6.2.0 照收(见「谁受伤」③)。
13
+
14
+ ### S-768 · 模型拒因信号:闭集码真源 + wire 形 + 一只读口两出口(additive)
15
+
16
+ `src/fleet.ts`(与 `RestartSignal` 同节)新导出:
17
+
18
+ - **成因码闭集**(`as const` 元组 + `z.enum` + 型):`MODEL_ENTRY_REFUSAL_CODES = ["config.model_no_brain",
19
+ "config.model_entry_invalid"]` / `ModelEntryRefusalCode`;`MODEL_PLANE_REFUSAL_CODES = ["config.default_model_missing",
20
+ "config.model_refused_name_minted", "config.model_route_endpoint_missing", "config.gateway_fallback_without_primary"]` /
21
+ `ModelPlaneRefusalCode`。词**逐字**取自引擎(server @ 2aa56fe7:`src/model-entry-refusal.ts:45-56` —— 条目两码 = S-704 逐条判据
22
+ 的两个成因,`config.default_model_missing` = S-705 模型面组拒,`config.model_refused_name_minted` = S-712 不变量组拒;
23
+ `src/model-route-endpoint.ts:40,42` —— 不变量 `model-route-endpoint` 的两码,模型面 stage 在 `src/config-center/apply-effective.ts:664-665`
24
+ 判);红先文件里的期望字面是抄来的,不从实现派生。**组拒码镜像引擎组拒码表的整族**(发版方裁 1):不按「今天热路径上可达」
25
+ 挑 —— 可达性会变,表不变(`config.gateway_fallback_without_primary` 今天在热路径上实际不可达,同一 env 下 boot 恒先拒,照收)。
26
+ **真源上移**:此前闭集只活在引擎一侧,中心要穷举就得手抄(C-R75);上移后两边 import 同一份。**谁在加码时编译红**(对抗腿
27
+ F8 订正首稿「双侧编译红」):按码穷举 `switch` 的读方(中心)必红;生产方只有持**以码为键**的表(对码穷举 `switch` /
28
+ `Record<码, …>`)才红 —— 只拿 `satisfies` 把自己的码值约束成本包闭集的子集,证明的是「子集」不是「相等」,加码照绿。引擎提货
29
+ 时要留一张以码为键的表。
30
+ - **两个面,刻意分开**(发版方裁 3:失败方向 = 语义方向 —— 这是**诊断**键,集外词 / 形坏绝不能把一台健康实例打成不可路由):
31
+ · **`ModelRefusalsWire`**(zod)= wire 上走的、心跳存行里存的:`{ entries: record(非空名 → 非空串), plane?: 非空串 }` + 空信号
32
+ refine —— 只判**形律**,码是开放串;本包版本不认识的码照过(对这个读方是「没说」,存行照存,中心升级本包后同一行读出新码)。
33
+ · **`ModelRefusalsSignal`**(纯型)= `{ entries: Record<名, ModelEntryRefusalCode>, plane?: ModelPlaneRefusalCode }` =
34
+ `readModelRefusals` 的返回型;两只 `z.enum` 仍导出,仍是加码时读方的编译红源。
35
+ - **值只码**:引擎侧拒因事实里的上游 `id` / `provider` / 坏字段键 / 拒因句都不上 wire —— `entry_invalid` 行是没人验过的行,
36
+ `id` / `provider` 可能是坏行原样;句子是散文不是契约;中心要的就是「哪个名、什么码」。
37
+ - **`plane` 单码**(发版方裁 2):一个候选恰一个组拒判决 —— 引擎按固定序判模型面不变量、首个不成立即拒;Runner 换装步的拒
38
+ 没有码,不在本信号射程。**`entries` 与 `plane` 描述同一份最新候选目录**(发版方裁 6):`plane` 在场时即那份被拒的候选
39
+ (逐条拒因通常就是它没有缺省的原因);组拒期间引擎自己的拒因表不换代、选择通道仍按上一代拒因执行,所以此时 `entries` 读作
40
+ 「这份目录为什么落不了地」,不是「此刻点这些名会被拒」。
41
+ - **在场律同 `RestartSignal`**:在场 ⇔ 至少一条**可报**的拒(名能出门的条目拒,或一个组拒);空信号(`entries` 空且无 `plane`)
42
+ 被形律拒 ——「没有」只有一种拼法(键缺席);键内「没有条目拒」拼作 `entries: {}`(键必在)—— 每层各一种拼法。
43
+ - **`readModelRefusals(body)`** = **一只读口两出口**:传 `/health` 体或已 parse 的公告都行(只读 `modelRefusals` 键)。先按
44
+ `ModelRefusalsWire` 判形(形不合 / 缺席 / 非对象 / 访问器抛 ⇒ `undefined`,不抛),再按两只闭集收窄:集外 entry 丢、集外
45
+ `plane` 丢,丢到空 ⇒ `undefined`(与「没说」同渲)。**只答键、不判版本**(发版方裁 4)。与 `readRestartSignal` 共用一只私有
46
+ 读口 `readHealthSignal(health, key, schema)`(`readRestartSignal` 行为逐字不变,6.1.0 的 11 格照绿)。
47
+ - **`InstanceAnnouncement.modelRefusals = ModelRefusalsWire.optional().catch(undefined)`**(`InstanceRegistration` 随 extend 同带):
48
+ 集外码照过;形坏(空信号 / 非串值 / `plane` 数组 …)**降为缺席**,不拒整条心跳(与 `restart` 不同 —— 那一键的形仍门住整行;
49
+ 发版方采纳);键因此可能以 `undefined` 值在场。中心若要给形坏留痕,自己对请求原体的该字段 `ModelRefusalsWire.safeParse`
50
+ (parse 之前),读法口不替它记。每拍整行重报 ⇒ 不带键的一拍 = 不再有可报的拒,存行整替不合并。**缺席读法 = 读版本,不读
51
+ 缺席**(发版方裁 4,不加能力位,与 server 读 core 新键的约定同形):更早的引擎从不发此键,缺席 = **不可知**;读方看公告上已有的
52
+ `version`,≥ 首发此键的引擎版本(server 7.103.0,以 server 发布帖为准)才把缺席读成「没有可报的拒」。
53
+ - `/health` 一侧 **`InstanceHealth` 形不动**(passthrough,`modelRefusals` 不入类型;类型化 = 宽读改严判 = major,与
54
+ `restart` 同一笔 7.0.0 欠账)—— `/health` 上的形律与收窄都由 `readModelRefusals` 执法。
55
+
56
+ ### S-768 · 名筛两只谓词:`isDisclosableIdentity`(B3,原私有改名导出)+ `isReportableRefusalName`(entries 键)(additive)
57
+
58
+ `src/config-fns.ts` 私有 `isIdentityString(v): v is string` 改名导出为 `isDisclosableIdentity(v: string): boolean`(发版方裁 5:
59
+ 留,server 阶段 ② import 它、不复制);判据不变(`redactSecrets(v) === v`;非字符串答 false、不抛 —— 验修轮 N5)。它是「用户写的
60
+ 身份串(条目 `name` / `id`)能否原样出门」的**唯一**判据 —— S-712 被丢条目身份今天就由它执法(秘密形 ⇒ 按无名处置:不带,而不是
61
+ 打码)。**此前引擎一侧没有这只谓词**(它读的是身份键的缺席;它自己的另一只脱敏器是另一套探测):不导出,引擎只能手抄一行判据或
62
+ 改用另一只探测器 —— 同一条律两个判官。签名**刻意不是**类型谓词(对抗腿 F5:谓词形 `v is string` 对一些字符串答 false,调用方
63
+ 否定分支里一个 `string` 被收窄成 `never`,tsc 实红);「是不是字符串」由调用点判(`readEntryIdentity` 两处 `typeof … === "string" &&`)。
64
+
65
+ - **`isReportableRefusalName(name: string): boolean`**(验修轮 N1)=「一个条目名能不能当 `entries` 的键」的**唯一**判据,
66
+ 引擎铸键前只调这一只:① wire schema **自己载得住**这个键(就地拿 `ModelRefusalsWire` 判:空串不过键规则;`__proto__` 被 zod
67
+ 的 record 静默丢掉 —— JSON 解析出的自有属性也丢 —— 载不住;不写名单)∧ ② `isDisclosableIdentity`。首稿把 `__proto__` 写成
68
+ 文字里的第二条律、而 `isDisclosableIdentity("__proto__")` 答 true(验修轮实测)—— 收成一只。住在 `config-fns.ts`:`fleet.ts`
69
+ 被 `types.ts` 引,它再引 `config-fns.ts` 会成环(三种加载序实测无环)。`__proto__` 的丢法另有 characterization 钉。
70
+ - **schema 刻意不复判秘密**:wire 的键只判非空,不跑本谓词 —— 秘密探测器是会变宽的启发式,执法在铸点(引擎);中心展示前可以
71
+ 再过一次同一只谓词(丢,不拒)。
72
+
73
+ ### 仓内两件(发版方追加,不影响发布物的契约面)
74
+
75
+ - **开发期引擎钉 `@sema-agent/core` ^2.1.0(实装 2.1.0)→ `7.33.1` 精确**(devDependencies,与 server 同钉):上游漂移门 ②③
76
+ (`test/upstream-drift.test.ts`)与安全合并表词表门(`test/safety-merge-spec.test.ts` 读 `SAFETY_AXIS_VOCABULARY`)此前在对 core 2.1.0
77
+ (npmjs 发布于 2026-07-29,其后 core 已走到 7.33.1)做「漂移」检查 —— 门是瞎的。**提货当时读数**:门 ③ `PROJECT_ID_REGEX` 三格与安全合并表两格在 7.33.1
78
+ 上绿;**门 ② 两格红** —— 原文「ModelRole 声明里有不是字符串字面量的成分(`typeofMODEL_ROLES[number]`)」:core 7.33.1
79
+ `dist/core/model-seat.d.ts:42` 把 `ModelRole` 写成 `(typeof MODEL_ROLES)[number]`,门按设计 fail-loud(「上游改了导出形,修这条
80
+ 查找,别删门」)。**发版方裁后落地(阶段 ①c)**:门 ② 的上游词表改读 core **运行期** `MODEL_ROLES`(dev-only import,与门 ③ /
81
+ 安全合并表门同形),删 `.d.ts` 字面量解析腿及其专属变异格(读实现不读文本,C-R24);查找改后门按设计报陈旧豁免 ——
82
+ 「引擎 ModelRole:登记表里有已经不存在的差 —— 陈旧豁免,删掉那几行: expected [ 'consolidate' ] to deeply equal []」(门 ② 两格
83
+ 1 红 / 1 绿)—— 删 `consolidate: ours-only` 登记行(core 7.33.1 已含该词,原理由句自己写明「随开发期引擎升版复核本行」)后 2 / 2 绿;
84
+ `advisor: upstream-only` 留。core 7.33.1 `MODEL_ROLES` = default / summarize / subagent / team / synthesize / advisor / verifier /
85
+ classifier / consolidate。
86
+ - **README「zod 单实例」订正为两实例现状**:server / CLI 在 zod 4、config-center 产品仍在 zod 3,zod-4 宿主树里本包的 zod 3 是
87
+ 嵌套的第二实例;跨缝传纯数据、不混两实例的 schema 对象。升 zod 4 归一是本包的一个 major(S-777);本版不动依赖与 peer 段。
88
+
89
+ ### 三问 · 消费方 · 红先 · 复审
90
+
91
+ - **谁需要** = web-admin KL-35(实例页逐台列「这台拒了哪些条目、为什么」,[8445])+ server 7.103.0(S-768 阶段 ②:一只取值口
92
+ 两出口 —— `/health.modelRefusals` 与心跳 `modelRefusals`;两张码表改 import 本包闭集(并留一张以码为键的表);铸键前调
93
+ `isReportableRefusalName`,日志 / 拒因句里要不要写出一个名字问 `isDisclosableIdentity`;组拒码当数据带出 —— 引擎的组拒记录
94
+ 只存不变量 id,`model-route-endpoint` 一个 id 两个码,按判决那刻的码)。
95
+ - **谁受伤** = 无(新导出 + 新可选键;`InstanceHealth` 一字不动;`readRestartSignal` / 被丢条目身份行为不变;旧中心收到带
96
+ `modelRefusals` 的心跳:`InstanceAnnouncement` 是默认 strip 的 `z.object`,键被剥,行为同今)。**风险如实登记**:
97
+ ① ~~心跳里 `modelRefusals` 形不合 ⇒ 整条心跳 400~~ —— 发版方裁 3 后**不再成立**:形坏降为缺席、集外码照过(对抗腿按 web-admin
98
+ 心跳路由读法实测的「连拒两窗后实例掉出可路由集」这一形,本版已无从触发);② **版本错位**:引擎日后发一个新码(本包下一 minor
99
+ 尾插),装着旧版本包的中心**照收心跳**,只是新码在它那里「没说」,直到中心升级本包;部署序仍建议中心先提货,但不再是补偿
100
+ 手段;③ 判档:6.1.0 收下(剥掉)的任何带 `modelRefusals` 的心跳体,6.2.0 照收 —— 对抗腿 F4 的判档张力(strip 对象加类型化
101
+ 可选键 = 宽读改严判)随之消解。
102
+ - **补偿** = 无旋钮(additive,无需补偿)。
103
+ - **消费方** = server(阶段 ②,如上);web-admin(收键、存 wire 形、经 `readModelRefusals` 渲;缺席按引擎 `version` 分「没有可报的
104
+ 拒 / 不可知」;按码穷举 `switch`;能力显式表态制 @点名);cli n/a;core n/a。
105
+ - **红先**:`test/model-refusals-6.2.0.test.ts` 对 6.1.0 源 **18 红 / 4 绿**(绿 = additive 不带键控制格、restart / ready 负控、
106
+ `InstanceHealth` 形不动、类型面格(`expectTypeOf`,由 tsc 执法、运行期空跑)),tsc 36 错全在本文件;改源后 22 / 22、tsc 0。
107
+ B3 两格对 `0d4227d` **2 红** → 24 / 24。复审修复与阶段 ①b 裁定的红先见下(文件现 36 格)。
108
+ - **对抗复审**(独立 opus 腿一轮,只读、不喂结论、沙箱逐条复现;codex 当日额度墙未投):9 条。**真修**:F5(谓词签名,
109
+ tsc 红先 2 错 → 0)· F8(「双侧编译红」订正)· F2 的「秘密形名下的拒 ⇒ 整键缺席」(在场律改写为「可报的拒」)· F9(公开 d.ts
110
+ 里指向私有函数的 JSDoc、B3 组标题里本仓验不了的断言)· F3 附带的 `__proto__` 键(登记边界 + characterization 钉)。**发版方裁
111
+ 后落地**:F1(组拒码收整族,裁 1)· F3(wire 开放串 + 读方收窄 + 心跳上形坏降为缺席,裁 3)· F2 其余(读版本、不加能力位,
112
+ 裁 4)· F4(随裁 3 消解)。**驳 / 不改**:F6(读口里按谓词丢秘密形键:执法留在铸点)· F7(两层「没有」各一种拼法;`ModelRefusalsWire`
113
+ 是 `ZodEffects`、无 `.shape` / `.extend`,与 `RestartSignal` 的 `ZodObject` 形不同 —— 登记)· F9 命名(`readModelRefusals` 与
114
+ `readRestartSignal` 不对称:名按派车书;与引擎现有同名常量 / 型撞名:引擎提货时换成 import,阶段 ② 的事)。
115
+ - **验修轮**(同一 opus 腿,只核 `10f4d85..0450c94`,不开新检测面):F5 / F8 修复成立,相对 `10f4d85` 无运行期回归;新 6 条全处置:
116
+ **N1**(`__proto__` 成了文字里的第二条律 ⇒ 收成一只 `isReportableRefusalName`)· **N5**(`isDisclosableIdentity` 非字符串由答
117
+ false 变抛 TypeError ⇒ 回到答 false)· **N2 / N3**(组拒期间 `entries` 的读法;无码组拒 —— 裁 1 后已无此类)补进接入档 ·
118
+ **N4 / N6**(旧签名句、llms-full「= 无拒」、格数)订正。两格红先:对 `0450c94` **2 红 / 26 绿** → 28 / 28。
119
+ - **阶段 ①b · 发版方六裁落地**(每裁一格红先):裁 1 组拒码四码(`0d3a48d` 1 红 → 绿)· 裁 2 `plane` 单码 + 注(`4f924fb` 1 红 →
120
+ 绿)· 裁 3 wire 开放串 + 读方收窄(`c36745c` 12 红 / tsc 3 错 → 绿;本体两格 = 集外码心跳 `success`、收窄读数,其余 10 格是
121
+ 形律格改指 `ModelRefusalsWire`)· 裁 3 附 心跳上形坏降为缺席(`5289ad9` 3 红 → 绿)· 裁 4 读版本不读缺席(`1014997` 1 红 → 绿,
122
+ 另一格「读口不判版本」是控制格,生来绿)· 裁 5 `isDisclosableIdentity` 留(无改动,既有格照绿)· 裁 6 同一份候选目录(`a53ab22`
123
+ 1 红 → 绿)。阶段 ①b 的改动本身未再过复审(复审终止律:一轮 + 一次验修已用完)。
124
+
125
+ ## 6.1.0 — 2026-09-25 — 四件 additive:`entry-dropped` 身份(S-712)· 中心凭证信封 + 三态判别器(S-698)· `RestartSignal` 一处定义(S-741)· 中心凭证两键名 + 二选一判官(S-742);`redactSecrets` 私钥 PEM 串内任意位置且判在替换前(6.0.0 起三面既有盲点顺带关)
126
+
127
+ > 本节四件全 additive(新可选键 / 新导出 / 新文件),零删零改名;版号与节标题由发版方写。判档口径见下方「发版纪律」:
128
+ > 新键 / 新导出 = minor。
129
+
130
+ ### S-712 · `entry-dropped` 告警带被丢条目的身份(`identity?`,additive)
131
+
132
+ `EffectiveReadWarning` 的 `entry-dropped` 臂加可选键 `identity?: DroppedEntryIdentity`(`{ name?: string; id?: string;
133
+ enabled?: boolean }`,新导出类型);`DroppedEntry`(`salvageEntries` 的记账行)同带。**一条律,不分域**:只从被丢条目
134
+ **原文**取 —— 字符串 `name` / `id`、布尔 `enabled`,只认自有**数据**属性(不触发访问器),类型不对 ⇒ 不带该键(不猜、
135
+ 不 `String()` 转换),`redactSecrets` 会改动的串 ⇒ 不带该键(与坐标段、`error` 文案**同一只**脱敏判据:坐标段必须
136
+ 在场所以打码出门;身份可缺,打过码的串不再是身份,所以是**不带**而不是打码),三键都读不出 ⇒ 不带 `identity`(不铸
137
+ 空对象)。**缺席 = 读不出**(非字符串,或不能原样出门),两种原因刻意不分:消费方的正确处置相同(身份未知)。铸点单一:数组 / Record 两种集合形共用
138
+ 一只记账行铸点,域面(`buildEffective`)与 wire 面(skills manifest)共用一只告警铸点;`projectWarning` 的
139
+ `entry-dropped` 臂把 `identity` 带进投影(缺席不铸键)。
140
+
141
+ - **谁需要** = server(S-712 半场):它今天拿告警坐标回读原文档取被丢模型条目的身份(`read-warnings.ts`
142
+ `droppedEntryIdentity(rawDoc, path)`),本地腿的原文档是解析之后再读一次盘得来的,两次读之间文件一改就对不上,
143
+ 只能 fail-closed 整组拒;身份随告警走之后这只函数换数据源、原文档入参删除。
144
+ - **谁受伤** = 运行期无(additive 可选键;持 `EffectiveReadWarning` 穷举 `switch` 的消费方零编译红,没有新 kind)。
145
+ **提货成本一处**(对抗轮 #1 订正:初稿写「web-admin 无适配器」是错的):web-admin 有一致性适配器
146
+ (`src/lib/config/conformance-adapter.ts`)与**自己入库**的语料,其中 `wa05-credential-as-unknown-key-in-a-salvaged-skills-entry`
147
+ 在一条被丢的 skills 条目(`name:"broken"`)上断言 `entry-dropped`,且 `test/settings-schema-conformance.test.ts` 要求
148
+ 每条 `wa*` 在 domains 腿 `pass` ⇒ 提货 6.1.0 时这一条会报告警差(多出 `identity:{name:"broken"}`),要按新读数重新
149
+ 加冕 —— 发布帖 @web-admin 点名。server 两条读腿的告警直接来自本包 `onWarning`,提货即带身份,不红。
150
+ - **消费方** = server(`splitFoldedReadWarnings` 改读 `w.identity`;`enabled` 缺席按今天的 `!== false` 口径自判;
151
+ `name` 缺席 = 读不出(非字符串**或**脱敏会改动它),模型面按身份未知处置时拒因句请说「没有可读的名字」,别说
152
+ 「不是字符串」—— 对抗轮 #4 的二义在契约层刻意不分,措辞归读方);
153
+ web-admin / cli 可选读(管理面 / 壳在「条目被丢」提示里点名);core n/a。
154
+ - **一致性语料再加冕**(`npm run conformance:bless`,additive 键):6 条 case 的 `domains` 期望多出 `identity`
155
+ (`c01` / `c02` / `c23` / `cli01` / `cli02` / `t04`,共 8 条告警;零删、零改其它字段,`baseline.json` 零变化)。
156
+ kit 自带语料随包走,消费方无需为它 bless(自己入库的语料见上一条)。**新语料 `c40-s712-identity-only-what-reads-out`**
157
+ (对抗轮 #5):被丢条目的 `name` 是 Bearer 形串、`enabled` 是 `"yes"` ⇒ 身份只剩 `id`;正控条目三键齐全。变异门
158
+ `test/conformance-mutation.test.ts` 为身份铸点登记两靶(脱敏判据 / enabled 只收布尔),加 `c40` 前两靶变异体全绿,
159
+ 加后各红。第三条律(只认数据属性)JSON 表达不出 getter,由单测看着,如实登记。
160
+ - **historical 归一器加投影级钩子**(对抗轮 #3,`./conformance` additive):`ConformanceNormalizer.warning?(p)`,先于
161
+ `path` 作用、两侧同过;`scripts/conformance-historical.mjs` 的有损归一器用它剥掉 `entry-dropped.identity` —— 不剥的话,
162
+ 照 `docs/CONFORMANCE.md` 重跑 historical 会把 6 条 case 记成旧版行为红(2.1.0 由 1 条涨到 7 条,
163
+ `test/conformance-corpus.test.ts` 随之红;对抗腿实跑)。修后在隔离副本里真重跑 2.1.0 / 6.0.0:行为红 1 / 0,
164
+ `firstRedOn` 零变化,语料测试 12 / 12;入库证据未重写(读数与入库一致)。
165
+ - **红先**:`test/entry-dropped-identity-6.1.0.test.ts` 对 6.0.0 源 **9 红 / 2 绿**(绿的两格 = 读不出 ⇒ 缺席、
166
+ 好条目零告警的控制格),改源后 11 / 11。对抗轮(独立 opus 腿,codex 缺席顶替)追加:#2 秘密判据两格(NBSP 分隔 /
167
+ 短令牌的 Bearer 串随身份明文出门;判据与坐标脱敏同一只)对首版 **2 红** → 修后绿;#3 投影钩子一格 **1 红**(tsc 同红)→
168
+ 绿;#5 变异两靶 **2 红**(变异体存活)→ 加 `c40` 后绿。**验修轮**(同一 opus 腿,只核修复提交):11 条原发现
169
+ 读数全闭合;新 **N1 [medium]**:#2 改用 `redactSecrets` 判据后,PEM 私钥(头里带空格,脱敏器的定点两遍够不着)作
170
+ `name` / `id` 随身份明文出门(首版的 `containsSecretToken` 扣得住,6.0.0 无身份 —— 秘密轴回归;同一盲点在 6.0.0 的
171
+ 坐标 / `error` 脱敏里本就存在)⇒ **在源头修** `redactSecrets`:出口不变量 `containsSecretToken(redactSecrets(x)) === false`,
172
+ 定点两遍之后探测器仍判阳 ⇒ 整串打码;坐标、`error` 文案、身份三面一起补上,身份判据仍是 `redactSecrets(v) === v`
173
+ 一只。红先两格(PEM 身份 / 脱敏器出口不变量)对 `0397672` **2 红** → 绿。**N4 [nit]**:`c40` 加「短 Bearer 令牌」与
174
+ 「PEM 私钥」两条被丢条目,变异门加两靶(身份判据退回旧探测器 / 去掉整串兜底),对旧 `c40` **2 红**(变异体存活)→
175
+ 新 `c40` 下绿。**N3 [nit]**(`readEffectiveWire` 的 `Number.isFinite` 与 `RestartSignal` 的 `z.number().finite()` 是同一条
176
+ 判据的两处拼写)登记不修:语义等价,抽一只共用定义要为一行判据新开叶模块,留作下次触碰时顺手。
177
+ **codex 一审**(gpt-6-astra,`--base 5153d2a`,对 `7b92977`,needs-attention 1 [high]):PEM 私钥的 header 不在串首
178
+ (前导换行 / `key: ` 前缀)时,探测器(`SECRET_LITERAL_RE` 串首锚 + 按空格分词拆散带空格的 header)看不见,
179
+ `redactSecrets` 原样放行 ⇒ `identity.id` / 投影 / Record 坐标带出整把私钥(6.0.0 的告警不暴露它)。**源头修**:
180
+ 新导出 `PRIVATE_KEY_PEM_HEADER`(`types.ts`,私钥 PEM header 的**唯一**定义,无锚)—— `SECRET_LITERAL_RE` 由它拼出
181
+ (源串与 6.0.0 逐字相同,有钉),`redactSecrets` 的整串兜底加「串内任意位置见到它 ⇒ 整串扣留」;坐标 / `error` /
182
+ 身份三面同一只。**刻意不**放宽校验侧探测器 `containsSecretToken`(让它拒串中 PEM = 收紧它校验的每个字段 = major),
183
+ 登记为欠账。红先三格(前导换行 + 前缀的身份 / 脱敏器 / 源串拼装钉)对 `7b92977` **3 红** → 绿;`c40` 加一条带
184
+ `key: ` 前缀 PEM 的被丢条目,变异门加一靶(去掉「串内任意位置」一道),对旧 `c40` **1 红**(变异体存活)→ 新 `c40` 下绿。
185
+ **codex 验修轮**(`--base 7b92977`,对 `050c8e6`,needs-attention 1 [high]):修复自身的**顺序缺口** —— `sk-note:-----BEGIN
186
+ PRIVATE KEY-----…` 这类密钥形前缀**紧贴** header 的串,令牌那一遍先把 `sk-note:-----BEGIN` 整段换成 `[redacted-secret]`,
187
+ 替换后的 header 检查看不到 header,私钥体原样出门。修:`PRIVATE_KEY_PEM_HEADER` 改判**原文**(任何替换之前),仍是
188
+ `redactSecrets` 一处。红先四面各一格(坐标 / error / 身份 / 投影 + 秘密扫描,Ed25519 私钥现场生成)对 `050c8e6`
189
+ **3 红 / 1 绿**(身份面 `050c8e6` 已扣留)→ 4 绿;`c40` 加该形(作被丢条目 `id` 与 `roles` 坏键),变异门加一靶
190
+ (检查挪回替换之后)对旧 `c40` **1 红** → 绿。**对 6.0.0 的判定**:同一夹具在 `5153d2a` 的独立树上跑,坐标 / error /
191
+ 投影三面**同样泄**(6.0.0 起的既有盲点,不是本版回归);身份面是本版新面,`050c8e6` 已扣留。本版把三面既有盲点一并关掉。
192
+ - **边界(如实登记)**:身份只取 `name` / `id` / `enabled` 三键;server 现有载体还读 `provider`,它不是身份,
193
+ 不在本键里 —— 要不要把 `provider` 也随告警走是 server 半场的决定(可继续按坐标回读或从投影外取),本版不猜。
194
+
195
+ ### S-698 · 中心凭证文件的信封形 `CenterCredentialEnvelope` + 三态判别器 `parseCenterCredentialFile`(新文件,additive)
196
+
197
+ 新模块 `src/center-credential.ts`(主 barrel 导出,不开新 subpath —— 与 `cross-session.ts` 同:纯 zod、browser-safe):
198
+ `CENTER_CREDENTIAL_ENVELOPE_KIND = "user-session"`、`CenterCredentialEnvelope`(zod,`.strict()`;`kind` 字面、
199
+ `accessToken`(JWT,`looksLikeJwt`)/ `refreshToken` 走与裸行**同一条令牌律**(可打印 ASCII 0x20–0x7E、首尾可见、中间可有空格)、`expiresAt` =
200
+ RFC 3339 date-time(zod `datetime({offset:true})` 管日历有效性,再补「秒必备 + 偏移必须 `±hh:mm` 且时 00–23 / 分 00–59」
201
+ zod 放过的形;不收闰秒 `:60`)、`issuer` = `NON_SECRET_URL`(签发它的中心的基础 URL;与 `remoteExec` 的 `apiUrl` /
202
+ `endpoint` 同一只判据,不另写更窄的一份))、类型 `CenterCredentialFile` 与 `parseCenterCredentialFile(text)`。判别律
203
+ (design/325 §1)一条、唯一实现:`trim()`(含 BOM)后空 ⇒ `unreadable("empty")`;`{` 开头 ⇒ 只按信封读,失败 = `unreadable`
204
+ (**不回落**裸行);否则取首行(第一个 CR / LF 之前),是令牌 ⇒ `bare`,不是 ⇒ `unreadable`(放不进 Authorization
205
+ 头的行不是 bearer)。`reason` 不回显文件内容(只点字段名 + zod 问题码 +
206
+ 未知键个数;`JSON.parse` 的报错会引用原文片段,不转述)。
207
+
208
+ - **谁需要** = S-698 长寿引擎续签:壳登录后可能关掉、引擎继续跑几天,访问令牌过期后中心请求逐条失败;信封让
209
+ 引擎手里有 refresh token 与到期时刻,续签器(server 半场)据此在到期前续。
210
+ - **谁受伤** = 无(新导出、新文件)。server 改调本判别器之后,相对它今天的读法(整段 `trim()` 后当 bearer)有
211
+ **三处行为差**,如实登记,都只在今天本就发不对的文件上可见:① **多行**裸行文件 ⇒ 只取首行(今天发出去的 header
212
+ 含换行、不合法);② `trim()` 后以 `{` 开头的文件 ⇒ 今天当 bearer 发出去,本判别器判信封、读不成即 `unreadable`
213
+ (判别律本身);③ 首行含控制 / 非 ASCII 字符 ⇒ `unreadable`(前面粘了不可见字符的信封今天会连同 refreshToken 一起当 bearer
214
+ 发出门 —— 对抗轮 #7)。**如实登记**:Latin-1 令牌今天能用(HTTP 头的 ByteString 收、中心逐字比得上),这里判
215
+ `unreadable`(非 ASCII 与不可见字符分不开,取 fail-closed);含空格的口令式共享令牌照收(验修轮 N2)。同一个令牌
216
+ 走 `SEMA_REGISTRY_TOKEN`(env 腿)不过本律 —— env 值逐字承运与 server 今天同,要不要让 env 值也过令牌律归 server 半场。
217
+ - **消费方** = server(`src/center-credential.ts` 文件形 `read()` 改调 `parseCenterCredentialFile`:`bare` ⇒ 既有
218
+ `classify`;`envelope` ⇒ `accessToken` 按 `user-jwt` 分箱 + 续签器读 `refreshToken` / `expiresAt` / `issuer`;
219
+ `unreadable` ⇒ 既有不可读臂);cli(L-653 壳半场:登录 / device-code 成功后写信封,写前用 `CenterCredentialEnvelope`
220
+ 自校);web-admin n/a(端点已在);core n/a。
221
+ - **红先**:`test/center-credential-envelope-6.1.0.test.ts`(经主 barrel 取,导出面同测)对 6.0.0 源 **14 红 / 0 绿**,
222
+ 改源后 14 / 14。对抗轮(独立 opus 腿,codex 缺席顶替)追加四格,对首版 **4 红** → 修后 18 / 18:#6 偏移越界
223
+ (`+99:99` / `+24:00` / `+23:60` / `-99:00` 首版判信封、`Date.parse` 为 NaN)· #7 `{` 前粘 trim 不剥的字符
224
+ (`\u0085` / `\u0001` / `\u00AD` …)首版整份信封判 bare · #7 裸行令牌律 · #10 `accessToken` 带 CRLF / 是 `wpt_` 拉取令牌
225
+ 首版判信封。验修轮 N2(令牌律首版拒掉含空格的口令式共享令牌)两格对 `6f22564` **2 红** → 放宽为「可打印 ASCII、
226
+ 首尾可见」后绿。
227
+
228
+ ### S-742 · 中心凭证两个声明键名 `CENTER_CREDENTIAL_ENV` + 二选一判官 `judgeCenterCredentialDeclaration`(additive)
229
+
230
+ 同 `src/center-credential.ts` 尾段:`CENTER_CREDENTIAL_ENV = { token: "SEMA_REGISTRY_TOKEN", tokenFile:
231
+ "SEMA_REGISTRY_TOKEN_FILE" } as const`;类型 `CenterCredentialDeclaration`;`judgeCenterCredentialDeclaration(env)` →
232
+ `{from:"env"; token}` | `{from:"file"; path}` | `{from:"absent"}`。规则从 server `readCenterCredentialDeclaration`
233
+ 上提、**逐字同**:空串 = 未设(不 trim);两个都设(非空)⇒ throw,拒句逐字取 server c60beebe
234
+ `src/center-credential.ts:93-96`(「… are both set — pick exactly one: … Two writers on one credential is a guess, not a
235
+ config.」,点名两键、不回显值);只读这两个键。**一处有意的差**:server 在这一步把文件路径解析成绝对路径
236
+ (`node:path` `resolve`),本包纯契约不碰 `node:path`、也不知道读方的 cwd ⇒ `path` 原样返回,解析留给读方。
237
+
238
+ - **谁需要** = 管理面(给 worker 渲染 env)与壳(给本机引擎写 env)今天各自手抄这两个键名,规则只活在 server
239
+ —— 手抄键名会静默漂(C-R75);上提后三方 import 同一份键名与同一条规则([8325] 一请,server [8326] 认领)。
240
+ - **谁受伤** = 无(新导出)。
241
+ - **消费方** = server(`readCenterCredentialDeclaration` 改为 `judgeCenterCredentialDeclaration(env)` + 自己的
242
+ `resolvePath`;`CENTER_TOKEN_ENV` / `CENTER_TOKEN_FILE_ENV` 两常量改引 `CENTER_CREDENTIAL_ENV`);web-admin(worker env
243
+ 渲染处两键名改 import,[8327] 同波一天);cli(本机引擎 env 写入处改 import;写前可用判官自校不会同设);core n/a。
244
+ - **红先**:`test/center-credential-declaration-6.1.0.test.ts`(env / file / absent / both 四格 + 空串两格 + 键名 +
245
+ 只读两键,期望拒句是从 server 源**抄来**的字面,不从实现派生)对 6.0.0 源 **7 红 / 0 绿**,改源后 7 / 7。
246
+
247
+ ### S-741 · 重启信号 `RestartSignal` 定义一次:心跳键 `InstanceAnnouncement.restart?` + `/health` 读法 `readRestartSignal`(additive)
248
+
249
+ `src/fleet.ts` 新导出 `RestartSignal`(zod + 类型)= `{ reasons: z.array(z.string().min(1)).min(1), version: z.number().finite(),
250
+ since: z.number().int().nonnegative() }` 与 `readRestartSignal(health: unknown): RestartSignal | undefined`;`InstanceAnnouncement`
251
+ 加 `restart: RestartSignal.optional()`(同一只 schema 对象,测试按运行期对象同一钉住;`InstanceRegistration` =
252
+ `InstanceAnnouncement.extend(…)`,随之同带)。**在场 ⇔ 需要重启**,形里没有 `required` 布尔。`/health` 一侧:
253
+ **`InstanceHealth` 形不动**(passthrough,`restart` 与顶层冗余布尔 `restartRequired` 照 6.0.0 原样承运、不入类型),
254
+ 读它走 `readRestartSignal`(对 `health.restart` safeParse,形不合 / 缺席 / 非对象 / 访问器抛错 ⇒ `undefined`,不抛;
255
+ 只认 `restart`,不读 `restartRequired`)。`version` 与 `readEffectiveWire` 读配置版本**同一条判据**(有限数)。
256
+
257
+ > **形的裁定(发布方,两次)**:① 任务书初稿写的是 `{ required: true, reasons, version: string, since: string }`。
258
+ > 亲读 server c60beebe:`/health` 已发 `restart: { reasons, version, since }`(`src/http/dispatch.ts:271`),`version` =
259
+ > 中心配置版本(**number**,`src/config-center/restart-signal.ts:101-108`),`since` = epoch ms(**number**),对象里
260
+ > 没有 `required`;web-admin 自审稿 §2.4 也明写「不要 `restartRequired` 布尔 + `restart` 两键 —— 布尔与在场性冗余」。
261
+ > 照初稿做,心跳与 `/health` 由同一只 getter 铸出两种形。主会话裁:取已发 wire 形,`required` 不要。
262
+ > ② 对抗轮(独立 opus 腿)#8:同一个配置版本,`readEffectiveWire` 收任意有限数、首版 `RestartSignal` 却要非负整数 ⇒
263
+ > 中心一发非整数版本,整条心跳 400、实例不登记 —— 主会话裁 `version` 改 `z.number().finite()`,一个量一条判据。
264
+ > #9:首版把 `InstanceHealth.restart` 类型化 = 宽读改严判(形不合的 `/health` 体由过变拒),照「发版纪律」字面是
265
+ > major —— 主会话裁 **6.1.0 不做**,`InstanceHealth` 形不动,另给 `readRestartSignal`。**欠账**:`InstanceHealth.restart`
266
+ > 上严判是 **7.0.0 项**(届时按硬 breaking 三句办:类型化、形不合即拒,消费方 tsc / parse 红即通知)。
267
+
268
+ - **谁需要** = T3 心跳形(web-admin 自审稿 §2.4,[8323] ④ / [8326] ②):引擎心跳带重启信号 → 中心按策略给恰好需要
269
+ 的 worker 打 `recreateNonce` → 编排器照旧执行。心跳成了 `/health` 之外的第二个出口,所以形只定义一次。
270
+ - **谁受伤** = 无(新可选键 + 新导出;`InstanceHealth` 一字不动)。旧 web-admin 收到带 `restart` 的心跳:
271
+ `InstanceAnnouncement` 是默认 strip 的 `z.object`,该键被剥,行为同今。心跳里 `restart` 形不合 ⇒ 新 web-admin 对
272
+ 整条心跳 400(可选子键拖整行,如实登记;server 铸点与 `/health` 同一只 getter,形由本 schema 约束)。
273
+ - **消费方** = server(S-741 半场:`FleetClientOpts` 加 restart getter —— 与 `/health` 同一只 `restartState` —— 心跳体带
274
+ `restart`,翻转即刻再 announce;`/health` 零改);web-admin(`/api/fleet/instances` 收键、存、策略旋钮缺省关,[8327]
275
+ 同波一天;存行整替不合并:不带 `restart` 的一拍 = 不再需要;若中心探针要读 `/health` 的重启信号,用 `readRestartSignal`,
276
+ 别自己 parse `health.restart`);cli n/a;core n/a。
277
+ - **文档句同批(主会话追加,不改类型)**:`InstanceAnnouncement.ready` 的注由「self-declared: bootDone && !draining」改为
278
+ 「self-declared readiness: model roster landed AND not draining (server ≥7.102.0; older engines: !draining only)」——
279
+ server 车OE S-740 起心跳 `ready` 与 `/health` 就绪同一只谓词;旧注与 server 今天的实现(`ready = !isDraining()`)
280
+ 和明天的实现都不符。`test/docs-hygiene.test.ts` 外部版本白名单登 server `7.102.0`(注里点名的是 server 版本,
281
+ 不是本包)。
282
+ - **红先**:首版 `test/restart-signal-6.1.0.test.ts` 对 6.0.0 源 **6 红 / 3 绿**(绿的三格:server 今天 `/health` 真形
283
+ parse 必过 —— 6.0.0 下它靠 passthrough 过,照初稿的 `required` + 字符串形它会红;不带 `restart` 照旧过的 additive 格;
284
+ 类型面格(`expectTypeOf`,由 `tsc` 执法、运行期空跑)),首版改源后 9 / 9。二裁改写后的本文件对**首版实现**
285
+ **6 红 / 5 绿**(红:`readRestartSignal` 缺席四格、`InstanceHealth` 形不动格、version 有限数格),改源后 11 / 11。
286
+
9
287
  ## 6.0.0 — 2026-09-24 — `crossSessionInbound` 词表随 core 7.30.0 改为 `wake / next-turn / hold / refuse`,`accept` 退役(BREAKING,枚举改词;server S-616 提货同批)
10
288
 
11
289
  > **版号裁定(发布方)**:本批只有一处改动 —— `CROSS_SESSION_INBOUND_VALUES` 由 `["accept","hold","refuse"]`
package/README.md CHANGED
@@ -35,9 +35,18 @@ server (the workers), and the client (desktop/CLI) seams. No two copies of the s
35
35
  (≤`0.19.0` 发在旧名 `@sema-agent/registry-core` 下,冻结)。
36
36
 
37
37
  The package is **pure**: zero `next`/`react`/server imports, zero DB drivers. Its only runtime dependency is
38
- `zod` (a `dependency` pinned `^3.24.0` — the same range the host repos (config-center 产品 / `sema-server` / CLI) use, so
39
- package managers dedup to a **single shared zod instance**, keeping cross-instance schema validation / `instanceof`
40
- sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
38
+ `zod` (a `dependency` pinned `^3.24.0`, also declared as a `peerDependency` with the same range). `npm ls --prod`
39
+ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
40
+
41
+ > **zod instances today: two, not one** (corrected in 6.2.0 — this section used to promise a single shared instance).
42
+ > The range was chosen so every host would dedup to one zod, but the hosts have since moved apart: `sema-server` and the
43
+ > CLI depend on zod 4 (`^4.x`), the config-center product (`sema-web-admin`) is still on zod 3. In a zod-4 host the
44
+ > package manager installs this package's own zod 3 NESTED under it — the server tree today has zod 4 at the top level
45
+ > and zod 3 at `node_modules/@sema-agent/settings-schema/node_modules/zod`. There, this package's schemas and the host's
46
+ > are two zod instances: cross the seam with plain data (parse with the schemas / functions this package exports), and
47
+ > never mix schema objects of the two instances (`instanceof ZodError`, composing a host schema with one of ours). Only
48
+ > a zod-3 host (today the config-center product) still dedups to one instance. Moving this package to zod 4 is a major
49
+ > of its own (S-777); 6.2.0 does not touch the dependency or peer range.
41
50
 
42
51
  ## What's in it
43
52
 
@@ -95,6 +104,23 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
95
104
  (`shouldForceJwksRefresh`, `jwksWithinOfflineGrace`, `classifyTimeClaims`, `toVerifiedIdentity`).
96
105
  The normative rules R1–R10 in its header are the service verifier's implementation spec.
97
106
  `VerifiedIdentity` carries an optional `scope` (the token's active tenant scope — see `api/scopes`).
107
+ - **Center credential file** (`center-credential.ts`, main barrel; S-698): the file behind
108
+ `SEMA_REGISTRY_TOKEN_FILE` is one of two KINDS — a bare line (one static token — the same kind of file as today:
109
+ its first line, printable ASCII with no control / non-ASCII characters; user JWT vs worker pull token stays the reader's `looksLikeJwt` call) or a
110
+ `CenterCredentialEnvelope` (strict JSON: `kind: "user-session"` = `CENTER_CREDENTIAL_ENVELOPE_KIND`, a JWT
111
+ `accessToken` and a `refreshToken` under the same token law as the bare line, RFC 3339
112
+ `expiresAt` with seconds and a `Z` / `±hh:mm` offset in range, `issuer` = the issuing center's base URL judged
113
+ by the same `NON_SECRET_URL` the `remoteExec` endpoints use — same-origin is the reader's comparison).
114
+ `parseCenterCredentialFile(text)` is the ONE discrimination law — trimmed text starting with `{` is read ONLY
115
+ as an envelope (a failure is `unreadable`, never a fallback to a bare line); anything else is a bare line,
116
+ and a first line that is not a token is `unreadable` too (never sent as a bearer); empty is `unreadable`. The `reason` names envelope fields / zod issue codes / an unknown-key COUNT, never file
117
+ content. Pure: reading, atomically writing and renewing the file are the consumers' job. S-742 lifts the
118
+ DECLARATION face here too: `CENTER_CREDENTIAL_ENV` (`{ token: "SEMA_REGISTRY_TOKEN", tokenFile:
119
+ "SEMA_REGISTRY_TOKEN_FILE" }` — one spelling for the center rendering worker env, the shell writing its local
120
+ engine's env, and the server reading it) and `judgeCenterCredentialDeclaration(env)` → `env` / `file` /
121
+ `absent`, the one pick-exactly-one rule: an empty string is unset, BOTH set (non-empty) throws (the refusal
122
+ names the two keys, never a value), the file path comes back verbatim (resolving a relative one against a cwd
123
+ is the reader's job).
98
124
  - **Multi-tenant scopes** (`api/scopes.ts`, subpath `./api/scopes`): the scope contract — the `Scope`
99
125
  entity (`{id,name,createdAt}`, reserved default `"global"` always exists), per-scope `ScopeMember`
100
126
  rows (`{scopeId,principal,role}` on the existing viewer<editor<publisher<admin ladder), the optional
@@ -118,7 +144,16 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
118
144
  tolerance on the same read path: one malformed catalog entry is dropped on its own (named through
119
145
  `onWarning` as `entry-dropped`) and its siblings survive, instead of the whole domain falling to
120
146
  its schema default. It is the SAME `DOMAIN_READ_FALLBACK` table that decides where this applies — gate
121
- domains keep failing loud, entry by entry included. **3.0.0** re-casts every coordinate these warnings
147
+ domains keep failing loud, entry by entry included. **S-712** (additive) lets each `entry-dropped` carry an
148
+ optional `identity?: { name?, id?, enabled? }` read from the dropped entry's OWN text (`DroppedEntryIdentity`):
149
+ a string `name` / `id` and a boolean `enabled`, own data properties only, each key present only when it
150
+ reads with that exact type and `redactSecrets` leaves it unchanged — the SAME redactor the coordinates and the
151
+ error text go through (not guessed, not coerced, not masked — a masked string identifies nothing); nothing
152
+ readable ⇒ no `identity` key (absent = "not safely readable", whichever the reason). A consumer learns WHICH entry was
153
+ dropped without re-reading the source document by coordinate (the document it holds may not be the one this
154
+ read saw). `identity` is part of the warning projection, so the conformance gate compares it (corpus
155
+ case `c40` pins the two laws JSON can express; the historical normalizer strips it, since older versions cannot
156
+ mint it — `ConformanceNormalizer.warning?`). **3.0.0** re-casts every coordinate these warnings
122
157
  carry (`entry-dropped.path`, `unknown-keys-*.keys`) from a dot-joined STRING to a structured
123
158
  `ConfigPath = readonly (string | number)[]` — a string segment is an object key (verbatim, `.` included),
124
159
  a number segment is an array index — because the string form was a LOSSY encoding: a key literally named
@@ -141,7 +176,29 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
141
176
  `./node`): the durable self-wake record shape + `parseSchedulerStore` / `serializeSchedulerStore`
142
177
  (forward-compatible: unknown fields survive a round-trip, malformed records are dropped one by one).
143
178
  - **Fleet contract** (`fleet.ts`, subpath `./fleet`): drain protocol, instance discovery, dispatch policy,
144
- quota lease, usage report, quota-reset event.
179
+ quota lease, usage report, quota-reset event. **S-741** adds the restart signal, defined ONCE:
180
+ `RestartSignal` (`{ reasons: string[] (≥1, non-empty words), version: number (the config-center version that
181
+ differs from boot — finite, judged exactly as `readEffectiveWire` judges a config version), since: number (epoch
182
+ ms, non-negative integer) }` — exactly what the server already ships on /health). The heartbeat carries it as
183
+ `InstanceAnnouncement.restart?` (so `InstanceRegistration` carries it too); on /health, `InstanceHealth` is
184
+ UNCHANGED (passthrough — `restart` and the redundant top-level `restartRequired` ride untyped, so today's lenient
185
+ read stays lenient) and `readRestartSignal(health)` reads `health.restart` against the one shape (`undefined` on
186
+ any mismatch, never throws). Presence is the signal (present ⇔ restart required; no `required` boolean inside).
187
+ A heartbeat re-announces the full row, so one without `restart` means "no longer required". Typing
188
+ `InstanceHealth.restart` strictly is left for the next major. **S-768** adds the model-refusals signal the same
189
+ way (one mint point, two exits, ONE reader): the wire face `ModelRefusalsWire` (`{ entries: { <catalog entry name>:
190
+ <code> }, plane?: <code> }` with codes as open strings — name → code is the whole wire, no upstream id / provider /
191
+ field keys / prose) rides the heartbeat as `InstanceAnnouncement.modelRefusals?` and /health (`InstanceHealth` again
192
+ unchanged); `readModelRefusals(body)` reads either exit and narrows to `ModelRefusalsSignal` — the codes are CLOSED
193
+ sets owned here, `MODEL_ENTRY_REFUSAL_CODES` (`config.model_no_brain`, `config.model_entry_invalid`) and
194
+ `MODEL_PLANE_REFUSAL_CODES` (`config.default_model_missing`, `config.model_refused_name_minted`,
195
+ `config.model_route_endpoint_missing`, `config.gateway_fallback_without_primary`), and a code this package version
196
+ does not know is dropped ("not said"). It is a diagnostic key, so its failure direction is "not said": an unknown
197
+ code or a shape violation on the heartbeat never fails the announcement (the key drops to absent). Present ⇔ at
198
+ least one reportable refusal; absence is read by the announced `version` — an engine that predates the key never
199
+ sends it (UNKNOWN, not "none"). A name that cannot ride out is left out by the engine, judged by ONE predicate,
200
+ `isReportableRefusalName` (the wire can carry the key — `__proto__` it cannot — and `isDisclosableIdentity`, the
201
+ secret-shape rule the dropped-entry identity on read warnings already uses).
145
202
  - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
146
203
  carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side,
147
204
  a consumer's projection, or the key's own guard). Today: `models[].extraBody`, `models[].compat` (1.10.0),
@@ -1 +1 @@
1
- 6.0.0
1
+ 6.2.0
@@ -72,6 +72,9 @@
72
72
  "warnings": [
73
73
  {
74
74
  "domain": "models",
75
+ "identity": {
76
+ "name": "broken-no-provider"
77
+ },
75
78
  "kind": "entry-dropped",
76
79
  "path": [
77
80
  "models",
@@ -74,6 +74,9 @@
74
74
  },
75
75
  {
76
76
  "domain": "hosts",
77
+ "identity": {
78
+ "name": "UPPER-BAD"
79
+ },
77
80
  "kind": "entry-dropped",
78
81
  "path": [
79
82
  "hosts",
@@ -84,6 +84,9 @@
84
84
  "warnings": [
85
85
  {
86
86
  "domain": "models",
87
+ "identity": {
88
+ "name": "bad-entry"
89
+ },
87
90
  "kind": "entry-dropped",
88
91
  "path": [
89
92
  "models",
@@ -0,0 +1,166 @@
1
+ {
2
+ "domains": {
3
+ "outcome": "effective",
4
+ "effective": {
5
+ "collab": {
6
+ "templates": []
7
+ },
8
+ "entitlement": {
9
+ "budgetGroups": [],
10
+ "groupBindings": [],
11
+ "subjects": [],
12
+ "tiers": []
13
+ },
14
+ "execution": {
15
+ "allowedLanes": [
16
+ "host"
17
+ ],
18
+ "required": false
19
+ },
20
+ "governance": {},
21
+ "hosts": {
22
+ "hosts": []
23
+ },
24
+ "limits": {},
25
+ "mcp": {
26
+ "servers": []
27
+ },
28
+ "models": {
29
+ "models": [
30
+ {
31
+ "api": "openai-completions",
32
+ "apiKeyEnv": "OPENAI_API_KEY",
33
+ "enabled": true,
34
+ "id": "keep-id",
35
+ "name": "keep",
36
+ "provider": "openai",
37
+ "quotaWeight": 1,
38
+ "reasoning": false,
39
+ "tier": "standard",
40
+ "vision": false
41
+ }
42
+ ],
43
+ "roles": {
44
+ "summarize": {
45
+ "model": "keep"
46
+ }
47
+ },
48
+ "tierGroups": []
49
+ },
50
+ "plugins": {
51
+ "plugins": []
52
+ },
53
+ "projects": {
54
+ "projects": {}
55
+ },
56
+ "prompts": {
57
+ "bindings": []
58
+ },
59
+ "rosters": {
60
+ "rosters": []
61
+ },
62
+ "runtime": {},
63
+ "scenarios": {
64
+ "scenarios": []
65
+ },
66
+ "skills": {
67
+ "skills": []
68
+ },
69
+ "systems": {
70
+ "systems": []
71
+ },
72
+ "workers": {
73
+ "workers": []
74
+ }
75
+ },
76
+ "warnings": [
77
+ {
78
+ "domain": "models",
79
+ "identity": {
80
+ "id": "withheld-id"
81
+ },
82
+ "kind": "entry-dropped",
83
+ "path": [
84
+ "models",
85
+ 1
86
+ ]
87
+ },
88
+ {
89
+ "domain": "models",
90
+ "identity": {
91
+ "enabled": false,
92
+ "id": "off-id",
93
+ "name": "off-model"
94
+ },
95
+ "kind": "entry-dropped",
96
+ "path": [
97
+ "models",
98
+ 2
99
+ ]
100
+ },
101
+ {
102
+ "domain": "models",
103
+ "identity": {
104
+ "id": "short-bearer-id"
105
+ },
106
+ "kind": "entry-dropped",
107
+ "path": [
108
+ "models",
109
+ 3
110
+ ]
111
+ },
112
+ {
113
+ "domain": "models",
114
+ "identity": {
115
+ "id": "pem-id"
116
+ },
117
+ "kind": "entry-dropped",
118
+ "path": [
119
+ "models",
120
+ 4
121
+ ]
122
+ },
123
+ {
124
+ "domain": "models",
125
+ "identity": {
126
+ "name": "prefixed-pem"
127
+ },
128
+ "kind": "entry-dropped",
129
+ "path": [
130
+ "models",
131
+ 5
132
+ ]
133
+ },
134
+ {
135
+ "domain": "models",
136
+ "identity": {
137
+ "name": "glued-pem"
138
+ },
139
+ "kind": "entry-dropped",
140
+ "path": [
141
+ "models",
142
+ 6
143
+ ]
144
+ },
145
+ {
146
+ "domain": "models",
147
+ "kind": "entry-dropped",
148
+ "path": [
149
+ "roles",
150
+ "[redacted-secret]"
151
+ ]
152
+ }
153
+ ],
154
+ "metadata": {
155
+ "version": 11,
156
+ "updatedAt": "2026-09-25T00:00:00.000Z"
157
+ }
158
+ },
159
+ "wire": {
160
+ "derive": true
161
+ },
162
+ "localRoot": {
163
+ "derive": true,
164
+ "localVersion": 527399447574205
165
+ }
166
+ }