@sema-agent/settings-schema 9.0.0 → 10.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +17 -1
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +2 -1
  5. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +2 -1
  6. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +4 -2
  7. package/conformance/cases/c23-mixed-validity-models/expected.json +2 -1
  8. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +14 -7
  9. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +14 -7
  10. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +10 -5
  11. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/meta.json +2 -2
  12. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +4 -2
  13. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +10 -5
  14. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/expected.json +2 -1
  15. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +2 -1
  16. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +104 -0
  17. package/conformance/cases/c69-deep-encoded-token-past-every-reading/input.json +26 -0
  18. package/conformance/cases/c69-deep-encoded-token-past-every-reading/meta.json +9 -0
  19. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +104 -0
  20. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/input.json +26 -0
  21. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/meta.json +9 -0
  22. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +4 -2
  23. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +4 -2
  24. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +2 -1
  25. package/dist/config-fns.d.ts +33 -7
  26. package/dist/config-fns.js +48 -21
  27. package/dist/conformance/types.d.ts +1 -1
  28. package/dist/types.d.ts +11 -8
  29. package/dist/types.js +16 -9
  30. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,107 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 10.1.0 — 2026-10-10 — 被丢条目带机读原因 `reasonCode`(minor,additive:新导出 + 可选新键,零删零改名)
10
+
11
+ > 本节一件,additive(新导出 + 新字段),零删零改名。判档口径见下方「发版纪律」:新键 / 新导出 = minor。
12
+
13
+ ### S-1295 · 被丢条目带**机读原因**(`reasonCode`,闭集,additive)
14
+
15
+ `DroppedEntry`(`salvageEntries` 的逐条记账行)与 `EffectiveReadWarning` 的 `entry-dropped` 臂各加可选键 `reasonCode?: DroppedEntryReasonCode`
16
+ (本包铸的每一条都带 —— 铸点返回型是必填窄型,漏带词编译红;公开型上可选只为让手工构造这两个公开型的消费方不因本版编译红 —— 新键按发版纪律是可选键)。
17
+ 新导出 `DROPPED_ENTRY_REASON_CODES`(闭集常量)与 `DroppedEntryReasonCode`(型)。`error` 句照旧,不属本件。
18
+
19
+ **词与丢弃点一一对应,没有 other 兜底**(`DroppedEntry` 只有一个铸点,它的判据 `judgeEntry` 按「谁判、判成什么样」分三种丢法,读实现所得):
20
+
21
+ | 词 | 丢弃点 |
22
+ |---|---|
23
+ | `entry-schema-invalid` | 条目(数组元素 / Record 的值)没过它自己的条目 schema |
24
+ | `entry-key-invalid` | Record 的**键**没过键 schema(键先判:键坏的条目不再算值) |
25
+ | `entry-unreadable` | 判的过程抛了(病态访问器 / Proxy 陷阱);文案可能带秘密,`error` 只留一句中性话,词本身是闭集字面 |
26
+
27
+ 投影(`projectWarning`)把 `reasonCode` 带进去(缺席不铸键),一致性门比它;historical 模式的归一器与 `identity` 一同两侧剥掉(旧版铸不出)。
28
+
29
+ - **谁需要** = server `GET /v1/models` 的丢弃条目自报(S-1295):wire 上只许闭集词,不许 zod 句子;词随告警过境,server 不手抄。之后 client-core / cli 读出。
30
+ - **谁受伤** = 无。两处都是可选键:10.0.0 形的 `DroppedEntry` / `EntrySalvageResult` / `entry-dropped` 告警字面量照常编译(`test/dropped-entry-reason-code-s1295.test.ts` 的编译格咬着,
31
+ server 测试里手写的 `entry-dropped` 字面量同属此形);`DroppedEntry` 在 server / web-admin / sdk / client-core / cli / core 的 `src` 与 `test` 里零引用(逐仓 grep 核过)。
32
+ 自己**重铸** `entry-dropped` 告警再交给一致性 kit 的读腿会在告警面报 `missing` / `extra`。
33
+ - **谁补偿** = 一致性语料已再加冕(`npm run conformance:bless`,16 条 case 的 40 处告警投影(跨腿计)多出 `reasonCode`,零删、零改其它字段,`baseline.json` 零变化),消费方跑 release 门前随包拿到新语料;
34
+ 消费方**自己入库**的语料(例:web-admin `wa05-*`)提货时要按新读数重新加冕。
35
+ - **边界(如实登记)**:① 本表只管 `DroppedEntry`;读路径上另一条按名剔条目的臂(`hosts-grandfathered`,主机名超长)是它自己的告警臂,不带词;② 词按**丢弃点**分,
36
+ 不按 schema 里「哪个字段为什么没过」细分(那是 `error` 句的事,句子不是契约);③ `entry-unreadable` 的病态访问器 JSON 语料表达不出,由
37
+ `test/dropped-entry-reason-code-s1295.test.ts` 看着(语料里 `entry-key-invalid` / `entry-schema-invalid` 由 `conformance/cases` 的金样与变异靶压着)。
38
+ - **红先**:`test/dropped-entry-reason-code-s1295.test.ts` 对 10.0.0 源 **16 红 / 0 绿**(`reasonCode` 字段不存在、`DROPPED_ENTRY_REASON_CODES` 未导出),改源后 16 / 16;
39
+ 逐点变异自证(8 处变异全杀)。codex 一审 [medium] 后补两格(旧形字面量编译格 + 本包铸的行恒带词):对「公开型必填」的中间稿 `tsc` 红(TS2741),改可选后绿。
40
+
41
+ ## 10.0.0 — 2026-10-09 — 私钥 PEM 头认串中任意位置(BREAKING:写入校验认串中任意位置的私钥 PEM 头;读侧同套 schema ⇒ 存量私钥串的条目被丢 / 域 throw)
42
+
43
+ > **判档 = major**(README「发版纪律」:校验收紧、让同一份配置读写出不同结果 ⇒ major)。破坏的是**持久数据面**,不是类型面:导出面登记表零 diff(没有新增 / 删除 / 改形的导出),
44
+ > 但读侧与写侧共用同一套 schema,存量配置里已经带着私钥串的部署,升级后读侧丢条目 / 整域 throw(下面「谁受伤」)。硬 breaking:无别名、无开关、无兼容读。
45
+
46
+ ### 一、病
47
+
48
+ 校验侧探测器 `containsSecretToken`(经 `scanForSecret`)读私钥 PEM 头只认**串首**:头里带空格,按令牌切开的那一遍永远隔不出它;头在串首时靠 `SECRET_LITERAL_RE` 的行首锚分支。
49
+ `"x -----BEGIN PRIVATE KEY-----"` / `"key: -----BEGIN RSA PRIVATE KEY-----"` / `"…\n-----BEGIN OPENSSH PRIVATE KEY-----"` 因此都答 `false` —— 凡走这只探测器的自由文本写口,都能把整把私钥写进配置、
50
+ 进域行 / 变更历史 / 发布快照 / `/effective`。展示侧 `redactSecrets` 读的是同一只 `PRIVATE_KEY_PEM_HEADER`,却早已在串中任意位置、任一归一化形上判阳 —— **写与显两只判官对「头在串中」这一格答案不一致**。
51
+
52
+ ### 二、改(一处)
53
+
54
+ `scanForSecret` 加一条:私钥 PEM 头(`PRIVATE_KEY_PEM_HEADER`,`-----BEGIN[A-Z0-9 ]*PRIVATE KEY`,大小写不敏感,既有全部变体 —— 裸 `PRIVATE KEY` / `RSA` / `EC` / `DSA` / `OPENSSH` / `ENCRYPTED` / `PGP …`)在串中**任意位置**命中即判阳。
55
+ `containsSecretToken` 对原文与前三层百分号解码都跑 `scanForSecret`,所以编码过的头(`x%20-----BEGIN%20PRIVATE%20KEY`,三层之内)同样命中。**同一只正则**:展示侧在读的就是它,包内没有第二份(`src/` 里非注释行出现私钥头字样的只有那一处定义,有测试钉着)。
56
+ 公钥 / 证书头照旧不命中;其余令牌形(`sk-` / `ghp_` / JWT / Bearer / AKIA / AIza …)一字不动。`SECRET_LITERAL_RE` 的源串逐字不变(它是导出的单令牌字面形,下游据它的源串生成表)。
57
+ `redactSecrets` 行为不变(同三形读数仍是整串 `[redacted-secret]`);它头注里「`containsSecretToken` 刻意不在这里放宽」那一句随之改真,`containsSecretToken` / `PRIVATE_KEY_PEM_HEADER` 头注里「只认串首」的说法同改。
58
+
59
+ ### 三、一处改全部受益的写口(逐个核过,都在测试里灌同一张表)
60
+
61
+ 经 `containsSecretToken` 的写口(源:`src/types.ts`、`src/remote-exec.ts`):
62
+ - `ModelEntry.extraBody` / `compat` / `headers` —— 经 `vetFreeFormJson`(`extraBodyUnsafe` 描述符走查读每个字符串值与每个键;序列化克隆再扫一遍);
63
+ - URL 面 `NON_SECRET_URL`(路径 / 查询 / 片段,`urlQueryFragmentHasSecret`):`ModelEntry.baseUrl`、mcp http `url`、worker `coords.gitApiBaseUrl`、会话镜像引擎 URL、`remoteExec` 的 `apiUrl` / `s3Snapshot.endpoint`、中心凭证 `issuer`;
64
+ - mcp stdio `command` / `args[]`;
65
+ - 插件源 `PLUGIN_ORIGIN`;
66
+ - `remoteExec` ssh 的 `host` / `username`;
67
+ - `WorkerSpec.env` 值;`runtime.execution.coords` 值(含远端 docker 守护进程的 `DOCKER_HOST`)与 `runtime.store.db` 的 `host` / `user` / `database`(车道 / SQL 坐标);
68
+ - 授权覆盖值 `EntitlementOverride.value`(串,或 JSON 序列化后的串)。
69
+
70
+ 不受影响:HTTP 头名(字符集本就不收空格,私钥头不可能写进去);`ENV_NAME` 字段(字符集与 AKIA 形判据,不走这只探测器)。
71
+
72
+ ### 四、三问
73
+
74
+ - **谁需要**:把凭据误写进自由文本键的部署 —— 例如把整把私钥贴进 `extraBody` 的某个值、mcp 参数、worker `env` 值,或带前缀(`key: …`)/ 换行后贴;这些以前都被放行并落进持久层。
75
+ - **谁受伤**:新写入不会有人受伤(只收紧)。**存量里已经带着这种串的配置**,读写都会变:下一次写回被拒;读侧同一只判官也拒 —— 可降级域(`models` / `mcp` / `plugins` 等)该条按坏条目逐条丢并报 `entry-dropped`(同域其余条目存活),
76
+ `workers` / `entitlement` 等 gate 域沿用既有 fail-closed(整域读拒,拒句点名字段、不带值)。这是读侧的真实后果,不只是「写回被拒」—— 也是本版判 major 的原因。
77
+ - **补偿**:拒句是固定句、不回显值,zod issue 的 `path` 指到字段,运维据此找到并删掉那个值;把私钥挪到 `secretRefs` / `secretEnv`(env-NAME)并**轮换这把密钥**(它已经进过配置)。展示侧本来就把这类串整串扣成 `[redacted-secret]`,所以日志 / 告警里没有泄露面;泄露面是持久层里那份原文。没有开关可以关掉这条拒 —— 刻意不留旋钮(私钥进配置不是可调的偏好)。
78
+ **升级前先清存量**(本包没有现成的清单命令 —— CLI 只有 `migrate-workers`,所以是人工步骤):① 在要升级的配置源(本地 `config.d/*.json`、中心的域行 / 变更历史导出)里不分大小写搜 `-----BEGIN` 后接 `PRIVATE KEY`
79
+ (正则 `-----BEGIN[A-Z0-9 ]*PRIVATE KEY`,另搜百分号编码形 `PRIVATE%20KEY`);`.env` 本来就是放秘密值的地方,里面的值不经这只探测器,不在此列。② 或写几行脚本:用 `@sema-agent/settings-schema/types` 的 `containsSecretToken`
80
+ 对每份 JSON 里的每个字符串值与键各跑一遍,命中的就是存量私钥串;要把命中打印出来,过 `redactSecrets`(它把命中的串读成 `[redacted-secret]`,清单里不会再抄一份私钥)。③ 命中的值挪进 `secretEnv` / `secretRefs` 所指的 env-NAME,轮换这把密钥,再升级。
81
+
82
+ ### 用户可见变化(谁受影响 / 怎么改 / 不改会怎样)
83
+
84
+ 1. **自由文本写口的提交方(管理面 / CLI / 手写 `config.d`)**:带私钥头的串 —— 不论头在串首、前缀文字之后、`key: ` 之后还是换行之后 —— 现在都被拒;公钥 / 证书照旧过。不改会怎样:提交被拒,按拒句修。
85
+ 2. **持有存量这类串的部署**:见「谁受伤」与「补偿」。升级前先按「补偿」里的人工步骤清单,把私钥挪进 `secretEnv` 并轮换;不清就升级,读侧会丢条目 / 整域 throw。
86
+ 3. **读侧消费方(server 读 `/effective`、`loadLocalConfig`)**:对干净配置零变化。
87
+ 4. **共用 `containsSecretToken` 做展示 / 门判的其它仓**:它们对带私钥头的串现在也判阳(更严,方向一致);未在这里逐仓核对其断言 —— 提货时跑自己的套件。
88
+ 5. **持有导出面快照的测试 / 脚本**:无变化(导出面零 diff)。
89
+
90
+ ### 红先 / 门
91
+
92
+ - 先写钉、对着改前的树跑红,再实现:`test/private-key-anywhere-10.0.0.test.ts` —— 探测器本身(三形 × 全部既有头变体 × 百分号层 × 负控)、二十个写口位置各灌同一张表(中间形拒 / 串首形照旧拒 / 公钥与证书照旧放 / 不含 PEM 的长串照旧放)、
93
+ 自由文本写口的拒句不回显值、展示侧同三形读数不变、读侧后果(可降级域逐条丢、gate 域整域读拒)、单源钉、2 MB 对抗串线性钉、一格 🚧 缺口特征钉。写钉时对改前的树 96 格里 33 红;终稿 103 格(后补写口与缺口钉),把 `scanForSecret` 那一行拿掉复跑 37 红 66 绿,实装后 103 绿。
94
+ - 既有钉随之改真(都是借「探测器只认串首」这个缺口触发的格,按缺口登记规矩翻;实装后这两格先红,翻后绿):`test/disclosable-identity-6.3.0.test.ts` 的 R1 前提格(`containsSecretToken(encoded)` 由判阴翻判阳)与 R3 附的冻结 oracle
95
+ (由「与 6.2.0 逐格相同」改为「6.2.0 ∪ 私钥头任意位置」,头的正则在 oracle 里冻一份字面,不读 src 导出)。
96
+ - 一致性语料的变异律随之红了两个靶,按「活着的变异 = 语料缺口」补语料而不是删靶:展示侧脱敏器的「归一化没走完 ⇒ 整串扣」与「私钥头判在原文的归一化形上」这两面墙,此前分别靠
97
+ c43 的五层编码私钥头与 c41 / c43 的编码私钥头压着 —— 探测器认任意位置之后,那几条被探测器那一遍先扣下,两面墙失去杀手。新增 `c69-deep-encoded-token-past-every-reading`(九层编码的 `sk-` 形令牌)与
98
+ `c70-rewritten-token-in-front-of-encoded-pem-header`(`sk-note:` 紧贴编码私钥头、私钥体在换行之后),加冕脚本只加冕这两条;既有金样(`expected.json`)一字未动,只有 c43 的 `meta.json` 里一句已不成立的现在时说明(「五层那条只有『没走完』这一位扣得住」)订正。语料版本戳随包版本。
99
+ - 导出面登记表(`docs/API-SURFACE.md`)重生成:零 diff。
100
+
101
+ ### 未闭环(如实)
102
+
103
+ - **写与显对私钥头的读法宽度仍不一致 —— 这是缺陷,不是立场**:`containsSecretToken`(写侧)只读原文 + 前三层宽容百分号解码;**四层及更深**的编码、`+` 代空格(`-----BEGIN+PRIVATE+KEY-----`,form-urlencoded 读法)的头,
104
+ 写侧今天判阴(放行),展示侧整串扣。本版只把「头位置」这一维收到任意位置,没动读法宽度;同一个问题两只读法(`percentLayers` 与 `secretScanForms`)作答,收口方向是并成一只读法(写侧读展示侧那一组形),
105
+ 那是另一件设计,不在这次改里临时加探测臂。`test/private-key-anywhere-10.0.0.test.ts` 有一格 🚧 特征钉登记它,翻转条件写在钉里。这条缺口对其它令牌形(`sk-` 等)同样存在、早已如此,并非本版引入。
106
+ - **`SECRET_LITERAL_RE` 仍带串首锚的私钥头一支**:它是导出面,下游(例如按它的源串生成匹配表的展示层)据此判串首形;`scanForSecret` 里新加的任意位置判先于它,这一支在探测器内部已是冗余,但改它是导出正则的变更。
107
+ 直接用 `SECRET_LITERAL_RE` 整串判定的消费方,串中的私钥头仍看不见 —— 它们应改用 `containsSecretToken`。
108
+ - **存量数据没有迁移器**:升级不动持久层里已有的串;处置是运维删值 + 轮换密钥(见补偿)。本版不提供自动清洗(自动改写配置里的值不是契约层的事)。
109
+
9
110
  ## 9.0.0 — 2026-10-09 — 能力读数三态读器 · 世代账两键由串改数(BREAKING)
10
111
 
11
112
  > **判档 = major**(README「发版纪律」:导出删除 / 改名、类型 retype ⇒ major)。**硬 breaking:删改直接做,零别名,消费方 tsc 红即通知。** 两件:
package/README.md CHANGED
@@ -48,6 +48,13 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
48
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
49
  > of its own (S-777); 6.2.0 does not touch the dependency or peer range.
50
50
 
51
+ ## 升级指引:9.0.0 → 10.0.0(BREAKING:写入校验认串中任意位置的私钥 PEM 头;读侧同套 schema ⇒ 存量私钥串的条目被丢 / 域 throw;导出面零变化)
52
+
53
+ 自由文本写口(`extraBody` / `compat` / `headers`、URL、mcp stdio 参数、插件源、ssh 登录、worker env、车道坐标、覆盖值)此前只在私钥 PEM 头位于**串首**时拒;`"x -----BEGIN PRIVATE KEY-----"` / `"key: -----BEGIN RSA PRIVATE KEY-----"` / 换行之后的头都放行,而展示侧 `redactSecrets` 早就认串中任意位置。10.0.0 起 `containsSecretToken` 在原文与前三层百分号解码上都认串中任意位置的头(与展示侧读同一只 `PRIVATE_KEY_PEM_HEADER`),头的**位置**写与显同判;公钥 / 证书头照旧不拒,其余令牌形不变。⚠️ 读法的**宽度**写侧仍窄于展示侧:四层及更深的百分号编码、`+` 代空格的头,写侧今天仍放行(展示侧整串扣),见 CHANGELOG 10.0.0「未闭环」。**代码动作:无**(导出面零变化)。**存量配置里若已有这种串 —— 这就是本版判 major 的原因**:下一次写回被拒(拒句固定、不回显值,`path` 指到字段);读侧同一只判官也拒 ——
54
+ `models` / `mcp` / `plugins` 等可降级域该条按坏条目逐条丢并报 `entry-dropped`(同域其余条目存活),`workers` / `entitlement` 等 gate 域沿用既有的 fail-closed(整域读拒)。
55
+ **升级前先清存量**(本包没有现成的清单命令,人工步骤):不分大小写搜配置源里的 `-----BEGIN[A-Z0-9 ]*PRIVATE KEY`(另搜 `PRIVATE%20KEY`),或用 `@sema-agent/settings-schema/types` 的 `containsSecretToken` 对每份 JSON 的字符串值与键各跑一遍(打印命中时过 `redactSecrets`,清单里不再抄一份私钥);
56
+ 命中的值挪进 `secretEnv` / `secretRefs`(env-NAME)并轮换这把密钥(它已经进过配置)。逐条见 CHANGELOG 10.0.0。
57
+
51
58
  ## 升级指引:8.3.1 → 9.0.0(BREAKING:两件,硬 breaking —— 删改直接做,零别名)
52
59
 
53
60
  **① 能力读数改三态读器**:`readCenterCapabilities(body)` 删,换 `readCapabilityReading(reply): CapabilityReading`(类型 `CapabilityReading` 同入)。输入改成心跳 **2xx 的整只答复体**(键查找进读器),
@@ -300,7 +307,16 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
300
307
  dropped without re-reading the source document by coordinate (the document it holds may not be the one this
301
308
  read saw). `identity` is part of the warning projection, so the conformance gate compares it (corpus
302
309
  cases `c40` / `c41` pin the laws JSON can express; the historical normalizer strips it, since older versions cannot
303
- mint it — `ConformanceNormalizer.warning?`). **3.0.0** re-casts every coordinate these warnings
310
+ mint it — `ConformanceNormalizer.warning?`). **S-1295** (additive) adds `reasonCode` — WHY the entry was
311
+ dropped, as a machine-readable CLOSED word (`DROPPED_ENTRY_REASON_CODES` / `DroppedEntryReasonCode`; also on
312
+ `DroppedEntry` from `salvageEntries`): `entry-schema-invalid` (the entry — array element or Record value — failed its
313
+ own entry schema), `entry-key-invalid` (a Record key failed the key schema), `entry-unreadable` (validation threw while
314
+ reading it — a hostile accessor / Proxy trap; its message is never surfaced). One word per drop point, no `other`
315
+ fallback; the `error` sentence is unchanged and stays free text, so a consumer that must put a reason on a wire
316
+ reads the word, not the sentence. Every `entry-dropped` this package mints carries it (the key is optional on the
317
+ warning type only so consumers that hand-build that union keep compiling); like `identity` it is part of the warning
318
+ projection, compared by the conformance gate and stripped by the historical normalizer. The set covers `DroppedEntry`
319
+ only — the host-name clamp (`hosts-grandfathered`) is its own warning arm and carries no word. **3.0.0** re-casts every coordinate these warnings
304
320
  carry (`entry-dropped.path`, `unknown-keys-*.keys`) from a dot-joined STRING to a structured
305
321
  `ConfigPath = readonly (string | number)[]` — a string segment is an object key (verbatim, `.` included),
306
322
  a number segment is an array index — because the string form was a LOSSY encoding: a key literally named
@@ -1 +1 @@
1
- 9.0.0
1
+ 10.0.0
@@ -79,7 +79,8 @@
79
79
  "path": [
80
80
  "models",
81
81
  0
82
- ]
82
+ ],
83
+ "reasonCode": "entry-schema-invalid"
83
84
  },
84
85
  {
85
86
  "domain": "models",
@@ -81,7 +81,8 @@
81
81
  "path": [
82
82
  "hosts",
83
83
  2
84
- ]
84
+ ],
85
+ "reasonCode": "entry-schema-invalid"
85
86
  }
86
87
  ],
87
88
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "[redacted-secret]"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  },
85
86
  {
86
87
  "domain": "models",
@@ -88,7 +89,8 @@
88
89
  "path": [
89
90
  "roles",
90
91
  "[redacted-secret]"
91
- ]
92
+ ],
93
+ "reasonCode": "entry-key-invalid"
92
94
  }
93
95
  ],
94
96
  "metadata": {
@@ -91,7 +91,8 @@
91
91
  "path": [
92
92
  "models",
93
93
  1
94
- ]
94
+ ],
95
+ "reasonCode": "entry-schema-invalid"
95
96
  }
96
97
  ],
97
98
  "metadata": {
@@ -83,7 +83,8 @@
83
83
  "path": [
84
84
  "models",
85
85
  1
86
- ]
86
+ ],
87
+ "reasonCode": "entry-schema-invalid"
87
88
  },
88
89
  {
89
90
  "domain": "models",
@@ -96,7 +97,8 @@
96
97
  "path": [
97
98
  "models",
98
99
  2
99
- ]
100
+ ],
101
+ "reasonCode": "entry-schema-invalid"
100
102
  },
101
103
  {
102
104
  "domain": "models",
@@ -107,7 +109,8 @@
107
109
  "path": [
108
110
  "models",
109
111
  3
110
- ]
112
+ ],
113
+ "reasonCode": "entry-schema-invalid"
111
114
  },
112
115
  {
113
116
  "domain": "models",
@@ -118,7 +121,8 @@
118
121
  "path": [
119
122
  "models",
120
123
  4
121
- ]
124
+ ],
125
+ "reasonCode": "entry-schema-invalid"
122
126
  },
123
127
  {
124
128
  "domain": "models",
@@ -129,7 +133,8 @@
129
133
  "path": [
130
134
  "models",
131
135
  5
132
- ]
136
+ ],
137
+ "reasonCode": "entry-schema-invalid"
133
138
  },
134
139
  {
135
140
  "domain": "models",
@@ -140,7 +145,8 @@
140
145
  "path": [
141
146
  "models",
142
147
  6
143
- ]
148
+ ],
149
+ "reasonCode": "entry-schema-invalid"
144
150
  },
145
151
  {
146
152
  "domain": "models",
@@ -148,7 +154,8 @@
148
154
  "path": [
149
155
  "roles",
150
156
  "[redacted-secret]"
151
- ]
157
+ ],
158
+ "reasonCode": "entry-key-invalid"
152
159
  }
153
160
  ],
154
161
  "metadata": {
@@ -83,7 +83,8 @@
83
83
  "path": [
84
84
  "models",
85
85
  1
86
- ]
86
+ ],
87
+ "reasonCode": "entry-schema-invalid"
87
88
  },
88
89
  {
89
90
  "domain": "models",
@@ -94,7 +95,8 @@
94
95
  "path": [
95
96
  "models",
96
97
  2
97
- ]
98
+ ],
99
+ "reasonCode": "entry-schema-invalid"
98
100
  },
99
101
  {
100
102
  "domain": "models",
@@ -105,7 +107,8 @@
105
107
  "path": [
106
108
  "models",
107
109
  3
108
- ]
110
+ ],
111
+ "reasonCode": "entry-schema-invalid"
109
112
  },
110
113
  {
111
114
  "domain": "models",
@@ -117,7 +120,8 @@
117
120
  "path": [
118
121
  "models",
119
122
  4
120
- ]
123
+ ],
124
+ "reasonCode": "entry-schema-invalid"
121
125
  },
122
126
  {
123
127
  "domain": "models",
@@ -129,7 +133,8 @@
129
133
  "path": [
130
134
  "models",
131
135
  5
132
- ]
136
+ ],
137
+ "reasonCode": "entry-schema-invalid"
133
138
  },
134
139
  {
135
140
  "domain": "models",
@@ -140,7 +145,8 @@
140
145
  "path": [
141
146
  "models",
142
147
  6
143
- ]
148
+ ],
149
+ "reasonCode": "entry-schema-invalid"
144
150
  },
145
151
  {
146
152
  "domain": "models",
@@ -148,7 +154,8 @@
148
154
  "path": [
149
155
  "roles",
150
156
  "[redacted-secret]"
151
- ]
157
+ ],
158
+ "reasonCode": "entry-key-invalid"
152
159
  }
153
160
  ],
154
161
  "metadata": {
@@ -83,7 +83,8 @@
83
83
  "path": [
84
84
  "models",
85
85
  1
86
- ]
86
+ ],
87
+ "reasonCode": "entry-schema-invalid"
87
88
  },
88
89
  {
89
90
  "domain": "models",
@@ -95,7 +96,8 @@
95
96
  "path": [
96
97
  "models",
97
98
  2
98
- ]
99
+ ],
100
+ "reasonCode": "entry-schema-invalid"
99
101
  },
100
102
  {
101
103
  "domain": "models",
@@ -103,7 +105,8 @@
103
105
  "path": [
104
106
  "roles",
105
107
  "[redacted-secret]"
106
- ]
108
+ ],
109
+ "reasonCode": "entry-key-invalid"
107
110
  },
108
111
  {
109
112
  "domain": "models",
@@ -111,7 +114,8 @@
111
114
  "path": [
112
115
  "roles",
113
116
  "[redacted-secret]"
114
- ]
117
+ ],
118
+ "reasonCode": "entry-key-invalid"
115
119
  },
116
120
  {
117
121
  "domain": "models",
@@ -119,7 +123,8 @@
119
123
  "path": [
120
124
  "roles",
121
125
  "[redacted-secret]"
122
- ]
126
+ ],
127
+ "reasonCode": "entry-key-invalid"
123
128
  }
124
129
  ],
125
130
  "metadata": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contributor": "contract",
3
- "claim": "6.3.0(codex gpt-6-astra r1 F1 部分修,S-792):归一化那一处的两件事压在语料上。① `+` 代空格:扁平无换行的 `-----BEGIN+PRIVATE+KEY-----<base64>-----END+PRIVATE+KEY-----` 全在身份白名单字符集内,6.2.0 起它过身份与 refusal 两谓词 ⇒ 被丢条目的 id 必须不带(`secretScanForms` 把 `+` 读作空格的形交 PEM 头判,身份否决随之);URLSearchParams 形(`+` 代空格 + `%0A`)作 roles 坏键 ⇒ 坐标段整段扣、error 不带私钥。② 解码界上 fail-closed:四层 / 五层百分号编码作 roles 坏键 ⇒ 三层走完仍能再解,整串扣(codex r2 起展示侧的边界读法对末层再解一层,四层那条因此也被 PEM 头判扣下;五层那条只有「没走完」这一位扣得住 —— 变异门的 `unresolved` 靶靠它)。正控:`o3-mini+high` 作 id 逐字带出(`+` 在真实 id 里照旧可披露)。",
3
+ "claim": "6.3.0(codex gpt-6-astra r1 F1 部分修,S-792):归一化那一处的两件事压在语料上。① `+` 代空格:扁平无换行的 `-----BEGIN+PRIVATE+KEY-----<base64>-----END+PRIVATE+KEY-----` 全在身份白名单字符集内,6.2.0 起它过身份与 refusal 两谓词 ⇒ 被丢条目的 id 必须不带(`secretScanForms` 把 `+` 读作空格的形交 PEM 头判,身份否决随之);URLSearchParams 形(`+` 代空格 + `%0A`)作 roles 坏键 ⇒ 坐标段整段扣、error 不带私钥。② 解码界上 fail-closed:四层 / 五层百分号编码作 roles 坏键 ⇒ 三层走完仍能再解,整串扣(codex r2 起展示侧的边界读法对末层再解一层,四层那条因此也被 PEM 头判扣下;五层那条只有「没走完」这一位扣得住 —— 变异门的 `unresolved` 靶靠它)。正控:`o3-mini+high` 作 id 逐字带出(`+` 在真实 id 里照旧可披露)。 【10.0.0 订正】探测器改认串中任意位置的私钥头之后,它对每个归一化形再读三层,五层那条也被探测器那一遍扣下,「没走完」这一位不再独自承重于本条;`unresolved` 靶的语料杀手改为 c69。",
4
4
  "firstRedOn": [],
5
5
  "secrets": [
6
6
  "MC4CAQAwBQYDK2VwBCIEIFAKEC43FLATPLUSIDBODY0000000000000000000000",
@@ -8,5 +8,5 @@
8
8
  "MC4CAQAwBQYDK2VwBCIEIFAKEC43DEEPENCODEDROLEKEYBODY00000000000000",
9
9
  "MC4CAQAwBQYDK2VwBCIEIFAKEC43FIVELAYERROLEKEYBODY0000000000000000"
10
10
  ],
11
- "note": "车PE(2026-09-27)红先:加本条时 6.3.0 修前(A 已修、B 未修)读腿在三腿上报告警差 + 秘密扫描命中。变异门为 `secretScanForms` 的 `+` 读法与 `redactSecrets` 的 `unresolved` 一位各登记一靶,本条杀。firstRedOn 为空:historical 阶梯证据真跑于本条入库之前,那一栏只收 historical 真跑。 codex r2 收敛(车PE 同日):边界读法上线后四层那条不再单靠 `unresolved`,追加五层一条(`FAKEC43FIVELAYER…`)使该靶仍有语料杀。"
11
+ "note": "车PE(2026-09-27)红先:加本条时 6.3.0 修前(A 已修、B 未修)读腿在三腿上报告警差 + 秘密扫描命中。变异门为 `secretScanForms` 的 `+` 读法与 `redactSecrets` 的 `unresolved` 一位各登记一靶,本条杀。firstRedOn 为空:historical 阶梯证据真跑于本条入库之前,那一栏只收 historical 真跑。 codex r2 收敛(车PE 同日):边界读法上线后四层那条不再单靠 `unresolved`,追加五层一条(`FAKEC43FIVELAYER…`)使该靶仍有语料杀。 【10.0.0】`unresolved` 靶改由 c69 杀,本条对该靶不再红。"
12
12
  }
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "[redacted-secret]"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  },
85
86
  {
86
87
  "domain": "models",
@@ -88,7 +89,8 @@
88
89
  "path": [
89
90
  "roles",
90
91
  "path%2Fto%E4%B8%AD"
91
- ]
92
+ ],
93
+ "reasonCode": "entry-key-invalid"
92
94
  },
93
95
  {
94
96
  "domain": "models",
@@ -110,7 +110,8 @@
110
110
  "path": [
111
111
  "models",
112
112
  1
113
- ]
113
+ ],
114
+ "reasonCode": "entry-schema-invalid"
114
115
  },
115
116
  {
116
117
  "domain": "models",
@@ -121,7 +122,8 @@
121
122
  "path": [
122
123
  "models",
123
124
  2
124
- ]
125
+ ],
126
+ "reasonCode": "entry-schema-invalid"
125
127
  },
126
128
  {
127
129
  "domain": "models",
@@ -132,7 +134,8 @@
132
134
  "path": [
133
135
  "models",
134
136
  3
135
- ]
137
+ ],
138
+ "reasonCode": "entry-schema-invalid"
136
139
  },
137
140
  {
138
141
  "domain": "models",
@@ -144,7 +147,8 @@
144
147
  "path": [
145
148
  "models",
146
149
  4
147
- ]
150
+ ],
151
+ "reasonCode": "entry-schema-invalid"
148
152
  },
149
153
  {
150
154
  "domain": "models",
@@ -167,7 +171,8 @@
167
171
  "path": [
168
172
  "servers",
169
173
  1
170
- ]
174
+ ],
175
+ "reasonCode": "entry-schema-invalid"
171
176
  }
172
177
  ],
173
178
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "https://admin:hunter2@db.example.com/x"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  }
85
86
  ],
86
87
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "models",
82
82
  0
83
- ]
83
+ ],
84
+ "reasonCode": "entry-schema-invalid"
84
85
  }
85
86
  ],
86
87
  "metadata": {
@@ -0,0 +1,104 @@
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
+ "kind": "entry-dropped",
80
+ "path": [
81
+ "roles",
82
+ "[redacted-secret]"
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
85
+ }
86
+ ],
87
+ "metadata": {
88
+ "version": 15,
89
+ "updatedAt": "2026-10-09T00:00:00.000Z"
90
+ }
91
+ },
92
+ "strictWrite": {
93
+ "outcome": "rejected",
94
+ "stage": "validate",
95
+ "why": "拒:roles 里的坏键读腿丢(entry-dropped,坐标整段扣);严写 ⇒ 整次不受理。"
96
+ },
97
+ "wire": {
98
+ "derive": true
99
+ },
100
+ "localRoot": {
101
+ "derive": true,
102
+ "localVersion": 2824083313848700
103
+ }
104
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "meta": {
3
+ "version": 15,
4
+ "updatedAt": "2026-10-09T00: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
+ "roles": {
17
+ "summarize": {
18
+ "model": "keep"
19
+ },
20
+ "key=%252525252525252573k-live-FAKEC69NINELAYERROLEKEYTOKEN0000000000": {
21
+ "model": "keep"
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "10.0.0:展示侧脱敏器的「归一化没走完 ⇒ 整串扣」这一位压在语料上。`sk-` 形令牌的首字母 `s` 被九重百分号编码(`%2525…2573`)作 roles 坏键:探测器的三层解码、边界读法再解一层、探测器对每个形再读三层都够不着它 ⇒ 令牌不被任何定点遍认出,只有「还能再解」这一位把整段坐标扣成 `[redacted-secret]`。此前这一位靠 c43 的五层私钥头压着;10.0.0 起探测器认串中任意位置的私钥头,五层那条也被探测器那一遍扣下,这一位随之没了语料杀手 —— 本条补上。",
4
+ "firstRedOn": [],
5
+ "secrets": [
6
+ "k-live-FAKEC69NINELAYERROLEKEYTOKEN0000000000"
7
+ ],
8
+ "note": "变异门靶:`redactSecrets` 去 `scan.unresolved || ` —— 本条杀。不是红先复现:这一位的行为 9.0.0 与 10.0.0 相同(坐标整段扣),本条只是让它在语料里再有一个承重的杀手。firstRedOn 为空:不是 historical 阶梯证据。"
9
+ }
@@ -0,0 +1,104 @@
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
+ "kind": "entry-dropped",
80
+ "path": [
81
+ "roles",
82
+ "[redacted-secret]"
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
85
+ }
86
+ ],
87
+ "metadata": {
88
+ "version": 15,
89
+ "updatedAt": "2026-10-09T00:00:00.000Z"
90
+ }
91
+ },
92
+ "strictWrite": {
93
+ "outcome": "rejected",
94
+ "stage": "validate",
95
+ "why": "拒:roles 里的坏键读腿丢(entry-dropped,坐标整段扣);严写 ⇒ 整次不受理。"
96
+ },
97
+ "wire": {
98
+ "derive": true
99
+ },
100
+ "localRoot": {
101
+ "derive": true,
102
+ "localVersion": 1452540247706034
103
+ }
104
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "meta": {
3
+ "version": 15,
4
+ "updatedAt": "2026-10-09T00: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
+ "roles": {
17
+ "summarize": {
18
+ "model": "keep"
19
+ },
20
+ "sk-note:-----BEGIN%20PRIVATE%20KEY-----\nMC4CAQAwBQYDK2VwBCIEIFAKEC70REWRITTENHEADERBODY00000000000000\n-----END%20PRIVATE%20KEY-----": {
21
+ "model": "keep"
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "10.0.0:展示侧脱敏器「私钥头判在令牌替换之前、且判在归一化形上」这两件同时压在语料上。roles 坏键 = `sk-note:` 紧贴一个 `%20` 编码的私钥头,私钥体在真实换行之后(令牌字符集之外):定点遍先把 `sk-note:-----BEGIN%20PRIVATE%20KEY-----` 整个令牌换成 `[redacted-secret]`,头就没了,换行后的体与 END 行原样留着;只有对**原文的归一化形**(`%20` 解成空格)判私钥头,才会整串扣。此前这一位靠 c41 / c43 的编码私钥头压着,10.0.0 起探测器认串中任意位置的私钥头,那两条被探测器那一遍扣下,这一位随之没了语料杀手 —— 本条补上。",
4
+ "firstRedOn": [],
5
+ "secrets": [
6
+ "MC4CAQAwBQYDK2VwBCIEIFAKEC70REWRITTENHEADERBODY00000000000000"
7
+ ],
8
+ "note": "变异门靶:`redactSecrets` 把原文形上的私钥头判退回只测原文(`PRIVATE_KEY_PEM_HEADER.test(text)`)—— 本条杀。不是红先复现:坐标整段扣这一行为 9.0.0 与 10.0.0 相同。firstRedOn 为空:不是 historical 阶梯证据。"
9
+ }
@@ -76,7 +76,8 @@
76
76
  "path": [
77
77
  "models",
78
78
  0
79
- ]
79
+ ],
80
+ "reasonCode": "entry-schema-invalid"
80
81
  },
81
82
  {
82
83
  "domain": "models",
@@ -84,7 +85,8 @@
84
85
  "path": [
85
86
  "models",
86
87
  1
87
- ]
88
+ ],
89
+ "reasonCode": "entry-schema-invalid"
88
90
  }
89
91
  ],
90
92
  "metadata": {
@@ -89,7 +89,8 @@
89
89
  "path": [
90
90
  "models",
91
91
  0
92
- ]
92
+ ],
93
+ "reasonCode": "entry-schema-invalid"
93
94
  },
94
95
  {
95
96
  "domain": "models",
@@ -97,7 +98,8 @@
97
98
  "path": [
98
99
  "models",
99
100
  1
100
- ]
101
+ ],
102
+ "reasonCode": "entry-schema-invalid"
101
103
  }
102
104
  ],
103
105
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "models",
82
82
  1
83
- ]
83
+ ],
84
+ "reasonCode": "entry-schema-invalid"
84
85
  },
85
86
  {
86
87
  "domain": "models",
@@ -34,11 +34,14 @@ import { type DomainConfig, type DomainName, type EffectiveConfig, type Effectiv
34
34
  * The PEM header is judged BEFORE any replacement: the token pass can rewrite the header itself away
35
35
  * (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes `[redacted-secret]` and the
36
36
  * key body after it would ride out untouched — codex verification round). A PEM header carries spaces, so the
37
- * targeted passes can never isolate it, and the detector only sees it at the very start of a string. Every
38
- * free-text warning face that goes through here (coordinates, error text) inherits the same guarantee; the entry
39
- * identity face additionally has to be an identifier ({@link isDisclosableIdentity}). The validation-side detector
40
- * `containsSecretToken` is deliberately NOT widened here: making it reject a mid-string PEM would tighten every field
41
- * it validates (a major). */
37
+ * targeted passes can never isolate it. Every free-text warning face that goes through here (coordinates, error text)
38
+ * inherits the same guarantee; the entry identity face additionally has to be an identifier ({@link isDisclosableIdentity}).
39
+ * Write and display read the private-key header at the same PLACE and with the same regex since 10.0.0: the validation-side
40
+ * detector `containsSecretToken` reads {@link PRIVATE_KEY_PEM_HEADER} anywhere in a string too, on the raw text and its first three
41
+ * percent layers. They do NOT yet read the same FORMS: this redactor reads more (deeper layers, the `+` and boundary readings of
42
+ * `secretScanForms`), so a header spelled only in one of those forms is withheld on display but still passes write validation —
43
+ * an open gap (a pin in the 10.0.0 test file registers it), not a contract. The redactor also keeps its own PEM check on the ORIGINAL
44
+ * text's forms because the token pass above can rewrite the header away before `containsSecretToken` ever sees the result. */
42
45
  export declare function redactSecrets(text: string): string;
43
46
  /** Sanitize a ZodError IN PLACE so no surfaced error (message / `received` / nested `unionErrors`) echoes a
44
47
  * pasted secret. RECURSES into `unionErrors` (a `z.union` failure nests a ZodError per member — e.g. RoleTarget
@@ -189,12 +192,31 @@ export interface DroppedEntryIdentity {
189
192
  id?: string;
190
193
  enabled?: boolean;
191
194
  }
195
+ /**
196
+ * 一条条目为什么被丢 —— **机读原因**的闭集(S-1295)。词与**丢弃点**一一对应:`DroppedEntry` 只有一个铸点
197
+ * (`droppedEntry`),它的上游判据(`judgeEntry`)按「谁判、判成什么样」分三种丢法,每种一个词,没有 other 兜底:
198
+ * - `entry-schema-invalid` —— 条目(数组元素 / Record 的值)没过它自己的条目 schema;
199
+ * - `entry-key-invalid` —— Record 的**键**没过键 schema(键先判:键坏的条目不再算值);
200
+ * - `entry-unreadable` —— 判的过程抛了(病态访问器 / Proxy 陷阱;文案可能带秘密,所以只留一句中性话,见 {@link judgeEntry})。
201
+ *
202
+ * 为什么是词而不是 `error` 句:句子随 zod 小版本改字、可经脱敏,拿它当判据就是拿实现当契约;wire 上也不许有自由文本。
203
+ * 这张表由 `test/dropped-entry-reason-code-s1295.test.ts` 的两格咬住:穷举 `switch`(给表加词而不补臂 ⇒ `tsc` 红,门 = `npm run typecheck`)与
204
+ * 满射格(每个词至少有一个真丢弃点触发它 = 没有死词;每个被丢条目的词都在表里 = 没有无词的丢弃点)。
205
+ * 边界(如实登记):本表只管 `salvageEntries` 铸的 {@link DroppedEntry};读路径上另一条按名剔条目的臂
206
+ * (`hosts-grandfathered`,主机名超长)走它自己的告警臂,不是 `DroppedEntry`,不在本表。
207
+ */
208
+ export declare const DROPPED_ENTRY_REASON_CODES: readonly ["entry-schema-invalid", "entry-key-invalid", "entry-unreadable"];
209
+ export type DroppedEntryReasonCode = (typeof DROPPED_ENTRY_REASON_CODES)[number];
192
210
  /** 一条被丢掉的条目:`path` = 段坐标({@link ConfigPath};`["models", 3]` / `["roles", "summarize"]`),
193
- * `error` 已脱敏,`identity`(S-712)见 {@link DroppedEntryIdentity}(读不出 ⇒ 键缺席)。 */
211
+ * `error` 已脱敏,`identity`(S-712)见 {@link DroppedEntryIdentity}(读不出 ⇒ 键缺席),
212
+ * `reasonCode`(S-1295)= 为什么丢的**机读闭集词**,见 {@link DROPPED_ENTRY_REASON_CODES}。本包铸的每一行都带(铸点是唯一的,返回型是必填窄型
213
+ * {@link MintedDroppedEntry},漏带词 = 编译红);公开型上是可选键,只因为这个接口是公开导出的,手工构造它的消费方(夹具、适配器)不该被一个
214
+ * additive 的 minor 编译红 —— 发版纪律:新键是可选键。 */
194
215
  export interface DroppedEntry {
195
216
  path: ConfigPath;
196
217
  error: unknown;
197
218
  identity?: DroppedEntryIdentity;
219
+ reasonCode?: DroppedEntryReasonCode;
198
220
  }
199
221
  /** 可披露身份的**闭形**(6.3.0):字符集 `A-Z a-z 0-9 . _ : / @ + -`、长度 1–256(上限与 web-admin 读侧名长上限 256 对齐;`ModelEntry.name`
200
222
  * 本身不设上限,超 256 字的合法名按无名处置)。身份是**标识符**不是自由文本 ——
@@ -356,13 +378,17 @@ export type EffectiveReadWarning =
356
378
  * O(域里的条目数),与被读文档同阶,不放大,故不设条数上限(有上限的是**单条**告警里的键列表)。
357
379
  * 2.0.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。
358
380
  * **S-712**(additive):`identity` = 被丢条目原文里读得出的身份({@link DroppedEntryIdentity};
359
- * 读不出 ⇒ 键缺席),消费方据此知道「丢的是谁」而不必拿坐标回读原文档。 */
381
+ * 读不出 ⇒ 键缺席),消费方据此知道「丢的是谁」而不必拿坐标回读原文档。
382
+ * **S-1295**(additive):`reasonCode` = 为什么丢的机读闭集词({@link DROPPED_ENTRY_REASON_CODES}),消费方据此知道「为什么丢」
383
+ * 而不必解析 `error` 句。本包铸的每一条都带;类型上是可选键只因为这个联合有手工构造它的消费方(测试夹具、别家实现的适配器)——
384
+ * 新键按发版纪律是可选键,不因此让他们编译红。 */
360
385
  | {
361
386
  domain: DomainName;
362
387
  kind: "entry-dropped";
363
388
  path: ConfigPath;
364
389
  error: unknown;
365
390
  identity?: DroppedEntryIdentity;
391
+ reasonCode?: DroppedEntryReasonCode;
366
392
  }
367
393
  /** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
368
394
  | {
@@ -38,11 +38,14 @@ import { DOMAIN_SCHEMAS, GOVERNANCE_MIRROR_KEYS, HOSTS_ARRAY_MAX, HOST_NAME_MAX,
38
38
  * The PEM header is judged BEFORE any replacement: the token pass can rewrite the header itself away
39
39
  * (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes `[redacted-secret]` and the
40
40
  * key body after it would ride out untouched — codex verification round). A PEM header carries spaces, so the
41
- * targeted passes can never isolate it, and the detector only sees it at the very start of a string. Every
42
- * free-text warning face that goes through here (coordinates, error text) inherits the same guarantee; the entry
43
- * identity face additionally has to be an identifier ({@link isDisclosableIdentity}). The validation-side detector
44
- * `containsSecretToken` is deliberately NOT widened here: making it reject a mid-string PEM would tighten every field
45
- * it validates (a major). */
41
+ * targeted passes can never isolate it. Every free-text warning face that goes through here (coordinates, error text)
42
+ * inherits the same guarantee; the entry identity face additionally has to be an identifier ({@link isDisclosableIdentity}).
43
+ * Write and display read the private-key header at the same PLACE and with the same regex since 10.0.0: the validation-side
44
+ * detector `containsSecretToken` reads {@link PRIVATE_KEY_PEM_HEADER} anywhere in a string too, on the raw text and its first three
45
+ * percent layers. They do NOT yet read the same FORMS: this redactor reads more (deeper layers, the `+` and boundary readings of
46
+ * `secretScanForms`), so a header spelled only in one of those forms is withheld on display but still passes write validation —
47
+ * an open gap (a pin in the 10.0.0 test file registers it), not a contract. The redactor also keeps its own PEM check on the ORIGINAL
48
+ * text's forms because the token pass above can rewrite the header away before `containsSecretToken` ever sees the result. */
46
49
  export function redactSecrets(text) {
47
50
  const targeted = text
48
51
  .replace(/\b(bearer|basic)(\s+)\S+/gi, "$1$2[redacted-secret]")
@@ -220,6 +223,20 @@ function outgoingPath(p) {
220
223
  function outgoingSegment(seg) {
221
224
  return seg.length > DISCLOSABLE_IDENTITY_MAX ? "[redacted-secret]" : redactSecrets(seg);
222
225
  }
226
+ /**
227
+ * 一条条目为什么被丢 —— **机读原因**的闭集(S-1295)。词与**丢弃点**一一对应:`DroppedEntry` 只有一个铸点
228
+ * (`droppedEntry`),它的上游判据(`judgeEntry`)按「谁判、判成什么样」分三种丢法,每种一个词,没有 other 兜底:
229
+ * - `entry-schema-invalid` —— 条目(数组元素 / Record 的值)没过它自己的条目 schema;
230
+ * - `entry-key-invalid` —— Record 的**键**没过键 schema(键先判:键坏的条目不再算值);
231
+ * - `entry-unreadable` —— 判的过程抛了(病态访问器 / Proxy 陷阱;文案可能带秘密,所以只留一句中性话,见 {@link judgeEntry})。
232
+ *
233
+ * 为什么是词而不是 `error` 句:句子随 zod 小版本改字、可经脱敏,拿它当判据就是拿实现当契约;wire 上也不许有自由文本。
234
+ * 这张表由 `test/dropped-entry-reason-code-s1295.test.ts` 的两格咬住:穷举 `switch`(给表加词而不补臂 ⇒ `tsc` 红,门 = `npm run typecheck`)与
235
+ * 满射格(每个词至少有一个真丢弃点触发它 = 没有死词;每个被丢条目的词都在表里 = 没有无词的丢弃点)。
236
+ * 边界(如实登记):本表只管 `salvageEntries` 铸的 {@link DroppedEntry};读路径上另一条按名剔条目的臂
237
+ * (`hosts-grandfathered`,主机名超长)走它自己的告警臂,不是 `DroppedEntry`,不在本表。
238
+ */
239
+ export const DROPPED_ENTRY_REASON_CODES = ["entry-schema-invalid", "entry-key-invalid", "entry-unreadable"];
223
240
  /** 可披露身份与出门坐标段的**长度上界**(一只常量,两处用:{@link DISCLOSABLE_IDENTITY_RE} 的上限、{@link outgoingSegment}
224
241
  * 的「超长不扫描」界)。与 web-admin 读侧名长上限 256 对齐。 */
225
242
  const DISCLOSABLE_IDENTITY_MAX = 256;
@@ -287,24 +304,34 @@ function readEntryIdentity(entry) {
287
304
  /** 一条记账行 → 它的 `entry-dropped` 告警。域面({@link buildEffective})与 wire 面(skills manifest)共用
288
305
  * 这**一只** —— 告警形只在这里拼,新字段不会在一条腿上漏铸。 */
289
306
  function entryDroppedWarning(domain, d) {
290
- return d.identity === undefined ? { domain, kind: "entry-dropped", path: d.path, error: d.error } : { domain, kind: "entry-dropped", path: d.path, error: d.error, identity: d.identity };
307
+ return {
308
+ domain,
309
+ kind: "entry-dropped",
310
+ path: d.path,
311
+ error: d.error,
312
+ ...(d.reasonCode === undefined ? {} : { reasonCode: d.reasonCode }),
313
+ ...(d.identity === undefined ? {} : { identity: d.identity }),
314
+ };
291
315
  }
292
- /** 一条被丢条目的记账行 —— `salvageEntries` 两种集合形(数组 / Record)共用的**唯一**铸点。 */
293
- function droppedEntry(path, error, entry) {
316
+ /** 一条被丢条目的记账行 —— `salvageEntries` 两种集合形(数组 / Record)共用的**唯一**铸点。词随判决来({@link EntryVerdict}),
317
+ * 不在这里另行推断:判的那一刻才知道是哪种丢法。 */
318
+ function droppedEntry(path, verdict, entry) {
294
319
  const identity = readEntryIdentity(entry);
295
- return identity === undefined ? { path, error } : { path, error, identity };
320
+ const base = { path, error: verdict.error, reasonCode: verdict.reasonCode };
321
+ return identity === undefined ? base : { ...base, identity };
296
322
  }
297
323
  /** 一次「什么都没丢」的抢救的位移表(冻结的共享空表 —— {@link rebaseWarningPaths} 对它恒等)。 */
298
324
  const NO_ENTRIES_DROPPED = Object.freeze({});
299
- /** 一条条目的 parse —— 坏就返回它的(脱敏)错误。恶意 getter/Proxy 在遍历中抛错时**绝不**外泄其文案
300
- * (可能带 secret,与 parseDomain 的 R13 同纪律),只留一句中性话。 */
301
- function judgeEntry(schema, value) {
325
+ /** 一条条目的 parse —— 坏就返回它的判决(脱敏错误 + 闭集词)。`rejectedAs` = 调用点在集合里的位置赋予「schema 没过」的那个词
326
+ * (条目 / Record 值 ⇒ `entry-schema-invalid`;Record 键 ⇒ `entry-key-invalid`);判的过程抛了则恒为 `entry-unreadable`,与位置无关。
327
+ * 恶意 getter/Proxy 在遍历中抛错时**绝不**外泄其文案(可能带 secret,与 parseDomain 的 R13 同纪律),只留一句中性话。 */
328
+ function judgeEntry(schema, value, rejectedAs) {
302
329
  try {
303
330
  const r = schema.safeParse(value);
304
- return r.success ? undefined : sanitizeZodError(r.error);
331
+ return r.success ? undefined : { error: sanitizeZodError(r.error), reasonCode: rejectedAs };
305
332
  }
306
333
  catch {
307
- return new Error("entry could not be read during validation");
334
+ return { error: new Error("entry could not be read during validation"), reasonCode: "entry-unreadable" };
308
335
  }
309
336
  }
310
337
  /**
@@ -366,12 +393,12 @@ function salvageEntriesUnsafe(schema, raw) {
366
393
  // 会跟着一个在校验期间自我增殖的数组一路跑下去(病态 getter 在 zod 读字段时往同一个数组里再 push
367
394
  // 一条)——抢救是尽力而为的旁路,绝不许把同步读挂死。下标在回调里收,`filter` 本来就给 index。
368
395
  const kept = value.filter((entry, i) => {
369
- const error = judgeEntry(element, entry);
370
- if (error === undefined) {
396
+ const verdict = judgeEntry(element, entry, "entry-schema-invalid");
397
+ if (verdict === undefined) {
371
398
  keptFrom.push(i); // 压缩后第 keptFrom.length-1 条 = 原文档第 i 条
372
399
  return true;
373
400
  }
374
- bad.push(droppedEntry(outgoingPath([field, i]), error, entry));
401
+ bad.push(droppedEntry(outgoingPath([field, i]), verdict, entry));
375
402
  return false;
376
403
  });
377
404
  // 零幸存者 ⇒ 没有要抢救的东西(见头注:绝不凭空造出一个空集合)。
@@ -387,11 +414,11 @@ function salvageEntriesUnsafe(schema, raw) {
387
414
  const bad = [];
388
415
  const kept = {};
389
416
  for (const [key, entry] of Object.entries(value)) {
390
- const error = judgeEntry(keySchema, key) ?? judgeEntry(valueSchema, entry);
391
- if (error === undefined)
417
+ const verdict = judgeEntry(keySchema, key, "entry-key-invalid") ?? judgeEntry(valueSchema, entry, "entry-schema-invalid");
418
+ if (verdict === undefined)
392
419
  kept[key] = entry;
393
420
  else
394
- bad.push(droppedEntry(outgoingPath([field, key]), error, entry)); // 键是用户写的,可能就是一条粘错位置的凭据
421
+ bad.push(droppedEntry(outgoingPath([field, key]), verdict, entry)); // 键是用户写的,可能就是一条粘错位置的凭据
395
422
  }
396
423
  if (bad.length > 0 && Object.keys(kept).length > 0) {
397
424
  dropped.push(...bad);
@@ -483,7 +510,7 @@ export function projectWarning(w) {
483
510
  case "legacy-governance-lifted":
484
511
  return { kind: w.kind, domain: w.domain, keys: w.keys };
485
512
  case "entry-dropped":
486
- return { kind: w.kind, domain: w.domain, path: w.path, ...(w.identity === undefined ? {} : { identity: w.identity }) };
513
+ return { kind: w.kind, domain: w.domain, path: w.path, ...(w.identity === undefined ? {} : { identity: w.identity }), ...(w.reasonCode === undefined ? {} : { reasonCode: w.reasonCode }) };
487
514
  case "unread-config-file":
488
515
  return { kind: w.kind, file: w.file, ...(w.domain === undefined ? {} : { domain: w.domain }), why: w.why };
489
516
  default: {
@@ -229,7 +229,7 @@ export interface ConformanceNormalizer {
229
229
  /** 把一条坐标归一到共同空间。有损归一器在这里把段拼成串。 */
230
230
  path(p: readonly (string | number)[]): unknown;
231
231
  /** 把一条**告警投影**归一到共同空间(可选;先于 {@link ConformanceNormalizer.path} 作用,期望与实际两侧同过)。
232
- * 用途:剥掉旧版**铸不出**的 additive 字段(S-712 的 `entry-dropped.identity`)—— 旧版没有这个字段不是读法
232
+ * 用途:剥掉旧版**铸不出**的 additive 字段(S-712 的 `entry-dropped.identity`、S-1295 的 `entry-dropped.reasonCode`)—— 旧版没有这个字段不是读法
233
233
  * 分歧,不许被记成「旧版行为红」。剥掉的字段同样是有损的一部分,`why` 里写明。release 模式禁归一器,故
234
234
  * 这只钩子只作用于 historical 读数。 */
235
235
  warning?(p: WarningProjection): WarningProjection;
package/dist/types.d.ts CHANGED
@@ -17,11 +17,13 @@
17
17
  */
18
18
  import { z } from "zod";
19
19
  /** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
20
- * of that shape: {@link SECRET_LITERAL_RE} is composed from it (anchored, as one of its branches), and
21
- * `redactSecrets` (config-fns) tests it ANYWHERE in a string, on every display-side normalized form. It carries spaces,
22
- * so the token-splitting detector {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
23
- * a newline or a `key: ` prefix slipped past (S-712 codex review [high]), and a `key=`-prefixed percent-encoded one
24
- * hid it behind `%20` on the raw form, a form-urlencoded one behind `+` (6.3.0). Public keys / certificates never match. */
20
+ * of that shape, read ANYWHERE in a string by BOTH judges: the validation detector {@link containsSecretToken}
21
+ * (`scanForSecret`, on every percent layer — 10.0.0) and the display redactor `redactSecrets` (config-fns, on every display-side
22
+ * normalized form). {@link SECRET_LITERAL_RE} is also composed from it (anchored, as one of its branches — the exported per-token
23
+ * literal shape). The header carries spaces, so the token-splitting passes can never isolate it; before 10.0.0 the validation side
24
+ * therefore saw it only at the very start of a string, and a PEM behind a newline, a `key: ` prefix or any other text rode past
25
+ * every free-text write port while the display side already withheld it (a `key=`-prefixed percent-encoded
26
+ * one hid behind `%20` on the raw form, a form-urlencoded one behind `+`, 6.3.0). Public keys / certificates never match. */
25
27
  export declare const PRIVATE_KEY_PEM_HEADER: RegExp;
26
28
  export declare const SECRET_LITERAL_RE: RegExp;
27
29
  export declare const ENV_NAME: z.ZodEffects<z.ZodString, string, string>;
@@ -32,8 +34,9 @@ export declare const ENV_NAME: z.ZodEffects<z.ZodString, string, string>;
32
34
  export declare const hasNul: (s: string) => boolean;
33
35
  /** Does `s` contain a secret-shaped token? Scans every percent layer of it — the raw string AND up to three tolerant
34
36
  * percent-decoded layers (a secret can hide as `%73%6B-…`; the ONE definition of those layers is `percentLayers` in the
35
- * package-private `./secret-scan`) — so mcp args / dockerHost / URL components all get uniform percent-decode coverage.
36
- * Tokenized + JWT-aware (see scanForSecret). Shared by NON_SECRET_URL + dockerHost + mcp stdio. Round-4/5/11. (6.3.0 moved
37
+ * package-private `./secret-scan`) — so mcp args / execution coords / URL components all get uniform percent-decode coverage.
38
+ * Tokenized + JWT-aware (see scanForSecret), plus a private-key PEM header read ANYWHERE in any layer ({@link PRIVATE_KEY_PEM_HEADER},
39
+ * 10.0.0 — the display redactor reads the same regex). Shared by every free-text write port (NON_SECRET_URL, mcp stdio, execution coords, ssh login, worker env, …). Round-4/5/11. (6.3.0 moved
37
40
  * the decode loop out, unchanged in result — same layers, same bound, same verdict — and made it exception-free; the
38
41
  * display-side readings the redactor also judges are not this detector's.) We do NOT fail-closed on a bare `%` — that
39
42
  * would false-reject legit values like `100%` / `%s`. A secret glued to a malformed escape (`%ZZsk-`) is an accepted
@@ -41,7 +44,7 @@ export declare const hasNul: (s: string) => boolean;
41
44
  * R11/R13/R14. */
42
45
  export declare function containsSecretToken(s: string): boolean;
43
46
  /** Scan a URL's PATH + query + fragment (KEYS and values, decoded, tokenized) for a secret-shaped literal.
44
- * Userinfo is handled separately (NON_SECRET_URL bans it; dockerHost scans the ssh user). Round-4/5. */
47
+ * Userinfo is handled separately (NON_SECRET_URL bans it; the ssh username is scanned by `containsSecretToken`). Round-4/5. */
45
48
  export declare function urlQueryFragmentHasSecret(u: URL): boolean;
46
49
  export declare const NON_SECRET_URL: z.ZodEffects<z.ZodString, string, string>;
47
50
  /** Validate a free-form `extraBody`: returns true = UNSAFE (reject). A GETTER-FREE, descriptor-based, bounded
package/dist/types.js CHANGED
@@ -30,11 +30,13 @@ import { LANE_ENV_OWNER, MIGRATION_SIGNPOST, isMigratableEnvKey, laneEnvSeat, la
30
30
  // 0.1.32: this is the root fix for the public-key false-positive — replaces the 0.1.31 name-based exemption,
31
31
  // which review flagged as a bypass (a private key misnamed `*_PUBKEY` skipped scanning entirely).
32
32
  /** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
33
- * of that shape: {@link SECRET_LITERAL_RE} is composed from it (anchored, as one of its branches), and
34
- * `redactSecrets` (config-fns) tests it ANYWHERE in a string, on every display-side normalized form. It carries spaces,
35
- * so the token-splitting detector {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
36
- * a newline or a `key: ` prefix slipped past (S-712 codex review [high]), and a `key=`-prefixed percent-encoded one
37
- * hid it behind `%20` on the raw form, a form-urlencoded one behind `+` (6.3.0). Public keys / certificates never match. */
33
+ * of that shape, read ANYWHERE in a string by BOTH judges: the validation detector {@link containsSecretToken}
34
+ * (`scanForSecret`, on every percent layer — 10.0.0) and the display redactor `redactSecrets` (config-fns, on every display-side
35
+ * normalized form). {@link SECRET_LITERAL_RE} is also composed from it (anchored, as one of its branches — the exported per-token
36
+ * literal shape). The header carries spaces, so the token-splitting passes can never isolate it; before 10.0.0 the validation side
37
+ * therefore saw it only at the very start of a string, and a PEM behind a newline, a `key: ` prefix or any other text rode past
38
+ * every free-text write port while the display side already withheld it (a `key=`-prefixed percent-encoded
39
+ * one hid behind `%20` on the raw form, a form-urlencoded one behind `+`, 6.3.0). Public keys / certificates never match. */
38
40
  export const PRIVATE_KEY_PEM_HEADER = /-----BEGIN[A-Z0-9 ]*PRIVATE KEY/i;
39
41
  export const SECRET_LITERAL_RE = new RegExp(`^(sk-|gh[opus]_|github_pat_|xox[baprse]-|xapp-|bearer\\s|basic\\s|eyJ[A-Za-z0-9_-]{10,}\\.|AKIA[0-9A-Z]{12,}|AIza[0-9A-Za-z_-]{20,}|${PRIVATE_KEY_PEM_HEADER.source})`, "i");
40
42
  // 所有 *Env / *tokenEnv 字段的值是宿主机 env 变量的 NAME(不是值)。合法 env 变量名 + 拒绝 secret 形状的值(AKIA…
@@ -48,6 +50,10 @@ export const ENV_NAME = z
48
50
  // GITHUB_PAT_TOKEN / GHU_TOKEN (which the full SECRET_LITERAL_RE would). Round-5.
49
51
  .refine((s) => !/^AKIA[0-9A-Z]{12,}$/.test(s), "looks like an AWS access-key id, not an env-NAME — put the NAME of the env var here (the value lives in .env)");
50
52
  function scanForSecret(s) {
53
+ // A private-key PEM header ANYWHERE in the string (the one regex the display redactor reads too). The header carries spaces, so
54
+ // neither the start-anchored SECRET_LITERAL_RE below nor the tokenizer at the bottom can see it behind a prefix / newline. 10.0.0.
55
+ if (PRIVATE_KEY_PEM_HEADER.test(s))
56
+ return true;
51
57
  if (SECRET_LITERAL_RE.test(s))
52
58
  return true;
53
59
  // HTTP auth scheme + an opaque token, at a token boundary. SECRET_LITERAL_RE's `bearer\s`/`basic\s` only fire
@@ -77,8 +83,9 @@ function scanForSecret(s) {
77
83
  export const hasNul = (s) => s.includes("\0");
78
84
  /** Does `s` contain a secret-shaped token? Scans every percent layer of it — the raw string AND up to three tolerant
79
85
  * percent-decoded layers (a secret can hide as `%73%6B-…`; the ONE definition of those layers is `percentLayers` in the
80
- * package-private `./secret-scan`) — so mcp args / dockerHost / URL components all get uniform percent-decode coverage.
81
- * Tokenized + JWT-aware (see scanForSecret). Shared by NON_SECRET_URL + dockerHost + mcp stdio. Round-4/5/11. (6.3.0 moved
86
+ * package-private `./secret-scan`) — so mcp args / execution coords / URL components all get uniform percent-decode coverage.
87
+ * Tokenized + JWT-aware (see scanForSecret), plus a private-key PEM header read ANYWHERE in any layer ({@link PRIVATE_KEY_PEM_HEADER},
88
+ * 10.0.0 — the display redactor reads the same regex). Shared by every free-text write port (NON_SECRET_URL, mcp stdio, execution coords, ssh login, worker env, …). Round-4/5/11. (6.3.0 moved
82
89
  * the decode loop out, unchanged in result — same layers, same bound, same verdict — and made it exception-free; the
83
90
  * display-side readings the redactor also judges are not this detector's.) We do NOT fail-closed on a bare `%` — that
84
91
  * would false-reject legit values like `100%` / `%s`. A secret glued to a malformed escape (`%ZZsk-`) is an accepted
@@ -88,7 +95,7 @@ export function containsSecretToken(s) {
88
95
  return percentLayers(s).some((form) => scanForSecret(form));
89
96
  }
90
97
  /** Scan a URL's PATH + query + fragment (KEYS and values, decoded, tokenized) for a secret-shaped literal.
91
- * Userinfo is handled separately (NON_SECRET_URL bans it; dockerHost scans the ssh user). Round-4/5. */
98
+ * Userinfo is handled separately (NON_SECRET_URL bans it; the ssh username is scanned by `containsSecretToken`). Round-4/5. */
92
99
  export function urlQueryFragmentHasSecret(u) {
93
100
  let pathname = u.pathname;
94
101
  try {
@@ -160,7 +167,7 @@ export function extraBodyUnsafe(v) {
160
167
  const t = typeof cur;
161
168
  if (t === "string") {
162
169
  if (containsSecretToken(cur))
163
- return true; // tokenized (mid-string secret too) — sibling of URL/dockerHost/mcp. R8
170
+ return true; // tokenized (mid-string secret too) — sibling of URL/coords/mcp. R8
164
171
  continue;
165
172
  }
166
173
  if (t === "number") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "9.0.0",
3
+ "version": "10.1.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",