@sema-agent/settings-schema 6.2.0 → 6.4.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 (48) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +21 -9
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +166 -0
  5. package/conformance/cases/c41-s792-encoded-pem-identity/input.json +62 -0
  6. package/conformance/cases/c41-s792-encoded-pem-identity/meta.json +12 -0
  7. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +120 -0
  8. package/conformance/cases/c42-s792-unknown-key-secret-redacted/input.json +25 -0
  9. package/conformance/cases/c42-s792-unknown-key-secret-redacted/meta.json +11 -0
  10. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +137 -0
  11. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/input.json +44 -0
  12. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/meta.json +12 -0
  13. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +115 -0
  14. package/conformance/cases/c44-s792-utf8-escape-glued-token/input.json +30 -0
  15. package/conformance/cases/c44-s792-utf8-escape-glued-token/meta.json +10 -0
  16. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +185 -0
  17. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/input.json +78 -0
  18. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +11 -0
  19. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/expected.json +88 -0
  20. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/input.json +18 -0
  21. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +6 -0
  22. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/expected.json +98 -0
  23. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/input.json +26 -0
  24. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +6 -0
  25. package/conformance/cases/c48-s935-model-headers-transported/expected.json +115 -0
  26. package/conformance/cases/c48-s935-model-headers-transported/input.json +37 -0
  27. package/conformance/cases/c48-s935-model-headers-transported/meta.json +6 -0
  28. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +98 -0
  29. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/input.json +28 -0
  30. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/meta.json +9 -0
  31. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +0 -8
  32. package/conformance/cases/cli01-model-name-nfc-vs-nfd/meta.json +2 -1
  33. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +0 -8
  34. package/conformance/cases/cli02-control-and-zero-width-chars/meta.json +2 -1
  35. package/dist/config-fns.d.ts +53 -22
  36. package/dist/config-fns.js +87 -33
  37. package/dist/cross-session.d.ts +8 -5
  38. package/dist/cross-session.js +8 -5
  39. package/dist/fleet.d.ts +3 -2
  40. package/dist/fleet.js +3 -2
  41. package/dist/hash.d.ts +3 -3
  42. package/dist/hash.js +3 -3
  43. package/dist/hooks.d.ts +8 -8
  44. package/dist/secret-scan.d.ts +39 -0
  45. package/dist/secret-scan.js +110 -0
  46. package/dist/types.d.ts +65 -7
  47. package/dist/types.js +57 -44
  48. package/package.json +1 -1
@@ -21,15 +21,24 @@ import { type DomainConfig, type DomainName, type EffectiveConfig, type Effectiv
21
21
  * any secret-shaped run (`%` kept in the run so a percent-encoded secret stays whole for the percent-aware
22
22
  * containsSecretToken). invariant ①: a secret VALUE never rides an error. R13/R14.
23
23
  *
24
- * **Output invariant** (S-712 verification pass N1 + codex review [high] ×2): a string holding a private-key PEM
25
- * header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) is withheld WHOLE, and no result trips `containsSecretToken`.
26
- * The PEM header is judged on the ORIGINAL text, before any replacement: the token pass can rewrite the header
27
- * itself away (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes
28
- * `[redacted-secret]` and the key body after it would ride out untouched — codex verification round). A PEM header
29
- * carries spaces, so the targeted passes can never isolate it, and the detector only sees it at the very start of
30
- * a string. Every warning face that goes through here (coordinates, error text, entry identity) inherits the same
31
- * guarantee. The validation-side detector `containsSecretToken` is deliberately NOT widened here: making it reject
32
- * a mid-string PEM would tighten every field it validates (a major). */
24
+ * **Output invariant** (S-712 verification pass N1 + codex review [high] ×2; 6.3.0): a string holding a private-key
25
+ * PEM header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) in ANY of its {@link secretScanForms} `forms` is withheld WHOLE, a
26
+ * string whose normalization did not finish (`unresolved`) is withheld WHOLE, and no result trips `containsSecretToken` on
27
+ * any of its forms.
28
+ * The whole-string judgement is ONE rule — both detectors run over the normalized forms: the PEM header on every form of
29
+ * the ORIGINAL text, `containsSecretToken` on every form of the targeted result; what could not be normalized does not go
30
+ * out (6.3.0, codex r1 F1: `-----BEGIN+PRIVATE+KEY` form-encoded headers and four-layer encodings both rode out).
31
+ * 6.3.0 (codex review of v6.1.0 [high]): `key=` + a percent-encoded PEM showed its header only on a decoded form
32
+ * (`-----BEGIN%20PRIVATE%20KEY` — `%20` is not in `[A-Z0-9 ]`), and the detector's anchored decode missed it behind
33
+ * the `key=` prefix, so the whole key rode out through coordinates, zod `received` / `message` and entry identity.
34
+ * The PEM header is judged BEFORE any replacement: the token pass can rewrite the header itself away
35
+ * (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes `[redacted-secret]` and the
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). */
33
42
  export declare function redactSecrets(text: string): string;
34
43
  /** Sanitize a ZodError IN PLACE so no surfaced error (message / `received` / nested `unionErrors`) echoes a
35
44
  * pasted secret. RECURSES into `unionErrors` (a `z.union` failure nests a ZodError per member — e.g. RoleTarget
@@ -164,9 +173,10 @@ export declare function formatConfigPath(p: ConfigPath): string;
164
173
  /** 一条被丢掉的条目的**身份**(S-712):只从被丢条目**原文**里取,三键各自可缺。
165
174
  *
166
175
  * 律(一条,不分域):`name` / `id` 取**字符串**形、`enabled` 取**布尔**形,类型不对 ⇒ 不带该键(不猜、
167
- * 不 `String()` 转换);只认**数据属性**(不触发访问器);{@link redactSecrets} 会改动的串 ⇒ 不带该键 ——
168
- * 与坐标段同一只脱敏判据:坐标段必须在场,所以打码出门;身份是可缺的,打过码的串不再是任何条目的身份,
169
- * 所以是**不带**而不是打码。三键都读不出 ⇒ 整个身份缺席(不铸空对象)。
176
+ * 不 `String()` 转换);只认**数据属性**(不触发访问器);{@link isDisclosableIdentity} 说不可披露 ⇒ 不带该键
177
+ * (6.3.0 起:身份是**标识符**,须落在 {@link DISCLOSABLE_IDENTITY_RE} 白名单闭形里,且坐标脱敏器
178
+ * {@link redactSecrets} 原样放行 —— 坐标段必须在场,所以打码出门;身份是可缺的,打过码的串不再是任何条目的身份,
179
+ * 所以是**不带**而不是打码)。三键都读不出 ⇒ 整个身份缺席(不铸空对象)。
170
180
  *
171
181
  * **缺席 = 读不出**(不是字符串,或是字符串但不能原样出门),两种原因刻意不分:对消费方的正确处置相同
172
182
  * (身份未知 ⇒ 按未知处置,例如 fail-closed),而一个「被扣下」的标记本身就在说「这里有一段像凭据的东西」。
@@ -186,13 +196,25 @@ export interface DroppedEntry {
186
196
  error: unknown;
187
197
  identity?: DroppedEntryIdentity;
188
198
  }
189
- /** 身份串 —— 「一个用户写的身份串(条目的 `name` / `id`)能不能**原样**出门」的**唯一**判据:
190
- * {@link redactSecrets} 原样放行。入参是 `string`、返回 `boolean`,**刻意不是**类型谓词(`v is string`):它对一些
191
- * 字符串答 false,谓词形会让调用方的否定分支把一个 `string` 收窄成 `never`;「是不是字符串」由调用点自己判
192
- * (运行期收到非字符串照答 false、不抛)。秘密形的串按**无名**处置(不带,而不是打码:打过码的串不再是身份)。
193
- * 与坐标段脱敏、`error` 文案脱敏({@link sanitizeZodError})**同一只**判据:告警面上任何一处用户写的串,要么过这一只
194
- * 脱敏器出门,要么(可缺的身份)不出门。两只判据会在同一条告警里给出两种答案(对抗轮 #2 真复现:`Bearer` 后接 NBSP
195
- * 或短令牌时,旧判据放行、坐标却打码)。
199
+ /** 可披露身份的**闭形**(6.3.0):字符集 `A-Z a-z 0-9 . _ : / @ + -`、长度 1–256(上限与 web-admin 读侧名长上限 256 对齐;`ModelEntry.name`
200
+ * 本身不设上限,超 256 字的合法名按无名处置)。身份是**标识符**不是自由文本 ——
201
+ * 允许集才是它的语义:上游模型 id 的真实形(`accounts/fireworks/models/…`、`Qwen/Qwen2.5-72B-Instruct`、
202
+ * `claude-3-5-sonnet@20240620`、`deepseek-v4-flash:free`、`o3-mini+high`)都在集内;`%` / `=` / 空白 / 引号 / 反斜杠 /
203
+ * 控制与零宽字符 / 非 ASCII 一律在集外 —— 编码过的、带前缀的、多行的、会在日志里伪造行的串,不可能是一个标识符。
204
+ * 无旗标(无 `g` ⇒ `.test` 无状态,可放心共享这一只)。判据只有 {@link isDisclosableIdentity} 一只,消费方要
205
+ * 「这个名能不能写出去」问它,不要拿本常量自拼(集内仍有密钥形,见那一只的否决项)。 */
206
+ export declare const DISCLOSABLE_IDENTITY_RE: RegExp;
207
+ /** 身份串 —— 「一个用户写的身份串(条目的 `name` / `id`)能不能**原样**出门」的**唯一**判据(6.3.0 起两项合取):
208
+ * ① **形**:落在 {@link DISCLOSABLE_IDENTITY_RE} 白名单闭形里(不再是「不像密钥就可披露」—— 6.1.0 / 6.2.0 的黑名单
209
+ * 判据放行过 `key=` + 百分号编码的整把 PEM 私钥,codex 对 v6.1.0 复审 [high]);
210
+ * ② **否决**(「无密钥形」):坐标脱敏器 {@link redactSecrets} 原样放行 —— 集内照样有密钥形(`sk-…` / `ghp_…` /
211
+ * `AKIA…` / 三段 JWT 的字符全在集内),而且判它的必须是坐标段、`error` 文案({@link sanitizeZodError})用的**同一只**
212
+ * 脱敏器:两只判据会在同一条告警里给出两种答案(对抗轮 #2 真复现;集内的残例 = `@` 之后的单点 `eyJ…` 串,裸探测器
213
+ * 判阴、脱敏器会改,语料 c41 钉着);∧ 不是带 userinfo 的 URL —— 校验侧 `NON_SECRET_URL` 拒写的同一只判定
214
+ * (`carriesUserinfo`,包内私有;opus 腿 L1:`postgres://svc:pw@h` 的字符全在集内)。
215
+ * 入参是 `string`、返回 `boolean`,**刻意不是**类型谓词(`v is string`):它对一些字符串答 false,谓词形会让调用方的
216
+ * 否定分支把一个 `string` 收窄成 `never`;「是不是字符串」由调用点自己判(运行期收到非字符串照答 false、不抛)。
217
+ * 不可披露的串按**无名**处置(不带,而不是打码:打过码的串不再是身份)。
196
218
  * 执法面(同一条律):被丢条目的身份({@link DroppedEntryIdentity},本文件铸);`ModelRefusalsWire` 的 `entries` 键
197
219
  * (经 {@link isReportableRefusalName},S-768);引擎自己的日志 / 拒因句里要不要写出一个名字,同样问这一只。 */
198
220
  export declare function isDisclosableIdentity(v: string): boolean;
@@ -200,7 +222,7 @@ export declare function isDisclosableIdentity(v: string): boolean;
200
222
  * 不过 ⇒ 按无名处置(不进键、不计数)。两条律的合取,不手抄任何一条:
201
223
  * ① 这只 schema **自己载得住**这个键 —— 就地拿 schema 判(空串不过键规则;`__proto__` 这类名被 zod 的 record 静默丢掉,
202
224
  * 载不住),不写名单;
203
- * ② {@link isDisclosableIdentity}(秘密形 ⇒ 不出门)。
225
+ * ② {@link isDisclosableIdentity}(不是白名单闭形里的标识符,或是秘密形 ⇒ 不出门)。
204
226
  * 非字符串 ⇒ false。住在本文件而不是 `./fleet`:`fleet.ts` 被 `types.ts` 引,它再引本文件会成环。 */
205
227
  export declare function isReportableRefusalName(name: string): boolean;
206
228
  /** 抢救造成的**下标位移表**:数组字段名 → 该字段压缩后每个位置的**原**下标(`kept[j]` = 压缩后第 j 条
@@ -239,7 +261,7 @@ export interface EntrySalvageResult {
239
261
  * 造出一个空集合**。
240
262
  *
241
263
  * 🔴 **告警不许比它诊断的东西更危险**(复审 R1 两条,红先复现):① Record 的键是用户写的,完全可能就是
242
- * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
264
+ * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(6.3.0 起经 `outgoingPath` 这一道;不变量①:secret 值永不随错误外流,
243
265
  * 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
244
266
  * 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
245
267
  * 绝不把解析拖下水、更不回显它的错误(与同文件 {@link droppedKeys} 逐字同纪律)。
@@ -286,10 +308,14 @@ export declare function legacyGovernanceFromRuntime(rawRuntime: unknown): unknow
286
308
  * 0.19.0 加了两个 kind(additive union member —— 用 `switch` 穷举本类型的消费方需补臂):
287
309
  * `"unknown-keys-dropped"` / `"unread-config-file"`,成案见 {@link droppedKeys}(消费方
288
310
  * #322:配置写了、进程跑着、值没到,而没有任何一个字说出来)。 */
289
- export type EffectiveReadWarning = {
311
+ export type EffectiveReadWarning =
312
+ /** `droppedNames` 有界(6.3.0,S-813:与未知键同一只 `DROP_SCAN_MAX_KEYS`),超界时带 `droppedTotal`(= 按名剔掉的真实条数)——
313
+ * 缺席 = 列表是穷举。`truncatedFrom` 另是一件事:hosts 数组超 `HOSTS_ARRAY_MAX` 被切片前的长度。 */
314
+ {
290
315
  domain: "hosts";
291
316
  kind: "hosts-grandfathered";
292
317
  droppedNames: string[];
318
+ droppedTotal?: number;
293
319
  truncatedFrom?: number;
294
320
  } | {
295
321
  domain: DomainName;
@@ -559,6 +585,11 @@ export interface GrandfatherHostsReadResult {
559
585
  /** Host names DROPPED because they exceed HOST_NAME_MAX (63) — truncating would forge a NEW identity that
560
586
  * matches no ORCH_HOST_ID (and could collide into the duplicate-name refine), so the entry is skipped whole. */
561
587
  droppedNames: string[];
588
+ /** How many hosts were dropped by name — the TRUE count. `droppedNames` carries labels for at most `DROP_SCAN_MAX_KEYS` (64,
589
+ * the same bound as the unknown-key scan) of them; each label goes through the outgoing redaction, and a document with
590
+ * thousands of over-long names must not cost thousands of scans on every read (6.3.0, S-813). Same shape as
591
+ * {@link UnknownKeyScan}'s `{ keys, total }`. */
592
+ droppedTotal: number;
562
593
  /** Set when the hosts array (after drops) exceeded HOSTS_ARRAY_MAX (1024) and was sliced — the pre-slice length. */
563
594
  truncatedFrom?: number;
564
595
  /** 这次夹紧造成的**下标位移表**(与 {@link EntrySalvageResult.keptIndex} 同一种账、同一条律):本函数
@@ -17,6 +17,7 @@
17
17
  // its entry collections. zod is this package's one runtime dependency, so this costs nothing new.
18
18
  import { z } from "zod";
19
19
  import { MODEL_ENTRY_REFUSAL_CODES, ModelRefusalsWire } from "./fleet.js";
20
+ import { carriesUserinfo, secretScanForms } from "./secret-scan.js"; // package-private display-side normalization + the userinfo judgement (not in the barrel)
20
21
  import { DOMAIN_SCHEMAS, GOVERNANCE_MIRROR_KEYS, HOSTS_ARRAY_MAX, HOST_NAME_MAX, InfraCostRates, LimitsConfig, PRIVATE_KEY_PEM_HEADER, SkillsManifestSchema, containsSecretToken } from "./types.js";
21
22
  /** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
22
23
  * `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
@@ -24,20 +25,32 @@ import { DOMAIN_SCHEMAS, GOVERNANCE_MIRROR_KEYS, HOSTS_ARRAY_MAX, HOST_NAME_MAX,
24
25
  * any secret-shaped run (`%` kept in the run so a percent-encoded secret stays whole for the percent-aware
25
26
  * containsSecretToken). invariant ①: a secret VALUE never rides an error. R13/R14.
26
27
  *
27
- * **Output invariant** (S-712 verification pass N1 + codex review [high] ×2): a string holding a private-key PEM
28
- * header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) is withheld WHOLE, and no result trips `containsSecretToken`.
29
- * The PEM header is judged on the ORIGINAL text, before any replacement: the token pass can rewrite the header
30
- * itself away (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes
31
- * `[redacted-secret]` and the key body after it would ride out untouched — codex verification round). A PEM header
32
- * carries spaces, so the targeted passes can never isolate it, and the detector only sees it at the very start of
33
- * a string. Every warning face that goes through here (coordinates, error text, entry identity) inherits the same
34
- * guarantee. The validation-side detector `containsSecretToken` is deliberately NOT widened here: making it reject
35
- * a mid-string PEM would tighten every field it validates (a major). */
28
+ * **Output invariant** (S-712 verification pass N1 + codex review [high] ×2; 6.3.0): a string holding a private-key
29
+ * PEM header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) in ANY of its {@link secretScanForms} `forms` is withheld WHOLE, a
30
+ * string whose normalization did not finish (`unresolved`) is withheld WHOLE, and no result trips `containsSecretToken` on
31
+ * any of its forms.
32
+ * The whole-string judgement is ONE rule — both detectors run over the normalized forms: the PEM header on every form of
33
+ * the ORIGINAL text, `containsSecretToken` on every form of the targeted result; what could not be normalized does not go
34
+ * out (6.3.0, codex r1 F1: `-----BEGIN+PRIVATE+KEY` form-encoded headers and four-layer encodings both rode out).
35
+ * 6.3.0 (codex review of v6.1.0 [high]): `key=` + a percent-encoded PEM showed its header only on a decoded form
36
+ * (`-----BEGIN%20PRIVATE%20KEY` — `%20` is not in `[A-Z0-9 ]`), and the detector's anchored decode missed it behind
37
+ * the `key=` prefix, so the whole key rode out through coordinates, zod `received` / `message` and entry identity.
38
+ * The PEM header is judged BEFORE any replacement: the token pass can rewrite the header itself away
39
+ * (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes `[redacted-secret]` and the
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). */
36
46
  export function redactSecrets(text) {
37
47
  const targeted = text
38
48
  .replace(/\b(bearer|basic)(\s+)\S+/gi, "$1$2[redacted-secret]")
39
49
  .replace(/[A-Za-z0-9_./:+=%-]{6,}/g, (tok) => (containsSecretToken(tok) ? "[redacted-secret]" : tok));
40
- return PRIVATE_KEY_PEM_HEADER.test(text) || containsSecretToken(targeted) ? "[redacted-secret]" : targeted;
50
+ const scan = secretScanForms(text);
51
+ const targetedForms = targeted === text ? scan.forms : secretScanForms(targeted).forms; // 没替换过(最常见)⇒ 同一组形,不再归一化一遍
52
+ const whole = scan.unresolved || scan.forms.some((form) => PRIVATE_KEY_PEM_HEADER.test(form)) || targetedForms.some((form) => containsSecretToken(form));
53
+ return whole ? "[redacted-secret]" : targeted;
41
54
  }
42
55
  /** Sanitize a ZodError IN PLACE so no surfaced error (message / `received` / nested `unionErrors`) echoes a
43
56
  * pasted secret. RECURSES into `unionErrors` (a `z.union` failure nests a ZodError per member — e.g. RoleTarget
@@ -185,23 +198,55 @@ const PLAIN_PATH_SEGMENT = /^(?!(?:0|[1-9]\d*)$)[^."\u0000-\u001f]+$/;
185
198
  export function formatConfigPath(p) {
186
199
  return p.map((seg) => (typeof seg === "number" ? String(seg) : PLAIN_PATH_SEGMENT.test(seg) ? seg : JSON.stringify(seg))).join(".");
187
200
  }
188
- /** 身份串 —— 「一个用户写的身份串(条目的 `name` / `id`)能不能**原样**出门」的**唯一**判据:
189
- * {@link redactSecrets} 原样放行。入参是 `string`、返回 `boolean`,**刻意不是**类型谓词(`v is string`):它对一些
190
- * 字符串答 false,谓词形会让调用方的否定分支把一个 `string` 收窄成 `never`;「是不是字符串」由调用点自己判
191
- * (运行期收到非字符串照答 false、不抛)。秘密形的串按**无名**处置(不带,而不是打码:打过码的串不再是身份)。
192
- * 与坐标段脱敏、`error` 文案脱敏({@link sanitizeZodError})**同一只**判据:告警面上任何一处用户写的串,要么过这一只
193
- * 脱敏器出门,要么(可缺的身份)不出门。两只判据会在同一条告警里给出两种答案(对抗轮 #2 真复现:`Bearer` 后接 NBSP
194
- * 或短令牌时,旧判据放行、坐标却打码)。
201
+ /** 本文件铸的、带用户写的键段的告警坐标出门前的**唯一**一道(6.3.0,codex r1 F2):字符串段过 {@link outgoingSegment} —— 与 `error` 文案
202
+ * ({@link sanitizeZodError})、身份否决({@link isDisclosableIdentity})**同一只**脱敏器,不另设判据;数字段(下标)原样。
203
+ * 键段是用户写的,完全可能就是一条粘错位置的凭据:被丢 Record 条目的键、未知键告警(`unknown-keys-dropped` /
204
+ * `-carried`)的键名都一样(后者 0.19.0 起从不过脱敏器,`sk-…` 形的未知键名原样进告警与投影)。schema 已知的字段名
205
+ * 脱敏器原样放行,所以坐标里「它是哪个字段」照旧可读。经它的坐标就是出门形 —— {@link droppedKeys} /
206
+ * {@link carriedUnknownKeys} 这两只导出函数给消费方的 `keys` 同样是它的产物。`legacy-governance-lifted.keys` 不经它:那些键只来自
207
+ * 闭集 {@link GOVERNANCE_MIRROR_KEYS},按构造没有用户写的串(opus 腿 I1)。 */
208
+ function outgoingPath(p) {
209
+ return p.map((seg) => (typeof seg === "string" ? outgoingSegment(seg) : seg));
210
+ }
211
+ /** 一段用户写的串(坐标键段、`hosts-grandfathered` 被夹紧的主机名)出门前的那一道:长过 {@link DISCLOSABLE_IDENTITY_MAX}(与身份白名单同一只上界)
212
+ * ⇒ 不扫描、整段 `[redacted-secret]`(标识符不会那么长;fail-closed 且 O(1) —— opus 独立腿 M1:6.3.0 首稿让每个未知键名
213
+ * 不论多长都过脱敏器,64 个 16 KB 的键名把一次读拖到 20 s 以上);否则过 {@link redactSecrets}。 */
214
+ function outgoingSegment(seg) {
215
+ return seg.length > DISCLOSABLE_IDENTITY_MAX ? "[redacted-secret]" : redactSecrets(seg);
216
+ }
217
+ /** 可披露身份与出门坐标段的**长度上界**(一只常量,两处用:{@link DISCLOSABLE_IDENTITY_RE} 的上限、{@link outgoingSegment}
218
+ * 的「超长不扫描」界)。与 web-admin 读侧名长上限 256 对齐。 */
219
+ const DISCLOSABLE_IDENTITY_MAX = 256;
220
+ /** 可披露身份的**闭形**(6.3.0):字符集 `A-Z a-z 0-9 . _ : / @ + -`、长度 1–256(上限与 web-admin 读侧名长上限 256 对齐;`ModelEntry.name`
221
+ * 本身不设上限,超 256 字的合法名按无名处置)。身份是**标识符**不是自由文本 ——
222
+ * 允许集才是它的语义:上游模型 id 的真实形(`accounts/fireworks/models/…`、`Qwen/Qwen2.5-72B-Instruct`、
223
+ * `claude-3-5-sonnet@20240620`、`deepseek-v4-flash:free`、`o3-mini+high`)都在集内;`%` / `=` / 空白 / 引号 / 反斜杠 /
224
+ * 控制与零宽字符 / 非 ASCII 一律在集外 —— 编码过的、带前缀的、多行的、会在日志里伪造行的串,不可能是一个标识符。
225
+ * 无旗标(无 `g` ⇒ `.test` 无状态,可放心共享这一只)。判据只有 {@link isDisclosableIdentity} 一只,消费方要
226
+ * 「这个名能不能写出去」问它,不要拿本常量自拼(集内仍有密钥形,见那一只的否决项)。 */
227
+ export const DISCLOSABLE_IDENTITY_RE = new RegExp(`^[A-Za-z0-9._:/@+-]{1,${DISCLOSABLE_IDENTITY_MAX}}$`);
228
+ /** 身份串 —— 「一个用户写的身份串(条目的 `name` / `id`)能不能**原样**出门」的**唯一**判据(6.3.0 起两项合取):
229
+ * ① **形**:落在 {@link DISCLOSABLE_IDENTITY_RE} 白名单闭形里(不再是「不像密钥就可披露」—— 6.1.0 / 6.2.0 的黑名单
230
+ * 判据放行过 `key=` + 百分号编码的整把 PEM 私钥,codex 对 v6.1.0 复审 [high]);
231
+ * ② **否决**(「无密钥形」):坐标脱敏器 {@link redactSecrets} 原样放行 —— 集内照样有密钥形(`sk-…` / `ghp_…` /
232
+ * `AKIA…` / 三段 JWT 的字符全在集内),而且判它的必须是坐标段、`error` 文案({@link sanitizeZodError})用的**同一只**
233
+ * 脱敏器:两只判据会在同一条告警里给出两种答案(对抗轮 #2 真复现;集内的残例 = `@` 之后的单点 `eyJ…` 串,裸探测器
234
+ * 判阴、脱敏器会改,语料 c41 钉着);∧ 不是带 userinfo 的 URL —— 校验侧 `NON_SECRET_URL` 拒写的同一只判定
235
+ * (`carriesUserinfo`,包内私有;opus 腿 L1:`postgres://svc:pw@h` 的字符全在集内)。
236
+ * 入参是 `string`、返回 `boolean`,**刻意不是**类型谓词(`v is string`):它对一些字符串答 false,谓词形会让调用方的
237
+ * 否定分支把一个 `string` 收窄成 `never`;「是不是字符串」由调用点自己判(运行期收到非字符串照答 false、不抛)。
238
+ * 不可披露的串按**无名**处置(不带,而不是打码:打过码的串不再是身份)。
195
239
  * 执法面(同一条律):被丢条目的身份({@link DroppedEntryIdentity},本文件铸);`ModelRefusalsWire` 的 `entries` 键
196
240
  * (经 {@link isReportableRefusalName},S-768);引擎自己的日志 / 拒因句里要不要写出一个名字,同样问这一只。 */
197
241
  export function isDisclosableIdentity(v) {
198
- return typeof v === "string" && redactSecrets(v) === v; // 非字符串(JS / any 调用方)⇒ false,不抛(秘密轴 fail-closed)
242
+ // 非字符串(JS / any 调用方)⇒ false,不抛(秘密轴 fail-closed)
243
+ return typeof v === "string" && DISCLOSABLE_IDENTITY_RE.test(v) && redactSecrets(v) === v && !carriesUserinfo(v);
199
244
  }
200
245
  /** S-768 —— 「一个目录条目名能不能当 {@link ModelRefusalsWire} `entries` 的键」的**唯一**判据;引擎铸键前只调这一只,
201
246
  * 不过 ⇒ 按无名处置(不进键、不计数)。两条律的合取,不手抄任何一条:
202
247
  * ① 这只 schema **自己载得住**这个键 —— 就地拿 schema 判(空串不过键规则;`__proto__` 这类名被 zod 的 record 静默丢掉,
203
248
  * 载不住),不写名单;
204
- * ② {@link isDisclosableIdentity}(秘密形 ⇒ 不出门)。
249
+ * ② {@link isDisclosableIdentity}(不是白名单闭形里的标识符,或是秘密形 ⇒ 不出门)。
205
250
  * 非字符串 ⇒ false。住在本文件而不是 `./fleet`:`fleet.ts` 被 `types.ts` 引,它再引本文件会成环。 */
206
251
  export function isReportableRefusalName(name) {
207
252
  if (!isDisclosableIdentity(name))
@@ -274,7 +319,7 @@ function judgeEntry(schema, value) {
274
319
  * 造出一个空集合**。
275
320
  *
276
321
  * 🔴 **告警不许比它诊断的东西更危险**(复审 R1 两条,红先复现):① Record 的键是用户写的,完全可能就是
277
- * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
322
+ * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(6.3.0 起经 `outgoingPath` 这一道;不变量①:secret 值永不随错误外流,
278
323
  * 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
279
324
  * 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
280
325
  * 绝不把解析拖下水、更不回显它的错误(与同文件 {@link droppedKeys} 逐字同纪律)。
@@ -320,7 +365,7 @@ function salvageEntriesUnsafe(schema, raw) {
320
365
  keptFrom.push(i); // 压缩后第 keptFrom.length-1 条 = 原文档第 i 条
321
366
  return true;
322
367
  }
323
- bad.push(droppedEntry([field, i], error, entry));
368
+ bad.push(droppedEntry(outgoingPath([field, i]), error, entry));
324
369
  return false;
325
370
  });
326
371
  // 零幸存者 ⇒ 没有要抢救的东西(见头注:绝不凭空造出一个空集合)。
@@ -340,7 +385,7 @@ function salvageEntriesUnsafe(schema, raw) {
340
385
  if (error === undefined)
341
386
  kept[key] = entry;
342
387
  else
343
- bad.push(droppedEntry([field, redactSecrets(key)], error, entry)); // 键是用户写的,可能就是一条粘错位置的凭据
388
+ bad.push(droppedEntry(outgoingPath([field, key]), error, entry)); // 键是用户写的,可能就是一条粘错位置的凭据
344
389
  }
345
390
  if (bad.length > 0 && Object.keys(kept).length > 0) {
346
391
  dropped.push(...bad);
@@ -420,7 +465,7 @@ export const WARNING_KINDS = [
420
465
  export function projectWarning(w) {
421
466
  switch (w.kind) {
422
467
  case "hosts-grandfathered":
423
- return { kind: w.kind, domain: w.domain, droppedNames: w.droppedNames, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
468
+ return { kind: w.kind, domain: w.domain, droppedNames: w.droppedNames, ...(w.droppedTotal === undefined ? {} : { droppedTotal: w.droppedTotal }), ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
424
469
  case "domain-defaulted":
425
470
  return { kind: w.kind, domain: w.domain };
426
471
  // 两臂字段相同但**不合并**:合并后 `kind` 塌成两员的联合,TS 就再也认不出它属于投影联合的哪一员
@@ -597,7 +642,7 @@ function collectDropped(domain, raw, parsed, prefix, depth, spared, acc) {
597
642
  continue; // 扩展命名空间:按契约放的键,不是未知键
598
643
  acc.total += 1;
599
644
  if (acc.keys.length < DROP_SCAN_MAX_KEYS)
600
- acc.keys.push([...prefix, k]);
645
+ acc.keys.push(outgoingPath([...prefix, k]));
601
646
  continue;
602
647
  }
603
648
  collectDropped(domain, raw[k], parsed[k], [...prefix, k], depth + 1, spared, acc);
@@ -683,7 +728,7 @@ export function carriedUnknownKeys(domain, parsed) {
683
728
  continue;
684
729
  acc.total += 1;
685
730
  if (acc.keys.length < DROP_SCAN_MAX_KEYS)
686
- acc.keys.push([...at, k]);
731
+ acc.keys.push(outgoingPath([...at, k]));
687
732
  }
688
733
  }
689
734
  return acc;
@@ -767,15 +812,23 @@ export const DOMAIN_READ_FALLBACK = {
767
812
  * observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
768
813
  export function grandfatherHostsRead(raw) {
769
814
  if (raw === null || typeof raw !== "object" || !Array.isArray(raw.hosts)) {
770
- return { raw, droppedNames: [], keptIndex: NO_ENTRIES_DROPPED }; // not the expected shape — let parseDomain rule on it
815
+ return { raw, droppedNames: [], droppedTotal: 0, keptIndex: NO_ENTRIES_DROPPED }; // not the expected shape — let parseDomain rule on it
771
816
  }
772
817
  const original = raw.hosts;
773
818
  const droppedNames = [];
819
+ let droppedTotal = 0;
774
820
  const keptFrom = [];
775
821
  let hosts = original.filter((h, i) => {
776
822
  const name = h !== null && typeof h === "object" ? h.name : undefined;
777
823
  if (typeof name === "string" && name.length > HOST_NAME_MAX) {
778
- droppedNames.push(name.slice(0, HOST_NAME_MAX) + "…"); // audit label — clipped so the warning itself stays bounded
824
+ droppedTotal += 1;
825
+ // 先钳后扫(S-813):只给前 DROP_SCAN_MAX_KEYS 个铸标签,其余只计数 —— 出门那一道每个名都要归一化一遍,不能随文档条数放大。
826
+ if (droppedNames.length >= DROP_SCAN_MAX_KEYS)
827
+ return false;
828
+ // 出门前先过同一道(6.3.0,opus 腿 M2):用户写的主机名完全可能就是一条粘错位置的凭据,夹紧只管载荷上界、不管秘密 ——
829
+ // 此前 `sk-live-…` 形 / 编码 PEM 形以 63 字原文前缀出门。判在夹紧**之前**(截断会把秘密形截成探测器认不出的半截)。
830
+ const out = outgoingSegment(name);
831
+ droppedNames.push(out.length > HOST_NAME_MAX ? out.slice(0, HOST_NAME_MAX) + "…" : out); // audit label — clipped so the warning itself stays bounded
779
832
  return false;
780
833
  }
781
834
  keptFrom.push(i); // 夹紧后第 keptFrom.length-1 条 = 原文档第 i 条
@@ -786,11 +839,11 @@ export function grandfatherHostsRead(raw) {
786
839
  truncatedFrom = hosts.length;
787
840
  hosts = hosts.slice(0, HOSTS_ARRAY_MAX);
788
841
  }
789
- if (droppedNames.length === 0 && truncatedFrom === undefined)
790
- return { raw, droppedNames, keptIndex: NO_ENTRIES_DROPPED }; // identity — byte-stable
842
+ if (droppedTotal === 0 && truncatedFrom === undefined)
843
+ return { raw, droppedNames, droppedTotal, keptIndex: NO_ENTRIES_DROPPED }; // identity — byte-stable
791
844
  // 位移**只**来自按名剔条目;纯截断留下的是原数组前缀,幸存者下标不动 ⇒ 不登记(见 EntryKeptIndex)。
792
- const keptIndex = droppedNames.length > 0 ? { hosts: keptFrom.slice(0, hosts.length) } : NO_ENTRIES_DROPPED;
793
- return { raw: { ...raw, hosts }, droppedNames, truncatedFrom, keptIndex };
845
+ const keptIndex = droppedTotal > 0 ? { hosts: keptFrom.slice(0, hosts.length) } : NO_ENTRIES_DROPPED;
846
+ return { raw: { ...raw, hosts }, droppedNames, droppedTotal, truncatedFrom, keptIndex };
794
847
  }
795
848
  export function buildEffective(version, getRaw, updatedAt, onWarning) {
796
849
  // 0.19.0 静默剥键/静默承运门:解析**成功**的域,把 schema 丢掉的键、以及 open-world 域里承运但本包
@@ -867,8 +920,9 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
867
920
  // hosts: the PRECISE grandfather for the 0.8.0 tightening runs BEFORE parse (0.7-era legal data over the
868
921
  // new bounds reads on, clamped + warned); any residual corruption falls to the generic guardrail above.
869
922
  const hostsGf = grandfatherHostsRead(getRaw("hosts"));
870
- if (hostsGf.droppedNames.length > 0 || hostsGf.truncatedFrom !== undefined) {
871
- onWarning?.({ domain: "hosts", kind: "hosts-grandfathered", droppedNames: hostsGf.droppedNames, truncatedFrom: hostsGf.truncatedFrom });
923
+ if (hostsGf.droppedTotal > 0 || hostsGf.truncatedFrom !== undefined) {
924
+ const clipped = hostsGf.droppedTotal > hostsGf.droppedNames.length ? { droppedTotal: hostsGf.droppedTotal } : {};
925
+ onWarning?.({ domain: "hosts", kind: "hosts-grandfathered", droppedNames: hostsGf.droppedNames, ...clipped, truncatedFrom: hostsGf.truncatedFrom });
872
926
  }
873
927
  return {
874
928
  version,
@@ -15,11 +15,11 @@
15
15
  * HELD cross-session message awaits approval」。**注意它有两个消费面**:远端停靠的审批对话框,与
16
16
  * 停审的跨会话消息;本模块只声明键形,哪个宿主消费哪一面由宿主自陈。
17
17
  *
18
- * ## `"default"` 这第四个拼写
18
+ * ## `"default"` 这第五个拼写
19
19
  *
20
- * 设置 UI 的选择器需要一个「不设」的可写值,CC 用字面量 `"default"`。它**不是第四种策略**:读取器
20
+ * 设置 UI 的选择器需要一个「不设」的可写值,CC 用字面量 `"default"`。它**不是第五种策略**:读取器
21
21
  * (`readCrossSessionInbound` / `readDialogExpiry`)把它与 `null`/缺席一同折成 `undefined`。所以
22
- * `CrossSessionInboundSetting` 的受理面是四拼写,而**值域闭集**(`CROSS_SESSION_INBOUND_VALUES`)只有三个。
22
+ * `CrossSessionInboundSetting` 的受理面是五拼写,而**值域闭集**(`CROSS_SESSION_INBOUND_VALUES`)只有四个。
23
23
  *
24
24
  * ## 坏值方向 = 响亮,不静默
25
25
  *
@@ -29,8 +29,11 @@
29
29
  *
30
30
  * ## 与引擎词表的锁步
31
31
  *
32
- * 引擎另有一份同词表(判定纯函数的输入域)。两份**必须逐字相等**;等值由消费方(同时装了本包与引擎
33
- * 的那一侧)用一格锁步钉住 —— 任一侧加词/改序,那一格先红。本包不 import 引擎(纯度不变量)。
32
+ * 两张词表与 dialogExpiry 缺省的语义属主是引擎(判定纯函数的输入域;缺省也铸在引擎),本模块的三只常量是
33
+ * 它们的**登记镜像** —— 保留镜像是因为本包不能运行期依赖引擎,删掉它们两键的 schema 就只剩 `z.string()`。
34
+ * 两份**必须逐字同序相等**;等值由**本仓自证**:`test/upstream-drift.test.ts` ④ 在 dev 期读引擎的运行期导出,
35
+ * 登记表恒空(零豁免)、顺序另钉 —— 任一侧加词/改序/改缺省,那一格先红。消费方不再替本仓守这面镜像,只钉
36
+ * 「自己读的是引擎那份」。本包不 import 引擎(纯度不变量;引擎只在 devDependency)。
34
37
  */
35
38
  import { z } from "zod";
36
39
  /**
@@ -15,11 +15,11 @@
15
15
  * HELD cross-session message awaits approval」。**注意它有两个消费面**:远端停靠的审批对话框,与
16
16
  * 停审的跨会话消息;本模块只声明键形,哪个宿主消费哪一面由宿主自陈。
17
17
  *
18
- * ## `"default"` 这第四个拼写
18
+ * ## `"default"` 这第五个拼写
19
19
  *
20
- * 设置 UI 的选择器需要一个「不设」的可写值,CC 用字面量 `"default"`。它**不是第四种策略**:读取器
20
+ * 设置 UI 的选择器需要一个「不设」的可写值,CC 用字面量 `"default"`。它**不是第五种策略**:读取器
21
21
  * (`readCrossSessionInbound` / `readDialogExpiry`)把它与 `null`/缺席一同折成 `undefined`。所以
22
- * `CrossSessionInboundSetting` 的受理面是四拼写,而**值域闭集**(`CROSS_SESSION_INBOUND_VALUES`)只有三个。
22
+ * `CrossSessionInboundSetting` 的受理面是五拼写,而**值域闭集**(`CROSS_SESSION_INBOUND_VALUES`)只有四个。
23
23
  *
24
24
  * ## 坏值方向 = 响亮,不静默
25
25
  *
@@ -29,8 +29,11 @@
29
29
  *
30
30
  * ## 与引擎词表的锁步
31
31
  *
32
- * 引擎另有一份同词表(判定纯函数的输入域)。两份**必须逐字相等**;等值由消费方(同时装了本包与引擎
33
- * 的那一侧)用一格锁步钉住 —— 任一侧加词/改序,那一格先红。本包不 import 引擎(纯度不变量)。
32
+ * 两张词表与 dialogExpiry 缺省的语义属主是引擎(判定纯函数的输入域;缺省也铸在引擎),本模块的三只常量是
33
+ * 它们的**登记镜像** —— 保留镜像是因为本包不能运行期依赖引擎,删掉它们两键的 schema 就只剩 `z.string()`。
34
+ * 两份**必须逐字同序相等**;等值由**本仓自证**:`test/upstream-drift.test.ts` ④ 在 dev 期读引擎的运行期导出,
35
+ * 登记表恒空(零豁免)、顺序另钉 —— 任一侧加词/改序/改缺省,那一格先红。消费方不再替本仓守这面镜像,只钉
36
+ * 「自己读的是引擎那份」。本包不 import 引擎(纯度不变量;引擎只在 devDependency)。
34
37
  */
35
38
  import { z } from "zod";
36
39
  // ── crossSessionInbound ────────────────────────────────────────────────────────────────────────────
package/dist/fleet.d.ts CHANGED
@@ -132,8 +132,9 @@ export type ModelPlaneRefusalCode = z.infer<typeof ModelPlaneRefusalCode>;
132
132
  * broken row held, and the sentence is prose, not contract. "Which name, which reason" is the whole wire. A name that
133
133
  * cannot ride out is treated as NAMELESS — the engine leaves it out of this map entirely (not keyed, not counted). The
134
134
  * ONE predicate is `isReportableRefusalName(name)` (main barrel): this schema can carry the key (non-empty; `__proto__`
135
- * is silently dropped by the record, so it cannot) AND `isDisclosableIdentity(name)` (not secret-shaped — the rule the
136
- * dropped-entry identity on read warnings already uses). A producer calls it before keying; this schema deliberately
135
+ * is silently dropped by the record, so it cannot) AND `isDisclosableIdentity(name)` (an identifier in the closed allowlist
136
+ * `DISCLOSABLE_IDENTITY_RE` — `A-Z a-z 0-9 . _ : / @ + -`, 1–256 chars — AND not secret-shaped, a URL userinfo included; the
137
+ * rule the dropped-entry identity on read warnings already uses, 6.3.0). A producer calls it before keying; this schema deliberately
137
138
  * does not re-judge secrecy (see the 6.2.0 notes).
138
139
  * - `plane` — present ⇔ the model plane of the latest catalog the engine was handed was refused as a group (the engine
139
140
  * keeps serving the model plane it had); ONE code, never a list — one candidate has exactly one group verdict (the
package/dist/fleet.js CHANGED
@@ -139,8 +139,9 @@ export const ModelPlaneRefusalCode = z.enum(MODEL_PLANE_REFUSAL_CODES);
139
139
  * broken row held, and the sentence is prose, not contract. "Which name, which reason" is the whole wire. A name that
140
140
  * cannot ride out is treated as NAMELESS — the engine leaves it out of this map entirely (not keyed, not counted). The
141
141
  * ONE predicate is `isReportableRefusalName(name)` (main barrel): this schema can carry the key (non-empty; `__proto__`
142
- * is silently dropped by the record, so it cannot) AND `isDisclosableIdentity(name)` (not secret-shaped — the rule the
143
- * dropped-entry identity on read warnings already uses). A producer calls it before keying; this schema deliberately
142
+ * is silently dropped by the record, so it cannot) AND `isDisclosableIdentity(name)` (an identifier in the closed allowlist
143
+ * `DISCLOSABLE_IDENTITY_RE` — `A-Z a-z 0-9 . _ : / @ + -`, 1–256 chars — AND not secret-shaped, a URL userinfo included; the
144
+ * rule the dropped-entry identity on read warnings already uses, 6.3.0). A producer calls it before keying; this schema deliberately
144
145
  * does not re-judge secrecy (see the 6.2.0 notes).
145
146
  * - `plane` — present ⇔ the model plane of the latest catalog the engine was handed was refused as a group (the engine
146
147
  * keeps serving the model plane it had); ONE code, never a list — one candidate has exactly one group verdict (the
package/dist/hash.d.ts CHANGED
@@ -23,9 +23,9 @@ export declare const MAX_HASH_WRAPPER_RESERVE = 32;
23
23
  /** Depth cap for a standalone FREE-FORM config value, reconciled with the hash cap MINUS the wrapper reserve so
24
24
  * that ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any
25
25
  * real free-form value (OpenAI-style request params nest <10 deep).
26
- * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody` and, since
27
- * 1.10.0, `ModelEntry.compat`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`), so one cap
28
- * serves both. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
26
+ * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody`, since 1.10.0
27
+ * `ModelEntry.compat`, since 6.4.0 `ModelEntry.headers`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`),
28
+ * so one cap serves them all. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
29
29
  * (renaming it would be a BREAKING change for a purely cosmetic gain). */
30
30
  export declare const MAX_EXTRA_BODY_DEPTH: number;
31
31
  /** Deterministic JSON with sorted object keys (so key order never affects the hash). */
package/dist/hash.js CHANGED
@@ -23,9 +23,9 @@ export const MAX_HASH_WRAPPER_RESERVE = 32;
23
23
  /** Depth cap for a standalone FREE-FORM config value, reconciled with the hash cap MINUS the wrapper reserve so
24
24
  * that ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any
25
25
  * real free-form value (OpenAI-style request params nest <10 deep).
26
- * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody` and, since
27
- * 1.10.0, `ModelEntry.compat`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`), so one cap
28
- * serves both. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
26
+ * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody`, since 1.10.0
27
+ * `ModelEntry.compat`, since 6.4.0 `ModelEntry.headers`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`),
28
+ * so one cap serves them all. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
29
29
  * (renaming it would be a BREAKING change for a purely cosmetic gain). */
30
30
  export const MAX_EXTRA_BODY_DEPTH = MAX_HASH_DEPTH - MAX_HASH_WRAPPER_RESERVE;
31
31
  /** Deterministic JSON with sorted object keys (so key order never affects the hash). */
package/dist/hooks.d.ts CHANGED
@@ -5545,20 +5545,20 @@ export declare const HttpHookEntry: z.ZodObject<{
5545
5545
  }, "strip", z.ZodTypeAny, {
5546
5546
  type: "http";
5547
5547
  url: string;
5548
+ headers?: Record<string, string> | undefined;
5548
5549
  if?: string | undefined;
5549
5550
  timeout?: number | undefined;
5550
5551
  statusMessage?: string | undefined;
5551
5552
  once?: boolean | undefined;
5552
- headers?: Record<string, string> | undefined;
5553
5553
  allowedEnvVars?: string[] | undefined;
5554
5554
  }, {
5555
5555
  type: "http";
5556
5556
  url: string;
5557
+ headers?: Record<string, string> | undefined;
5557
5558
  if?: string | undefined;
5558
5559
  timeout?: number | undefined;
5559
5560
  statusMessage?: string | undefined;
5560
5561
  once?: boolean | undefined;
5561
- headers?: Record<string, string> | undefined;
5562
5562
  allowedEnvVars?: string[] | undefined;
5563
5563
  }>;
5564
5564
  /** One configured hook (CC settings shape + shell superset fields; discriminated on `type`). */
@@ -5655,20 +5655,20 @@ export declare const HookEntry: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
5655
5655
  }, "strip", z.ZodTypeAny, {
5656
5656
  type: "http";
5657
5657
  url: string;
5658
+ headers?: Record<string, string> | undefined;
5658
5659
  if?: string | undefined;
5659
5660
  timeout?: number | undefined;
5660
5661
  statusMessage?: string | undefined;
5661
5662
  once?: boolean | undefined;
5662
- headers?: Record<string, string> | undefined;
5663
5663
  allowedEnvVars?: string[] | undefined;
5664
5664
  }, {
5665
5665
  type: "http";
5666
5666
  url: string;
5667
+ headers?: Record<string, string> | undefined;
5667
5668
  if?: string | undefined;
5668
5669
  timeout?: number | undefined;
5669
5670
  statusMessage?: string | undefined;
5670
5671
  once?: boolean | undefined;
5671
- headers?: Record<string, string> | undefined;
5672
5672
  allowedEnvVars?: string[] | undefined;
5673
5673
  }>]>;
5674
5674
  export type HookEntry = z.infer<typeof HookEntry>;
@@ -5773,20 +5773,20 @@ export declare const HookMatcherEntry: z.ZodObject<{
5773
5773
  }, "strip", z.ZodTypeAny, {
5774
5774
  type: "http";
5775
5775
  url: string;
5776
+ headers?: Record<string, string> | undefined;
5776
5777
  if?: string | undefined;
5777
5778
  timeout?: number | undefined;
5778
5779
  statusMessage?: string | undefined;
5779
5780
  once?: boolean | undefined;
5780
- headers?: Record<string, string> | undefined;
5781
5781
  allowedEnvVars?: string[] | undefined;
5782
5782
  }, {
5783
5783
  type: "http";
5784
5784
  url: string;
5785
+ headers?: Record<string, string> | undefined;
5785
5786
  if?: string | undefined;
5786
5787
  timeout?: number | undefined;
5787
5788
  statusMessage?: string | undefined;
5788
5789
  once?: boolean | undefined;
5789
- headers?: Record<string, string> | undefined;
5790
5790
  allowedEnvVars?: string[] | undefined;
5791
5791
  }>]>, "many">;
5792
5792
  }, "strip", z.ZodTypeAny, {
@@ -5819,11 +5819,11 @@ export declare const HookMatcherEntry: z.ZodObject<{
5819
5819
  } | {
5820
5820
  type: "http";
5821
5821
  url: string;
5822
+ headers?: Record<string, string> | undefined;
5822
5823
  if?: string | undefined;
5823
5824
  timeout?: number | undefined;
5824
5825
  statusMessage?: string | undefined;
5825
5826
  once?: boolean | undefined;
5826
- headers?: Record<string, string> | undefined;
5827
5827
  allowedEnvVars?: string[] | undefined;
5828
5828
  })[];
5829
5829
  matcher?: string | undefined;
@@ -5857,11 +5857,11 @@ export declare const HookMatcherEntry: z.ZodObject<{
5857
5857
  } | {
5858
5858
  type: "http";
5859
5859
  url: string;
5860
+ headers?: Record<string, string> | undefined;
5860
5861
  if?: string | undefined;
5861
5862
  timeout?: number | undefined;
5862
5863
  statusMessage?: string | undefined;
5863
5864
  once?: boolean | undefined;
5864
- headers?: Record<string, string> | undefined;
5865
5865
  allowedEnvVars?: string[] | undefined;
5866
5866
  })[];
5867
5867
  matcher?: string | undefined;