@sema-agent/settings-schema 6.0.0 → 6.1.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,168 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 6.1.0 — 2026-09-25 — 四件 additive:`entry-dropped` 身份(S-712)· 中心凭证信封 + 三态判别器(S-698)· `RestartSignal` 一处定义(S-741)· 中心凭证两键名 + 二选一判官(S-742);`redactSecrets` 私钥 PEM 串内任意位置且判在替换前(6.0.0 起三面既有盲点顺带关)
10
+
11
+ > 本节四件全 additive(新可选键 / 新导出 / 新文件),零删零改名;版号与节标题由发版方写。判档口径见下方「发版纪律」:
12
+ > 新键 / 新导出 = minor。
13
+
14
+ ### S-712 · `entry-dropped` 告警带被丢条目的身份(`identity?`,additive)
15
+
16
+ `EffectiveReadWarning` 的 `entry-dropped` 臂加可选键 `identity?: DroppedEntryIdentity`(`{ name?: string; id?: string;
17
+ enabled?: boolean }`,新导出类型);`DroppedEntry`(`salvageEntries` 的记账行)同带。**一条律,不分域**:只从被丢条目
18
+ **原文**取 —— 字符串 `name` / `id`、布尔 `enabled`,只认自有**数据**属性(不触发访问器),类型不对 ⇒ 不带该键(不猜、
19
+ 不 `String()` 转换),`redactSecrets` 会改动的串 ⇒ 不带该键(与坐标段、`error` 文案**同一只**脱敏判据:坐标段必须
20
+ 在场所以打码出门;身份可缺,打过码的串不再是身份,所以是**不带**而不是打码),三键都读不出 ⇒ 不带 `identity`(不铸
21
+ 空对象)。**缺席 = 读不出**(非字符串,或不能原样出门),两种原因刻意不分:消费方的正确处置相同(身份未知)。铸点单一:数组 / Record 两种集合形共用
22
+ 一只记账行铸点,域面(`buildEffective`)与 wire 面(skills manifest)共用一只告警铸点;`projectWarning` 的
23
+ `entry-dropped` 臂把 `identity` 带进投影(缺席不铸键)。
24
+
25
+ - **谁需要** = server(S-712 半场):它今天拿告警坐标回读原文档取被丢模型条目的身份(`read-warnings.ts`
26
+ `droppedEntryIdentity(rawDoc, path)`),本地腿的原文档是解析之后再读一次盘得来的,两次读之间文件一改就对不上,
27
+ 只能 fail-closed 整组拒;身份随告警走之后这只函数换数据源、原文档入参删除。
28
+ - **谁受伤** = 运行期无(additive 可选键;持 `EffectiveReadWarning` 穷举 `switch` 的消费方零编译红,没有新 kind)。
29
+ **提货成本一处**(对抗轮 #1 订正:初稿写「web-admin 无适配器」是错的):web-admin 有一致性适配器
30
+ (`src/lib/config/conformance-adapter.ts`)与**自己入库**的语料,其中 `wa05-credential-as-unknown-key-in-a-salvaged-skills-entry`
31
+ 在一条被丢的 skills 条目(`name:"broken"`)上断言 `entry-dropped`,且 `test/settings-schema-conformance.test.ts` 要求
32
+ 每条 `wa*` 在 domains 腿 `pass` ⇒ 提货 6.1.0 时这一条会报告警差(多出 `identity:{name:"broken"}`),要按新读数重新
33
+ 加冕 —— 发布帖 @web-admin 点名。server 两条读腿的告警直接来自本包 `onWarning`,提货即带身份,不红。
34
+ - **消费方** = server(`splitFoldedReadWarnings` 改读 `w.identity`;`enabled` 缺席按今天的 `!== false` 口径自判;
35
+ `name` 缺席 = 读不出(非字符串**或**脱敏会改动它),模型面按身份未知处置时拒因句请说「没有可读的名字」,别说
36
+ 「不是字符串」—— 对抗轮 #4 的二义在契约层刻意不分,措辞归读方);
37
+ web-admin / cli 可选读(管理面 / 壳在「条目被丢」提示里点名);core n/a。
38
+ - **一致性语料再加冕**(`npm run conformance:bless`,additive 键):6 条 case 的 `domains` 期望多出 `identity`
39
+ (`c01` / `c02` / `c23` / `cli01` / `cli02` / `t04`,共 8 条告警;零删、零改其它字段,`baseline.json` 零变化)。
40
+ kit 自带语料随包走,消费方无需为它 bless(自己入库的语料见上一条)。**新语料 `c40-s712-identity-only-what-reads-out`**
41
+ (对抗轮 #5):被丢条目的 `name` 是 Bearer 形串、`enabled` 是 `"yes"` ⇒ 身份只剩 `id`;正控条目三键齐全。变异门
42
+ `test/conformance-mutation.test.ts` 为身份铸点登记两靶(脱敏判据 / enabled 只收布尔),加 `c40` 前两靶变异体全绿,
43
+ 加后各红。第三条律(只认数据属性)JSON 表达不出 getter,由单测看着,如实登记。
44
+ - **historical 归一器加投影级钩子**(对抗轮 #3,`./conformance` additive):`ConformanceNormalizer.warning?(p)`,先于
45
+ `path` 作用、两侧同过;`scripts/conformance-historical.mjs` 的有损归一器用它剥掉 `entry-dropped.identity` —— 不剥的话,
46
+ 照 `docs/CONFORMANCE.md` 重跑 historical 会把 6 条 case 记成旧版行为红(2.1.0 由 1 条涨到 7 条,
47
+ `test/conformance-corpus.test.ts` 随之红;对抗腿实跑)。修后在隔离副本里真重跑 2.1.0 / 6.0.0:行为红 1 / 0,
48
+ `firstRedOn` 零变化,语料测试 12 / 12;入库证据未重写(读数与入库一致)。
49
+ - **红先**:`test/entry-dropped-identity-6.1.0.test.ts` 对 6.0.0 源 **9 红 / 2 绿**(绿的两格 = 读不出 ⇒ 缺席、
50
+ 好条目零告警的控制格),改源后 11 / 11。对抗轮(独立 opus 腿,codex 缺席顶替)追加:#2 秘密判据两格(NBSP 分隔 /
51
+ 短令牌的 Bearer 串随身份明文出门;判据与坐标脱敏同一只)对首版 **2 红** → 修后绿;#3 投影钩子一格 **1 红**(tsc 同红)→
52
+ 绿;#5 变异两靶 **2 红**(变异体存活)→ 加 `c40` 后绿。**验修轮**(同一 opus 腿,只核修复提交):11 条原发现
53
+ 读数全闭合;新 **N1 [medium]**:#2 改用 `redactSecrets` 判据后,PEM 私钥(头里带空格,脱敏器的定点两遍够不着)作
54
+ `name` / `id` 随身份明文出门(首版的 `containsSecretToken` 扣得住,6.0.0 无身份 —— 秘密轴回归;同一盲点在 6.0.0 的
55
+ 坐标 / `error` 脱敏里本就存在)⇒ **在源头修** `redactSecrets`:出口不变量 `containsSecretToken(redactSecrets(x)) === false`,
56
+ 定点两遍之后探测器仍判阳 ⇒ 整串打码;坐标、`error` 文案、身份三面一起补上,身份判据仍是 `redactSecrets(v) === v`
57
+ 一只。红先两格(PEM 身份 / 脱敏器出口不变量)对 `0397672` **2 红** → 绿。**N4 [nit]**:`c40` 加「短 Bearer 令牌」与
58
+ 「PEM 私钥」两条被丢条目,变异门加两靶(身份判据退回旧探测器 / 去掉整串兜底),对旧 `c40` **2 红**(变异体存活)→
59
+ 新 `c40` 下绿。**N3 [nit]**(`readEffectiveWire` 的 `Number.isFinite` 与 `RestartSignal` 的 `z.number().finite()` 是同一条
60
+ 判据的两处拼写)登记不修:语义等价,抽一只共用定义要为一行判据新开叶模块,留作下次触碰时顺手。
61
+ **codex 一审**(gpt-6-astra,`--base 5153d2a`,对 `7b92977`,needs-attention 1 [high]):PEM 私钥的 header 不在串首
62
+ (前导换行 / `key: ` 前缀)时,探测器(`SECRET_LITERAL_RE` 串首锚 + 按空格分词拆散带空格的 header)看不见,
63
+ `redactSecrets` 原样放行 ⇒ `identity.id` / 投影 / Record 坐标带出整把私钥(6.0.0 的告警不暴露它)。**源头修**:
64
+ 新导出 `PRIVATE_KEY_PEM_HEADER`(`types.ts`,私钥 PEM header 的**唯一**定义,无锚)—— `SECRET_LITERAL_RE` 由它拼出
65
+ (源串与 6.0.0 逐字相同,有钉),`redactSecrets` 的整串兜底加「串内任意位置见到它 ⇒ 整串扣留」;坐标 / `error` /
66
+ 身份三面同一只。**刻意不**放宽校验侧探测器 `containsSecretToken`(让它拒串中 PEM = 收紧它校验的每个字段 = major),
67
+ 登记为欠账。红先三格(前导换行 + 前缀的身份 / 脱敏器 / 源串拼装钉)对 `7b92977` **3 红** → 绿;`c40` 加一条带
68
+ `key: ` 前缀 PEM 的被丢条目,变异门加一靶(去掉「串内任意位置」一道),对旧 `c40` **1 红**(变异体存活)→ 新 `c40` 下绿。
69
+ **codex 验修轮**(`--base 7b92977`,对 `050c8e6`,needs-attention 1 [high]):修复自身的**顺序缺口** —— `sk-note:-----BEGIN
70
+ PRIVATE KEY-----…` 这类密钥形前缀**紧贴** header 的串,令牌那一遍先把 `sk-note:-----BEGIN` 整段换成 `[redacted-secret]`,
71
+ 替换后的 header 检查看不到 header,私钥体原样出门。修:`PRIVATE_KEY_PEM_HEADER` 改判**原文**(任何替换之前),仍是
72
+ `redactSecrets` 一处。红先四面各一格(坐标 / error / 身份 / 投影 + 秘密扫描,Ed25519 私钥现场生成)对 `050c8e6`
73
+ **3 红 / 1 绿**(身份面 `050c8e6` 已扣留)→ 4 绿;`c40` 加该形(作被丢条目 `id` 与 `roles` 坏键),变异门加一靶
74
+ (检查挪回替换之后)对旧 `c40` **1 红** → 绿。**对 6.0.0 的判定**:同一夹具在 `5153d2a` 的独立树上跑,坐标 / error /
75
+ 投影三面**同样泄**(6.0.0 起的既有盲点,不是本版回归);身份面是本版新面,`050c8e6` 已扣留。本版把三面既有盲点一并关掉。
76
+ - **边界(如实登记)**:身份只取 `name` / `id` / `enabled` 三键;server 现有载体还读 `provider`,它不是身份,
77
+ 不在本键里 —— 要不要把 `provider` 也随告警走是 server 半场的决定(可继续按坐标回读或从投影外取),本版不猜。
78
+
79
+ ### S-698 · 中心凭证文件的信封形 `CenterCredentialEnvelope` + 三态判别器 `parseCenterCredentialFile`(新文件,additive)
80
+
81
+ 新模块 `src/center-credential.ts`(主 barrel 导出,不开新 subpath —— 与 `cross-session.ts` 同:纯 zod、browser-safe):
82
+ `CENTER_CREDENTIAL_ENVELOPE_KIND = "user-session"`、`CenterCredentialEnvelope`(zod,`.strict()`;`kind` 字面、
83
+ `accessToken`(JWT,`looksLikeJwt`)/ `refreshToken` 走与裸行**同一条令牌律**(可打印 ASCII 0x20–0x7E、首尾可见、中间可有空格)、`expiresAt` =
84
+ RFC 3339 date-time(zod `datetime({offset:true})` 管日历有效性,再补「秒必备 + 偏移必须 `±hh:mm` 且时 00–23 / 分 00–59」
85
+ zod 放过的形;不收闰秒 `:60`)、`issuer` = `NON_SECRET_URL`(签发它的中心的基础 URL;与 `remoteExec` 的 `apiUrl` /
86
+ `endpoint` 同一只判据,不另写更窄的一份))、类型 `CenterCredentialFile` 与 `parseCenterCredentialFile(text)`。判别律
87
+ (design/325 §1)一条、唯一实现:`trim()`(含 BOM)后空 ⇒ `unreadable("empty")`;`{` 开头 ⇒ 只按信封读,失败 = `unreadable`
88
+ (**不回落**裸行);否则取首行(第一个 CR / LF 之前),是令牌 ⇒ `bare`,不是 ⇒ `unreadable`(放不进 Authorization
89
+ 头的行不是 bearer)。`reason` 不回显文件内容(只点字段名 + zod 问题码 +
90
+ 未知键个数;`JSON.parse` 的报错会引用原文片段,不转述)。
91
+
92
+ - **谁需要** = S-698 长寿引擎续签:壳登录后可能关掉、引擎继续跑几天,访问令牌过期后中心请求逐条失败;信封让
93
+ 引擎手里有 refresh token 与到期时刻,续签器(server 半场)据此在到期前续。
94
+ - **谁受伤** = 无(新导出、新文件)。server 改调本判别器之后,相对它今天的读法(整段 `trim()` 后当 bearer)有
95
+ **三处行为差**,如实登记,都只在今天本就发不对的文件上可见:① **多行**裸行文件 ⇒ 只取首行(今天发出去的 header
96
+ 含换行、不合法);② `trim()` 后以 `{` 开头的文件 ⇒ 今天当 bearer 发出去,本判别器判信封、读不成即 `unreadable`
97
+ (判别律本身);③ 首行含控制 / 非 ASCII 字符 ⇒ `unreadable`(前面粘了不可见字符的信封今天会连同 refreshToken 一起当 bearer
98
+ 发出门 —— 对抗轮 #7)。**如实登记**:Latin-1 令牌今天能用(HTTP 头的 ByteString 收、中心逐字比得上),这里判
99
+ `unreadable`(非 ASCII 与不可见字符分不开,取 fail-closed);含空格的口令式共享令牌照收(验修轮 N2)。同一个令牌
100
+ 走 `SEMA_REGISTRY_TOKEN`(env 腿)不过本律 —— env 值逐字承运与 server 今天同,要不要让 env 值也过令牌律归 server 半场。
101
+ - **消费方** = server(`src/center-credential.ts` 文件形 `read()` 改调 `parseCenterCredentialFile`:`bare` ⇒ 既有
102
+ `classify`;`envelope` ⇒ `accessToken` 按 `user-jwt` 分箱 + 续签器读 `refreshToken` / `expiresAt` / `issuer`;
103
+ `unreadable` ⇒ 既有不可读臂);cli(L-653 壳半场:登录 / device-code 成功后写信封,写前用 `CenterCredentialEnvelope`
104
+ 自校);web-admin n/a(端点已在);core n/a。
105
+ - **红先**:`test/center-credential-envelope-6.1.0.test.ts`(经主 barrel 取,导出面同测)对 6.0.0 源 **14 红 / 0 绿**,
106
+ 改源后 14 / 14。对抗轮(独立 opus 腿,codex 缺席顶替)追加四格,对首版 **4 红** → 修后 18 / 18:#6 偏移越界
107
+ (`+99:99` / `+24:00` / `+23:60` / `-99:00` 首版判信封、`Date.parse` 为 NaN)· #7 `{` 前粘 trim 不剥的字符
108
+ (`\u0085` / `\u0001` / `\u00AD` …)首版整份信封判 bare · #7 裸行令牌律 · #10 `accessToken` 带 CRLF / 是 `wpt_` 拉取令牌
109
+ 首版判信封。验修轮 N2(令牌律首版拒掉含空格的口令式共享令牌)两格对 `6f22564` **2 红** → 放宽为「可打印 ASCII、
110
+ 首尾可见」后绿。
111
+
112
+ ### S-742 · 中心凭证两个声明键名 `CENTER_CREDENTIAL_ENV` + 二选一判官 `judgeCenterCredentialDeclaration`(additive)
113
+
114
+ 同 `src/center-credential.ts` 尾段:`CENTER_CREDENTIAL_ENV = { token: "SEMA_REGISTRY_TOKEN", tokenFile:
115
+ "SEMA_REGISTRY_TOKEN_FILE" } as const`;类型 `CenterCredentialDeclaration`;`judgeCenterCredentialDeclaration(env)` →
116
+ `{from:"env"; token}` | `{from:"file"; path}` | `{from:"absent"}`。规则从 server `readCenterCredentialDeclaration`
117
+ 上提、**逐字同**:空串 = 未设(不 trim);两个都设(非空)⇒ throw,拒句逐字取 server c60beebe
118
+ `src/center-credential.ts:93-96`(「… are both set — pick exactly one: … Two writers on one credential is a guess, not a
119
+ config.」,点名两键、不回显值);只读这两个键。**一处有意的差**:server 在这一步把文件路径解析成绝对路径
120
+ (`node:path` `resolve`),本包纯契约不碰 `node:path`、也不知道读方的 cwd ⇒ `path` 原样返回,解析留给读方。
121
+
122
+ - **谁需要** = 管理面(给 worker 渲染 env)与壳(给本机引擎写 env)今天各自手抄这两个键名,规则只活在 server
123
+ —— 手抄键名会静默漂(C-R75);上提后三方 import 同一份键名与同一条规则([8325] 一请,server [8326] 认领)。
124
+ - **谁受伤** = 无(新导出)。
125
+ - **消费方** = server(`readCenterCredentialDeclaration` 改为 `judgeCenterCredentialDeclaration(env)` + 自己的
126
+ `resolvePath`;`CENTER_TOKEN_ENV` / `CENTER_TOKEN_FILE_ENV` 两常量改引 `CENTER_CREDENTIAL_ENV`);web-admin(worker env
127
+ 渲染处两键名改 import,[8327] 同波一天);cli(本机引擎 env 写入处改 import;写前可用判官自校不会同设);core n/a。
128
+ - **红先**:`test/center-credential-declaration-6.1.0.test.ts`(env / file / absent / both 四格 + 空串两格 + 键名 +
129
+ 只读两键,期望拒句是从 server 源**抄来**的字面,不从实现派生)对 6.0.0 源 **7 红 / 0 绿**,改源后 7 / 7。
130
+
131
+ ### S-741 · 重启信号 `RestartSignal` 定义一次:心跳键 `InstanceAnnouncement.restart?` + `/health` 读法 `readRestartSignal`(additive)
132
+
133
+ `src/fleet.ts` 新导出 `RestartSignal`(zod + 类型)= `{ reasons: z.array(z.string().min(1)).min(1), version: z.number().finite(),
134
+ since: z.number().int().nonnegative() }` 与 `readRestartSignal(health: unknown): RestartSignal | undefined`;`InstanceAnnouncement`
135
+ 加 `restart: RestartSignal.optional()`(同一只 schema 对象,测试按运行期对象同一钉住;`InstanceRegistration` =
136
+ `InstanceAnnouncement.extend(…)`,随之同带)。**在场 ⇔ 需要重启**,形里没有 `required` 布尔。`/health` 一侧:
137
+ **`InstanceHealth` 形不动**(passthrough,`restart` 与顶层冗余布尔 `restartRequired` 照 6.0.0 原样承运、不入类型),
138
+ 读它走 `readRestartSignal`(对 `health.restart` safeParse,形不合 / 缺席 / 非对象 / 访问器抛错 ⇒ `undefined`,不抛;
139
+ 只认 `restart`,不读 `restartRequired`)。`version` 与 `readEffectiveWire` 读配置版本**同一条判据**(有限数)。
140
+
141
+ > **形的裁定(发布方,两次)**:① 任务书初稿写的是 `{ required: true, reasons, version: string, since: string }`。
142
+ > 亲读 server c60beebe:`/health` 已发 `restart: { reasons, version, since }`(`src/http/dispatch.ts:271`),`version` =
143
+ > 中心配置版本(**number**,`src/config-center/restart-signal.ts:101-108`),`since` = epoch ms(**number**),对象里
144
+ > 没有 `required`;web-admin 自审稿 §2.4 也明写「不要 `restartRequired` 布尔 + `restart` 两键 —— 布尔与在场性冗余」。
145
+ > 照初稿做,心跳与 `/health` 由同一只 getter 铸出两种形。主会话裁:取已发 wire 形,`required` 不要。
146
+ > ② 对抗轮(独立 opus 腿)#8:同一个配置版本,`readEffectiveWire` 收任意有限数、首版 `RestartSignal` 却要非负整数 ⇒
147
+ > 中心一发非整数版本,整条心跳 400、实例不登记 —— 主会话裁 `version` 改 `z.number().finite()`,一个量一条判据。
148
+ > #9:首版把 `InstanceHealth.restart` 类型化 = 宽读改严判(形不合的 `/health` 体由过变拒),照「发版纪律」字面是
149
+ > major —— 主会话裁 **6.1.0 不做**,`InstanceHealth` 形不动,另给 `readRestartSignal`。**欠账**:`InstanceHealth.restart`
150
+ > 上严判是 **7.0.0 项**(届时按硬 breaking 三句办:类型化、形不合即拒,消费方 tsc / parse 红即通知)。
151
+
152
+ - **谁需要** = T3 心跳形(web-admin 自审稿 §2.4,[8323] ④ / [8326] ②):引擎心跳带重启信号 → 中心按策略给恰好需要
153
+ 的 worker 打 `recreateNonce` → 编排器照旧执行。心跳成了 `/health` 之外的第二个出口,所以形只定义一次。
154
+ - **谁受伤** = 无(新可选键 + 新导出;`InstanceHealth` 一字不动)。旧 web-admin 收到带 `restart` 的心跳:
155
+ `InstanceAnnouncement` 是默认 strip 的 `z.object`,该键被剥,行为同今。心跳里 `restart` 形不合 ⇒ 新 web-admin 对
156
+ 整条心跳 400(可选子键拖整行,如实登记;server 铸点与 `/health` 同一只 getter,形由本 schema 约束)。
157
+ - **消费方** = server(S-741 半场:`FleetClientOpts` 加 restart getter —— 与 `/health` 同一只 `restartState` —— 心跳体带
158
+ `restart`,翻转即刻再 announce;`/health` 零改);web-admin(`/api/fleet/instances` 收键、存、策略旋钮缺省关,[8327]
159
+ 同波一天;存行整替不合并:不带 `restart` 的一拍 = 不再需要;若中心探针要读 `/health` 的重启信号,用 `readRestartSignal`,
160
+ 别自己 parse `health.restart`);cli n/a;core n/a。
161
+ - **文档句同批(主会话追加,不改类型)**:`InstanceAnnouncement.ready` 的注由「self-declared: bootDone && !draining」改为
162
+ 「self-declared readiness: model roster landed AND not draining (server ≥7.102.0; older engines: !draining only)」——
163
+ server 车OE S-740 起心跳 `ready` 与 `/health` 就绪同一只谓词;旧注与 server 今天的实现(`ready = !isDraining()`)
164
+ 和明天的实现都不符。`test/docs-hygiene.test.ts` 外部版本白名单登 server `7.102.0`(注里点名的是 server 版本,
165
+ 不是本包)。
166
+ - **红先**:首版 `test/restart-signal-6.1.0.test.ts` 对 6.0.0 源 **6 红 / 3 绿**(绿的三格:server 今天 `/health` 真形
167
+ parse 必过 —— 6.0.0 下它靠 passthrough 过,照初稿的 `required` + 字符串形它会红;不带 `restart` 照旧过的 additive 格;
168
+ 类型面格(`expectTypeOf`,由 `tsc` 执法、运行期空跑)),首版改源后 9 / 9。二裁改写后的本文件对**首版实现**
169
+ **6 红 / 5 绿**(红:`readRestartSignal` 缺席四格、`InstanceHealth` 形不动格、version 有限数格),改源后 11 / 11。
170
+
9
171
  ## 6.0.0 — 2026-09-24 — `crossSessionInbound` 词表随 core 7.30.0 改为 `wake / next-turn / hold / refuse`,`accept` 退役(BREAKING,枚举改词;server S-616 提货同批)
10
172
 
11
173
  > **版号裁定(发布方)**:本批只有一处改动 —— `CROSS_SESSION_INBOUND_VALUES` 由 `["accept","hold","refuse"]`
package/README.md CHANGED
@@ -95,6 +95,23 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
95
95
  (`shouldForceJwksRefresh`, `jwksWithinOfflineGrace`, `classifyTimeClaims`, `toVerifiedIdentity`).
96
96
  The normative rules R1–R10 in its header are the service verifier's implementation spec.
97
97
  `VerifiedIdentity` carries an optional `scope` (the token's active tenant scope — see `api/scopes`).
98
+ - **Center credential file** (`center-credential.ts`, main barrel; S-698): the file behind
99
+ `SEMA_REGISTRY_TOKEN_FILE` is one of two KINDS — a bare line (one static token — the same kind of file as today:
100
+ 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
101
+ `CenterCredentialEnvelope` (strict JSON: `kind: "user-session"` = `CENTER_CREDENTIAL_ENVELOPE_KIND`, a JWT
102
+ `accessToken` and a `refreshToken` under the same token law as the bare line, RFC 3339
103
+ `expiresAt` with seconds and a `Z` / `±hh:mm` offset in range, `issuer` = the issuing center's base URL judged
104
+ by the same `NON_SECRET_URL` the `remoteExec` endpoints use — same-origin is the reader's comparison).
105
+ `parseCenterCredentialFile(text)` is the ONE discrimination law — trimmed text starting with `{` is read ONLY
106
+ as an envelope (a failure is `unreadable`, never a fallback to a bare line); anything else is a bare line,
107
+ 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
108
+ content. Pure: reading, atomically writing and renewing the file are the consumers' job. S-742 lifts the
109
+ DECLARATION face here too: `CENTER_CREDENTIAL_ENV` (`{ token: "SEMA_REGISTRY_TOKEN", tokenFile:
110
+ "SEMA_REGISTRY_TOKEN_FILE" }` — one spelling for the center rendering worker env, the shell writing its local
111
+ engine's env, and the server reading it) and `judgeCenterCredentialDeclaration(env)` → `env` / `file` /
112
+ `absent`, the one pick-exactly-one rule: an empty string is unset, BOTH set (non-empty) throws (the refusal
113
+ names the two keys, never a value), the file path comes back verbatim (resolving a relative one against a cwd
114
+ is the reader's job).
98
115
  - **Multi-tenant scopes** (`api/scopes.ts`, subpath `./api/scopes`): the scope contract — the `Scope`
99
116
  entity (`{id,name,createdAt}`, reserved default `"global"` always exists), per-scope `ScopeMember`
100
117
  rows (`{scopeId,principal,role}` on the existing viewer<editor<publisher<admin ladder), the optional
@@ -118,7 +135,16 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
118
135
  tolerance on the same read path: one malformed catalog entry is dropped on its own (named through
119
136
  `onWarning` as `entry-dropped`) and its siblings survive, instead of the whole domain falling to
120
137
  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
138
+ domains keep failing loud, entry by entry included. **S-712** (additive) lets each `entry-dropped` carry an
139
+ optional `identity?: { name?, id?, enabled? }` read from the dropped entry's OWN text (`DroppedEntryIdentity`):
140
+ a string `name` / `id` and a boolean `enabled`, own data properties only, each key present only when it
141
+ reads with that exact type and `redactSecrets` leaves it unchanged — the SAME redactor the coordinates and the
142
+ error text go through (not guessed, not coerced, not masked — a masked string identifies nothing); nothing
143
+ readable ⇒ no `identity` key (absent = "not safely readable", whichever the reason). A consumer learns WHICH entry was
144
+ dropped without re-reading the source document by coordinate (the document it holds may not be the one this
145
+ read saw). `identity` is part of the warning projection, so the conformance gate compares it (corpus
146
+ case `c40` pins the two laws JSON can express; the historical normalizer strips it, since older versions cannot
147
+ mint it — `ConformanceNormalizer.warning?`). **3.0.0** re-casts every coordinate these warnings
122
148
  carry (`entry-dropped.path`, `unknown-keys-*.keys`) from a dot-joined STRING to a structured
123
149
  `ConfigPath = readonly (string | number)[]` — a string segment is an object key (verbatim, `.` included),
124
150
  a number segment is an array index — because the string form was a LOSSY encoding: a key literally named
@@ -141,7 +167,16 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
141
167
  `./node`): the durable self-wake record shape + `parseSchedulerStore` / `serializeSchedulerStore`
142
168
  (forward-compatible: unknown fields survive a round-trip, malformed records are dropped one by one).
143
169
  - **Fleet contract** (`fleet.ts`, subpath `./fleet`): drain protocol, instance discovery, dispatch policy,
144
- quota lease, usage report, quota-reset event.
170
+ quota lease, usage report, quota-reset event. **S-741** adds the restart signal, defined ONCE:
171
+ `RestartSignal` (`{ reasons: string[] (≥1, non-empty words), version: number (the config-center version that
172
+ differs from boot — finite, judged exactly as `readEffectiveWire` judges a config version), since: number (epoch
173
+ ms, non-negative integer) }` — exactly what the server already ships on /health). The heartbeat carries it as
174
+ `InstanceAnnouncement.restart?` (so `InstanceRegistration` carries it too); on /health, `InstanceHealth` is
175
+ UNCHANGED (passthrough — `restart` and the redundant top-level `restartRequired` ride untyped, so today's lenient
176
+ read stays lenient) and `readRestartSignal(health)` reads `health.restart` against the one shape (`undefined` on
177
+ any mismatch, never throws). Presence is the signal (present ⇔ restart required; no `required` boolean inside).
178
+ A heartbeat re-announces the full row, so one without `restart` means "no longer required". Typing
179
+ `InstanceHealth.restart` strictly is left for the next major.
145
180
  - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
146
181
  carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side,
147
182
  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.1.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
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "meta": {
3
+ "version": 11,
4
+ "updatedAt": "2026-09-25T00:00:00.000Z"
5
+ },
6
+ "domains": {
7
+ "models": {
8
+ "models": [
9
+ {
10
+ "name": "keep",
11
+ "provider": "openai",
12
+ "id": "keep-id",
13
+ "apiKeyEnv": "OPENAI_API_KEY"
14
+ },
15
+ {
16
+ "name": "Authorization: Bearer FAKE-S712-CORPUS-00000000",
17
+ "provider": "openai",
18
+ "id": "withheld-id",
19
+ "enabled": "yes",
20
+ "apiKeyEnv": "not an env name"
21
+ },
22
+ {
23
+ "name": "off-model",
24
+ "provider": "openai",
25
+ "id": "off-id",
26
+ "enabled": false,
27
+ "apiKeyEnv": "not an env name"
28
+ },
29
+ {
30
+ "name": "tok=Bearer abc1234",
31
+ "provider": "openai",
32
+ "id": "short-bearer-id",
33
+ "apiKeyEnv": "not an env name"
34
+ },
35
+ {
36
+ "name": "-----BEGIN RSA PRIVATE KEY-----\nMIIEFAKES712CORPUSPEMBODY0000\n-----END RSA PRIVATE KEY-----",
37
+ "provider": "openai",
38
+ "id": "pem-id",
39
+ "apiKeyEnv": "not an env name"
40
+ },
41
+ {
42
+ "name": "prefixed-pem",
43
+ "provider": "openai",
44
+ "id": "key: -----BEGIN OPENSSH PRIVATE KEY-----\nMIIEFAKES712CODEXPREFIXED0000\n-----END OPENSSH PRIVATE KEY-----",
45
+ "apiKeyEnv": "not an env name"
46
+ },
47
+ {
48
+ "name": "glued-pem",
49
+ "provider": "openai",
50
+ "id": "sk-note:-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwFAKES712GLUEDCORPUS0000000000\n-----END PRIVATE KEY-----",
51
+ "apiKeyEnv": "not an env name"
52
+ }
53
+ ],
54
+ "roles": {
55
+ "summarize": {
56
+ "model": "keep"
57
+ },
58
+ "sk-note:-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwFAKES712GLUEDCORPUS0000000000\n-----END PRIVATE KEY-----": {
59
+ "model": "keep"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "S-712 身份律的两道闸压在语料上:被丢条目的 name 是一段脱敏器会改动的串(Bearer 形)⇒ identity 不带 name(同一条告警里坐标会打码,身份不许把原文带出去);enabled 是字符串 \"yes\" ⇒ 不带 enabled;id 是干净字符串 ⇒ 照带。另两条被丢条目压脱敏判据的两道边:`tok=Bearer abc1234`(短令牌,探测器 containsSecretToken 不判阳、脱敏器会改)与一整段 PEM 私钥(头里带空格,脱敏器的定点两遍够不着、靠整串兜底)⇒ 两条的 name 都不带。正控:另一条被丢条目 name / id / enabled:false 三键齐全逐字带出。读腿若放宽任一闸,告警面多出字段即红;秘密清单同时由告警面零命中门看着。",
4
+ "firstRedOn": [],
5
+ "secrets": [
6
+ "FAKE-S712-CORPUS-00000000",
7
+ "abc1234",
8
+ "MIIEFAKES712CORPUSPEMBODY0000",
9
+ "MIIEFAKES712CODEXPREFIXED0000",
10
+ "MC4CAQAwBQYDK2VwFAKES712GLUEDCORPUS0000000000"
11
+ ],
12
+ "note": "对抗轮 #5(车OD 2026-09-25):变异门 `test/conformance-mutation.test.ts` 为 readEntryIdentity 登记两靶(`redactSecrets(v) === v` → `true`、`typeof enabled === \"boolean\"` → `enabled !== void 0`),加本条之前两靶变异体全绿(语料缺口),加本条之后两靶各红。第三条律(只认数据属性、不触发访问器)JSON 表达不出 getter,不在本条射程内,由单测看着。firstRedOn 为空:旧版铸不出 identity,historical 归一器两侧剥掉 identity,本条在旧版上不计行为红 —— 它是护栏,不是真门。 验修轮 N4 / N1(车OD 2026-09-25):加入短令牌与 PEM 两条 —— 身份判据退回旧的 containsSecretToken 时短令牌那条带出 name ⇒ 本条红;redactSecrets 去掉整串兜底时 PEM 那条带出 name ⇒ 本条红。 codex 一审 [high](2026-09-25):再加一条 `id` = 带 `key: ` 前缀的 PEM 私钥(header 不在串首,探测器看不见)⇒ 身份只剩 name;redactSecrets 去掉「串内任意位置 PEM header」一道时本条红。 codex 验修轮 [high](2026-09-25):再加 `sk-note:` 紧贴私钥 PEM header 的串,既作被丢条目的 `id`(身份面)又作 `roles` 的坏键(坐标面 + zod error 面);redactSecrets 把 PEM 检查挪回替换之后时本条红(令牌那一遍先把 `sk-note:-----BEGIN` 换掉)。"
13
+ }
@@ -72,6 +72,10 @@
72
72
  "warnings": [
73
73
  {
74
74
  "domain": "models",
75
+ "identity": {
76
+ "id": "café-id",
77
+ "name": "café"
78
+ },
75
79
  "kind": "entry-dropped",
76
80
  "path": [
77
81
  "models",
@@ -80,6 +84,10 @@
80
84
  },
81
85
  {
82
86
  "domain": "models",
87
+ "identity": {
88
+ "id": "café-id",
89
+ "name": "café"
90
+ },
83
91
  "kind": "entry-dropped",
84
92
  "path": [
85
93
  "models",
@@ -85,6 +85,10 @@
85
85
  "warnings": [
86
86
  {
87
87
  "domain": "models",
88
+ "identity": {
89
+ "id": "a\u0007b-id",
90
+ "name": "a\u0007b"
91
+ },
88
92
  "kind": "entry-dropped",
89
93
  "path": [
90
94
  "models",
@@ -93,6 +97,10 @@
93
97
  },
94
98
  {
95
99
  "domain": "models",
100
+ "identity": {
101
+ "id": "zero​width-id",
102
+ "name": "zero​width"
103
+ },
96
104
  "kind": "entry-dropped",
97
105
  "path": [
98
106
  "models",
@@ -72,6 +72,10 @@
72
72
  "warnings": [
73
73
  {
74
74
  "domain": "models",
75
+ "identity": {
76
+ "id": "bad-id",
77
+ "name": "bad"
78
+ },
75
79
  "kind": "entry-dropped",
76
80
  "path": [
77
81
  "models",
@@ -266,13 +266,13 @@ export declare const VerifiedIdentity: z.ZodObject<{
266
266
  }, "strip", z.ZodTypeAny, {
267
267
  principal: string;
268
268
  role?: string | undefined;
269
- scope?: string | undefined;
270
269
  grantId?: string | undefined;
270
+ scope?: string | undefined;
271
271
  }, {
272
272
  principal: string;
273
273
  role?: string | undefined;
274
- scope?: string | undefined;
275
274
  grantId?: string | undefined;
275
+ scope?: string | undefined;
276
276
  }>;
277
277
  export type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
278
278
  /** The verifier's total result — exactly one of ok/error. */