@sema-agent/settings-schema 14.6.0 → 14.8.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 (95) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/README.md +89 -5
  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.d.ts +15 -0
  16. package/dist/atomic-write.js +109 -5
  17. package/dist/catalog-manages.d.ts +15 -0
  18. package/dist/catalog-manages.js +60 -0
  19. package/dist/center-refs.d.ts +68 -0
  20. package/dist/center-refs.js +70 -0
  21. package/dist/config-d-file.js +33 -15
  22. package/dist/config-dir-migration.js +1 -0
  23. package/dist/config-fns.js +147 -81
  24. package/dist/config-root-documents.d.ts +106 -0
  25. package/dist/config-root-documents.js +93 -0
  26. package/dist/config-root-node.d.ts +117 -0
  27. package/dist/config-root-node.js +260 -0
  28. package/dist/conformance/canonical.js +4 -4
  29. package/dist/conformance/contract-adapter.js +26 -1
  30. package/dist/conformance/corpus.js +4 -4
  31. package/dist/conformance/derive.js +2 -2
  32. package/dist/conformance/run.d.ts +3 -0
  33. package/dist/conformance/run.js +53 -12
  34. package/dist/conformance/temp-roots.d.ts +31 -0
  35. package/dist/conformance/temp-roots.js +184 -0
  36. package/dist/cross-session.d.ts +4 -4
  37. package/dist/declared-capture.d.ts +41 -0
  38. package/dist/declared-capture.js +344 -0
  39. package/dist/default-model.d.ts +1 -1
  40. package/dist/default-model.js +5 -4
  41. package/dist/env-model-table.d.ts +163 -0
  42. package/dist/env-model-table.js +257 -0
  43. package/dist/file-edit.js +11 -15
  44. package/dist/fleet.d.ts +36 -7
  45. package/dist/fleet.js +83 -32
  46. package/dist/hash.d.ts +0 -9
  47. package/dist/hash.js +64 -7
  48. package/dist/hooks-config.js +18 -3
  49. package/dist/http-hook-allowlists.d.ts +22 -9
  50. package/dist/http-hook-allowlists.js +169 -91
  51. package/dist/index.d.ts +7 -0
  52. package/dist/index.js +7 -0
  53. package/dist/mcp-request-entry.d.ts +67 -13
  54. package/dist/mcp-request-entry.js +132 -93
  55. package/dist/mcp-server-name.d.ts +17 -0
  56. package/dist/mcp-server-name.js +19 -0
  57. package/dist/migration-receipt.d.ts +10 -6
  58. package/dist/migration-receipt.js +5 -3
  59. package/dist/node.d.ts +1 -0
  60. package/dist/node.js +2 -0
  61. package/dist/one-time-migrations.d.ts +4 -2
  62. package/dist/one-time-migrations.js +236 -96
  63. package/dist/own-key-read.d.ts +247 -0
  64. package/dist/own-key-read.js +449 -0
  65. package/dist/plain-json-object.d.ts +11 -1
  66. package/dist/plain-json-object.js +37 -1
  67. package/dist/remote-exec.js +9 -1
  68. package/dist/retired-form.d.ts +19 -0
  69. package/dist/retired-form.js +24 -1
  70. package/dist/safety-merge-spec.d.ts +6 -6
  71. package/dist/safety-merge-spec.js +50 -37
  72. package/dist/sandbox-git.d.ts +64 -0
  73. package/dist/sandbox-git.js +89 -0
  74. package/dist/scheduler-store.js +19 -11
  75. package/dist/settings-units.js +14 -12
  76. package/dist/tier-words.d.ts +33 -0
  77. package/dist/tier-words.js +43 -0
  78. package/dist/types.d.ts +12 -2
  79. package/dist/types.js +35 -6
  80. package/dist/url-shape.d.ts +16 -0
  81. package/dist/url-shape.js +18 -0
  82. package/dist/web-admin-credential.js +5 -2
  83. package/dist/web-admin-session-url.js +2 -2
  84. package/dist/worker-row-verdict.d.ts +11 -0
  85. package/dist/worker-row-verdict.js +58 -2
  86. package/dist/workers-rows.js +38 -10
  87. package/dist/zod-unwrap.d.ts +6 -0
  88. package/dist/zod-unwrap.js +14 -7
  89. package/docs/API-SURFACE.md +100 -5
  90. package/docs/CONSUMER-INTEGRATION.md +250 -2
  91. package/llms-full.txt +26 -4
  92. package/llms.txt +11 -4
  93. package/package.json +2 -2
  94. package/dist/definition-copy.d.ts +0 -23
  95. package/dist/definition-copy.js +0 -52
@@ -0,0 +1,257 @@
1
+ /**
2
+ * env 模型表 —— `MODEL_ID` / `MODEL_CHEAP_ID` 两槽与它们铸出的角色底 —— 的**一份读法**,与写者在写 `config.d/models.json` 之前调的**计划助手**
3
+ * (纯、零 I/O、浏览器安全;读配置根上的决定文档的函数在 `./node`)。
4
+ *
5
+ * ## 律
6
+ * · **env 两槽只在这里读一次**({@link readEnvModelSlots}):`MODEL_ID` 缺席或空串 = 没有主槽(纯空白算在场 —— 与引擎的 env 路由读口同尺);
7
+ * cheap 槽四态({@link EnvCheapSlot})。「env 模型表已声明」({@link envModelTableDeclared})与角色底({@link buildEnvModelBaseline})都由它派生 ——
8
+ * 这道闸的执行方(引擎一侧;今天引擎还不强制)与写者的计划助手调同一份(一次性迁移 SS-M11 只用两槽点名被顶替的 env 模型,铸不铸不看它),不各写一份。env 快照按「读到即算」(下标读;原型分层的快照里继承来的键算在场)。
9
+ * 引擎另有「名册占位 ⇒ env 表算空」一格,那是引擎自己的部署事实,不在本包。
10
+ * · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 → summarize 会悬空 ⇒
11
+ * `fix_first`(不可确认:每个任务都要解 summarize,采纳后任务全拒)→ env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
12
+ * 调用方带回的确认键等于这一次的 ⇒ `write_with_decision`(先经 `writeEnvModelsDecision` 写 `replaced`,再写目录;两次写在写者对 models.json 的同一把锁里)→
13
+ * 否则 `confirm`(把回执给用户看)。执法在引擎一侧(本地腿的顶替闸;今天引擎不拒这个组),本助手只是让写者在写之前知道这道闸强制起来会不会拒、会顶掉什么。
14
+ * · **回执按 id**:每个退场的 env 条目带 `servedAs`(候选里同 id 的启用条目名 —— 上游模型仍被服务,形可能不同);每个 env 底角色带写前绑谁
15
+ * (env 表键 + 它解到的 id)与采纳后落到哪个条目(`{name, id}`)。缺省落点经 `resolveCatalogDefaultModel`(与引擎的 stage 同一只解析器);
16
+ * 绑到停用条目或目录外的名 = `dangles`。确认键 = 回执的定序 JSON —— 后果(落点名 / id、env 槽、决定读数)一变,旧键就过期。
17
+ * · 可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了;决定性的 summarize 那一格由引擎的不变量兜。
18
+ */
19
+ import { z } from "zod";
20
+ import { entryEnabledFlag, catalogManagesModelPlane } from "./catalog-manages.js";
21
+ import { parseDomainSalvaged, isDisclosableIdentity, resolveActiveTiers, withheldNameToken } from "./config-fns.js";
22
+ import { ENV_MODELS_DOCUMENT } from "./config-root-documents.js";
23
+ import { resolveCatalogDefaultModel } from "./default-model.js";
24
+ import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
25
+ import { landCatalogWord } from "./tier-words.js";
26
+ import { stableStringify } from "./hash.js";
27
+ import { captureJson, dataDescriptor, isolatedObject } from "./own-key-read.js";
28
+ import { ROSTER_CHEAP_ROLES, ROSTER_PRIMARY_ROLES } from "./resolve-roster.js";
29
+ /** env 模型表的键名(闭集)。 */
30
+ export const ENV_MODEL_TABLE_KEYS = { mainId: "MODEL_ID", cheapId: "MODEL_CHEAP_ID", verifier: "MODEL_VERIFIER", codeRoles: "MODEL_CODE_ROLES" };
31
+ /** env 表里主模型的别名键(角色底的 `default` 绑它;引擎 env 表的条目键 = 这个别名 + 主 id + cheap id)。 */
32
+ export const DEFAULT_MODEL_KEY = "default";
33
+ /** cheap 槽铸出来的 id(不铸 ⇒ `undefined`)。 */
34
+ export function mintedCheapId(slot) {
35
+ switch (slot.kind) {
36
+ case "minted":
37
+ return slot.id;
38
+ case "absent":
39
+ case "without_main":
40
+ case "same_as_main":
41
+ return undefined;
42
+ default: {
43
+ const unhandled = slot;
44
+ throw new Error(`mintedCheapId: unhandled slot ${JSON.stringify(unhandled)}`);
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * 包内(不进包根):把 env 模型表的键(缺省四把;调用方只要两槽时只拍两把)**读一次**进一份快照(下标读,同「读到即算」)。计划助手与一次性迁移在**要用的那一刻**拍、
50
+ * 此后的每个读法都答自同一份 —— 一只 getter 两次读答两个值时,判「env 表已声明」与列 env 模型不会各读到一个(那样结局与回执自相矛盾);用不到的键不读。
51
+ */
52
+ export function snapshotEnvModelTable(env, keys = Object.values(ENV_MODEL_TABLE_KEYS)) {
53
+ const snapshot = isolatedObject(); // the package's copy container (no inherited members to read)
54
+ for (const key of keys)
55
+ Object.defineProperty(snapshot, key, dataDescriptor(env[key]));
56
+ return snapshot;
57
+ }
58
+ /** env 两槽的**唯一读口**(见文件头)。`mainId` = `MODEL_ID`(空串 = 没有主槽)。 */
59
+ export function readEnvModelSlots(env) {
60
+ const mainId = env[ENV_MODEL_TABLE_KEYS.mainId] ?? "";
61
+ const rawCheapId = env[ENV_MODEL_TABLE_KEYS.cheapId];
62
+ const cheapSlot = !rawCheapId
63
+ ? { kind: "absent" }
64
+ : mainId === ""
65
+ ? { kind: "without_main", id: rawCheapId }
66
+ : rawCheapId === mainId
67
+ ? { kind: "same_as_main", id: rawCheapId }
68
+ : { kind: "minted", id: rawCheapId };
69
+ return { mainId, cheapSlot };
70
+ }
71
+ /** **env 模型表已声明吗**(= `MODEL_ID` 在场且非空串;纯空白算在场)—— 引擎一侧的顶替闸(今天还不强制)与计划助手共用的那一只谓词。 */
72
+ export function envModelTableDeclared(env) {
73
+ return readEnvModelSlots(env).mainId !== "";
74
+ }
75
+ /** 逗号串:逐项 trim、丢空项。 */
76
+ const csv = (raw) => (raw ?? "")
77
+ .split(",")
78
+ .map((s) => s.trim())
79
+ .filter(Boolean);
80
+ /**
81
+ * env 模型表的**角色底**(纯数据):条目(主槽在 ⇒ 主;cheap 槽铸了 ⇒ cheap)、角色(verifier ⇒ `MODEL_VERIFIER` 指 env 表里的键时是它、否则 `default`;
82
+ * 主角色族(`ROSTER_PRIMARY_ROLES`)⇒ `default`;cheap 角色族(`ROSTER_CHEAP_ROLES`)⇒ cheap id,cheap 没铸 ⇒ `default`;`MODEL_CODE_ROLES` 点名的、角色底自己有的键
83
+ * 加开发模式提示词标记)。env 表键 = 别名 `default` + 主 id + cheap id(主槽不在 ⇒ 表空)。
84
+ */
85
+ export function buildEnvModelBaseline(env) {
86
+ const { mainId, cheapSlot } = readEnvModelSlots(env);
87
+ const cheapId = mintedCheapId(cheapSlot);
88
+ const entries = mainId === "" ? [] : [{ id: mainId, slot: "main", env: ENV_MODEL_TABLE_KEYS.mainId }, ...(cheapId === undefined ? [] : [{ id: cheapId, slot: "cheap", env: ENV_MODEL_TABLE_KEYS.cheapId }])];
89
+ const tableKeys = new Set(mainId === "" ? [] : [DEFAULT_MODEL_KEY, mainId, ...(cheapId === undefined ? [] : [cheapId])]);
90
+ const rawVerifier = env[ENV_MODEL_TABLE_KEYS.verifier];
91
+ const verifier = !rawVerifier ? { kind: "unset" } : tableKeys.has(rawVerifier) ? { kind: "bound", ref: rawVerifier } : { kind: "rejected", raw: rawVerifier };
92
+ const roles = new Map([["verifier", verifier.kind === "bound" ? verifier.ref : DEFAULT_MODEL_KEY]]);
93
+ for (const r of ROSTER_PRIMARY_ROLES)
94
+ roles.set(r, DEFAULT_MODEL_KEY);
95
+ for (const r of ROSTER_CHEAP_ROLES)
96
+ roles.set(r, cheapId ?? DEFAULT_MODEL_KEY);
97
+ for (const name of csv(env[ENV_MODEL_TABLE_KEYS.codeRoles])) {
98
+ const role = name;
99
+ const cur = roles.get(role);
100
+ if (cur === undefined)
101
+ continue; // 只装饰角色底自己有的键(Map 没有原型上的名字)
102
+ roles.set(role, { model: typeof cur === "string" ? cur : cur.model, promptRole: "code" });
103
+ }
104
+ return { entries, roles: Object.fromEntries(roles), verifier };
105
+ }
106
+ const ENV_MODEL_TABLE_TARGETS = ["local", "remote", "none"];
107
+ const decisionKindOf = (decision) => {
108
+ const kind = decision?.kind;
109
+ switch (kind) {
110
+ case "absent":
111
+ case "replaced":
112
+ case "kept":
113
+ case "unreadable":
114
+ return kind;
115
+ default:
116
+ throw new TypeError(`planEnvModelTableWrite: decision must be a readEnvModelsDecision answer (got kind ${JSON.stringify(kind)})`);
117
+ }
118
+ };
119
+ /** 候选按引擎本地读腿的视图读(逐条抢救;整份读不进 ⇒ 抛 TypeError —— 引擎读不进的文档,本助手不替它算回执,先修文档)。 */
120
+ function candidateView(candidate) {
121
+ try {
122
+ return parseDomainSalvaged("models", candidate);
123
+ }
124
+ catch (e) {
125
+ const first = e instanceof z.ZodError ? e.issues[0] : undefined;
126
+ const why = first === undefined ? (e instanceof Error ? e.message : String(e)) : `${first.path.join(".") || "(document)"}: ${first.message}`;
127
+ throw new TypeError(`planEnvModelTableWrite: the candidate does not read as a models document — fix it first (${why})`);
128
+ }
129
+ }
130
+ function buildReceipt(baseline, view, decision) {
131
+ const enabled = view.models.filter((m) => entryEnabledFlag(m) !== false).map((m) => ({ name: m.name, id: m.id }));
132
+ const byName = new Map(enabled.map((e) => [e.name, e]));
133
+ const tiers = resolveActiveTiers(view);
134
+ /** 一个名采纳后落到哪条:经与引擎同一张档位展开(目录键先赢;env 底继承来的 `flash` 一类档位词按活跃组解)。 */
135
+ const land = (word) => {
136
+ const name = landCatalogWord(word, (n) => byName.has(n), tiers);
137
+ const e = name === undefined ? undefined : byName.get(name);
138
+ return e === undefined ? undefined : { name: e.name, id: e.id };
139
+ };
140
+ const main = baseline.entries.find((e) => e.slot === "main");
141
+ const idOf = (ref) => (ref === DEFAULT_MODEL_KEY ? (main?.id ?? null) : (baseline.entries.find((e) => e.id === ref)?.id ?? null));
142
+ /** 目录缺省的落点 —— 恒是 `resolveCatalogDefaultModel` 的答案(与引擎的 stage 同一只解析器;它的第一级就是候选自己的 `roles.default`,点名停用条目时它落到下一级)。
143
+ * `bound` = 候选自己绑了 `default`:解析器用的正是那一级 ⇒ `candidate_binds`,否则 `catalog_default`。 */
144
+ const viaCatalogDefault = (bound) => {
145
+ const fallback = enabled[0];
146
+ if (fallback === undefined)
147
+ return { kind: "dangles", landing: null };
148
+ const d = resolveCatalogDefaultModel(view, (n) => byName.has(n), fallback.name);
149
+ if (d.kind === "select")
150
+ return { kind: bound ? "candidate_binds" : "catalog_default", landing: null };
151
+ const l = land(d.name);
152
+ if (l === undefined)
153
+ return { kind: "dangles", landing: null };
154
+ return { kind: bound && d.source === "roles.default" ? "candidate_binds" : "catalog_default", landing: l };
155
+ };
156
+ const afterOf = (role, ref) => {
157
+ const bound = Object.hasOwn(view.roles, role) ? view.roles[role] : undefined;
158
+ if (role === DEFAULT_MODEL_KEY)
159
+ return viaCatalogDefault(bound !== undefined); // the default role lands where the catalog default resolves, bound or not
160
+ if (bound !== undefined) {
161
+ if (!("model" in bound))
162
+ return { kind: "candidate_binds", landing: null };
163
+ const l = land(bound.model);
164
+ return l === undefined ? { kind: "dangles", landing: null } : { kind: "candidate_binds", landing: l };
165
+ }
166
+ if (ref === DEFAULT_MODEL_KEY)
167
+ return viaCatalogDefault(false);
168
+ const l = land(ref);
169
+ return l === undefined ? { kind: "dangles", landing: null } : { kind: "candidate_entry", landing: l };
170
+ };
171
+ return {
172
+ decision,
173
+ envModels: baseline.entries.map((e) => ({ id: e.id, slot: e.slot, env: e.env, servedAs: enabled.filter((c) => c.id === e.id).map((c) => c.name) })),
174
+ roles: Object.entries(baseline.roles).map(([role, spec]) => {
175
+ const ref = typeof spec === "string" ? spec : spec.model;
176
+ const promptRole = typeof spec === "string" ? undefined : spec.promptRole;
177
+ return { role, before: { ref, id: idOf(ref) }, ...(promptRole === undefined ? {} : { promptRole }), after: afterOf(role, ref) };
178
+ }),
179
+ };
180
+ }
181
+ /**
182
+ * 写者在写 `config.d/models.json` 之前调的**计划**(判序见文件头)。纯函数:不读环境、不读盘、不改入参。
183
+ * 目标腿 / 决定读数不在闭集 ⇒ `TypeError`;候选读不出(撤销的 Proxy 等)或整份读不进 models 域(重名、悬空的角色名 …)⇒ `TypeError`(先修文档)。
184
+ */
185
+ export function planEnvModelTableWrite(input) {
186
+ const decision = decisionKindOf(input.decision);
187
+ const target = input.target;
188
+ if (!ENV_MODEL_TABLE_TARGETS.includes(target))
189
+ throw new TypeError(`planEnvModelTableWrite: target must be one of ${ENV_MODEL_TABLE_TARGETS.join(" / ")} (got ${JSON.stringify(target)})`);
190
+ switch (target) {
191
+ case "remote":
192
+ case "none":
193
+ return { kind: "write", why: "not_read" };
194
+ case "local":
195
+ break;
196
+ default: {
197
+ const unhandled = target;
198
+ throw new TypeError(`planEnvModelTableWrite: unhandled target ${JSON.stringify(unhandled)}`);
199
+ }
200
+ }
201
+ const copy = captureJson(input.candidate);
202
+ if (!copy.ok)
203
+ throw new TypeError(`planEnvModelTableWrite: the candidate could not be read — ${copy.sentence}`);
204
+ if (!catalogManagesModelPlane(copy.value))
205
+ return { kind: "write", why: "not_managing" };
206
+ const env = snapshotEnvModelTable(input.env); // read once: the receipt and the env-table rule answer from the same reading
207
+ const receipt = buildReceipt(buildEnvModelBaseline(env), candidateView(copy.value), decision);
208
+ if (receipt.roles.find((r) => r.role === "summarize")?.after.kind === "dangles")
209
+ return { kind: "fix_first", code: SUMMARIZE_MODEL_MISSING_CODE, receipt };
210
+ if (!envModelTableDeclared(env))
211
+ return { kind: "write", why: "env_table_empty" };
212
+ if (decision === "replaced")
213
+ return { kind: "write", why: "acknowledged", receipt };
214
+ const confirmKey = stableStringify(receipt);
215
+ const intent = input.intent;
216
+ if (intent !== undefined && intent.kind === "replace" && intent.confirmKey === confirmKey)
217
+ return { kind: "write_with_decision", receipt };
218
+ return { kind: "confirm", receipt, confirmKey, intent: intent === undefined ? "absent" : "stale" };
219
+ }
220
+ // ── 拒句投影 ─────────────────────────────────────────────────────────────────────────────────────────────
221
+ /** 句子里的一个名 / id:可披露 ⇒ 原样(加引号);否则 ⇒ 不可逆记号(秘密形不出门)。 */
222
+ const shown = (v) => (isDisclosableIdentity(v) ? JSON.stringify(v) : withheldNameToken(v));
223
+ const landingText = (after) => {
224
+ if (after.landing !== null)
225
+ return `${shown(after.landing.name)} (id ${shown(after.landing.id)})`;
226
+ return after.kind === "dangles" ? "nothing (no enabled catalog entry)" : "an entry the engine selects ({select})";
227
+ };
228
+ /**
229
+ * 计划停下来的那两种结局(`confirm` / `fix_first`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
230
+ * 写类结局不是拒 ⇒ `TypeError`。
231
+ */
232
+ export function envModelTableWriteRefusal(plan) {
233
+ const file = `${ENV_MODELS_DOCUMENT}.json`;
234
+ const kind = plan.kind;
235
+ switch (kind) {
236
+ case "confirm": {
237
+ const r = plan.receipt;
238
+ const models = r.envModels.map((e) => `${e.env} ${shown(e.id)} (${e.slot} slot) ${e.servedAs.length > 0 ? `stays served as ${e.servedAs.map(shown).join(", ")}` : "stops being served"}`).join("; ");
239
+ const roles = r.roles.map((row) => `${row.role} → ${landingText(row.after)}`).join(", ");
240
+ const decision = r.decision === "unreadable"
241
+ ? ` The config root's ${file} is unreadable; confirming overwrites it as a whole.`
242
+ : r.decision === "kept"
243
+ ? ` The config root's ${file} says "kept"; confirming overwrites it.`
244
+ : "";
245
+ return (`writing this models document replaces the env model table on the local config leg: ${models}; after adoption ${roles}.${decision} ` +
246
+ `To let the catalog replace them, confirm — the writer records ${file} = {"envModels":"replaced"} in the config root first, then writes the models document; to keep the env models, do not write it.`);
247
+ }
248
+ case "fix_first": {
249
+ const row = plan.receipt.roles.find((x) => x.role === "summarize");
250
+ const ref = row === undefined ? "" : ` (env binding ${shown(row.before.ref)})`;
251
+ return (`this models document leaves the summarize role unresolved${ref}: it lands on no enabled catalog entry, so every task would be refused after adoption (${SUMMARIZE_MODEL_MISSING_CODE}). ` +
252
+ "Bind roles.summarize to an enabled entry of the document, or add an enabled entry under the name it points at, then plan the write again.");
253
+ }
254
+ default:
255
+ throw new TypeError(`envModelTableWriteRefusal: only a "confirm" or "fix_first" plan is a refusal (got ${JSON.stringify(kind)})`);
256
+ }
257
+ }
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). */
package/dist/fleet.d.ts CHANGED
@@ -163,9 +163,15 @@ export type RestartSignal = z.infer<typeof RestartSignal>;
163
163
  * `config.model_refused_name_minted` — a refused entry's name would be re-minted onto another entry by the
164
164
  * tier / alias expansion (or the dropped-entry carrier cannot prove it would not be);
165
165
  * `config.model_route_endpoint_missing` — an openai-compatible route of the candidate resolves to no endpoint;
166
- * `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary.
167
- * Source (engine): the first two in `src/model-entry-refusal.ts`, the last two in `src/model-route-endpoint.ts:40,42`
168
- * (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`).
166
+ * `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary;
167
+ * `config.catalog_replaces_env_models` ({@link CATALOG_REPLACES_ENV_MODELS_CODE}) — on the LOCAL config leg, a candidate that
168
+ * manages the model plane would replace a declared env model table (`MODEL_ID` …) while the config root's decision document
169
+ * (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving;
170
+ * `config.summarize_model_missing` ({@link SUMMARIZE_MODEL_MISSING_CODE}) — the summarize role, which every task resolves,
171
+ * would land on no servable entry (every task would be refused after adoption).
172
+ * Source (engine): the first two in `src/model-entry-refusal.ts`, the next two in `src/model-route-endpoint.ts:40,42`
173
+ * (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`); the
174
+ * last two are named HERE first (the code is a contract word: this package names it and exports the two constants for the engine to read; the engine does not mint them today).
169
175
  */
170
176
  export declare const MODEL_ENTRY_REFUSAL_CODES: readonly ["config.model_no_brain", "config.model_entry_invalid"];
171
177
  export declare const ModelEntryRefusalCode: z.ZodEnum<{
@@ -173,8 +179,14 @@ export declare const ModelEntryRefusalCode: z.ZodEnum<{
173
179
  "config.model_entry_invalid": "config.model_entry_invalid";
174
180
  }>;
175
181
  export type ModelEntryRefusalCode = z.infer<typeof ModelEntryRefusalCode>;
176
- export declare const MODEL_PLANE_REFUSAL_CODES: readonly ["config.default_model_missing", "config.model_refused_name_minted", "config.model_route_endpoint_missing", "config.gateway_fallback_without_primary"];
182
+ /** The env-model-table confirmation gate's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}). */
183
+ export declare const CATALOG_REPLACES_ENV_MODELS_CODE: "config.catalog_replaces_env_models";
184
+ /** The summarize invariant's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}); a writer's plan answers `fix_first` with it. */
185
+ export declare const SUMMARIZE_MODEL_MISSING_CODE: "config.summarize_model_missing";
186
+ export declare const MODEL_PLANE_REFUSAL_CODES: readonly ["config.default_model_missing", "config.model_refused_name_minted", "config.model_route_endpoint_missing", "config.gateway_fallback_without_primary", "config.catalog_replaces_env_models", "config.summarize_model_missing"];
177
187
  export declare const ModelPlaneRefusalCode: z.ZodEnum<{
188
+ "config.catalog_replaces_env_models": "config.catalog_replaces_env_models";
189
+ "config.summarize_model_missing": "config.summarize_model_missing";
178
190
  "config.default_model_missing": "config.default_model_missing";
179
191
  "config.model_refused_name_minted": "config.model_refused_name_minted";
180
192
  "config.model_route_endpoint_missing": "config.model_route_endpoint_missing";
@@ -548,6 +560,17 @@ export declare const FLEET_GENERATION_ENV = "FLEET_GENERATION";
548
560
  * {@link FLEET_GENERATION_ENV} it is a reserved worker env name (`WorkerSpec.env` / `secretEnv` cannot set it).
549
561
  */
550
562
  export declare const FLEET_SCOPE_ENV = "FLEET_SCOPE";
563
+ /**
564
+ * The env name a fleet member's advertise address travels under — the base URL this instance tells the control plane it can be
565
+ * reached at; the control plane appends request paths to it. The operator or the config renderer sets it (on Kubernetes the renderer
566
+ * writes a `$(POD_IP)` placeholder in the value and the kubelet expands it), and the engine reads it back. Both sides import THIS
567
+ * constant; neither spells the name. The address is a base that request paths are appended to, so a reader judges a present, non-empty
568
+ * value with `pathBaseUrlFlaws` (main barrel) — the one judge of every such seat, which forbids a credential, a
569
+ * query and a fragment there and allows a path prefix; judge the value AFTER the deployment has expanded its placeholders. Absent or
570
+ * the empty string means "no address declared" (the `${NAME:-}` idiom of a compose file), and that reading is the caller's, not the
571
+ * judge's.
572
+ */
573
+ export declare const FLEET_ADVERTISE_ADDRESS_ENV = "FLEET_ADVERTISE_ADDRESS";
551
574
  /**
552
575
  * The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
553
576
  * engine renews it with web-admin; web-admin answers per the lifecycle object it holds for that instance. Three constants
@@ -555,9 +578,15 @@ export declare const FLEET_SCOPE_ENV = "FLEET_SCOPE";
555
578
  * - {@link SERVING_LEASE_TTL_DEFAULT_MS} — the lease length when the operator sets none (10 min).
556
579
  * - {@link SERVING_LEASE_TTL_FLOOR_MS} — the shortest lease there is: a configured TTL below it is treated as it (1 min),
557
580
  * the same way {@link DRAIN_GRACE_FLOOR_MS} protects a fat-fingered grace.
558
- * - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — web-admin's clock-skew allowance: it adds this to a lease's recorded expiry
559
- * before it counts that lease as over. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a
560
- * second constant that could drift from it.
581
+ * - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — the clock-skew allowance for reading a lease deadline an engine REPORTS. The engine
582
+ * says `EngineSignal.leaseUntil` (epoch ms: its own computation from the granted `ttlMs`, on its own clock); a reader that asks
583
+ * "does this member's lease still hold?" compares that instant with ITS OWN now — two clocks that need not agree — so it counts
584
+ * the lease as held only while `leaseUntil >= now + SERVING_LEASE_DRIFT_MARGIN_MS`. That is the conservative direction: a skew
585
+ * up to the margin can make a lease that still holds read as short, never one that is over read as held. It is an allowance for
586
+ * reading a reported deadline, not part of the lease clock: the engine enforces the lease on its own monotonic clock and
587
+ * web-admin keeps its own expiry from its store time (see {@link ServingLeaseResponse}) — the two sides never compare clocks
588
+ * to enforce. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a second constant that could
589
+ * drift from it.
561
590
  */
562
591
  export declare const SERVING_LEASE_TTL_DEFAULT_MS = 600000;
563
592
  export declare const SERVING_LEASE_TTL_FLOOR_MS = 60000;
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
- try {
177
- if (!isPlainJsonObject(body))
178
- return { kind: "not-an-object" };
179
- const parsed = schema.safeParse(pick(body));
180
- return parsed.success ? { kind: "read", value: parsed.data } : { kind: "invalid" };
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
- catch {
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
@@ -214,17 +231,29 @@ const wholeBody = (body) => body;
214
231
  * `config.model_refused_name_minted` — a refused entry's name would be re-minted onto another entry by the
215
232
  * tier / alias expansion (or the dropped-entry carrier cannot prove it would not be);
216
233
  * `config.model_route_endpoint_missing` — an openai-compatible route of the candidate resolves to no endpoint;
217
- * `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary.
218
- * Source (engine): the first two in `src/model-entry-refusal.ts`, the last two in `src/model-route-endpoint.ts:40,42`
219
- * (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`).
234
+ * `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary;
235
+ * `config.catalog_replaces_env_models` ({@link CATALOG_REPLACES_ENV_MODELS_CODE}) — on the LOCAL config leg, a candidate that
236
+ * manages the model plane would replace a declared env model table (`MODEL_ID` …) while the config root's decision document
237
+ * (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving;
238
+ * `config.summarize_model_missing` ({@link SUMMARIZE_MODEL_MISSING_CODE}) — the summarize role, which every task resolves,
239
+ * would land on no servable entry (every task would be refused after adoption).
240
+ * Source (engine): the first two in `src/model-entry-refusal.ts`, the next two in `src/model-route-endpoint.ts:40,42`
241
+ * (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`); the
242
+ * last two are named HERE first (the code is a contract word: this package names it and exports the two constants for the engine to read; the engine does not mint them today).
220
243
  */
221
244
  export const MODEL_ENTRY_REFUSAL_CODES = ["config.model_no_brain", "config.model_entry_invalid"];
222
245
  export const ModelEntryRefusalCode = z.enum(MODEL_ENTRY_REFUSAL_CODES);
246
+ /** The env-model-table confirmation gate's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}). */
247
+ export const CATALOG_REPLACES_ENV_MODELS_CODE = "config.catalog_replaces_env_models";
248
+ /** The summarize invariant's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}); a writer's plan answers `fix_first` with it. */
249
+ export const SUMMARIZE_MODEL_MISSING_CODE = "config.summarize_model_missing";
223
250
  export const MODEL_PLANE_REFUSAL_CODES = [
224
251
  "config.default_model_missing",
225
252
  "config.model_refused_name_minted",
226
253
  "config.model_route_endpoint_missing",
227
254
  "config.gateway_fallback_without_primary",
255
+ CATALOG_REPLACES_ENV_MODELS_CODE,
256
+ SUMMARIZE_MODEL_MISSING_CODE,
228
257
  ];
229
258
  export const ModelPlaneRefusalCode = z.enum(MODEL_PLANE_REFUSAL_CODES);
230
259
  /**
@@ -285,11 +314,14 @@ const isModelPlaneRefusalCode = (c) => MODEL_PLANE_REFUSAL_CODES.includes(c);
285
314
  * at all — it answers the key only and never judges the engine's version (see {@link InstanceAnnouncement}`.modelRefusals`).
286
315
  */
287
316
  export function readModelRefusals(body) {
288
- const wire = readSignalBody(body, (b) => b.modelRefusals, ModelRefusalsWire);
317
+ const wire = readSignalBody(body, "modelRefusals", ModelRefusalsWire);
289
318
  if (wire === undefined)
290
319
  return undefined;
291
320
  const entries = Object.fromEntries(Object.entries(wire.entries).filter((e) => isModelEntryRefusalCode(e[1])));
292
- const plane = wire.plane !== undefined && isModelPlaneRefusalCode(wire.plane) ? wire.plane : undefined;
321
+ // the validated output is an ordinary object: an absent optional `plane` stays absent (own key only), never read off a polluted `Object.prototype`
322
+ const planeRead = ownValueOf(wire, "plane");
323
+ const planeWord = planeRead.ok ? planeRead.value : undefined;
324
+ const plane = typeof planeWord === "string" && isModelPlaneRefusalCode(planeWord) ? planeWord : undefined;
293
325
  if (Object.keys(entries).length === 0 && plane === undefined)
294
326
  return undefined;
295
327
  return plane === undefined ? { entries } : { entries, plane };
@@ -363,7 +395,7 @@ export const ENGINE_SIGNAL_KEY = "engineSignal";
363
395
  * dialect; the carrier is the one key).
364
396
  */
365
397
  export function readEngineSignal(body) {
366
- return readSignalBody(body, (b) => b[ENGINE_SIGNAL_KEY], EngineSignal);
398
+ return readSignalBody(body, ENGINE_SIGNAL_KEY, EngineSignal);
367
399
  }
368
400
  /** The `/health` self-description every orchestrator reads. Liveness and readiness are deliberately SPLIT: `status` stays ok while
369
401
  * draining (the process is alive); readiness is {@link judgeReadiness} over the engine signal the body carries under
@@ -556,6 +588,17 @@ export const FLEET_GENERATION_ENV = "FLEET_GENERATION";
556
588
  * {@link FLEET_GENERATION_ENV} it is a reserved worker env name (`WorkerSpec.env` / `secretEnv` cannot set it).
557
589
  */
558
590
  export const FLEET_SCOPE_ENV = "FLEET_SCOPE";
591
+ /**
592
+ * The env name a fleet member's advertise address travels under — the base URL this instance tells the control plane it can be
593
+ * reached at; the control plane appends request paths to it. The operator or the config renderer sets it (on Kubernetes the renderer
594
+ * writes a `$(POD_IP)` placeholder in the value and the kubelet expands it), and the engine reads it back. Both sides import THIS
595
+ * constant; neither spells the name. The address is a base that request paths are appended to, so a reader judges a present, non-empty
596
+ * value with `pathBaseUrlFlaws` (main barrel) — the one judge of every such seat, which forbids a credential, a
597
+ * query and a fragment there and allows a path prefix; judge the value AFTER the deployment has expanded its placeholders. Absent or
598
+ * the empty string means "no address declared" (the `${NAME:-}` idiom of a compose file), and that reading is the caller's, not the
599
+ * judge's.
600
+ */
601
+ export const FLEET_ADVERTISE_ADDRESS_ENV = "FLEET_ADVERTISE_ADDRESS";
559
602
  /**
560
603
  * The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
561
604
  * engine renews it with web-admin; web-admin answers per the lifecycle object it holds for that instance. Three constants
@@ -563,9 +606,15 @@ export const FLEET_SCOPE_ENV = "FLEET_SCOPE";
563
606
  * - {@link SERVING_LEASE_TTL_DEFAULT_MS} — the lease length when the operator sets none (10 min).
564
607
  * - {@link SERVING_LEASE_TTL_FLOOR_MS} — the shortest lease there is: a configured TTL below it is treated as it (1 min),
565
608
  * the same way {@link DRAIN_GRACE_FLOOR_MS} protects a fat-fingered grace.
566
- * - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — web-admin's clock-skew allowance: it adds this to a lease's recorded expiry
567
- * before it counts that lease as over. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a
568
- * second constant that could drift from it.
609
+ * - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — the clock-skew allowance for reading a lease deadline an engine REPORTS. The engine
610
+ * says `EngineSignal.leaseUntil` (epoch ms: its own computation from the granted `ttlMs`, on its own clock); a reader that asks
611
+ * "does this member's lease still hold?" compares that instant with ITS OWN now — two clocks that need not agree — so it counts
612
+ * the lease as held only while `leaseUntil >= now + SERVING_LEASE_DRIFT_MARGIN_MS`. That is the conservative direction: a skew
613
+ * up to the margin can make a lease that still holds read as short, never one that is over read as held. It is an allowance for
614
+ * reading a reported deadline, not part of the lease clock: the engine enforces the lease on its own monotonic clock and
615
+ * web-admin keeps its own expiry from its store time (see {@link ServingLeaseResponse}) — the two sides never compare clocks
616
+ * to enforce. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a second constant that could
617
+ * drift from it.
569
618
  */
570
619
  export const SERVING_LEASE_TTL_DEFAULT_MS = 600_000;
571
620
  export const SERVING_LEASE_TTL_FLOOR_MS = 60_000;
@@ -647,14 +696,14 @@ export const SERVING_LEASE_VERDICTS = ["granted", "denied", "unknown"];
647
696
  /** Web-admin side: the request body of a serving-lease renewal. Shape mismatch / non-object / throwing accessor ⇒ `undefined`,
648
697
  * never throws (same guarded reader as every other read in this file). */
649
698
  export function readServingLeaseRequest(body) {
650
- return readSignalBody(body, wholeBody, ServingLeaseRequest);
699
+ return readSignalBody(body, WHOLE_BODY, ServingLeaseRequest);
651
700
  }
652
701
  /** Engine side: the reply to a renewal, judged by its `verdict` key: `granted` needs a valid `ttlMs`, `denied` is `denied`
653
702
  * whatever its `reason` (see {@link ServingLeaseReading}), `unknown` is `unknown`. A non-object, a throwing accessor, a
654
703
  * `verdict` outside the three words or a `granted` without a valid `ttlMs` ⇒ `undefined`, never throws — and `undefined`
655
704
  * is read like an unreachable web-admin, never like `denied` and never like `granted`. */
656
705
  export function readServingLeaseResponse(body) {
657
- return readSignalBody(body, wholeBody, ServingLeaseReadFace);
706
+ return readSignalBody(body, WHOLE_BODY, ServingLeaseReadFace);
658
707
  }
659
708
  /**
660
709
  * What web-admin says it can do: capability bits an engine reads before it starts using a web-admin-side endpoint.
@@ -704,14 +753,16 @@ const CapabilitiesCarrier = z.object({ [HEARTBEAT_REPLY_CAPABILITIES_KEY]: WebAd
704
753
  * other reader here.
705
754
  */
706
755
  export function readCapabilityReading(reply) {
707
- const judged = judgeSignalBody(reply, wholeBody, CapabilitiesCarrier);
756
+ const judged = judgeSignalBody(reply, WHOLE_BODY, CapabilitiesCarrier);
708
757
  switch (judged.kind) {
709
758
  case "not-an-object":
710
759
  return { kind: "unreadable", why: "not-an-object" };
711
760
  case "invalid":
712
761
  return { kind: "unreadable", why: "invalid-capabilities" };
713
762
  case "read": {
714
- const capabilities = judged.value[HEARTBEAT_REPLY_CAPABILITIES_KEY];
763
+ // 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)
764
+ const carried = ownValueOf(judged.value, HEARTBEAT_REPLY_CAPABILITIES_KEY);
765
+ const capabilities = carried.ok ? carried.value : undefined;
715
766
  return capabilities === undefined ? { kind: "undeclared" } : { kind: "declared", capabilities };
716
767
  }
717
768
  default: {
@@ -755,11 +806,11 @@ export const DrainReceiptState = z.enum(DRAIN_RECEIPT_STATES);
755
806
  export const DrainReceipt = EngineDraining.extend({ state: DrainReceiptState });
756
807
  /** Engine side: the request body of the drain endpoint. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
757
808
  export function readDrainRequest(body) {
758
- return readSignalBody(body, wholeBody, DrainRequest);
809
+ return readSignalBody(body, WHOLE_BODY, DrainRequest);
759
810
  }
760
811
  /** Caller side: the drain endpoint's answer. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
761
812
  export function readDrainReceipt(body) {
762
- return readSignalBody(body, wholeBody, DrainReceipt);
813
+ return readSignalBody(body, WHOLE_BODY, DrainReceipt);
763
814
  }
764
815
  // ── Domain 3: dispatch policy (traffic + failover + LB — three domains, one gateway) ──────────────────────
765
816
  export const DISPATCH_STRATEGIES = ["round-robin", "least-inflight", "consistent-hash"];
package/dist/hash.d.ts CHANGED
@@ -1,12 +1,3 @@
1
- /**
2
- * Tiny stable content hash — shared by `workerSpecHash` (reconciler drift detection) and the per-worker
3
- * `/effective` ETag (so an unrelated roster change doesn't churn a worker's cache). Zero-dep, deterministic.
4
- *
5
- * FNV-1a **64-bit** (BigInt) → 16 hex. NOT cryptographic; it's a change-detector. 64-bit pushes the
6
- * birthday-collision boundary to ~5B distinct inputs — a collision would silently SUPPRESS a needed
7
- * container restart (drift) or serve a stale 304 (ETag), so we don't want the 32-bit ~77k boundary here
8
- * (council finding). Output length isn't a contract (values are only compared for equality before/after).
9
- */
10
1
  /** Max nesting stableStringify will hash. A real EffectiveConfig nests ~10-15 deep; this generous cap turns a
11
2
  * pathologically deep OR cyclic value (e.g. a poisoned `model.extraBody` = z.unknown()) into a DETERMINISTIC
12
3
  * throw instead of a `RangeError: Maximum call stack` that would crash every scoped-worker `/effective` poll