@sema-agent/settings-schema 14.5.0 → 14.7.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 (77) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +46 -3
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.js +10 -7
  16. package/dist/bundle.d.ts +1 -1
  17. package/dist/bundle.js +1 -1
  18. package/dist/config-d-file.js +33 -15
  19. package/dist/config-dir-migration.js +2 -1
  20. package/dist/config-fns.js +142 -78
  21. package/dist/conformance/canonical.js +7 -10
  22. package/dist/conformance/contract-adapter.js +26 -1
  23. package/dist/conformance/corpus.js +5 -5
  24. package/dist/conformance/derive.js +6 -5
  25. package/dist/conformance/run.js +44 -6
  26. package/dist/cross-session.d.ts +4 -4
  27. package/dist/declared-capture.d.ts +41 -0
  28. package/dist/declared-capture.js +344 -0
  29. package/dist/file-edit.js +11 -15
  30. package/dist/file-store.d.ts +1 -31
  31. package/dist/file-store.js +3 -30
  32. package/dist/fleet.d.ts +11 -0
  33. package/dist/fleet.js +60 -26
  34. package/dist/fs-failure.d.ts +12 -7
  35. package/dist/fs-failure.js +53 -29
  36. package/dist/hash.d.ts +0 -9
  37. package/dist/hash.js +64 -7
  38. package/dist/hooks-config.d.ts +2 -2
  39. package/dist/hooks-config.js +129 -33
  40. package/dist/http-hook-allowlists.d.ts +27 -10
  41. package/dist/http-hook-allowlists.js +189 -25
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -1
  44. package/dist/mcp-request-entry.d.ts +13 -8
  45. package/dist/mcp-request-entry.js +246 -107
  46. package/dist/one-time-migrations.js +207 -95
  47. package/dist/own-key-read.d.ts +247 -0
  48. package/dist/own-key-read.js +449 -0
  49. package/dist/plain-json-object.d.ts +38 -3
  50. package/dist/plain-json-object.js +84 -7
  51. package/dist/portable-domains.d.ts +31 -0
  52. package/dist/portable-domains.js +30 -0
  53. package/dist/remote-exec.js +9 -1
  54. package/dist/safety-merge-spec.d.ts +88 -6
  55. package/dist/safety-merge-spec.js +246 -17
  56. package/dist/sandbox-git.d.ts +64 -0
  57. package/dist/sandbox-git.js +89 -0
  58. package/dist/scheduler-store.js +21 -12
  59. package/dist/settings-units.d.ts +85 -0
  60. package/dist/settings-units.js +177 -0
  61. package/dist/types.d.ts +6 -2
  62. package/dist/types.js +65 -56
  63. package/dist/url-shape.d.ts +27 -5
  64. package/dist/url-shape.js +29 -5
  65. package/dist/web-admin-credential.js +5 -2
  66. package/dist/web-admin-session-url.js +2 -2
  67. package/dist/worker-row-verdict.d.ts +11 -0
  68. package/dist/worker-row-verdict.js +51 -1
  69. package/dist/workers-rows.d.ts +2 -1
  70. package/dist/workers-rows.js +41 -12
  71. package/dist/zod-unwrap.d.ts +6 -0
  72. package/dist/zod-unwrap.js +14 -7
  73. package/docs/API-SURFACE.md +50 -5
  74. package/docs/CONSUMER-INTEGRATION.md +178 -0
  75. package/llms-full.txt +34 -5
  76. package/llms.txt +6 -3
  77. package/package.json +1 -1
@@ -50,11 +50,11 @@ export type CrossSessionInboundValue = (typeof CROSS_SESSION_INBOUND_VALUES)[num
50
50
  export declare const CROSS_SESSION_INBOUND_UNSET: "default";
51
51
  /** 文件层受理面:四个显式值 + `"default"`(未设)。 */
52
52
  export declare const CrossSessionInboundSetting: z.ZodEnum<{
53
+ refuse: "refuse";
53
54
  default: "default";
54
55
  wake: "wake";
55
56
  "next-turn": "next-turn";
56
57
  hold: "hold";
57
- refuse: "refuse";
58
58
  }>;
59
59
  export type CrossSessionInboundSettingValue = z.infer<typeof CrossSessionInboundSetting>;
60
60
  /** 四值闭集(CC 逐字)。`"never"` = 不设期限。 */
@@ -64,8 +64,8 @@ export type DialogExpiryValue = (typeof DIALOG_EXPIRY_VALUES)[number];
64
64
  export declare const DIALOG_EXPIRY_DEFAULT: DialogExpiryValue;
65
65
  /** 文件层受理面:四个值 + `"default"`(未设 ⇒ 落 {@link DIALOG_EXPIRY_DEFAULT})。 */
66
66
  export declare const DialogExpirySetting: z.ZodEnum<{
67
- never: "never";
68
67
  default: "default";
68
+ never: "never";
69
69
  "60s": "60s";
70
70
  "5m": "5m";
71
71
  "10m": "10m";
@@ -78,15 +78,15 @@ export type DialogExpirySettingValue = z.infer<typeof DialogExpirySetting>;
78
78
  */
79
79
  export declare const CrossSessionSettings: z.ZodObject<{
80
80
  crossSessionInbound: z.ZodOptional<z.ZodEnum<{
81
+ refuse: "refuse";
81
82
  default: "default";
82
83
  wake: "wake";
83
84
  "next-turn": "next-turn";
84
85
  hold: "hold";
85
- refuse: "refuse";
86
86
  }>>;
87
87
  dialogExpiry: z.ZodOptional<z.ZodEnum<{
88
- never: "never";
89
88
  default: "default";
89
+ never: "never";
90
90
  "60s": "60s";
91
91
  "5m": "5m";
92
92
  "10m": "10m";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 包内私有(不进主桶):**按声明键投影的快照,与校验合在一起** —— 校验器(zod)读一个对象时只读它声明的键、剥掉其余;要保护它别读到 `Object.prototype` 上被污染的继承值、别碰会抛的 getter,
3
+ * 就得给它一份**自有键副本**。整份深拷({@link captureJson})会把「校验器根本不读」的键(没声明的键里放着 `Date` / `Map` / 函数 / 环 / 一个会抛的 getter)也读进来 ——
4
+ * 一个本来读得通的 body 变成整体读不出。所以:**只捕获校验器会读的键**,而「会读哪些键」随联合选哪一臂而定,所以投影与校验一起走({@link parseDeclared})。
5
+ *
6
+ * · 对象形(`z.object`,没有 catchall —— 即「剥掉没声明的键」那一种):只读它声明的键(`shape` 的键),每个声明键的值按**对应子 schema** 递归;没声明的键一个都不读不拷;
7
+ * · **判别联合**:先按判别键(与声明键同一只描述符读)选臂,**只投影选中那一臂**的声明键;判别键**只读一次** —— 选臂的那个词就是快照里交给校验器的那个词;
8
+ * 判别键缺席 / 不是任何一臂的词 ⇒ 只交过去判别键(校验器只看它就拒);
9
+ * · **普通联合**(顶层):按序逐臂 —— 一臂一份只含该臂声明键的快照、**只拿该臂的 schema 校验**,该臂通过才接受、**第一臂通过即止**,没通过才轮到下一臂(与 zod 按序试臂一致;不取并集;
10
+ * 拿整个联合去校验的话,后臂会用默认值接受前一臂的残缺投影);**互斥联合**(`z.xor`)所有臂都走一遍、恰一臂通过才接受(两臂都通过 zod 拒,这里同样拒)。
11
+ * 嵌在对象里的普通联合、以及套着包装层的联合当不透明:整个值深拷、对整只 schema 校验;
12
+ * · `unknown` / `any`(投影走到的位置上):校验器自己不看它的内容 ⇒ 调用方的原值原样交过去(后面接的 transform 是包自己写的);
13
+ * · 其余(标量、`record`、数组、`strict` 的对象 —— 校验器要读全部键):整个值深拷(不透明)。
14
+ * 声明键的律与内核同:自有但不可枚举 ⇒ 读不成纯数据、整份拒({@link NOT_PLAIN_DATA}),**不当缺席**;自有 getter 抛 / 陷阱抛 ⇒ {@link UNINSPECTABLE};只靠继承 ⇒ 缺席。
15
+ * 任何一份快照拿不到(读不出 / 拒)⇒ 整个读不出(不再试后面的臂:v14.6.0 里校验器在那里读到的会抛)。守护在 {@link parseDeclared} 入口一处:门的 schema 内省、快照递归与校验同在里面。
16
+ *
17
+ * **门 = 闭集白名单**(`admit`,每只 schema 第一次进来时走一遍整棵 schema 树,通过的记下;递归 schema 按「节点 × 位置」只判一次):只放行**与 zod 同答**(判决与数据,对 JSON 输入)的节点种类
18
+ * —— 每一种都按 zod 4.4 / 4.6 的读法推过、有对 zod 逐例比判决与数据的钉;反例搜索不是穷举,找到不同答的形就从放行单拿掉 ——
19
+ * 叶子(不跑用户函数、没有子 schema)· 不跑看得见输入的函数的包装层(`optional` / `nullable` / `default` / `prefault`)· 后处理 `.transform(f)`(`f` 看到的是里面那只 schema 的输出,两边同一份)·
20
+ * 对象(声明键不是符号、也不叫寻常对象从 `Object.prototype` 继承来的名字 —— zod 在原值上读到继承的成员,投影读成缺席)· 判别联合(不带 `unionFallback`)· 普通 / 互斥联合 · `record` · 数组 ·
21
+ * 投影走到的位置上的 `unknown` / `any`。对象 / 数组 / record 上挂的检查(refine / superRefine / 长度 …)看到的是 zod 自己铸的新输出,两边同一份。
22
+ * 其余一律**响亮拒**(抛 `parseDeclared: unsupported schema shape — …`;这是包自己的 schema 选错了读法,不是调用方的值读不出):`.catch`(兜底函数看到整个原值 —— zod ≥ 4.6 交 `ctx.input`)·
23
+ * 输入侧变换(`z.preprocess`、字段位上单独的 `z.transform`、两侧都是 schema 的管道)· `z.custom`(谓词看到的是副本)· 深拷节点之下的 `unknown` / `any`(副本会进输出;passthrough 对象同因)·
24
+ * 声明键叫继承名 / 是符号(有限键集 record 的键同此)· loose record · 顶层普通联合自己挂的检查 · 带 `unionFallback` 的判别联合 · `tuple` / `intersection` / `map` / `set` / `lazy` / `readonly` …(没推过)。
25
+ * 不逐形补黑名单:新形默认拒。校验只问判决、不要句子(`evaluate` 给 zod 逐次 `error` 参数):副本的原型不是 `Object.prototype`,zod 拼拒句时会读副本上用户写的自有 `constructor`。
26
+ */
27
+ import { z } from "zod";
28
+ /**
29
+ * 对 `value` 校验的守护读法(`schema` 的 safeParse):校验器读的是投影后的自有键快照,不是调用方的对象。读不出 / 不是纯数据 / 校验不过 ⇒ `{ ok: false }`(不抛);
30
+ * 联合按 {@link evaluate} 的语义逐臂。`schema` 不在白名单里(见文件头)⇒ 抛 `parseDeclared: unsupported schema shape — …`(不论输入)。
31
+ * **守护边界就在这里一处**(内核的 `inspect`):白名单门对 schema 的内省(zod 的惰性属性 —— 声明键表、判别联合的 `propValues` —— 第一次被读时才物化,物化时 zod 用字面量描述符调
32
+ * `defineProperty`、往 `{}` 上按键名累加:`Object.prototype` 被污染成 `get` / `set` / `value` / 判别键名时,物化在这一步抛)、投影快照的递归(递归 schema × 很深的数据会爆栈)与校验,
33
+ * 全在同一只守护里 —— 抛了 ⇒ `{ ok: false }`(与 v14.6.0 那一层守护同向:读口答「读不出」,不把引擎的错 / 嵌入方的抛出值带出去)。
34
+ * 「不支持」是门**交回**的判语、在守护外抛:它是包自己的 schema 选错了读法,与输入无关。
35
+ */
36
+ export declare function parseDeclared<S extends z.ZodType>(schema: S, value: unknown): {
37
+ readonly ok: true;
38
+ readonly data: z.output<S>;
39
+ } | {
40
+ readonly ok: false;
41
+ };
@@ -0,0 +1,344 @@
1
+ /**
2
+ * 包内私有(不进主桶):**按声明键投影的快照,与校验合在一起** —— 校验器(zod)读一个对象时只读它声明的键、剥掉其余;要保护它别读到 `Object.prototype` 上被污染的继承值、别碰会抛的 getter,
3
+ * 就得给它一份**自有键副本**。整份深拷({@link captureJson})会把「校验器根本不读」的键(没声明的键里放着 `Date` / `Map` / 函数 / 环 / 一个会抛的 getter)也读进来 ——
4
+ * 一个本来读得通的 body 变成整体读不出。所以:**只捕获校验器会读的键**,而「会读哪些键」随联合选哪一臂而定,所以投影与校验一起走({@link parseDeclared})。
5
+ *
6
+ * · 对象形(`z.object`,没有 catchall —— 即「剥掉没声明的键」那一种):只读它声明的键(`shape` 的键),每个声明键的值按**对应子 schema** 递归;没声明的键一个都不读不拷;
7
+ * · **判别联合**:先按判别键(与声明键同一只描述符读)选臂,**只投影选中那一臂**的声明键;判别键**只读一次** —— 选臂的那个词就是快照里交给校验器的那个词;
8
+ * 判别键缺席 / 不是任何一臂的词 ⇒ 只交过去判别键(校验器只看它就拒);
9
+ * · **普通联合**(顶层):按序逐臂 —— 一臂一份只含该臂声明键的快照、**只拿该臂的 schema 校验**,该臂通过才接受、**第一臂通过即止**,没通过才轮到下一臂(与 zod 按序试臂一致;不取并集;
10
+ * 拿整个联合去校验的话,后臂会用默认值接受前一臂的残缺投影);**互斥联合**(`z.xor`)所有臂都走一遍、恰一臂通过才接受(两臂都通过 zod 拒,这里同样拒)。
11
+ * 嵌在对象里的普通联合、以及套着包装层的联合当不透明:整个值深拷、对整只 schema 校验;
12
+ * · `unknown` / `any`(投影走到的位置上):校验器自己不看它的内容 ⇒ 调用方的原值原样交过去(后面接的 transform 是包自己写的);
13
+ * · 其余(标量、`record`、数组、`strict` 的对象 —— 校验器要读全部键):整个值深拷(不透明)。
14
+ * 声明键的律与内核同:自有但不可枚举 ⇒ 读不成纯数据、整份拒({@link NOT_PLAIN_DATA}),**不当缺席**;自有 getter 抛 / 陷阱抛 ⇒ {@link UNINSPECTABLE};只靠继承 ⇒ 缺席。
15
+ * 任何一份快照拿不到(读不出 / 拒)⇒ 整个读不出(不再试后面的臂:v14.6.0 里校验器在那里读到的会抛)。守护在 {@link parseDeclared} 入口一处:门的 schema 内省、快照递归与校验同在里面。
16
+ *
17
+ * **门 = 闭集白名单**(`admit`,每只 schema 第一次进来时走一遍整棵 schema 树,通过的记下;递归 schema 按「节点 × 位置」只判一次):只放行**与 zod 同答**(判决与数据,对 JSON 输入)的节点种类
18
+ * —— 每一种都按 zod 4.4 / 4.6 的读法推过、有对 zod 逐例比判决与数据的钉;反例搜索不是穷举,找到不同答的形就从放行单拿掉 ——
19
+ * 叶子(不跑用户函数、没有子 schema)· 不跑看得见输入的函数的包装层(`optional` / `nullable` / `default` / `prefault`)· 后处理 `.transform(f)`(`f` 看到的是里面那只 schema 的输出,两边同一份)·
20
+ * 对象(声明键不是符号、也不叫寻常对象从 `Object.prototype` 继承来的名字 —— zod 在原值上读到继承的成员,投影读成缺席)· 判别联合(不带 `unionFallback`)· 普通 / 互斥联合 · `record` · 数组 ·
21
+ * 投影走到的位置上的 `unknown` / `any`。对象 / 数组 / record 上挂的检查(refine / superRefine / 长度 …)看到的是 zod 自己铸的新输出,两边同一份。
22
+ * 其余一律**响亮拒**(抛 `parseDeclared: unsupported schema shape — …`;这是包自己的 schema 选错了读法,不是调用方的值读不出):`.catch`(兜底函数看到整个原值 —— zod ≥ 4.6 交 `ctx.input`)·
23
+ * 输入侧变换(`z.preprocess`、字段位上单独的 `z.transform`、两侧都是 schema 的管道)· `z.custom`(谓词看到的是副本)· 深拷节点之下的 `unknown` / `any`(副本会进输出;passthrough 对象同因)·
24
+ * 声明键叫继承名 / 是符号(有限键集 record 的键同此)· loose record · 顶层普通联合自己挂的检查 · 带 `unionFallback` 的判别联合 · `tuple` / `intersection` / `map` / `set` / `lazy` / `readonly` …(没推过)。
25
+ * 不逐形补黑名单:新形默认拒。校验只问判决、不要句子(`evaluate` 给 zod 逐次 `error` 参数):副本的原型不是 `Object.prototype`,zod 拼拒句时会读副本上用户写的自有 `constructor`。
26
+ */
27
+ import { z } from "zod";
28
+ import { NOT_PLAIN_DATA, NO_PRIOR_READS, UNINSPECTABLE, captureJson, dataDescriptor, inspect, isolatedObject, readDeclaredKey } from "./own-key-read.js";
29
+ import { jsonShapeOf } from "./plain-json-object.js";
30
+ import { unwrapOnce } from "./zod-unwrap.js";
31
+ const failed = (sentence) => ({ ok: false, sentence });
32
+ /** 判语的句子(只在这里拼一次)。 */
33
+ const unsupportedShape = (what) => `parseDeclared: unsupported schema shape — ${what}`;
34
+ /** 包装层的层数界(schema 作者写出来的常数,8 层远超实际)。 */
35
+ const WRAPPER_BOUND = 8;
36
+ /** 剥掉投影会穿过的包装层拿内里的 schema:`optional` / `nullable` / `default` / `prefault` 与后处理 `.transform(f)`。`.catch` 与输入侧的变换不穿 —— 停在它们身上(白名单门拒它们)。 */
37
+ function innerOf(schema) {
38
+ let cur = schema;
39
+ for (let i = 0; i < WRAPPER_BOUND; i++) {
40
+ const step = unwrapOnce(cur);
41
+ if (step === undefined || step.via === "preprocess" || cur instanceof z.ZodCatch)
42
+ return cur;
43
+ cur = step.next;
44
+ }
45
+ return cur;
46
+ }
47
+ /**
48
+ * 对象 schema 的声明键表:读 `def.shape` —— zod 自己校验时读的就是这一份(它在第一次被读时才物化,物化失败下次再试);**不读** classic 面的 `object.shape`:
49
+ * 那是 zod 4.4 的另一只惰性属性,物化时抛(例:`Object.prototype` 被污染)就永远答 `undefined`,而 zod 自己校验从不读它 —— 读它会让门在污染撤掉之后仍读不出,v14.6.0 不会。
50
+ */
51
+ const shapeOf = (object) => object._zod.def.shape;
52
+ /** 普通联合(含互斥联合 `z.xor`),不是判别联合 —— `evaluate` 逐臂走它;白名单门按同一个判据认它。 */
53
+ const isPlainUnion = (schema) => schema instanceof z.ZodUnion && !(schema instanceof z.ZodDiscriminatedUnion);
54
+ /** 寻常对象从 `Object.prototype` 继承来的成员名(ES 规定的闭集)。声明键叫这些名字时,zod 在调用方的值上 `[[Get]]` 读到继承来的成员,投影把它读成缺席 —— 两边不同答。 */
55
+ const ORDINARY_OBJECT_MEMBERS = new Set(["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf", "__proto__", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__"]);
56
+ /** 走过白名单门的 schema(按根;fleet 每次请求都是那七只,不重走)。 */
57
+ const ADMITTED = new WeakSet();
58
+ /** 白名单门(见文件头):整棵 schema 树只含放行的形 ⇒ 记下、交回 `undefined`;否则交回不支持的那一句(见 {@link Refusal})。与 `evaluate` / `snapshotOf` 同一套判据(`isPlainUnion` / `innerOf`)。 */
59
+ function admit(schema) {
60
+ if (ADMITTED.has(schema))
61
+ return undefined;
62
+ const refusal = admitEvaluated(schema, new Map());
63
+ if (refusal === undefined)
64
+ ADMITTED.add(schema);
65
+ return refusal;
66
+ }
67
+ /** 一串子节点逐个过门,第一句不支持就停(交回它)。 */
68
+ function admitEach(children, at, seen) {
69
+ for (const child of children) {
70
+ const refusal = admitAt(child, at, seen);
71
+ if (refusal !== undefined)
72
+ return refusal;
73
+ }
74
+ return undefined;
75
+ }
76
+ /** `evaluate` 走的那一层:普通联合逐臂;其余是一份投影。 */
77
+ function admitEvaluated(schema, seen) {
78
+ if (isPlainUnion(schema)) {
79
+ // `evaluate` 逐臂拿**臂的** schema 校验:挂在联合本身上的检查(`.refine` / `.superRefine`)一次都不跑 —— zod 会跑,两边不同答
80
+ if ((schema._zod.def.checks ?? []).length > 0)
81
+ return "a union with checks of its own at the top (`.refine` on the union: the per-arm evaluation validates with each arm's schema, so the union's own checks would never run)";
82
+ for (const option of schema._zod.def.options) {
83
+ const refusal = admitEvaluated(option, seen);
84
+ if (refusal !== undefined)
85
+ return refusal;
86
+ }
87
+ return undefined;
88
+ }
89
+ return admitAt(schema, "projected", seen);
90
+ }
91
+ function admitAt(schema, at, seen) {
92
+ const inner = innerOf(schema);
93
+ const positions = seen.get(inner) ?? new Set();
94
+ if (positions.has(at))
95
+ return undefined;
96
+ seen.set(inner, positions.add(at));
97
+ const kind = inner._zod.def.type;
98
+ switch (kind) {
99
+ // 叶子:不跑用户函数、没有子 schema —— 校验器只看这个值本身(原始值两边是同一个;对象两边都按形拒)
100
+ case "string":
101
+ case "number":
102
+ case "int":
103
+ case "boolean":
104
+ case "bigint":
105
+ case "symbol":
106
+ case "null":
107
+ case "undefined":
108
+ case "void":
109
+ case "never":
110
+ case "nan":
111
+ case "date":
112
+ case "literal":
113
+ case "enum":
114
+ case "template_literal":
115
+ case "file":
116
+ return undefined;
117
+ case "unknown":
118
+ case "any":
119
+ return at === "copied" ? "`unknown` / `any` under a copied node (record / array / a union inside a field / a catchall — passthrough included): the copy, not the caller's value, would reach the output" : undefined;
120
+ case "object": {
121
+ const object = inner;
122
+ const shape = shapeOf(object);
123
+ if (Object.getOwnPropertySymbols(shape).length > 0)
124
+ return "a symbol key (the projection reads string keys only)";
125
+ const catchall = object._zod.def.catchall;
126
+ for (const key of Object.keys(shape)) {
127
+ if (ORDINARY_OBJECT_MEMBERS.has(key))
128
+ return `a declared key named \`${key}\` (zod reads the member an ordinary object inherits; the projection reads it as absent)`;
129
+ const refusal = admitAt(shape[key], catchall === undefined ? at : "copied", seen);
130
+ if (refusal !== undefined)
131
+ return refusal;
132
+ }
133
+ return catchall !== undefined ? admitAt(catchall, "copied", seen) : undefined;
134
+ }
135
+ case "union": {
136
+ const options = inner._zod.def.options;
137
+ if (inner instanceof z.ZodDiscriminatedUnion) {
138
+ if (inner._zod.def.unionFallback === true)
139
+ return "a discriminated union with `unionFallback` (zod falls back to trying every arm on the full value; the projection of one arm's declared keys cannot say what it would do)";
140
+ return admitEach(options, at, seen);
141
+ }
142
+ return admitEach(options, "copied", seen); // 走到这里的普通联合是不透明的(嵌在字段里 / 套着包装层):整个值深拷
143
+ }
144
+ case "record": {
145
+ const record = inner;
146
+ if (record._zod.def.mode === "loose")
147
+ return "a loose record (keys the key schema does not match ride through by reference — the copy, not the caller's value, would reach the output)";
148
+ // 有限键集(枚举 / 字面量键):zod 逐个按键名在值上 `[[Get]]` —— 键名是继承名时读到继承的成员,副本上读成缺席(与对象声明键同一条)
149
+ for (const key of record._zod.def.keyType._zod.values ?? []) {
150
+ if (typeof key === "string" && ORDINARY_OBJECT_MEMBERS.has(key))
151
+ return `a record key named \`${key}\` in a finite key set (zod reads the member an ordinary object inherits; the copy reads it as absent)`;
152
+ }
153
+ return admitEach([record._zod.def.keyType, record._zod.def.valueType], "copied", seen);
154
+ }
155
+ case "array":
156
+ return admitAt(inner.element, "copied", seen);
157
+ case "catch":
158
+ return "`.catch` (its fallback sees the caller's whole value — zod ≥ 4.6 hands it `ctx.input`; a projection changes what it sees)";
159
+ case "pipe":
160
+ return `a pipe the projection does not pass through — an input-side \`z.preprocess(f, …)\` or \`a.pipe(b)\` (the function / the second schema would see a projection or a copy, not what zod sees), or wrappers nested deeper than ${WRAPPER_BOUND} layers`;
161
+ case "transform":
162
+ return "a bare `z.transform(f)` (an input-side function: it would see the projection, not the caller's value)";
163
+ case "custom":
164
+ return "`z.custom` / `z.instanceof` (the predicate would see the projection, not the caller's value)";
165
+ case "optional":
166
+ case "nullable":
167
+ case "default":
168
+ case "prefault":
169
+ return `wrappers nested deeper than ${WRAPPER_BOUND} layers`;
170
+ case "tuple":
171
+ case "intersection":
172
+ case "map":
173
+ case "set":
174
+ case "lazy":
175
+ case "promise":
176
+ case "function":
177
+ case "readonly":
178
+ case "nonoptional":
179
+ case "success":
180
+ return `\`${kind}\` (not on the allow-list: the projection has not been proven to answer as zod does)`;
181
+ default: {
182
+ const unhandled = kind;
183
+ return `an unknown schema kind \`${String(unhandled)}\``;
184
+ }
185
+ }
186
+ }
187
+ /** 一个值要不要进对象投影:是朴素对象 ⇒ `undefined`(继续投影);不是对象(`null` / 原始值)⇒ 原样交过去(校验器自己拒);数组 / 类实例 / 读不出 ⇒ 深拷(同一套坏形与句子)。 */
188
+ function nonObjectSnapshot(value) {
189
+ const kind = jsonShapeOf(value);
190
+ switch (kind) {
191
+ case "plain":
192
+ return undefined;
193
+ case "null":
194
+ case "primitive":
195
+ return { ok: true, value };
196
+ case "array":
197
+ case "non_plain_object":
198
+ case "uninspectable":
199
+ return captureJson(value);
200
+ default: {
201
+ const unhandled = kind;
202
+ throw new Error(`declared-capture: unhandled shape ${String(unhandled)}`);
203
+ }
204
+ }
205
+ }
206
+ /** 对象形投影:声明键逐个读(自有可枚举数据属性 ⇒ 取值按子 schema 递归;继承 / 缺席 ⇒ 跳过;隐藏 / 访问器 / 读不出 ⇒ 整份拒)。`prior` 里已有的键用那一次的读数,不再读。 */
207
+ function objectSnapshot(shape, value, prior) {
208
+ const skip = nonObjectSnapshot(value);
209
+ if (skip !== undefined)
210
+ return skip;
211
+ const copy = isolatedObject();
212
+ for (const key of Object.keys(shape)) {
213
+ const read = prior.get(key) ?? readDeclaredKey(value, key);
214
+ switch (read.presence) {
215
+ case "own": {
216
+ const child = snapshotOf(shape[key], read.value, NO_PRIOR_READS);
217
+ if (!child.ok)
218
+ return child;
219
+ Object.defineProperty(copy, key, dataDescriptor(child.value));
220
+ break;
221
+ }
222
+ case "inherited":
223
+ case "absent":
224
+ break;
225
+ case "hidden":
226
+ return failed(NOT_PLAIN_DATA);
227
+ case "unreadable":
228
+ return failed(UNINSPECTABLE);
229
+ default: {
230
+ const unhandled = read;
231
+ throw new Error(`declared-capture: unhandled presence ${String(unhandled.presence)}`);
232
+ }
233
+ }
234
+ }
235
+ return { ok: true, value: copy };
236
+ }
237
+ /**
238
+ * 判别联合:按判别键选臂,只投影那一臂;判别键缺席 / 不是任何一臂的词 ⇒ 只交过去判别键。**判别键只读一次**:这一次的读数交给选中那一臂的投影复用(`prior`),
239
+ * 所以快照里的判别词就是选臂的那个词 —— 校验器拿快照按判别键派发,落到的就是选中的这一臂(没有第二次读,也就没有「第二次读到另一个词、落到另一臂」)。
240
+ */
241
+ function discriminatedSnapshot(union, value, prior) {
242
+ const skip = nonObjectSnapshot(value);
243
+ if (skip !== undefined)
244
+ return skip;
245
+ const discriminator = union._zod.def.discriminator;
246
+ const read = prior.get(discriminator) ?? readDeclaredKey(value, discriminator);
247
+ if (read.presence === "hidden")
248
+ return failed(NOT_PLAIN_DATA);
249
+ if (read.presence === "unreadable")
250
+ return failed(UNINSPECTABLE);
251
+ const word = read.presence === "own" ? read.value : undefined;
252
+ const arms = union._zod.def.options;
253
+ const chosen = arms.find((arm) => innerOf(arm)._zod.propValues?.[discriminator]?.has(word) === true);
254
+ if (chosen !== undefined)
255
+ return snapshotOf(chosen, value, new Map(prior).set(discriminator, read));
256
+ // 没有一臂认这个词(或键缺席):校验器只看判别键就拒,别的键一个都不碰
257
+ const only = isolatedObject();
258
+ if (read.presence === "own") {
259
+ const kept = captureJson(word);
260
+ if (!kept.ok)
261
+ return kept;
262
+ Object.defineProperty(only, discriminator, dataDescriptor(kept.value));
263
+ }
264
+ return { ok: true, value: only };
265
+ }
266
+ /**
267
+ * `schema` 在 `value` 上会读的键的快照(恰一份;普通联合不在这里 —— 它逐臂各走一遍,见 `evaluate`;嵌在对象里的普通联合当不透明,整个值深拷)。
268
+ * `prior`:同一个值上已经读过的声明键(判别联合读过的判别键;投影走进字段值时是一张新的空表)—— **快照里这些键的在场与值恒是那一次的读数,不读第二次**:投影直接复用;
269
+ * 整份深拷的臂(`strict` / 带 catchall 的对象)把它交给 `captureJson`,列成员时这些键取那一次的读数、不再问描述符(所以判别联合选臂的词就是交给校验器的词,不论臂怎么拍快照)。
270
+ */
271
+ function snapshotOf(schema, value, prior) {
272
+ const inner = innerOf(schema);
273
+ if (inner instanceof z.ZodUnknown || inner instanceof z.ZodAny)
274
+ return { ok: true, value };
275
+ if (inner instanceof z.ZodDiscriminatedUnion)
276
+ return discriminatedSnapshot(inner, value, prior);
277
+ if (inner instanceof z.ZodObject && inner._zod.def.catchall === undefined)
278
+ return objectSnapshot(shapeOf(inner), value, prior);
279
+ return captureJson(value, "isolated", prior);
280
+ }
281
+ /**
282
+ * 按 zod 的联合语义逐臂各走一遍:一臂 = 只含该臂声明键的快照 + **只拿该臂的 schema 校验**(拿整个联合去校验的话,后臂会用默认值接受前一臂的残缺投影)。
283
+ * · 普通联合:按序,第一臂通过即止(不取并集);
284
+ * · 互斥联合(`z.xor`:`def.inclusive === false`):**所有**臂都走一遍,恰一臂通过才接受 —— 两臂都通过 zod 拒(这里同样拒,不是取第一臂丢掉另一臂的键);
285
+ * · 其余(含判别联合:它按判别键选臂,见 `discriminatedSnapshot`):恰一份快照、拿整只 schema 校验。
286
+ * 联合外面套着包装层(`optional` / `default` / `catch` …)⇒ 当不透明(整个值深拷、对整只 schema 校验),包装层的语义由 zod 自己走。
287
+ */
288
+ function evaluate(schema, value) {
289
+ if (isPlainUnion(schema)) {
290
+ const exclusive = schema._zod.def.inclusive === false;
291
+ let accepted;
292
+ for (const option of schema._zod.def.options) {
293
+ const verdict = evaluate(option, value);
294
+ if (verdict.kind === "unreadable")
295
+ return verdict;
296
+ if (verdict.kind !== "ok")
297
+ continue;
298
+ if (!exclusive)
299
+ return verdict;
300
+ if (accepted !== undefined)
301
+ return { kind: "rejected" }; // xor:两臂都通过 ⇒ 拒
302
+ accepted = verdict;
303
+ }
304
+ return accepted ?? { kind: "rejected" };
305
+ }
306
+ const snapshot = snapshotOf(schema, value, NO_PRIOR_READS);
307
+ if (!snapshot.ok)
308
+ return { kind: "unreadable" };
309
+ // 只问判决、不要句子(parseDeclared 从不交出拒因):逐次 `error` 参数让 zod 不进 locale 拼句 —— 拼句会对副本做 `parsedType`(副本的原型不是 Object.prototype,
310
+ // 用户写的自有 `constructor` 键会被读成类型名、转串时抛),联合 / 互斥联合会因此把一臂判成「读不出」、不再试后面的臂,与 zod 在原值上的判决不同。只读 `success` / `data`,
311
+ // 不构造错误对象(所以不用 `guardedSafeParse`);校验过程抛 ⇒ 入口那一只守护(`inspect`)接住
312
+ const parsed = schema.safeParse(snapshot.value, { error: () => "rejected" });
313
+ return parsed.success ? { kind: "ok", data: parsed.data } : { kind: "rejected" };
314
+ }
315
+ /**
316
+ * 对 `value` 校验的守护读法(`schema` 的 safeParse):校验器读的是投影后的自有键快照,不是调用方的对象。读不出 / 不是纯数据 / 校验不过 ⇒ `{ ok: false }`(不抛);
317
+ * 联合按 {@link evaluate} 的语义逐臂。`schema` 不在白名单里(见文件头)⇒ 抛 `parseDeclared: unsupported schema shape — …`(不论输入)。
318
+ * **守护边界就在这里一处**(内核的 `inspect`):白名单门对 schema 的内省(zod 的惰性属性 —— 声明键表、判别联合的 `propValues` —— 第一次被读时才物化,物化时 zod 用字面量描述符调
319
+ * `defineProperty`、往 `{}` 上按键名累加:`Object.prototype` 被污染成 `get` / `set` / `value` / 判别键名时,物化在这一步抛)、投影快照的递归(递归 schema × 很深的数据会爆栈)与校验,
320
+ * 全在同一只守护里 —— 抛了 ⇒ `{ ok: false }`(与 v14.6.0 那一层守护同向:读口答「读不出」,不把引擎的错 / 嵌入方的抛出值带出去)。
321
+ * 「不支持」是门**交回**的判语、在守护外抛:它是包自己的 schema 选错了读法,与输入无关。
322
+ */
323
+ export function parseDeclared(schema, value) {
324
+ const run = inspect(() => {
325
+ const refusal = admit(schema);
326
+ return refusal !== undefined ? { kind: "unsupported", what: refusal } : evaluate(schema, value);
327
+ });
328
+ if (!run.ok)
329
+ return { ok: false };
330
+ const verdict = run.value;
331
+ switch (verdict.kind) {
332
+ case "unsupported":
333
+ throw new Error(unsupportedShape(verdict.what));
334
+ case "ok":
335
+ return { ok: true, data: verdict.data };
336
+ case "rejected":
337
+ case "unreadable":
338
+ return { ok: false };
339
+ default: {
340
+ const unhandled = verdict;
341
+ throw new Error(`parseDeclared: unhandled verdict ${String(unhandled.kind)}`);
342
+ }
343
+ }
344
+ }
package/dist/file-edit.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { mkdir } from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import { writeFileAtomic } from "./atomic-write.js";
11
+ import { emptyDocument, guardedSafeParse } from "./own-key-read.js";
11
12
  import { collectEnvRefs } from "./secret-refs.js";
12
13
  import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
13
14
  import { configDDocumentText } from "./config-d-file.js";
@@ -33,28 +34,23 @@ const DOMAINS_LIST = Object.keys(DOMAIN_SCHEMAS).join(", ");
33
34
  * (clear message) if `domain` is not a real domain name (a misuse, not invalid user data). */
34
35
  export function validateDomain(domain, raw) {
35
36
  assertDomainName(domain);
36
- let r;
37
- try {
38
- r = DOMAIN_SCHEMAS[domain].safeParse(raw === undefined ? {} : raw); // explicit null is corrupt, not defaults (R4)
39
- }
40
- catch {
37
+ // explicit null is corrupt, not defaults (R4); the first read of `error` sits in the same guard as `safeParse` (zod 4.6 builds the sentences on that read)
38
+ const schema = DOMAIN_SCHEMAS[domain];
39
+ const judged = guardedSafeParse(() => schema.safeParse(raw === undefined ? emptyDocument() : raw), sanitizeZodError);
40
+ if (!judged.ok) {
41
41
  // a hostile getter/Proxy trap threw during traversal — never surface its (secret-bearing) message. R13.
42
42
  return { ok: false, issues: [{ code: "custom", ...buildQuotedIssue `invalid "${fixed(domain)}" config (a value could not be read during validation)`, path: [] }] };
43
43
  }
44
- if (r.success)
45
- return { ok: true, value: r.data };
46
- return { ok: false, issues: sanitizeZodError(r.error).issues }; // recursive redaction (R13/R14) — no secret echo
44
+ if (judged.value.success)
45
+ return { ok: true, value: judged.value.data };
46
+ return { ok: false, issues: judged.value.error.issues }; // redacted inside the guard (R13/R14) — no secret echo
47
47
  }
48
48
  /** Same, for the remote-exec contract (a standalone contract, not a DOMAIN_SCHEMAS member). */
49
49
  export function validateRemoteExec(raw) {
50
- let r;
51
- try {
52
- r = RemoteExecSpec.safeParse(raw);
53
- }
54
- catch {
50
+ const judged = guardedSafeParse(() => RemoteExecSpec.safeParse(raw), sanitizeZodError);
51
+ if (!judged.ok)
55
52
  return { ok: false, issues: [{ code: "custom", ...buildQuotedIssue `invalid remoteExec (a value could not be read during validation)`, path: [] }] };
56
- }
57
- return r.success ? { ok: true, value: r.data } : { ok: false, issues: sanitizeZodError(r.error).issues }; // no secret echo (R14)
53
+ return judged.value.success ? { ok: true, value: judged.value.data } : { ok: false, issues: judged.value.error.issues }; // redacted inside the guard (R14)
58
54
  }
59
55
  /** Absolute path to a domain's JSON file under a config root (mirrors FileConfigStore.domainPath). Guards the
60
56
  * name (no `../../secret` traversal even if a caller bypasses the DomainName type). */
@@ -1,22 +1,7 @@
1
1
  import { type EffectiveReadWarning } from "./config-fns.js";
2
2
  import type { ConfigReader } from "./reader.js";
3
3
  import { type DomainConfig, type DomainName, type EffectiveConfig, type RetiredDomainName } from "./types.js";
4
- /** The domains a single-machine TOC carries: what `FileConfigStore` reads out of `config.d/`, and the
5
- * **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
6
- * {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
7
- *
8
- * ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
9
- * / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
10
- * bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
11
- * `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
12
- *
13
- * 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
14
- * 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
15
- * 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
16
- * 今日的兜底:漏登记的文件会被 {@link FileConfigStore} 的 `"unread-config-file"` 警告点名(fail-loud,
17
- * 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
18
- export declare const PORTABLE_DOMAINS: readonly ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
19
- export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
4
+ export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS, type PortableDomain } from "./portable-domains.js";
20
5
  /** `config.d/` 里**合法存在但不是域文档**的文件名(不进 `"unread-config-file"` 告警)。`remote-exec`
21
6
  * 不是 `DOMAIN_SCHEMAS` 成员(resolver 不读它),按 service 表决住在这个目录,由 loadRemoteExec 读
22
7
  * ——**它有自己的读取与校验路径**,所以既不是「未知文件」也不是「域文件」。
@@ -24,21 +9,6 @@ export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
24
9
  * **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
25
10
  * 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
26
11
  export declare const NON_DOMAIN_CONFIG_FILES: ReadonlySet<string>;
27
- /**
28
- * 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
29
- *
30
- * `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
31
- * 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
32
- * (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
33
- * 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
34
- */
35
- export declare const NON_PORTABLE_DOMAIN_REASONS: {
36
- readonly workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。";
37
- readonly hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。";
38
- readonly systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。";
39
- readonly projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。";
40
- readonly entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。";
41
- };
42
12
  /** One unreadable/corrupt domain FILE surfaced by `getEffective({tolerant:true})`(0.10.12,[897]②)。
43
13
  * `error` 是 #readDomainRaw 的 sanitized 文案(带文件路径语境、绝不回显文件内容)。 */
44
14
  export type DomainReadError = {
@@ -29,25 +29,13 @@ import path from "node:path";
29
29
  import { buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud } from "./config-fns.js";
30
30
  import { configDPath, readConfigDFile, refuseRetiredDomainFiles } from "./config-d-file.js";
31
31
  import { stableHash } from "./hash.js";
32
+ import { PORTABLE_DOMAINS } from "./portable-domains.js";
32
33
  import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
33
34
  import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
34
35
  /** The retired domain names (`RETIRED_DOMAINS` keys) — read from `config.d/` for their tombstone only. */
35
36
  const RETIRED_DOMAIN_NAMES = Object.keys(RETIRED_DOMAINS);
36
- /** The domains a single-machine TOC carries: what `FileConfigStore` reads out of `config.d/`, and the
37
- * **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
38
- * {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
39
- *
40
- * ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
41
- * / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
42
- * bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
43
- * `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
44
- *
45
- * 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
46
- * 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
47
- * 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
48
- * 今日的兜底:漏登记的文件会被 {@link FileConfigStore} 的 `"unread-config-file"` 警告点名(fail-loud,
49
- * 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
50
- export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
37
+ // 便携域表的定义住在纯叶 ./portable-domains.ts(14.6.0;零 node 依赖,主桶模块可 import);这里原名转出口,`./node` 子路径逐名不变。
38
+ export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS } from "./portable-domains.js";
51
39
  /** `config.d/` 里**合法存在但不是域文档**的文件名(不进 `"unread-config-file"` 告警)。`remote-exec`
52
40
  * 不是 `DOMAIN_SCHEMAS` 成员(resolver 不读它),按 service 表决住在这个目录,由 loadRemoteExec 读
53
41
  * ——**它有自己的读取与校验路径**,所以既不是「未知文件」也不是「域文件」。
@@ -55,21 +43,6 @@ export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins"
55
43
  * **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
56
44
  * 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
57
45
  export const NON_DOMAIN_CONFIG_FILES = new Set([`${REMOTE_EXEC_DOCUMENT}.json`]);
58
- /**
59
- * 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
60
- *
61
- * `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
62
- * 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
63
- * (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
64
- * 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
65
- */
66
- export const NON_PORTABLE_DOMAIN_REASONS = {
67
- workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。",
68
- hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。",
69
- systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。",
70
- projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。",
71
- entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。",
72
- };
73
46
  export class FileConfigStore {
74
47
  root;
75
48
  opts;
package/dist/fleet.d.ts CHANGED
@@ -548,6 +548,17 @@ export declare const FLEET_GENERATION_ENV = "FLEET_GENERATION";
548
548
  * {@link FLEET_GENERATION_ENV} it is a reserved worker env name (`WorkerSpec.env` / `secretEnv` cannot set it).
549
549
  */
550
550
  export declare const FLEET_SCOPE_ENV = "FLEET_SCOPE";
551
+ /**
552
+ * The env name a fleet member's advertise address travels under — the base URL this instance tells the control plane it can be
553
+ * reached at; the control plane appends request paths to it. The operator or the config renderer sets it (on Kubernetes the renderer
554
+ * writes a `$(POD_IP)` placeholder in the value and the kubelet expands it), and the engine reads it back. Both sides import THIS
555
+ * constant; neither spells the name. The address is a base that request paths are appended to, so a reader judges a present, non-empty
556
+ * value with `pathBaseUrlFlaws` (main barrel) — the one judge of every such seat, which forbids a credential, a
557
+ * query and a fragment there and allows a path prefix; judge the value AFTER the deployment has expanded its placeholders. Absent or
558
+ * the empty string means "no address declared" (the `${NAME:-}` idiom of a compose file), and that reading is the caller's, not the
559
+ * judge's.
560
+ */
561
+ export declare const FLEET_ADVERTISE_ADDRESS_ENV = "FLEET_ADVERTISE_ADDRESS";
551
562
  /**
552
563
  * The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
553
564
  * engine renews it with web-admin; web-admin answers per the lifecycle object it holds for that instance. Three constants