@sema-agent/settings-schema 14.5.0 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +46 -3
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.js +10 -7
  16. package/dist/bundle.d.ts +1 -1
  17. package/dist/bundle.js +1 -1
  18. package/dist/config-d-file.js +33 -15
  19. package/dist/config-dir-migration.js +2 -1
  20. package/dist/config-fns.js +142 -78
  21. package/dist/conformance/canonical.js +7 -10
  22. package/dist/conformance/contract-adapter.js +26 -1
  23. package/dist/conformance/corpus.js +5 -5
  24. package/dist/conformance/derive.js +6 -5
  25. package/dist/conformance/run.js +44 -6
  26. package/dist/cross-session.d.ts +4 -4
  27. package/dist/declared-capture.d.ts +41 -0
  28. package/dist/declared-capture.js +344 -0
  29. package/dist/file-edit.js +11 -15
  30. package/dist/file-store.d.ts +1 -31
  31. package/dist/file-store.js +3 -30
  32. package/dist/fleet.d.ts +11 -0
  33. package/dist/fleet.js +60 -26
  34. package/dist/fs-failure.d.ts +12 -7
  35. package/dist/fs-failure.js +53 -29
  36. package/dist/hash.d.ts +0 -9
  37. package/dist/hash.js +64 -7
  38. package/dist/hooks-config.d.ts +2 -2
  39. package/dist/hooks-config.js +129 -33
  40. package/dist/http-hook-allowlists.d.ts +27 -10
  41. package/dist/http-hook-allowlists.js +189 -25
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -1
  44. package/dist/mcp-request-entry.d.ts +13 -8
  45. package/dist/mcp-request-entry.js +246 -107
  46. package/dist/one-time-migrations.js +207 -95
  47. package/dist/own-key-read.d.ts +247 -0
  48. package/dist/own-key-read.js +449 -0
  49. package/dist/plain-json-object.d.ts +38 -3
  50. package/dist/plain-json-object.js +84 -7
  51. package/dist/portable-domains.d.ts +31 -0
  52. package/dist/portable-domains.js +30 -0
  53. package/dist/remote-exec.js +9 -1
  54. package/dist/safety-merge-spec.d.ts +88 -6
  55. package/dist/safety-merge-spec.js +246 -17
  56. package/dist/sandbox-git.d.ts +64 -0
  57. package/dist/sandbox-git.js +89 -0
  58. package/dist/scheduler-store.js +21 -12
  59. package/dist/settings-units.d.ts +85 -0
  60. package/dist/settings-units.js +177 -0
  61. package/dist/types.d.ts +6 -2
  62. package/dist/types.js +65 -56
  63. package/dist/url-shape.d.ts +27 -5
  64. package/dist/url-shape.js +29 -5
  65. package/dist/web-admin-credential.js +5 -2
  66. package/dist/web-admin-session-url.js +2 -2
  67. package/dist/worker-row-verdict.d.ts +11 -0
  68. package/dist/worker-row-verdict.js +51 -1
  69. package/dist/workers-rows.d.ts +2 -1
  70. package/dist/workers-rows.js +41 -12
  71. package/dist/zod-unwrap.d.ts +6 -0
  72. package/dist/zod-unwrap.js +14 -7
  73. package/docs/API-SURFACE.md +50 -5
  74. package/docs/CONSUMER-INTEGRATION.md +178 -0
  75. package/llms-full.txt +34 -5
  76. package/llms.txt +6 -3
  77. package/package.json +1 -1
@@ -33,7 +33,8 @@ import { outgoingPath, redactSecrets } from "./config-fns.js";
33
33
  import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
34
34
  import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
35
35
  import { INTEGER } from "./integer.js";
36
- import { isPlainJsonObject } from "./plain-json-object.js";
36
+ import { NOT_PLAIN_DATA, UNINSPECTABLE, UninspectableError, captureJson, dataDescriptor, emptyDocument, inspect, ownEntriesOf, ownOrThrow, readDeclaredKey, sentenceOfMinted } from "./own-key-read.js";
37
+ import { isJsonArray, isPlainJsonObject, jsonShapeOf } from "./plain-json-object.js";
37
38
  import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
38
39
  import { RETIRED_ROLE_WORDS } from "./retired-role-words.js";
39
40
  import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
@@ -198,8 +199,22 @@ export const ONE_TIME_MIGRATIONS = [
198
199
  * 重跑恒等:第二次跑零改动,只剩第一次就 `refused` 的那几张收据(那些坐标原样留着,读侧照旧拒)。
199
200
  */
200
201
  export function migrateStoredDocuments(docs, opts) {
201
- if (!isPlainJsonObject(docs))
202
- throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
202
+ const docsShape = jsonShapeOf(docs);
203
+ switch (docsShape) {
204
+ case "plain":
205
+ break;
206
+ case "uninspectable": // 文档集本身读不出(撤销的 Proxy / 陷阱抛):不是「形状不对」,是「读不出」—— 带全包那一句
207
+ throw new TypeError(`migrateStoredDocuments: the stored documents could not be read — ${UNINSPECTABLE}`);
208
+ case "non_plain_object":
209
+ case "array":
210
+ case "null":
211
+ case "primitive":
212
+ throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
213
+ default: {
214
+ const unhandled = docsShape;
215
+ throw new Error(`migrateStoredDocuments: unhandled shape ${String(unhandled)}`);
216
+ }
217
+ }
203
218
  switch (opts.coverage) {
204
219
  case "scope":
205
220
  case "partial":
@@ -209,20 +224,35 @@ export function migrateStoredDocuments(docs, opts) {
209
224
  throw new TypeError(`migrateStoredDocuments: coverage must be "scope" or "partial" (got ${JSON.stringify(unhandled)})`);
210
225
  }
211
226
  }
212
- const out = { ...docs };
227
+ // 工作副本是**隔离原型**(`COPY_PROTOTYPE`,没有继承来的成员)的袋(它的读没有继承可说:`Object.prototype.models` 被污染成数据值时,`out.models` 不会读到一份「存储文档」),文档集只读一次(每个域文档一个值);
228
+ // 之后每一步对文档里固定键的读都经内核的自有键读(`ownOrThrow`),读不出的存储文档 ⇒ 下面那一处统一抛 TypeError(带全包唯一那一句)—— 迁移本来就是抛的读口,不另造失败臂
229
+ const entries = ownEntriesOf(docs);
230
+ if (!entries.ok)
231
+ throw new TypeError(`migrateStoredDocuments: the stored documents could not be read — ${entries.sentence}`);
232
+ const out = emptyDocument();
233
+ for (const [k, v] of entries.value)
234
+ Object.defineProperty(out, k, dataDescriptor(v));
213
235
  const receipts = [];
214
236
  // 步序的律:纯改写步在前,拿新 schema 终判改写结果的步(SS-M4)最后 —— 它判的是**全部**改写之后的行,别的步能修好的形(例:SS-M7 剥掉的
215
237
  // 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 };
238
+ try {
239
+ stepRuntimeDomain(out, opts.coverage, receipts);
240
+ stepHostsBounds(out, receipts);
241
+ stepModelKeyOneOf(out, receipts);
242
+ stepAtMention(out, receipts);
243
+ stepRetiredRoleWords(out, receipts);
244
+ stepModelsDefault(out, receipts);
245
+ stepPromptsAxes(out, receipts);
246
+ stepUrlFragments(out, receipts);
247
+ stepWorkers6x(out, receipts);
248
+ }
249
+ catch (e) {
250
+ const sentence = sentenceOfMinted(e);
251
+ if (sentence !== undefined)
252
+ throw new TypeError(`migrateStoredDocuments: a stored document could not be read — ${sentence}`);
253
+ throw e;
254
+ }
255
+ return { docs: { ...out }, receipts }; // 交出去的是寻常对象(隔离原型的袋只是工作副本)
226
256
  }
227
257
  /**
228
258
  * 迁移一份冻结的 effective(见文件头「入口」):域键过与 {@link migrateStoredDocuments} 同一组步(coverage `"scope"` —— effective 里每个域
@@ -230,29 +260,68 @@ export function migrateStoredDocuments(docs, opts) {
230
260
  * 不是对象 ⇒ 原样返回(判形是读者的事)。
231
261
  */
232
262
  export function migrateEffectiveSnapshot(effective) {
233
- if (!isPlainJsonObject(effective))
234
- return { effective, receipts: [] };
235
- const docs = {};
236
- for (const [k, v] of Object.entries(effective))
263
+ // 判形是读者的事:不是朴素对象(null / 数组 / 原始值 / 类实例)⇒ 原样返回;**读不出**(撤销的 Proxy / 陷阱抛)不是「不是这个形」,是读不出 ⇒ 抛(与文档集同一句)
264
+ const effectiveShape = jsonShapeOf(effective);
265
+ switch (effectiveShape) {
266
+ case "plain":
267
+ break;
268
+ case "uninspectable":
269
+ throw new TypeError(`migrateEffectiveSnapshot: the effective snapshot could not be read — ${UNINSPECTABLE}`);
270
+ case "non_plain_object":
271
+ case "array":
272
+ case "null":
273
+ case "primitive":
274
+ return { effective, receipts: [] };
275
+ default: {
276
+ const unhandled = effectiveShape;
277
+ throw new Error(`migrateEffectiveSnapshot: unhandled shape ${String(unhandled)}`);
278
+ }
279
+ }
280
+ const read = ownEntriesOf(effective);
281
+ if (!read.ok)
282
+ throw new TypeError(`migrateEffectiveSnapshot: the effective snapshot could not be read — ${read.sentence}`);
283
+ const entriesOfEffective = read.value;
284
+ const docs = emptyDocument();
285
+ for (const [k, v] of entriesOfEffective)
237
286
  if (isDomainName(k) || isRetiredDomainName(k))
238
- docs[k] = v;
287
+ Object.defineProperty(docs, k, dataDescriptor(v));
239
288
  const m = migrateStoredDocuments(docs, { coverage: "scope" });
240
289
  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
290
  if (!changed)
242
291
  return { effective, receipts: m.receipts };
243
- const next = {};
244
- for (const [k, v] of Object.entries(effective)) {
292
+ const next = emptyDocument();
293
+ for (const [k, v] of entriesOfEffective) {
245
294
  if (!(isDomainName(k) || isRetiredDomainName(k)))
246
- next[k] = v;
295
+ Object.defineProperty(next, k, dataDescriptor(v));
247
296
  else if (Object.prototype.hasOwnProperty.call(m.docs, k))
248
- next[k] = m.docs[k];
297
+ Object.defineProperty(next, k, dataDescriptor(m.docs[k]));
249
298
  }
250
299
  for (const [k, v] of Object.entries(m.docs))
251
300
  if (!Object.prototype.hasOwnProperty.call(next, k))
252
- next[k] = v;
253
- return { effective: next, receipts: m.receipts };
301
+ Object.defineProperty(next, k, dataDescriptor(v));
302
+ return { effective: { ...next }, receipts: m.receipts };
254
303
  }
255
- const has = (o, k) => isPlainJsonObject(o) && Object.prototype.hasOwnProperty.call(o, k);
304
+ /** `o` 是朴素对象且**自有**键 `k` 在场(描述符律,与 `ownOrThrow` 同一只核:访问器 / 不可枚举的自有键不当缺席 ⇒ 抛带「不是纯数据」那一句;读不出 ⇒ 抛;迁移本来就抛)。 */
305
+ const has = (o, k) => {
306
+ if (!isPlainJsonObject(o))
307
+ return false;
308
+ const read = readDeclaredKey(o, k);
309
+ switch (read.presence) {
310
+ case "own":
311
+ return true;
312
+ case "inherited":
313
+ case "absent":
314
+ return false;
315
+ case "hidden":
316
+ throw new UninspectableError(NOT_PLAIN_DATA);
317
+ case "unreadable":
318
+ throw new UninspectableError();
319
+ default: {
320
+ const unhandled = read;
321
+ throw new Error(`has: unhandled presence ${String(unhandled.presence)}`);
322
+ }
323
+ }
324
+ };
256
325
  const without = (o, k) => {
257
326
  const { [k]: _gone, ...rest } = o;
258
327
  return rest;
@@ -282,7 +351,7 @@ const LIMIT_KEYS = ["rateLimitPerMin", "maxTaskCostUsd", "maxTaskTokens", "maxPr
282
351
  function stepRuntimeDomain(out, coverage, receipts) {
283
352
  if (!Object.prototype.hasOwnProperty.call(out, "runtime"))
284
353
  return;
285
- const raw = out.runtime;
354
+ const raw = ownOrThrow(out, "runtime");
286
355
  if (raw === undefined)
287
356
  return;
288
357
  const refuse = (reason) => void receipts.push(refused("SS-M1", "runtime", [], reason));
@@ -299,14 +368,14 @@ function stepRuntimeDomain(out, coverage, receipts) {
299
368
  const parsed = RUNTIME_10X.safeParse(raw);
300
369
  if (!parsed.success)
301
370
  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;
371
+ const limitsRaw = ownOrThrow(out, "limits");
303
372
  if (limitsRaw !== undefined && !isPlainJsonObject(limitsRaw))
304
373
  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);
374
+ const trio = GOVERNANCE_TRIO.filter((k) => has(raw, k) && ownOrThrow(raw, k) !== undefined);
306
375
  if (trio.length > 0) {
307
376
  // 迁移前的读法:governance 域**从未设过**(店里没有这份文档)⇒ 读侧从 runtime 抬升三闸;设过 ⇒ 设过的那份赢,runtime 上的三闸从未生效。
308
- if (out.governance === undefined) {
309
- out.governance = Object.fromEntries(trio.map((k) => [k, raw[k]]));
377
+ if (ownOrThrow(out, "governance") === undefined) {
378
+ out.governance = Object.fromEntries(trio.map((k) => [k, ownOrThrow(raw, k)]));
310
379
  for (const k of trio)
311
380
  receipts.push(moved("SS-M1", "runtime", [k], { document: "governance", path: [k] }));
312
381
  }
@@ -340,14 +409,17 @@ function stepRuntimeDomain(out, coverage, receipts) {
340
409
  }
341
410
  // ── SS-M2:hosts 超界 ───────────────────────────────────────────────────────────────────────────────────
342
411
  function stepHostsBounds(out, receipts) {
343
- const doc = out.hosts;
344
- if (!isPlainJsonObject(doc) || !Array.isArray(doc.hosts))
412
+ const doc = ownOrThrow(out, "hosts");
413
+ if (!isPlainJsonObject(doc))
414
+ return;
415
+ const hostRows = ownOrThrow(doc, "hosts");
416
+ if (!isJsonArray(hostRows))
345
417
  return;
346
- const rows = doc.hosts;
418
+ const rows = hostRows;
347
419
  let kept = [];
348
420
  const dropped = [];
349
421
  rows.forEach((row, index) => {
350
- const name = isPlainJsonObject(row) ? row.name : undefined;
422
+ const name = isPlainJsonObject(row) ? ownOrThrow(row, "name") : undefined;
351
423
  if (typeof name === "string" && name.length > HOST_NAME_MAX)
352
424
  dropped.push(index);
353
425
  else
@@ -394,18 +466,33 @@ const UNCHANGED = { kind: "unchanged" };
394
466
  * `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` ⇒ 搬进 `store`。表达不了 ⇒ 拒:值写错了栏、不认识的词、新旧两形并存、
395
467
  * `DB_BACKEND=pg` 配 MySQL 族坐标。不是 6.x 形的行(没有任何旧键,该有的块都在)⇒ 不动 —— 它读不读得进是 `readWorkersRows` 的事。
396
468
  */
397
- function migrateWorkerRow(raw) {
469
+ function migrateWorkerRow(row) {
398
470
  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");
471
+ // ① 判「这一行要不要迁」只做**浅读**:行上的固定键、`env` / `secretEnv` / `runtime` / `coords` / `secretRefs` 这几块的键名与在场(自有键,读不出 ⇒ 抛)—— 不碰别的字段,更不整行深拷:
472
+ // 没有 6.x 键的行(绝大多数)一个字段都不动,所以一份合法的、哪个别的字段嵌了多深的存量文档都不会因为这一步抛(v14.6.0 的答案);拒因也都是浅读给得出的,不必先拷。
473
+ const peek = (o, k) => (isPlainJsonObject(o) ? ownOrThrow(o, k) : undefined);
474
+ const keysOfRec = (rec) => {
475
+ if (!isPlainJsonObject(rec))
476
+ return [];
477
+ const keys = inspect(() => Object.keys(rec));
478
+ if (!keys.ok)
479
+ throw new UninspectableError();
480
+ return keys.value;
481
+ };
482
+ const enabled = peek(row, "enabled") !== false;
483
+ const rawEnv = peek(row, "env");
484
+ const rawSecretEnv = peek(row, "secretEnv");
485
+ const rawRuntime = peek(row, "runtime");
486
+ const rawSecretRefs = peek(row, "secretRefs");
487
+ const laneKeysOf = (rec) => keysOfRec(rec).filter(isLaneEnvKey);
488
+ const storeKeysOf = (rec) => (isPlainJsonObject(rec) ? STORE_ENV_KEYS.filter((k) => has(rec, k)) : []);
489
+ const envLaneKeys = laneKeysOf(rawEnv);
490
+ const secretLaneKeys = laneKeysOf(rawSecretEnv);
491
+ const envStoreKeys = storeKeysOf(rawEnv);
492
+ const secretStoreKeys = storeKeysOf(rawSecretEnv);
493
+ const oldStoreFields = SIX_X_STORE_FIELDS.filter(([c, k]) => has(peek(row, c), k));
494
+ const hasExecution = has(rawRuntime, "execution");
495
+ const hasStore = has(rawRuntime, "store");
409
496
  const laneTouched = envLaneKeys.length + secretLaneKeys.length > 0;
410
497
  const storeTouched = oldStoreFields.length + envStoreKeys.length + secretStoreKeys.length > 0;
411
498
  if (!laneTouched && !storeTouched)
@@ -414,7 +501,7 @@ function migrateWorkerRow(raw) {
414
501
  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
502
  }
416
503
  // 新形的店字段有两处:`runtime.store` 与 `secretRefs.dbPasswordEnv`。任一在场 ∧ 还有旧店字段 ⇒ 两形并存,不挑赢家。
417
- const newStoreFields = [...(hasStore ? ["runtime.store"] : []), ...(has(raw.secretRefs, "dbPasswordEnv") ? ["secretRefs.dbPasswordEnv"] : [])];
504
+ const newStoreFields = [...(hasStore ? ["runtime.store"] : []), ...(has(rawSecretRefs, "dbPasswordEnv") ? ["secretRefs.dbPasswordEnv"] : [])];
418
505
  if (newStoreFields.length > 0 && storeTouched) {
419
506
  const old = [...oldStoreFields.map(([c, k]) => `${c}.${k}`), ...envStoreKeys.map((k) => `env.${k}`), ...secretStoreKeys.map((k) => `secretEnv.${k}`)];
420
507
  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 +509,25 @@ function migrateWorkerRow(raw) {
422
509
  const secretStoreKey = secretStoreKeys[0];
423
510
  if (secretStoreKey !== undefined)
424
511
  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"))
512
+ if (has(rawSecretEnv, "REMOTE_EXEC"))
426
513
  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
514
  const needLane = !hasExecution && (enabled || laneTouched);
428
515
  const needStore = !hasStore && (enabled || storeTouched);
429
- const w = structuredClone(raw);
430
- const runtime = isPlainJsonObject(w.runtime) ? w.runtime : {};
516
+ // 收尾要问「原来的 env / secretEnv 有没有键」:在拷之前、浅读里问好
517
+ const envHadKeys = keysOfRec(rawEnv).length > 0;
518
+ const secretEnvHadKeys = keysOfRec(rawSecretEnv).length > 0;
519
+ // ② 只有**要改写的行**才拍快照:整行的自有键深副本(隔离原型,值只从每个成员那一份描述符里取,没有字面深度上限 —— JSON 往返能成的都拷得出);下面对 `env` / `coords` / `runtime` / `secretRefs` 里固定键的读
520
+ // 都读在副本上,没写的键不会被 `Object.prototype` 上被污染的同名数据值填上(那会把污染值写进改写后的存储行)。坏形只有三种:读不出 / 环 / 非纯数据 ⇒ 抛(迁移本来就抛,上一层统一成 TypeError)
521
+ const working = captureJson(row);
522
+ if (!working.ok)
523
+ throw new UninspectableError(working.sentence);
524
+ const w = working.value;
525
+ const runtime = isPlainJsonObject(w.runtime) ? w.runtime : emptyDocument();
431
526
  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 : {};
527
+ const env = isPlainJsonObject(w.env) ? w.env : emptyDocument();
528
+ const secretEnv = isPlainJsonObject(w.secretEnv) ? w.secretEnv : emptyDocument();
529
+ const coords = isPlainJsonObject(w.coords) ? w.coords : emptyDocument();
530
+ const secretRefs = isPlainJsonObject(w.secretRefs) ? w.secretRefs : emptyDocument();
436
531
  const dropped = [];
437
532
  const drop = (container, slot, key) => {
438
533
  delete container[key];
@@ -456,8 +551,8 @@ function migrateWorkerRow(raw) {
456
551
  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
552
  }
458
553
  const lane = rx;
459
- const laneCoords = {};
460
- const laneSecretRefs = {};
554
+ const laneCoords = emptyDocument();
555
+ const laneSecretRefs = emptyDocument();
461
556
  for (const k of envLaneKeys) {
462
557
  if (k === "REMOTE_EXEC") {
463
558
  delete env[k];
@@ -545,7 +640,9 @@ function migrateWorkerRow(raw) {
545
640
  }
546
641
  delete env.DB_BACKEND;
547
642
  const sqlCoordsPresent = sqlCoordFields.filter(([c, , k]) => Object.hasOwn(c, k));
548
- const store = { backend, session };
643
+ const store = emptyDocument();
644
+ store.backend = backend;
645
+ store.session = session;
549
646
  if (backend === "mysql") {
550
647
  if (hostSet) {
551
648
  const port = coords.tidbPort;
@@ -570,7 +667,7 @@ function migrateWorkerRow(raw) {
570
667
  for (const [c, slot, k] of sqlCoordsPresent)
571
668
  drop(c, slot, k); // 非 SQL 店:SQL 坐标是「声明不用」的惰性件
572
669
  }
573
- const memory = {};
670
+ const memory = emptyDocument();
574
671
  const engineRaw = env.MEMORY_ENGINE;
575
672
  if (engineRaw !== undefined) {
576
673
  const word = typeof engineRaw === "string" ? engineRaw.trim().toLowerCase() : undefined;
@@ -596,24 +693,30 @@ function migrateWorkerRow(raw) {
596
693
  }
597
694
  }
598
695
  // 被搬空的 env / secretEnv 整键去掉(它们是可选字段;留一个 `{}` 只是噪声)
599
- if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && isPlainJsonObject(raw.env) && Object.keys(raw.env).length > 0)
696
+ if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && envHadKeys)
600
697
  delete w.env;
601
- if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && isPlainJsonObject(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
698
+ if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && secretEnvHadKeys)
602
699
  delete w.secretEnv;
603
700
  // 改写后的行仍读不进 7.x 形 ⇒ 不写回半成品:拒,原文留盘(读口 `readWorkersRows` 扣下它)。判据与拒句是读口那一只(`workerRowVerdict`)。
604
701
  const verdict = workerRowVerdict(w, "migrated");
605
702
  if (verdict !== undefined)
606
703
  return refuse(verdict.field, verdict.reason);
607
- return { kind: "rewritten", row: w, dropped };
704
+ const out = captureJson(w, "ordinary"); // 交出去的是寻常对象(隔离原型的深副本只是工作副本)
705
+ if (!out.ok)
706
+ throw new UninspectableError(out.sentence);
707
+ return { kind: "rewritten", row: out.value, dropped };
608
708
  }
609
709
  function stepWorkers6x(out, receipts) {
610
- const doc = out.workers;
611
- if (!isPlainJsonObject(doc) || !Array.isArray(doc.workers))
710
+ const doc = ownOrThrow(out, "workers");
711
+ if (!isPlainJsonObject(doc))
712
+ return;
713
+ const workerRows = ownOrThrow(doc, "workers");
714
+ if (!isJsonArray(workerRows))
612
715
  return;
613
- const rows = doc.workers;
716
+ const rows = workerRows;
614
717
  let next;
615
718
  rows.forEach((row, index) => {
616
- if (!isPlainJsonObject(row) || typeof row.name !== "string")
719
+ if (!isPlainJsonObject(row) || typeof ownOrThrow(row, "name") !== "string")
617
720
  return; // 认不出名字:整域读侧判
618
721
  const r = migrateWorkerRow(row);
619
722
  switch (r.kind) {
@@ -640,16 +743,19 @@ function stepWorkers6x(out, receipts) {
640
743
  }
641
744
  // ── SS-M5:模型钥匙二选一 ──────────────────────────────────────────────────────────────────────────────
642
745
  function stepModelKeyOneOf(out, receipts) {
643
- const doc = out.models;
644
- if (!isPlainJsonObject(doc) || !Array.isArray(doc.models))
746
+ const doc = ownOrThrow(out, "models");
747
+ if (!isPlainJsonObject(doc))
748
+ return;
749
+ const modelRows = ownOrThrow(doc, "models");
750
+ if (!isJsonArray(modelRows))
645
751
  return;
646
- const rows = doc.models;
752
+ const rows = modelRows;
647
753
  let next;
648
754
  rows.forEach((row, index) => {
649
755
  if (!(has(row, "sealedApiKey") && has(row, "apiKeyEnv")))
650
756
  return;
651
757
  const r = row;
652
- if (r.sealedApiKey === undefined || r.apiKeyEnv === undefined)
758
+ if (ownOrThrow(r, "sealedApiKey") === undefined || ownOrThrow(r, "apiKeyEnv") === undefined)
653
759
  return;
654
760
  next ??= [...rows];
655
761
  next[index] = without(r, "apiKeyEnv");
@@ -663,12 +769,12 @@ function stepModelKeyOneOf(out, receipts) {
663
769
  function rewriteAtMention(holder, document, at, receipts) {
664
770
  if (!Object.prototype.hasOwnProperty.call(holder, "atModelAllowlist"))
665
771
  return undefined;
666
- const list = holder.atModelAllowlist;
772
+ const list = ownOrThrow(holder, "atModelAllowlist");
667
773
  if (Object.prototype.hasOwnProperty.call(holder, "atMention")) {
668
774
  receipts.push(refused("SS-M6", document, [...at, "atModelAllowlist"], "both atModelAllowlist and atMention are set — keep one; the migration does not pick a winner"));
669
775
  return undefined;
670
776
  }
671
- if (!Array.isArray(list) || !list.every((n) => typeof n === "string")) {
777
+ if (!isJsonArray(list) || !list.every((n) => typeof n === "string")) {
672
778
  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
779
  return undefined;
674
780
  }
@@ -677,14 +783,16 @@ function rewriteAtMention(holder, document, at, receipts) {
677
783
  return { ...without(holder, "atModelAllowlist"), atMention };
678
784
  }
679
785
  function stepAtMention(out, receipts) {
680
- if (isPlainJsonObject(out.models)) {
681
- const next = rewriteAtMention(out.models, "models", [], receipts);
786
+ const modelsDoc = ownOrThrow(out, "models");
787
+ if (isPlainJsonObject(modelsDoc)) {
788
+ const next = rewriteAtMention(modelsDoc, "models", [], receipts);
682
789
  if (next !== undefined)
683
790
  out.models = next;
684
791
  }
685
- const rosters = out.rosters;
686
- if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
687
- const rows = rosters.rosters;
792
+ const rosters = ownOrThrow(out, "rosters");
793
+ const rosterRows = isPlainJsonObject(rosters) ? ownOrThrow(rosters, "rosters") : undefined;
794
+ if (isPlainJsonObject(rosters) && isJsonArray(rosterRows)) {
795
+ const rows = rosterRows;
688
796
  let next;
689
797
  rows.forEach((row, index) => {
690
798
  if (!isPlainJsonObject(row))
@@ -715,20 +823,21 @@ function withoutRetiredRoles(roles, document, at, receipts) {
715
823
  return next;
716
824
  }
717
825
  function stepRetiredRoleWords(out, receipts) {
718
- const models = out.models;
826
+ const models = ownOrThrow(out, "models");
719
827
  if (isPlainJsonObject(models)) {
720
- const roles = withoutRetiredRoles(models.roles, "models", [], receipts);
828
+ const roles = withoutRetiredRoles(ownOrThrow(models, "roles"), "models", [], receipts);
721
829
  if (roles !== undefined)
722
830
  out.models = { ...models, roles };
723
831
  }
724
- const rosters = out.rosters;
725
- if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
726
- const rows = rosters.rosters;
832
+ const rosters = ownOrThrow(out, "rosters");
833
+ const rosterRows = isPlainJsonObject(rosters) ? ownOrThrow(rosters, "rosters") : undefined;
834
+ if (isPlainJsonObject(rosters) && isJsonArray(rosterRows)) {
835
+ const rows = rosterRows;
727
836
  let next;
728
837
  rows.forEach((row, index) => {
729
838
  if (!isPlainJsonObject(row))
730
839
  return;
731
- const roles = withoutRetiredRoles(row.roles, "rosters", ["rosters", index], receipts);
840
+ const roles = withoutRetiredRoles(ownOrThrow(row, "roles"), "rosters", ["rosters", index], receipts);
732
841
  if (roles === undefined)
733
842
  return;
734
843
  next ??= [...rows];
@@ -743,17 +852,18 @@ function stepRetiredRoleWords(out, receipts) {
743
852
  * = B 时主任务跑 A,core 的 `default` 角色解到 B)。所以并过去的是 `default` 的值:`roles.default` 缺席 ⇒ 搬入;同名 ⇒ 搬入(重复的那一份
744
853
  * 消失);异名 ⇒ 原 `roles.default` 作「被压过」删掉、`default` 搬入。`default` 不是非空串 ⇒ 拒(今天它让整个 models 域读不进,不替运维猜)。 */
745
854
  function stepModelsDefault(out, receipts) {
746
- const doc = out.models;
855
+ const doc = ownOrThrow(out, "models");
747
856
  if (!isPlainJsonObject(doc) || !Object.prototype.hasOwnProperty.call(doc, "default"))
748
857
  return;
749
- const name = doc.default;
858
+ const name = ownOrThrow(doc, "default");
750
859
  if (typeof name !== "string" || name === "") {
751
860
  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
861
  return;
753
862
  }
754
- const roles = isPlainJsonObject(doc.roles) ? doc.roles : {};
755
- const prior = roles.default;
756
- if (prior !== undefined && !(isPlainJsonObject(prior) && prior.model === name))
863
+ const docRoles = ownOrThrow(doc, "roles");
864
+ const roles = isPlainJsonObject(docRoles) ? docRoles : emptyDocument();
865
+ const prior = ownOrThrow(roles, "default");
866
+ if (prior !== undefined && !(isPlainJsonObject(prior) && ownOrThrow(prior, "model") === name))
757
867
  receipts.push(receipt("SS-M9", "models", ["roles", "default"], "dropped-shadowed"));
758
868
  receipts.push(moved("SS-M9", "models", ["default"], { document: "models", path: ["roles", "default"] }));
759
869
  out.models = { ...without(doc, "default"), roles: { ...roles, default: { model: name } } };
@@ -763,12 +873,13 @@ function stepModelsDefault(out, receipts) {
763
873
  * 从不下发任何内容)删掉。非空 `bindings` / 不是数组的 `bindings` / `catalog` 一律 refused、原样留着 —— 声明要经中心的卡编译器才编得成
764
874
  * overlay,catalog 的 digest 是引擎铸的,替运维机械转就是第二个编译器 / 第二个铸点。`prompts` 不是对象 ⇒ 不动(判形是读者的事)。 */
765
875
  function stepPromptsAxes(out, receipts) {
766
- const doc = out.prompts;
876
+ const doc = ownOrThrow(out, "prompts");
767
877
  if (!isPlainJsonObject(doc))
768
878
  return;
769
879
  let next = doc;
770
- if (has(doc, "bindings") && doc.bindings !== undefined) {
771
- if (Array.isArray(doc.bindings) && doc.bindings.length === 0) {
880
+ const bindings = has(doc, "bindings") ? ownOrThrow(doc, "bindings") : undefined;
881
+ if (bindings !== undefined) {
882
+ if (isJsonArray(bindings) && bindings.length === 0) {
772
883
  next = without(next, "bindings");
773
884
  receipts.push(receipt("SS-M10", "prompts", ["bindings"], "dropped-inert"));
774
885
  }
@@ -777,7 +888,7 @@ function stepPromptsAxes(out, receipts) {
777
888
  "(a local config.d copy was never read by the engine: delete the key)"));
778
889
  }
779
890
  }
780
- if (has(doc, "catalog") && doc.catalog !== undefined) {
891
+ if (has(doc, "catalog") && ownOrThrow(doc, "catalog") !== undefined) {
781
892
  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
893
  }
783
894
  if (next !== doc)
@@ -823,7 +934,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
823
934
  if (!(o instanceof z.ZodObject))
824
935
  continue;
825
936
  const lit = unwrapSchema(o.shape[disc]);
826
- if (lit instanceof z.ZodLiteral && lit.values.has(value[disc]))
937
+ if (lit instanceof z.ZodLiteral && lit.values.has(ownOrThrow(value, disc)))
827
938
  return stripFragments(o, value, path, onStrip, depth + 1);
828
939
  }
829
940
  return value;
@@ -843,14 +954,15 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
843
954
  for (const [k, fieldSchema] of Object.entries(s.shape)) {
844
955
  if (!Object.prototype.hasOwnProperty.call(value, k))
845
956
  continue;
846
- const v = stripFragments(fieldSchema, value[k], [...path, k], onStrip, depth + 1);
847
- if (v !== value[k])
957
+ const held = ownOrThrow(value, k);
958
+ const v = stripFragments(fieldSchema, held, [...path, k], onStrip, depth + 1);
959
+ if (v !== held)
848
960
  (next ??= { ...value })[k] = v;
849
961
  }
850
962
  return next ?? value;
851
963
  }
852
964
  if (s instanceof z.ZodArray) {
853
- if (!Array.isArray(value))
965
+ if (!isJsonArray(value))
854
966
  return value;
855
967
  let next;
856
968
  value.forEach((el, i) => {