@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
@@ -14,8 +14,10 @@ export interface OneTimeMigrationWindow {
14
14
  * · `refused` — 读写两面拒,拒句点名本行 id(经 `migrationSignpost` 铸;钉:每个 `refused` 行的 id 在 `src/` 至少一处铸点,
15
15
  * 且非 `refused` 行零铸点);
16
16
  * · `rewritten-on-read` — 读时改写、不拒(SS-M3:调度店的下一个写者落盘),没有拒句可点名;
17
- * · `not-stored` — 没有存储形(wire 删码位),谈不上拒。 */
18
- readonly oldForm: "refused" | "rewritten-on-read" | "not-stored";
17
+ * · `not-stored` — 没有存储形(wire 删码位),谈不上拒;
18
+ * · `read-as-default` — 旧形是一份文档的**缺席**(SS-M11:配置根上没有 `config.env-models.json`),读者按它的缺省读(`kept`),不拒、没有拒句可点名 ——
19
+ * 缺省读出来的后果由读者自己的响亮臂说(引擎的模型面组拒句点名出路)。 */
20
+ readonly oldForm: "refused" | "rewritten-on-read" | "not-stored" | "read-as-default";
19
21
  /** 引入这条迁移的版本。 */
20
22
  readonly introducedIn: string;
21
23
  /** 该版本在 npm 上的发布日(UTC 日历日 YYYY-MM-DD)。`null` = 未发布形,由抬 `package.json` 到 `introducedIn` 的发布准备提交回填。 */
@@ -11,7 +11,13 @@
11
11
  * **迁移只认形、不认义**:每一步的输入形在新契约下非法、输出形合法 ⇒ 重跑恒等,新写入的数据永远不会被当成旧数据再改一次,不需要版本围栏;
12
12
  * 同一串字节换意思的改动一律**换表示**(SS-M6 因此是「`atModelAllowlist` → `atMention`」换字段,而不是翻 `[]` 的义)。
13
13
  *
14
- * ## 入口(纯函数:不读环境、不读墙钟、不改入参、**不抬任何版本号**)
14
+ * ## 第二种机制:在配置根上铸一次事实(只有 SS-M11)
15
+ * 上面的「只认形」律管的是**改写存储文档**的迁移。SS-M11 不改写任何存储形:它在一份文档**缺席**处铸一份部署决定(配置根的 `config.env-models.json`,收据词 `minted`),
16
+ * 而「缺席」本身没有形可认 —— 所以它不归上面的律,另有三条:① 它读 env、碰文件系统,住 `./node`(`migrateEnvModelsDecision`),不在下面的纯函数入口里;
17
+ * ② 它靠**按 id 一份的运行记录**(`config.migration.SS-M11.json`,「不存在才建」)跑恰一次,建成运行记录的进程是本根唯一的执行者;
18
+ * ③ 它的旧形待遇是 `read-as-default`(缺席按读者的缺省 `kept` 读,没有拒句可点名)。窗、删法与删后由谁接住照样只写在登记表那一行。
19
+ *
20
+ * ## 入口(纯函数:不读环境、不读墙钟、不改入参、**不抬任何版本号**;SS-M11 除外,见上一节)
15
21
  * · {@link migrateStoredDocuments} —— 一组**存储文档**(域名 → 文档;还有本机的 `remote-exec` —— `config.d/remote-exec.json`,
16
22
  * 只有 SS-M7 的座位走查读它)。一个 scope 的全部草稿、一个 principal 层(个人 models / mcp / skills / plugins)、回滚史里的一条单域行,各调一次;`coverage` 说这组文档是不是一个 scope 的全集(只有 SS-M1 要知道:缺席的
17
23
  * `governance` 是「从未设过」还是「没给」)。缺的域 = 不动。
@@ -33,7 +39,8 @@ import { outgoingPath, redactSecrets } from "./config-fns.js";
33
39
  import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
34
40
  import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
35
41
  import { INTEGER } from "./integer.js";
36
- import { isPlainJsonObject } from "./plain-json-object.js";
42
+ import { NOT_PLAIN_DATA, UNINSPECTABLE, UninspectableError, captureJson, dataDescriptor, emptyDocument, inspect, ownEntriesOf, ownOrThrow, readDeclaredKey, sentenceOfMinted } from "./own-key-read.js";
43
+ import { isJsonArray, isPlainJsonObject, jsonShapeOf } from "./plain-json-object.js";
37
44
  import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
38
45
  import { RETIRED_ROLE_WORDS } from "./retired-role-words.js";
39
46
  import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
@@ -175,6 +182,28 @@ export const ONE_TIME_MIGRATIONS = [
175
182
  afterRemoval: "the prompts `bindings` / `catalog` tombstone fields (permanent): a residual key is refused on read and write (the prompts domain degrades " +
176
183
  "to its default — no center layer is served)",
177
184
  },
185
+ {
186
+ id: "SS-M11",
187
+ oldForm: "read-as-default",
188
+ introducedIn: "14.8.0",
189
+ releasedOn: "2026-10-27",
190
+ removableAfter: "2026-11-26",
191
+ rewrites: "nothing stored is rewritten — the config root's env-model-table decision is minted once (migrateEnvModelsDecision, ./node): when the run " +
192
+ "marker config.migration.SS-M11.json is absent (a marker that does not read counts as run), the decision and whether config.d/models.json " +
193
+ "manages the model plane (catalogManagesModelPlane) are read first; then the marker is created exclusively — the one process that creates " +
194
+ "it is the only one that may mint on this root, any other stops for good — and that minter creates {\"envModels\":\"replaced\"} " +
195
+ "exclusively only when the decision it read before the election is absent and the catalog manages the model plane (whether an env model " +
196
+ "table is declared does not matter: the env only names the replaced env models) — the reading before the decision existed (a managing " +
197
+ "catalog replaced the env model table). Outcomes: minted / exists (only EEXIST) / failed (errno + a manual way out); an interruption after the marker is " +
198
+ "never retried. An absent root is created owner-only, holding only the marker; config.d is never created",
199
+ removal: "the next major after the window: delete migrateEnvModelsDecision (the SS-M11 body), this row, SS-M11 from ONE_TIME_MIGRATION_IDS, the " +
200
+ "`minted` receipt word and the `read-as-default` oldForm word; the marker then has no reader — readMigrationMarker and the marker's " +
201
+ "entry in CONFIG_ROOT_DOCUMENTS go with it (a marker left on disk is inert)",
202
+ afterRemoval: "an absent decision reads as kept — the reading the group-refusal code config.catalog_replaces_env_models is for: this package exports " +
203
+ "that code constant and the decision document for the engine to read, enforcing it is the engine's side, and the engine does not " +
204
+ "refuse on it today. Where the engine refuses, the way out is to write config.env-models.json = {\"envModels\":\"replaced\"} in the " +
205
+ "config root; no signpost in this package",
206
+ },
178
207
  {
179
208
  // 一条 wire 位的删码登记,不是数据迁移(id 在 WIRE_RETIREMENT_IDS,不在收据闭集)。
180
209
  id: "SS-W1",
@@ -198,8 +227,22 @@ export const ONE_TIME_MIGRATIONS = [
198
227
  * 重跑恒等:第二次跑零改动,只剩第一次就 `refused` 的那几张收据(那些坐标原样留着,读侧照旧拒)。
199
228
  */
200
229
  export function migrateStoredDocuments(docs, opts) {
201
- if (!isPlainJsonObject(docs))
202
- throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
230
+ const docsShape = jsonShapeOf(docs);
231
+ switch (docsShape) {
232
+ case "plain":
233
+ break;
234
+ case "uninspectable": // 文档集本身读不出(撤销的 Proxy / 陷阱抛):不是「形状不对」,是「读不出」—— 带全包那一句
235
+ throw new TypeError(`migrateStoredDocuments: the stored documents could not be read — ${UNINSPECTABLE}`);
236
+ case "non_plain_object":
237
+ case "array":
238
+ case "null":
239
+ case "primitive":
240
+ throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
241
+ default: {
242
+ const unhandled = docsShape;
243
+ throw new Error(`migrateStoredDocuments: unhandled shape ${String(unhandled)}`);
244
+ }
245
+ }
203
246
  switch (opts.coverage) {
204
247
  case "scope":
205
248
  case "partial":
@@ -209,20 +252,35 @@ export function migrateStoredDocuments(docs, opts) {
209
252
  throw new TypeError(`migrateStoredDocuments: coverage must be "scope" or "partial" (got ${JSON.stringify(unhandled)})`);
210
253
  }
211
254
  }
212
- const out = { ...docs };
255
+ // 工作副本是**隔离原型**(`COPY_PROTOTYPE`,没有继承来的成员)的袋(它的读没有继承可说:`Object.prototype.models` 被污染成数据值时,`out.models` 不会读到一份「存储文档」),文档集只读一次(每个域文档一个值);
256
+ // 之后每一步对文档里固定键的读都经内核的自有键读(`ownOrThrow`),读不出的存储文档 ⇒ 下面那一处统一抛 TypeError(带全包唯一那一句)—— 迁移本来就是抛的读口,不另造失败臂
257
+ const entries = ownEntriesOf(docs);
258
+ if (!entries.ok)
259
+ throw new TypeError(`migrateStoredDocuments: the stored documents could not be read — ${entries.sentence}`);
260
+ const out = emptyDocument();
261
+ for (const [k, v] of entries.value)
262
+ Object.defineProperty(out, k, dataDescriptor(v));
213
263
  const receipts = [];
214
264
  // 步序的律:纯改写步在前,拿新 schema 终判改写结果的步(SS-M4)最后 —— 它判的是**全部**改写之后的行,别的步能修好的形(例:SS-M7 剥掉的
215
265
  // fragment)不会先被它拒掉(否则一跑 refused、二跑才过:既不是一次完成,也不幂等)。
216
- stepRuntimeDomain(out, opts.coverage, receipts);
217
- stepHostsBounds(out, receipts);
218
- stepModelKeyOneOf(out, receipts);
219
- stepAtMention(out, receipts);
220
- stepRetiredRoleWords(out, receipts);
221
- stepModelsDefault(out, receipts);
222
- stepPromptsAxes(out, receipts);
223
- stepUrlFragments(out, receipts);
224
- stepWorkers6x(out, receipts);
225
- return { docs: out, receipts };
266
+ try {
267
+ stepRuntimeDomain(out, opts.coverage, receipts);
268
+ stepHostsBounds(out, receipts);
269
+ stepModelKeyOneOf(out, receipts);
270
+ stepAtMention(out, receipts);
271
+ stepRetiredRoleWords(out, receipts);
272
+ stepModelsDefault(out, receipts);
273
+ stepPromptsAxes(out, receipts);
274
+ stepUrlFragments(out, receipts);
275
+ stepWorkers6x(out, receipts);
276
+ }
277
+ catch (e) {
278
+ const sentence = sentenceOfMinted(e);
279
+ if (sentence !== undefined)
280
+ throw new TypeError(`migrateStoredDocuments: a stored document could not be read — ${sentence}`);
281
+ throw e;
282
+ }
283
+ return { docs: { ...out }, receipts }; // 交出去的是寻常对象(隔离原型的袋只是工作副本)
226
284
  }
227
285
  /**
228
286
  * 迁移一份冻结的 effective(见文件头「入口」):域键过与 {@link migrateStoredDocuments} 同一组步(coverage `"scope"` —— effective 里每个域
@@ -230,29 +288,68 @@ export function migrateStoredDocuments(docs, opts) {
230
288
  * 不是对象 ⇒ 原样返回(判形是读者的事)。
231
289
  */
232
290
  export function migrateEffectiveSnapshot(effective) {
233
- if (!isPlainJsonObject(effective))
234
- return { effective, receipts: [] };
235
- const docs = {};
236
- for (const [k, v] of Object.entries(effective))
291
+ // 判形是读者的事:不是朴素对象(null / 数组 / 原始值 / 类实例)⇒ 原样返回;**读不出**(撤销的 Proxy / 陷阱抛)不是「不是这个形」,是读不出 ⇒ 抛(与文档集同一句)
292
+ const effectiveShape = jsonShapeOf(effective);
293
+ switch (effectiveShape) {
294
+ case "plain":
295
+ break;
296
+ case "uninspectable":
297
+ throw new TypeError(`migrateEffectiveSnapshot: the effective snapshot could not be read — ${UNINSPECTABLE}`);
298
+ case "non_plain_object":
299
+ case "array":
300
+ case "null":
301
+ case "primitive":
302
+ return { effective, receipts: [] };
303
+ default: {
304
+ const unhandled = effectiveShape;
305
+ throw new Error(`migrateEffectiveSnapshot: unhandled shape ${String(unhandled)}`);
306
+ }
307
+ }
308
+ const read = ownEntriesOf(effective);
309
+ if (!read.ok)
310
+ throw new TypeError(`migrateEffectiveSnapshot: the effective snapshot could not be read — ${read.sentence}`);
311
+ const entriesOfEffective = read.value;
312
+ const docs = emptyDocument();
313
+ for (const [k, v] of entriesOfEffective)
237
314
  if (isDomainName(k) || isRetiredDomainName(k))
238
- docs[k] = v;
315
+ Object.defineProperty(docs, k, dataDescriptor(v));
239
316
  const m = migrateStoredDocuments(docs, { coverage: "scope" });
240
317
  const changed = Object.keys(docs).some((k) => !Object.prototype.hasOwnProperty.call(m.docs, k) || m.docs[k] !== docs[k]) || Object.keys(m.docs).some((k) => !Object.prototype.hasOwnProperty.call(docs, k));
241
318
  if (!changed)
242
319
  return { effective, receipts: m.receipts };
243
- const next = {};
244
- for (const [k, v] of Object.entries(effective)) {
320
+ const next = emptyDocument();
321
+ for (const [k, v] of entriesOfEffective) {
245
322
  if (!(isDomainName(k) || isRetiredDomainName(k)))
246
- next[k] = v;
323
+ Object.defineProperty(next, k, dataDescriptor(v));
247
324
  else if (Object.prototype.hasOwnProperty.call(m.docs, k))
248
- next[k] = m.docs[k];
325
+ Object.defineProperty(next, k, dataDescriptor(m.docs[k]));
249
326
  }
250
327
  for (const [k, v] of Object.entries(m.docs))
251
328
  if (!Object.prototype.hasOwnProperty.call(next, k))
252
- next[k] = v;
253
- return { effective: next, receipts: m.receipts };
329
+ Object.defineProperty(next, k, dataDescriptor(v));
330
+ return { effective: { ...next }, receipts: m.receipts };
254
331
  }
255
- const has = (o, k) => isPlainJsonObject(o) && Object.prototype.hasOwnProperty.call(o, k);
332
+ /** `o` 是朴素对象且**自有**键 `k` 在场(描述符律,与 `ownOrThrow` 同一只核:访问器 / 不可枚举的自有键不当缺席 ⇒ 抛带「不是纯数据」那一句;读不出 ⇒ 抛;迁移本来就抛)。 */
333
+ const has = (o, k) => {
334
+ if (!isPlainJsonObject(o))
335
+ return false;
336
+ const read = readDeclaredKey(o, k);
337
+ switch (read.presence) {
338
+ case "own":
339
+ return true;
340
+ case "inherited":
341
+ case "absent":
342
+ return false;
343
+ case "hidden":
344
+ throw new UninspectableError(NOT_PLAIN_DATA);
345
+ case "unreadable":
346
+ throw new UninspectableError();
347
+ default: {
348
+ const unhandled = read;
349
+ throw new Error(`has: unhandled presence ${String(unhandled.presence)}`);
350
+ }
351
+ }
352
+ };
256
353
  const without = (o, k) => {
257
354
  const { [k]: _gone, ...rest } = o;
258
355
  return rest;
@@ -282,7 +379,7 @@ const LIMIT_KEYS = ["rateLimitPerMin", "maxTaskCostUsd", "maxTaskTokens", "maxPr
282
379
  function stepRuntimeDomain(out, coverage, receipts) {
283
380
  if (!Object.prototype.hasOwnProperty.call(out, "runtime"))
284
381
  return;
285
- const raw = out.runtime;
382
+ const raw = ownOrThrow(out, "runtime");
286
383
  if (raw === undefined)
287
384
  return;
288
385
  const refuse = (reason) => void receipts.push(refused("SS-M1", "runtime", [], reason));
@@ -299,14 +396,14 @@ function stepRuntimeDomain(out, coverage, receipts) {
299
396
  const parsed = RUNTIME_10X.safeParse(raw);
300
397
  if (!parsed.success)
301
398
  return refuse("the stored runtime document does not read under the 10.x runtime schema — the scope does not read today either; fix it by hand");
302
- const limitsRaw = out.limits;
399
+ const limitsRaw = ownOrThrow(out, "limits");
303
400
  if (limitsRaw !== undefined && !isPlainJsonObject(limitsRaw))
304
401
  return refuse("the scope's limits document is not a JSON object — the scope does not read today either; fix limits first");
305
- const trio = GOVERNANCE_TRIO.filter((k) => has(raw, k) && raw[k] !== undefined);
402
+ const trio = GOVERNANCE_TRIO.filter((k) => has(raw, k) && ownOrThrow(raw, k) !== undefined);
306
403
  if (trio.length > 0) {
307
404
  // 迁移前的读法:governance 域**从未设过**(店里没有这份文档)⇒ 读侧从 runtime 抬升三闸;设过 ⇒ 设过的那份赢,runtime 上的三闸从未生效。
308
- if (out.governance === undefined) {
309
- out.governance = Object.fromEntries(trio.map((k) => [k, raw[k]]));
405
+ if (ownOrThrow(out, "governance") === undefined) {
406
+ out.governance = Object.fromEntries(trio.map((k) => [k, ownOrThrow(raw, k)]));
310
407
  for (const k of trio)
311
408
  receipts.push(moved("SS-M1", "runtime", [k], { document: "governance", path: [k] }));
312
409
  }
@@ -340,14 +437,17 @@ function stepRuntimeDomain(out, coverage, receipts) {
340
437
  }
341
438
  // ── SS-M2:hosts 超界 ───────────────────────────────────────────────────────────────────────────────────
342
439
  function stepHostsBounds(out, receipts) {
343
- const doc = out.hosts;
344
- if (!isPlainJsonObject(doc) || !Array.isArray(doc.hosts))
440
+ const doc = ownOrThrow(out, "hosts");
441
+ if (!isPlainJsonObject(doc))
345
442
  return;
346
- const rows = doc.hosts;
443
+ const hostRows = ownOrThrow(doc, "hosts");
444
+ if (!isJsonArray(hostRows))
445
+ return;
446
+ const rows = hostRows;
347
447
  let kept = [];
348
448
  const dropped = [];
349
449
  rows.forEach((row, index) => {
350
- const name = isPlainJsonObject(row) ? row.name : undefined;
450
+ const name = isPlainJsonObject(row) ? ownOrThrow(row, "name") : undefined;
351
451
  if (typeof name === "string" && name.length > HOST_NAME_MAX)
352
452
  dropped.push(index);
353
453
  else
@@ -394,18 +494,33 @@ const UNCHANGED = { kind: "unchanged" };
394
494
  * `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` ⇒ 搬进 `store`。表达不了 ⇒ 拒:值写错了栏、不认识的词、新旧两形并存、
395
495
  * `DB_BACKEND=pg` 配 MySQL 族坐标。不是 6.x 形的行(没有任何旧键,该有的块都在)⇒ 不动 —— 它读不读得进是 `readWorkersRows` 的事。
396
496
  */
397
- function migrateWorkerRow(raw) {
497
+ function migrateWorkerRow(row) {
398
498
  const refuse = (field, reason) => ({ kind: "refused", field, reason });
399
- const enabled = raw.enabled !== false;
400
- const laneKeysOf = (rec) => (isPlainJsonObject(rec) ? Object.keys(rec).filter(isLaneEnvKey) : []);
401
- const storeKeysOf = (rec) => (isPlainJsonObject(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
402
- const envLaneKeys = laneKeysOf(raw.env);
403
- const secretLaneKeys = laneKeysOf(raw.secretEnv);
404
- const envStoreKeys = storeKeysOf(raw.env);
405
- const secretStoreKeys = storeKeysOf(raw.secretEnv);
406
- const oldStoreFields = SIX_X_STORE_FIELDS.filter(([c, k]) => has(raw[c], k));
407
- const hasExecution = has(raw.runtime, "execution");
408
- const hasStore = has(raw.runtime, "store");
499
+ // ① 判「这一行要不要迁」只做**浅读**:行上的固定键、`env` / `secretEnv` / `runtime` / `coords` / `secretRefs` 这几块的键名与在场(自有键,读不出 ⇒ 抛)—— 不碰别的字段,更不整行深拷:
500
+ // 没有 6.x 键的行(绝大多数)一个字段都不动,所以一份合法的、哪个别的字段嵌了多深的存量文档都不会因为这一步抛(v14.6.0 的答案);拒因也都是浅读给得出的,不必先拷。
501
+ const peek = (o, k) => (isPlainJsonObject(o) ? ownOrThrow(o, k) : undefined);
502
+ const keysOfRec = (rec) => {
503
+ if (!isPlainJsonObject(rec))
504
+ return [];
505
+ const keys = inspect(() => Object.keys(rec));
506
+ if (!keys.ok)
507
+ throw new UninspectableError();
508
+ return keys.value;
509
+ };
510
+ const enabled = peek(row, "enabled") !== false;
511
+ const rawEnv = peek(row, "env");
512
+ const rawSecretEnv = peek(row, "secretEnv");
513
+ const rawRuntime = peek(row, "runtime");
514
+ const rawSecretRefs = peek(row, "secretRefs");
515
+ const laneKeysOf = (rec) => keysOfRec(rec).filter(isLaneEnvKey);
516
+ const storeKeysOf = (rec) => (isPlainJsonObject(rec) ? STORE_ENV_KEYS.filter((k) => has(rec, k)) : []);
517
+ const envLaneKeys = laneKeysOf(rawEnv);
518
+ const secretLaneKeys = laneKeysOf(rawSecretEnv);
519
+ const envStoreKeys = storeKeysOf(rawEnv);
520
+ const secretStoreKeys = storeKeysOf(rawSecretEnv);
521
+ const oldStoreFields = SIX_X_STORE_FIELDS.filter(([c, k]) => has(peek(row, c), k));
522
+ const hasExecution = has(rawRuntime, "execution");
523
+ const hasStore = has(rawRuntime, "store");
409
524
  const laneTouched = envLaneKeys.length + secretLaneKeys.length > 0;
410
525
  const storeTouched = oldStoreFields.length + envStoreKeys.length + secretStoreKeys.length > 0;
411
526
  if (!laneTouched && !storeTouched)
@@ -414,7 +529,7 @@ function migrateWorkerRow(raw) {
414
529
  return refuse(["runtime", "execution"], `declares runtime.execution AND execution-lane keys in env / secretEnv (${[...envLaneKeys, ...secretLaneKeys].join(", ")}) — keep one form; the migration does not pick a winner`);
415
530
  }
416
531
  // 新形的店字段有两处:`runtime.store` 与 `secretRefs.dbPasswordEnv`。任一在场 ∧ 还有旧店字段 ⇒ 两形并存,不挑赢家。
417
- const newStoreFields = [...(hasStore ? ["runtime.store"] : []), ...(has(raw.secretRefs, "dbPasswordEnv") ? ["secretRefs.dbPasswordEnv"] : [])];
532
+ const newStoreFields = [...(hasStore ? ["runtime.store"] : []), ...(has(rawSecretRefs, "dbPasswordEnv") ? ["secretRefs.dbPasswordEnv"] : [])];
418
533
  if (newStoreFields.length > 0 && storeTouched) {
419
534
  const old = [...oldStoreFields.map(([c, k]) => `${c}.${k}`), ...envStoreKeys.map((k) => `env.${k}`), ...secretStoreKeys.map((k) => `secretEnv.${k}`)];
420
535
  return refuse(["runtime", "store"], `declares the 7.0.0 store fields (${newStoreFields.join(", ")}) AND 6.x store fields (${old.join(", ")}) — keep one form; the migration does not pick a winner`);
@@ -422,17 +537,25 @@ function migrateWorkerRow(raw) {
422
537
  const secretStoreKey = secretStoreKeys[0];
423
538
  if (secretStoreKey !== undefined)
424
539
  return refuse(["secretEnv", secretStoreKey], `${secretStoreKey} is set from a host secret — the migration cannot read it; declare runtime.store by hand`);
425
- if (has(raw.secretEnv, "REMOTE_EXEC"))
540
+ if (has(rawSecretEnv, "REMOTE_EXEC"))
426
541
  return refuse(["secretEnv", "REMOTE_EXEC"], "the execution lane is set from a host secret — the migration cannot read it; declare runtime.execution.lane by hand");
427
542
  const needLane = !hasExecution && (enabled || laneTouched);
428
543
  const needStore = !hasStore && (enabled || storeTouched);
429
- const w = structuredClone(raw);
430
- const runtime = isPlainJsonObject(w.runtime) ? w.runtime : {};
544
+ // 收尾要问「原来的 env / secretEnv 有没有键」:在拷之前、浅读里问好
545
+ const envHadKeys = keysOfRec(rawEnv).length > 0;
546
+ const secretEnvHadKeys = keysOfRec(rawSecretEnv).length > 0;
547
+ // ② 只有**要改写的行**才拍快照:整行的自有键深副本(隔离原型,值只从每个成员那一份描述符里取,没有字面深度上限 —— JSON 往返能成的都拷得出);下面对 `env` / `coords` / `runtime` / `secretRefs` 里固定键的读
548
+ // 都读在副本上,没写的键不会被 `Object.prototype` 上被污染的同名数据值填上(那会把污染值写进改写后的存储行)。坏形只有三种:读不出 / 环 / 非纯数据 ⇒ 抛(迁移本来就抛,上一层统一成 TypeError)
549
+ const working = captureJson(row);
550
+ if (!working.ok)
551
+ throw new UninspectableError(working.sentence);
552
+ const w = working.value;
553
+ const runtime = isPlainJsonObject(w.runtime) ? w.runtime : emptyDocument();
431
554
  w.runtime = runtime;
432
- const env = isPlainJsonObject(w.env) ? w.env : {};
433
- const secretEnv = isPlainJsonObject(w.secretEnv) ? w.secretEnv : {};
434
- const coords = isPlainJsonObject(w.coords) ? w.coords : {};
435
- const secretRefs = isPlainJsonObject(w.secretRefs) ? w.secretRefs : {};
555
+ const env = isPlainJsonObject(w.env) ? w.env : emptyDocument();
556
+ const secretEnv = isPlainJsonObject(w.secretEnv) ? w.secretEnv : emptyDocument();
557
+ const coords = isPlainJsonObject(w.coords) ? w.coords : emptyDocument();
558
+ const secretRefs = isPlainJsonObject(w.secretRefs) ? w.secretRefs : emptyDocument();
436
559
  const dropped = [];
437
560
  const drop = (container, slot, key) => {
438
561
  delete container[key];
@@ -456,8 +579,8 @@ function migrateWorkerRow(raw) {
456
579
  return refuse(["env", "REMOTE_EXEC"], `env.REMOTE_EXEC ${describeWord(rx)} is not an execution lane (${EXECUTION_LANE_WORDS.join(" | ")}) — the engine refuses to start with it; set runtime.execution.lane by hand`);
457
580
  }
458
581
  const lane = rx;
459
- const laneCoords = {};
460
- const laneSecretRefs = {};
582
+ const laneCoords = emptyDocument();
583
+ const laneSecretRefs = emptyDocument();
461
584
  for (const k of envLaneKeys) {
462
585
  if (k === "REMOTE_EXEC") {
463
586
  delete env[k];
@@ -545,7 +668,9 @@ function migrateWorkerRow(raw) {
545
668
  }
546
669
  delete env.DB_BACKEND;
547
670
  const sqlCoordsPresent = sqlCoordFields.filter(([c, , k]) => Object.hasOwn(c, k));
548
- const store = { backend, session };
671
+ const store = emptyDocument();
672
+ store.backend = backend;
673
+ store.session = session;
549
674
  if (backend === "mysql") {
550
675
  if (hostSet) {
551
676
  const port = coords.tidbPort;
@@ -570,7 +695,7 @@ function migrateWorkerRow(raw) {
570
695
  for (const [c, slot, k] of sqlCoordsPresent)
571
696
  drop(c, slot, k); // 非 SQL 店:SQL 坐标是「声明不用」的惰性件
572
697
  }
573
- const memory = {};
698
+ const memory = emptyDocument();
574
699
  const engineRaw = env.MEMORY_ENGINE;
575
700
  if (engineRaw !== undefined) {
576
701
  const word = typeof engineRaw === "string" ? engineRaw.trim().toLowerCase() : undefined;
@@ -596,24 +721,30 @@ function migrateWorkerRow(raw) {
596
721
  }
597
722
  }
598
723
  // 被搬空的 env / secretEnv 整键去掉(它们是可选字段;留一个 `{}` 只是噪声)
599
- if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && isPlainJsonObject(raw.env) && Object.keys(raw.env).length > 0)
724
+ if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && envHadKeys)
600
725
  delete w.env;
601
- if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && isPlainJsonObject(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
726
+ if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && secretEnvHadKeys)
602
727
  delete w.secretEnv;
603
728
  // 改写后的行仍读不进 7.x 形 ⇒ 不写回半成品:拒,原文留盘(读口 `readWorkersRows` 扣下它)。判据与拒句是读口那一只(`workerRowVerdict`)。
604
729
  const verdict = workerRowVerdict(w, "migrated");
605
730
  if (verdict !== undefined)
606
731
  return refuse(verdict.field, verdict.reason);
607
- return { kind: "rewritten", row: w, dropped };
732
+ const out = captureJson(w, "ordinary"); // 交出去的是寻常对象(隔离原型的深副本只是工作副本)
733
+ if (!out.ok)
734
+ throw new UninspectableError(out.sentence);
735
+ return { kind: "rewritten", row: out.value, dropped };
608
736
  }
609
737
  function stepWorkers6x(out, receipts) {
610
- const doc = out.workers;
611
- if (!isPlainJsonObject(doc) || !Array.isArray(doc.workers))
738
+ const doc = ownOrThrow(out, "workers");
739
+ if (!isPlainJsonObject(doc))
612
740
  return;
613
- const rows = doc.workers;
741
+ const workerRows = ownOrThrow(doc, "workers");
742
+ if (!isJsonArray(workerRows))
743
+ return;
744
+ const rows = workerRows;
614
745
  let next;
615
746
  rows.forEach((row, index) => {
616
- if (!isPlainJsonObject(row) || typeof row.name !== "string")
747
+ if (!isPlainJsonObject(row) || typeof ownOrThrow(row, "name") !== "string")
617
748
  return; // 认不出名字:整域读侧判
618
749
  const r = migrateWorkerRow(row);
619
750
  switch (r.kind) {
@@ -640,16 +771,19 @@ function stepWorkers6x(out, receipts) {
640
771
  }
641
772
  // ── SS-M5:模型钥匙二选一 ──────────────────────────────────────────────────────────────────────────────
642
773
  function stepModelKeyOneOf(out, receipts) {
643
- const doc = out.models;
644
- if (!isPlainJsonObject(doc) || !Array.isArray(doc.models))
774
+ const doc = ownOrThrow(out, "models");
775
+ if (!isPlainJsonObject(doc))
776
+ return;
777
+ const modelRows = ownOrThrow(doc, "models");
778
+ if (!isJsonArray(modelRows))
645
779
  return;
646
- const rows = doc.models;
780
+ const rows = modelRows;
647
781
  let next;
648
782
  rows.forEach((row, index) => {
649
783
  if (!(has(row, "sealedApiKey") && has(row, "apiKeyEnv")))
650
784
  return;
651
785
  const r = row;
652
- if (r.sealedApiKey === undefined || r.apiKeyEnv === undefined)
786
+ if (ownOrThrow(r, "sealedApiKey") === undefined || ownOrThrow(r, "apiKeyEnv") === undefined)
653
787
  return;
654
788
  next ??= [...rows];
655
789
  next[index] = without(r, "apiKeyEnv");
@@ -663,12 +797,12 @@ function stepModelKeyOneOf(out, receipts) {
663
797
  function rewriteAtMention(holder, document, at, receipts) {
664
798
  if (!Object.prototype.hasOwnProperty.call(holder, "atModelAllowlist"))
665
799
  return undefined;
666
- const list = holder.atModelAllowlist;
800
+ const list = ownOrThrow(holder, "atModelAllowlist");
667
801
  if (Object.prototype.hasOwnProperty.call(holder, "atMention")) {
668
802
  receipts.push(refused("SS-M6", document, [...at, "atModelAllowlist"], "both atModelAllowlist and atMention are set — keep one; the migration does not pick a winner"));
669
803
  return undefined;
670
804
  }
671
- if (!Array.isArray(list) || !list.every((n) => typeof n === "string")) {
805
+ if (!isJsonArray(list) || !list.every((n) => typeof n === "string")) {
672
806
  receipts.push(refused("SS-M6", document, [...at, "atModelAllowlist"], "atModelAllowlist is not a list of model names — it does not read today either; fix it by hand"));
673
807
  return undefined;
674
808
  }
@@ -677,14 +811,16 @@ function rewriteAtMention(holder, document, at, receipts) {
677
811
  return { ...without(holder, "atModelAllowlist"), atMention };
678
812
  }
679
813
  function stepAtMention(out, receipts) {
680
- if (isPlainJsonObject(out.models)) {
681
- const next = rewriteAtMention(out.models, "models", [], receipts);
814
+ const modelsDoc = ownOrThrow(out, "models");
815
+ if (isPlainJsonObject(modelsDoc)) {
816
+ const next = rewriteAtMention(modelsDoc, "models", [], receipts);
682
817
  if (next !== undefined)
683
818
  out.models = next;
684
819
  }
685
- const rosters = out.rosters;
686
- if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
687
- const rows = rosters.rosters;
820
+ const rosters = ownOrThrow(out, "rosters");
821
+ const rosterRows = isPlainJsonObject(rosters) ? ownOrThrow(rosters, "rosters") : undefined;
822
+ if (isPlainJsonObject(rosters) && isJsonArray(rosterRows)) {
823
+ const rows = rosterRows;
688
824
  let next;
689
825
  rows.forEach((row, index) => {
690
826
  if (!isPlainJsonObject(row))
@@ -715,20 +851,21 @@ function withoutRetiredRoles(roles, document, at, receipts) {
715
851
  return next;
716
852
  }
717
853
  function stepRetiredRoleWords(out, receipts) {
718
- const models = out.models;
854
+ const models = ownOrThrow(out, "models");
719
855
  if (isPlainJsonObject(models)) {
720
- const roles = withoutRetiredRoles(models.roles, "models", [], receipts);
856
+ const roles = withoutRetiredRoles(ownOrThrow(models, "roles"), "models", [], receipts);
721
857
  if (roles !== undefined)
722
858
  out.models = { ...models, roles };
723
859
  }
724
- const rosters = out.rosters;
725
- if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
726
- const rows = rosters.rosters;
860
+ const rosters = ownOrThrow(out, "rosters");
861
+ const rosterRows = isPlainJsonObject(rosters) ? ownOrThrow(rosters, "rosters") : undefined;
862
+ if (isPlainJsonObject(rosters) && isJsonArray(rosterRows)) {
863
+ const rows = rosterRows;
727
864
  let next;
728
865
  rows.forEach((row, index) => {
729
866
  if (!isPlainJsonObject(row))
730
867
  return;
731
- const roles = withoutRetiredRoles(row.roles, "rosters", ["rosters", index], receipts);
868
+ const roles = withoutRetiredRoles(ownOrThrow(row, "roles"), "rosters", ["rosters", index], receipts);
732
869
  if (roles === undefined)
733
870
  return;
734
871
  next ??= [...rows];
@@ -743,17 +880,18 @@ function stepRetiredRoleWords(out, receipts) {
743
880
  * = B 时主任务跑 A,core 的 `default` 角色解到 B)。所以并过去的是 `default` 的值:`roles.default` 缺席 ⇒ 搬入;同名 ⇒ 搬入(重复的那一份
744
881
  * 消失);异名 ⇒ 原 `roles.default` 作「被压过」删掉、`default` 搬入。`default` 不是非空串 ⇒ 拒(今天它让整个 models 域读不进,不替运维猜)。 */
745
882
  function stepModelsDefault(out, receipts) {
746
- const doc = out.models;
883
+ const doc = ownOrThrow(out, "models");
747
884
  if (!isPlainJsonObject(doc) || !Object.prototype.hasOwnProperty.call(doc, "default"))
748
885
  return;
749
- const name = doc.default;
886
+ const name = ownOrThrow(doc, "default");
750
887
  if (typeof name !== "string" || name === "") {
751
888
  receipts.push(refused("SS-M9", "models", ["default"], "models.default is not a catalog name — it does not read today either; fix it by hand"));
752
889
  return;
753
890
  }
754
- const roles = isPlainJsonObject(doc.roles) ? doc.roles : {};
755
- const prior = roles.default;
756
- if (prior !== undefined && !(isPlainJsonObject(prior) && prior.model === name))
891
+ const docRoles = ownOrThrow(doc, "roles");
892
+ const roles = isPlainJsonObject(docRoles) ? docRoles : emptyDocument();
893
+ const prior = ownOrThrow(roles, "default");
894
+ if (prior !== undefined && !(isPlainJsonObject(prior) && ownOrThrow(prior, "model") === name))
757
895
  receipts.push(receipt("SS-M9", "models", ["roles", "default"], "dropped-shadowed"));
758
896
  receipts.push(moved("SS-M9", "models", ["default"], { document: "models", path: ["roles", "default"] }));
759
897
  out.models = { ...without(doc, "default"), roles: { ...roles, default: { model: name } } };
@@ -763,12 +901,13 @@ function stepModelsDefault(out, receipts) {
763
901
  * 从不下发任何内容)删掉。非空 `bindings` / 不是数组的 `bindings` / `catalog` 一律 refused、原样留着 —— 声明要经中心的卡编译器才编得成
764
902
  * overlay,catalog 的 digest 是引擎铸的,替运维机械转就是第二个编译器 / 第二个铸点。`prompts` 不是对象 ⇒ 不动(判形是读者的事)。 */
765
903
  function stepPromptsAxes(out, receipts) {
766
- const doc = out.prompts;
904
+ const doc = ownOrThrow(out, "prompts");
767
905
  if (!isPlainJsonObject(doc))
768
906
  return;
769
907
  let next = doc;
770
- if (has(doc, "bindings") && doc.bindings !== undefined) {
771
- if (Array.isArray(doc.bindings) && doc.bindings.length === 0) {
908
+ const bindings = has(doc, "bindings") ? ownOrThrow(doc, "bindings") : undefined;
909
+ if (bindings !== undefined) {
910
+ if (isJsonArray(bindings) && bindings.length === 0) {
772
911
  next = without(next, "bindings");
773
912
  receipts.push(receipt("SS-M10", "prompts", ["bindings"], "dropped-inert"));
774
913
  }
@@ -777,7 +916,7 @@ function stepPromptsAxes(out, receipts) {
777
916
  "(a local config.d copy was never read by the engine: delete the key)"));
778
917
  }
779
918
  }
780
- if (has(doc, "catalog") && doc.catalog !== undefined) {
919
+ if (has(doc, "catalog") && ownOrThrow(doc, "catalog") !== undefined) {
781
920
  receipts.push(refused("SS-M10", "prompts", ["catalog"], "a v1 catalog cannot be converted (its digest is the engine's) — republish from the web-admin, or rebuild with the engine's buildPromptArtifact"));
782
921
  }
783
922
  if (next !== doc)
@@ -823,7 +962,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
823
962
  if (!(o instanceof z.ZodObject))
824
963
  continue;
825
964
  const lit = unwrapSchema(o.shape[disc]);
826
- if (lit instanceof z.ZodLiteral && lit.values.has(value[disc]))
965
+ if (lit instanceof z.ZodLiteral && lit.values.has(ownOrThrow(value, disc)))
827
966
  return stripFragments(o, value, path, onStrip, depth + 1);
828
967
  }
829
968
  return value;
@@ -843,14 +982,15 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
843
982
  for (const [k, fieldSchema] of Object.entries(s.shape)) {
844
983
  if (!Object.prototype.hasOwnProperty.call(value, k))
845
984
  continue;
846
- const v = stripFragments(fieldSchema, value[k], [...path, k], onStrip, depth + 1);
847
- if (v !== value[k])
985
+ const held = ownOrThrow(value, k);
986
+ const v = stripFragments(fieldSchema, held, [...path, k], onStrip, depth + 1);
987
+ if (v !== held)
848
988
  (next ??= { ...value })[k] = v;
849
989
  }
850
990
  return next ?? value;
851
991
  }
852
992
  if (s instanceof z.ZodArray) {
853
- if (!Array.isArray(value))
993
+ if (!isJsonArray(value))
854
994
  return value;
855
995
  let next;
856
996
  value.forEach((el, i) => {