@sema-agent/settings-schema 14.6.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.
- package/CHANGELOG.md +266 -0
- package/README.md +31 -3
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/config-d-file.js +33 -15
- package/dist/config-fns.js +142 -78
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.js +43 -6
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +11 -0
- package/dist/fleet.js +59 -26
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp-request-entry.d.ts +9 -7
- package/dist/mcp-request-entry.js +89 -82
- package/dist/one-time-migrations.js +207 -95
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/types.d.ts +6 -2
- package/dist/types.js +11 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +51 -1
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +35 -4
- package/docs/CONSUMER-INTEGRATION.md +178 -0
- package/llms-full.txt +23 -4
- package/llms.txt +3 -2
- package/package.json +1 -1
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
45
|
-
return { ok: true, value:
|
|
46
|
-
return { ok: false, issues:
|
|
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
|
-
|
|
51
|
-
|
|
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). */
|
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
|
package/dist/fleet.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { INTEGER } from "./integer.js";
|
|
22
22
|
import { fnv1a64 } from "./hash.js";
|
|
23
|
+
import { parseDeclared } from "./declared-capture.js";
|
|
24
|
+
import { readDeclaredKey, ownValueOf } from "./own-key-read.js";
|
|
23
25
|
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
24
26
|
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
25
27
|
// ── Web-admin's fleet endpoints (paths, one spelling) ─────────────────────────────────────────────────────
|
|
@@ -164,24 +166,41 @@ export const RestartSignal = z.object({
|
|
|
164
166
|
since: EpochMs,
|
|
165
167
|
withheld: z.boolean().optional(),
|
|
166
168
|
});
|
|
167
|
-
|
|
168
|
-
* The ONE guarded body judgement behind every wire-shape read in this file: `pick(body)` — one key of the body, or the body itself —
|
|
169
|
-
* is judged against that shape's one schema, and the verdict says WHY when it is not a read: `not-an-object` (the body is null, an
|
|
170
|
-
* array or a scalar) or `invalid` (the shape rejected it, or an accessor threw). It never throws (a probe must not die on one odd
|
|
171
|
-
* body). Almost every reader wants only "a read or nothing" and goes through {@link readSignalBody}; the one reader whose
|
|
172
|
-
* failure direction matters ({@link readCapabilityReading}) takes the reason. A second hand-written guard anywhere else in this
|
|
173
|
-
* package is the defect, not a convenience.
|
|
174
|
-
*/
|
|
169
|
+
const WHOLE_BODY = Symbol("whole body");
|
|
175
170
|
function judgeSignalBody(body, pick, schema) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
171
|
+
if (!isPlainJsonObject(body))
|
|
172
|
+
return { kind: "not-an-object" };
|
|
173
|
+
// the carrier key is read from the body's OWN keys (a polluted `Object.prototype.engineSignal` is not a signal the body carries) with the SAME descriptor check as a declared key:
|
|
174
|
+
// an own key that cannot be read as plain data (hidden — non-enumerable —, or an accessor that throws) is no readable signal, never "absent"
|
|
175
|
+
let target = body;
|
|
176
|
+
if (pick !== WHOLE_BODY) {
|
|
177
|
+
const carried = readDeclaredKey(body, pick);
|
|
178
|
+
switch (carried.presence) {
|
|
179
|
+
case "own":
|
|
180
|
+
target = carried.value;
|
|
181
|
+
break;
|
|
182
|
+
case "inherited":
|
|
183
|
+
case "absent":
|
|
184
|
+
target = undefined;
|
|
185
|
+
break;
|
|
186
|
+
case "hidden":
|
|
187
|
+
case "unreadable":
|
|
188
|
+
return { kind: "invalid" };
|
|
189
|
+
default: {
|
|
190
|
+
const unhandled = carried;
|
|
191
|
+
throw new Error(`judgeSignalBody: unhandled presence ${String(unhandled.presence)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
181
194
|
}
|
|
182
|
-
|
|
195
|
+
// the validator reads members with `[[Get]]`, required keys included: a key the body does not carry would be filled from a polluted `Object.prototype` (a granted lease from an empty reply, a
|
|
196
|
+
// ready signal from `{}`, a "declared" capability from nothing) — so it never sees the caller's object: it reads an own-key snapshot of exactly the keys it declares (for a union: of
|
|
197
|
+
// the arm it will try), on the kernel's isolated prototype `COPY_PROTOTYPE` (no inherited members; it still answers `toString` / `valueOf`), and a value that cannot be captured
|
|
198
|
+
// (an accessor — throwing or not —, a hidden own key, a trap that throws, a cycle, not plain data) is `invalid`. `parseDeclared` admits only a closed allow-list of schema shapes, each
|
|
199
|
+
// reasoned and pinned against zod's own verdict and data; any other shape is a loud throw — the package chose the wrong reader, not a caller value that cannot be read
|
|
200
|
+
const parsed = parseDeclared(schema, target);
|
|
201
|
+
if (!parsed.ok)
|
|
183
202
|
return { kind: "invalid" };
|
|
184
|
-
}
|
|
203
|
+
return { kind: "read", value: parsed.data };
|
|
185
204
|
}
|
|
186
205
|
/**
|
|
187
206
|
* The reader behind every wire-shape read in this file that has no use for a reason: the engine-owned signals on /health
|
|
@@ -193,8 +212,6 @@ function readSignalBody(body, pick, schema) {
|
|
|
193
212
|
const judged = judgeSignalBody(body, pick, schema);
|
|
194
213
|
return judged.kind === "read" ? judged.value : undefined;
|
|
195
214
|
}
|
|
196
|
-
/** `pick` for a shape whose keys sit flat on the body itself (as opposed to under one key of it). */
|
|
197
|
-
const wholeBody = (body) => body;
|
|
198
215
|
/**
|
|
199
216
|
* The model-refusal cause codes (S-768) — CLOSED sets, owned HERE, never a hand-copied second list. Adding a code is
|
|
200
217
|
* one edit in this file; who goes red at compile time: a reader that switches on the code exhaustively (a web-admin), and
|
|
@@ -285,11 +302,14 @@ const isModelPlaneRefusalCode = (c) => MODEL_PLANE_REFUSAL_CODES.includes(c);
|
|
|
285
302
|
* at all — it answers the key only and never judges the engine's version (see {@link InstanceAnnouncement}`.modelRefusals`).
|
|
286
303
|
*/
|
|
287
304
|
export function readModelRefusals(body) {
|
|
288
|
-
const wire = readSignalBody(body,
|
|
305
|
+
const wire = readSignalBody(body, "modelRefusals", ModelRefusalsWire);
|
|
289
306
|
if (wire === undefined)
|
|
290
307
|
return undefined;
|
|
291
308
|
const entries = Object.fromEntries(Object.entries(wire.entries).filter((e) => isModelEntryRefusalCode(e[1])));
|
|
292
|
-
|
|
309
|
+
// the validated output is an ordinary object: an absent optional `plane` stays absent (own key only), never read off a polluted `Object.prototype`
|
|
310
|
+
const planeRead = ownValueOf(wire, "plane");
|
|
311
|
+
const planeWord = planeRead.ok ? planeRead.value : undefined;
|
|
312
|
+
const plane = typeof planeWord === "string" && isModelPlaneRefusalCode(planeWord) ? planeWord : undefined;
|
|
293
313
|
if (Object.keys(entries).length === 0 && plane === undefined)
|
|
294
314
|
return undefined;
|
|
295
315
|
return plane === undefined ? { entries } : { entries, plane };
|
|
@@ -363,7 +383,7 @@ export const ENGINE_SIGNAL_KEY = "engineSignal";
|
|
|
363
383
|
* dialect; the carrier is the one key).
|
|
364
384
|
*/
|
|
365
385
|
export function readEngineSignal(body) {
|
|
366
|
-
return readSignalBody(body,
|
|
386
|
+
return readSignalBody(body, ENGINE_SIGNAL_KEY, EngineSignal);
|
|
367
387
|
}
|
|
368
388
|
/** The `/health` self-description every orchestrator reads. Liveness and readiness are deliberately SPLIT: `status` stays ok while
|
|
369
389
|
* draining (the process is alive); readiness is {@link judgeReadiness} over the engine signal the body carries under
|
|
@@ -556,6 +576,17 @@ export const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
|
556
576
|
* {@link FLEET_GENERATION_ENV} it is a reserved worker env name (`WorkerSpec.env` / `secretEnv` cannot set it).
|
|
557
577
|
*/
|
|
558
578
|
export const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
579
|
+
/**
|
|
580
|
+
* The env name a fleet member's advertise address travels under — the base URL this instance tells the control plane it can be
|
|
581
|
+
* reached at; the control plane appends request paths to it. The operator or the config renderer sets it (on Kubernetes the renderer
|
|
582
|
+
* writes a `$(POD_IP)` placeholder in the value and the kubelet expands it), and the engine reads it back. Both sides import THIS
|
|
583
|
+
* constant; neither spells the name. The address is a base that request paths are appended to, so a reader judges a present, non-empty
|
|
584
|
+
* value with `pathBaseUrlFlaws` (main barrel) — the one judge of every such seat, which forbids a credential, a
|
|
585
|
+
* query and a fragment there and allows a path prefix; judge the value AFTER the deployment has expanded its placeholders. Absent or
|
|
586
|
+
* the empty string means "no address declared" (the `${NAME:-}` idiom of a compose file), and that reading is the caller's, not the
|
|
587
|
+
* judge's.
|
|
588
|
+
*/
|
|
589
|
+
export const FLEET_ADVERTISE_ADDRESS_ENV = "FLEET_ADVERTISE_ADDRESS";
|
|
559
590
|
/**
|
|
560
591
|
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
561
592
|
* engine renews it with web-admin; web-admin answers per the lifecycle object it holds for that instance. Three constants
|
|
@@ -647,14 +678,14 @@ export const SERVING_LEASE_VERDICTS = ["granted", "denied", "unknown"];
|
|
|
647
678
|
/** Web-admin side: the request body of a serving-lease renewal. Shape mismatch / non-object / throwing accessor ⇒ `undefined`,
|
|
648
679
|
* never throws (same guarded reader as every other read in this file). */
|
|
649
680
|
export function readServingLeaseRequest(body) {
|
|
650
|
-
return readSignalBody(body,
|
|
681
|
+
return readSignalBody(body, WHOLE_BODY, ServingLeaseRequest);
|
|
651
682
|
}
|
|
652
683
|
/** Engine side: the reply to a renewal, judged by its `verdict` key: `granted` needs a valid `ttlMs`, `denied` is `denied`
|
|
653
684
|
* whatever its `reason` (see {@link ServingLeaseReading}), `unknown` is `unknown`. A non-object, a throwing accessor, a
|
|
654
685
|
* `verdict` outside the three words or a `granted` without a valid `ttlMs` ⇒ `undefined`, never throws — and `undefined`
|
|
655
686
|
* is read like an unreachable web-admin, never like `denied` and never like `granted`. */
|
|
656
687
|
export function readServingLeaseResponse(body) {
|
|
657
|
-
return readSignalBody(body,
|
|
688
|
+
return readSignalBody(body, WHOLE_BODY, ServingLeaseReadFace);
|
|
658
689
|
}
|
|
659
690
|
/**
|
|
660
691
|
* What web-admin says it can do: capability bits an engine reads before it starts using a web-admin-side endpoint.
|
|
@@ -704,14 +735,16 @@ const CapabilitiesCarrier = z.object({ [HEARTBEAT_REPLY_CAPABILITIES_KEY]: WebAd
|
|
|
704
735
|
* other reader here.
|
|
705
736
|
*/
|
|
706
737
|
export function readCapabilityReading(reply) {
|
|
707
|
-
const judged = judgeSignalBody(reply,
|
|
738
|
+
const judged = judgeSignalBody(reply, WHOLE_BODY, CapabilitiesCarrier);
|
|
708
739
|
switch (judged.kind) {
|
|
709
740
|
case "not-an-object":
|
|
710
741
|
return { kind: "unreadable", why: "not-an-object" };
|
|
711
742
|
case "invalid":
|
|
712
743
|
return { kind: "unreadable", why: "invalid-capabilities" };
|
|
713
744
|
case "read": {
|
|
714
|
-
|
|
745
|
+
// the validated output is an ordinary object: an absent optional key must stay absent, not be read off a polluted `Object.prototype` — own key only (it was parsed from a snapshot, so it cannot be unreadable)
|
|
746
|
+
const carried = ownValueOf(judged.value, HEARTBEAT_REPLY_CAPABILITIES_KEY);
|
|
747
|
+
const capabilities = carried.ok ? carried.value : undefined;
|
|
715
748
|
return capabilities === undefined ? { kind: "undeclared" } : { kind: "declared", capabilities };
|
|
716
749
|
}
|
|
717
750
|
default: {
|
|
@@ -755,11 +788,11 @@ export const DrainReceiptState = z.enum(DRAIN_RECEIPT_STATES);
|
|
|
755
788
|
export const DrainReceipt = EngineDraining.extend({ state: DrainReceiptState });
|
|
756
789
|
/** Engine side: the request body of the drain endpoint. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
757
790
|
export function readDrainRequest(body) {
|
|
758
|
-
return readSignalBody(body,
|
|
791
|
+
return readSignalBody(body, WHOLE_BODY, DrainRequest);
|
|
759
792
|
}
|
|
760
793
|
/** Caller side: the drain endpoint's answer. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
761
794
|
export function readDrainReceipt(body) {
|
|
762
|
-
return readSignalBody(body,
|
|
795
|
+
return readSignalBody(body, WHOLE_BODY, DrainReceipt);
|
|
763
796
|
}
|
|
764
797
|
// ── Domain 3: dispatch policy (traffic + failover + LB — three domains, one gateway) ──────────────────────
|
|
765
798
|
export const DISPATCH_STRATEGIES = ["round-robin", "least-inflight", "consistent-hash"];
|