@sema-agent/settings-schema 15.1.0 → 16.0.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 (70) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +60 -46
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c16-deep-nesting-205/expected.json +2 -2
  5. package/conformance/cases/c16-deep-nesting-205/meta.json +1 -1
  6. package/conformance/cases/c50-s784-lane-host-legal/expected.json +6 -2
  7. package/conformance/cases/c50-s784-lane-host-legal/input.json +3 -1
  8. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +6 -2
  9. package/conformance/cases/c51-s784-lane-e2b-legal/input.json +3 -1
  10. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +6 -2
  11. package/conformance/cases/c52-s784-lane-k8s-legal/input.json +3 -1
  12. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +6 -2
  13. package/conformance/cases/c53-s784-lane-ssh-legal/input.json +3 -1
  14. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +6 -2
  15. package/conformance/cases/c54-s784-lane-adb-legal/input.json +3 -1
  16. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +6 -2
  17. package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +3 -1
  18. package/conformance/cases/c56-s784-lane-device-legal/expected.json +8 -2
  19. package/conformance/cases/c56-s784-lane-device-legal/input.json +5 -2
  20. package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +1 -1
  21. package/conformance/cases/c58-s784-g2-multitenant-host/input.json +3 -1
  22. package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +3 -1
  23. package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +3 -1
  24. package/conformance/cases/c61-s784-g3-other-lane-key/input.json +3 -1
  25. package/conformance/cases/c62-s784-g7a-session-tidb/input.json +3 -1
  26. package/conformance/cases/c63-s784-host-two-replicas/input.json +3 -1
  27. package/conformance/cases/c64-s784-multitenant-local-store/input.json +3 -1
  28. package/conformance/cases/c65-s784-pg-without-db/input.json +3 -1
  29. package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +3 -1
  30. package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +1 -1
  31. package/conformance/cases/c68-s784-device-local-store/input.json +6 -1
  32. package/conformance/cases/cli04-env-value-with-newline/expected.json +6 -2
  33. package/conformance/cases/cli04-env-value-with-newline/input.json +3 -1
  34. package/dist/cli.d.ts +1 -1
  35. package/dist/config-d-file.d.ts +41 -5
  36. package/dist/config-d-file.js +105 -22
  37. package/dist/config-document-refusal.d.ts +39 -0
  38. package/dist/config-document-refusal.js +2 -0
  39. package/dist/device-relay.d.ts +21 -0
  40. package/dist/device-relay.js +23 -0
  41. package/dist/env-model-table.d.ts +2 -9
  42. package/dist/env-model-table.js +6 -14
  43. package/dist/execution-lane-index.d.ts +21 -17
  44. package/dist/execution-lane-index.js +28 -20
  45. package/dist/execution-lanes.d.ts +20 -5
  46. package/dist/execution-lanes.js +31 -7
  47. package/dist/file-store.d.ts +30 -3
  48. package/dist/file-store.js +89 -32
  49. package/dist/fleet.d.ts +6 -8
  50. package/dist/fleet.js +5 -7
  51. package/dist/hooks.d.ts +1 -1
  52. package/dist/index.d.ts +2 -1
  53. package/dist/index.js +4 -3
  54. package/dist/migration-receipt.d.ts +1 -1
  55. package/dist/migration-receipt.js +1 -1
  56. package/dist/one-time-migrations.d.ts +1 -1
  57. package/dist/one-time-migrations.js +166 -45
  58. package/dist/remote-exec.js +11 -4
  59. package/dist/safety-merge-spec.d.ts +1 -1
  60. package/dist/safety-merge-spec.js +1 -1
  61. package/dist/types.d.ts +15 -11
  62. package/dist/types.js +100 -49
  63. package/dist/url-shape.d.ts +18 -0
  64. package/dist/url-shape.js +22 -0
  65. package/docs/API-SURFACE.md +18 -7
  66. package/docs/CONFORMANCE.md +1 -7
  67. package/docs/CONSUMER-INTEGRATION.md +4 -4
  68. package/llms-full.txt +27 -25
  69. package/llms.txt +10 -8
  70. package/package.json +1 -1
@@ -36,8 +36,8 @@
36
36
  */
37
37
  import { z } from "zod";
38
38
  import { outgoingPath, redactSecrets } from "./config-fns.js";
39
- import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
40
- import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
39
+ import { LANE_ENV_OWNER, LATE_LANE_ENV_KEYS, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
40
+ import { EXECUTION_LANE_WORDS, STORE_BACKENDS } from "./execution-lanes.js";
41
41
  import { INTEGER } from "./integer.js";
42
42
  import { NOT_PLAIN_DATA, UNINSPECTABLE, UninspectableError, captureJson, dataDescriptor, emptyDocument, inspect, ownEntriesOf, ownOrThrow, readDeclaredKey, sentenceOfMinted } from "./own-key-read.js";
43
43
  import { isJsonArray, isPlainJsonObject, jsonShapeOf } from "./plain-json-object.js";
@@ -102,9 +102,10 @@ export const ONE_TIME_MIGRATIONS = [
102
102
  releasedOn: "2026-10-10",
103
103
  removableAfter: "2026-11-09",
104
104
  rewrites: "workers: a 6.x row (env.REMOTE_EXEC + lane keys, runtime.sessionBackend / memoryBackend / db, coords.tidb*, secretRefs.tidbPasswordEnv, store " +
105
- "keys in env) is rewritten into runtime.execution + runtime.store; a row the step would have to guess for, or that still does not read after the " +
106
- "rewrite, is refused and kept verbatim (readWorkersRows holds it on read). Runs last: the rewritten row is judged after every other step's " +
107
- "rewrite (SS-M7's fragment strip included)",
105
+ "keys in env) is rewritten into runtime.execution + runtime.store — since 16.0.0 straight into the 16.0.0 form: the lane lands as " +
106
+ "runtime.execution.lanes [<lane>], and env MEMORY_ENGINE_BACKEND \"tidb\" lands as store.memory.backend \"mysql\"; a row the step would have to guess for, or that still " +
107
+ "does not read after the rewrite, is refused and kept verbatim (readWorkersRows holds it on read). Runs last, in one workers-row step with SS-M12 (SS-M12's " +
108
+ "rewrite first, then this one's): the row is judged once, after both and after every other step's rewrite (SS-M7's fragment strip included)",
108
109
  removal: "the next major after the window: delete the SS-M4 step; the 6.x tombstone fields of WorkerSpec and the reserved-env signpost stay — their SS-M4 clause goes",
109
110
  afterRemoval: "the 6.x tombstone fields of WorkerSpec and the reserved-env refusal (permanent): a residual 6.x row is held by readWorkersRows, never rewritten",
110
111
  },
@@ -204,6 +205,25 @@ export const ONE_TIME_MIGRATIONS = [
204
205
  "refuse on it today. Where the engine refuses, the way out is to write config.env-models.json = {\"envModels\":\"replaced\"} in the " +
205
206
  "config root; no signpost in this package",
206
207
  },
208
+ {
209
+ id: "SS-M12",
210
+ oldForm: "refused",
211
+ introducedIn: "16.0.0",
212
+ releasedOn: "2026-10-30",
213
+ removableAfter: "2026-11-29",
214
+ rewrites: "workers: a 7.0–15.x row is rewritten into the 16.0.0 form, field by field — runtime.execution.lane L ⇒ runtime.execution.lanes [L] (moved); " +
215
+ "runtime.store.memory.backend \"tidb\" ⇒ \"mysql\" (rewritten: the same MySQL-family store, now one word with store.backend); DEVICE_RELAY_URL in env " +
216
+ "(the 15.x write gate took it as a plain env key) ⇒ runtime.execution.coords.DEVICE_RELAY_URL when the row declares the device lane (moved), dropped when " +
217
+ "it does not (dropped-inert: the engine reads it only on the device lane). Refused and kept verbatim (readWorkersRows holds it on read): a row carrying both " +
218
+ "lane and lanes, a DEVICE_RELAY_URL on a row that declares the device lane but sets it from a host secret (secretEnv: the value cannot be read) or writes it " +
219
+ "both in env and in coords, and a row that still does not read after the rewrite — a device row with no DEVICE_RELAY_URL among them (a required coordinate " +
220
+ "since 16.0.0; the step does not guess between a URL and \"none\"). Runs in one workers-row step with SS-M4, after every pure-rewrite step: this step's " +
221
+ "rewrite first, then SS-M4's, then one judgement of the row (a refusal of that judgement is filed under the newest migration that rewrote the row)",
222
+ removal: "the next major after the window: delete the SS-M12 step; the runtime.execution.lane tombstone field, the memory-backend refusal for \"tidb\" and the " +
223
+ "reserved-env signpost for DEVICE_RELAY_URL stay — their SS-M12 clause goes",
224
+ afterRemoval: "the runtime.execution.lane tombstone field, the memory-backend refusal for \"tidb\" and the reserved-env refusal for DEVICE_RELAY_URL (permanent): a residual " +
225
+ "15.x row is refused on write and held by readWorkersRows on read, never rewritten",
226
+ },
207
227
  {
208
228
  // 一条 wire 位的删码登记,不是数据迁移(id 在 WIRE_RETIREMENT_IDS,不在收据闭集)。
209
229
  id: "SS-W1",
@@ -261,8 +281,10 @@ export function migrateStoredDocuments(docs, opts) {
261
281
  for (const [k, v] of entries.value)
262
282
  Object.defineProperty(out, k, dataDescriptor(v));
263
283
  const receipts = [];
264
- // 步序的律:纯改写步在前,拿新 schema 终判改写结果的步(SS-M4)最后 —— 它判的是**全部**改写之后的行,别的步能修好的形(例:SS-M7 剥掉的
265
- // fragment)不会先被它拒掉(否则一跑 refused、二跑才过:既不是一次完成,也不幂等)。
284
+ // 步序的律:纯改写步在前,拿新 schema 终判改写结果的 workers 行步最后 —— 它判的是**全部**改写之后的行,别的步能修好的形(例:SS-M7 剥掉的
285
+ // fragment)不会先被它拒掉(否则一跑 refused、二跑才过:既不是一次完成,也不幂等)。同一条律落到行内:一行可以同时带 SS-M12 认的形(7.0+ 的单值车道)
286
+ // 与 SS-M4 认的形(6.x 的店字段),所以两只迁移在**一步**里按行做 —— SS-M12 先改(7.0+ 行 env 里的晚登记车道键 `DEVICE_RELAY_URL` 在 SS-M4 眼里是
287
+ // 「新旧两形并存」,先由 SS-M12 归位),SS-M4 再改,改完**判一次**;各判各的会互相卡死(一只判「另一只的形还在」而拒)。
266
288
  try {
267
289
  stepRuntimeDomain(out, opts.coverage, receipts);
268
290
  stepHostsBounds(out, receipts);
@@ -272,7 +294,7 @@ export function migrateStoredDocuments(docs, opts) {
272
294
  stepModelsDefault(out, receipts);
273
295
  stepPromptsAxes(out, receipts);
274
296
  stepUrlFragments(out, receipts);
275
- stepWorkers6x(out, receipts);
297
+ stepWorkersRows(out, receipts);
276
298
  }
277
299
  catch (e) {
278
300
  const sentence = sentenceOfMinted(e);
@@ -482,16 +504,21 @@ const MEMORY_ENGINE_OFF_WORDS = ["off", "false", "0", "no", "disabled"];
482
504
  const MYSQL_DEFAULT_PORT = 3306;
483
505
  const isLaneWord = (v) => typeof v === "string" && EXECUTION_LANE_WORDS.some((w) => w === v);
484
506
  const isStoreWord = (v) => typeof v === "string" && STORE_BACKENDS.some((w) => w === v);
485
- const isMemoryBackendWord = (v) => typeof v === "string" && MEMORY_ENGINE_BACKENDS.some((w) => w === v);
507
+ /** 6.x 引擎收的 `MEMORY_ENGINE_BACKEND` 词 → 今天的词(16.0.0 起 MySQL 族那一只叫 `mysql`,与店词同字)。键 = 6.x 引擎的闭集(它见 `mysql` 拒启)。 */
508
+ const SIX_X_MEMORY_BACKEND = new Map([
509
+ ["file", "file"],
510
+ ["pg", "pg"],
511
+ ["tidb", "mysql"],
512
+ ]);
486
513
  /** 拒因里只点名形如词的值(短、小写);其余一律不回显(值可能是粘错位置的凭据)。 */
487
514
  const describeWord = (v) => (typeof v === "string" && /^[a-z0-9][a-z0-9-]{0,31}$/.test(v) ? `"${v}"` : "(value not shown)");
488
515
  const UNCHANGED = { kind: "unchanged" };
489
516
  /**
490
- * 一行的 6.x 改写。规则(每个旧键恰好一种去处):车道 —— `env.REMOTE_EXEC` 是车道词 ⇒ `runtime.execution.lane`,同车道的 env 键 ⇒ `coords`、
517
+ * 一行的 6.x 改写。规则(每个旧键恰好一种去处):车道 —— `env.REMOTE_EXEC` 是车道词 ⇒ `runtime.execution.lanes`(一元表),同车道的 env 键 ⇒ `coords`、
491
518
  * 同车道的 `secretEnv` 密钥 ⇒ `secretRefs`;别车道的键与 `REQUIRE_EXECUTION_LANE` ⇒ 丢(引擎只读被点名车道那一臂);enabled 且无车道 ⇒ 拒
492
519
  * (不替运维猜车道)。店 —— `sessionBackend` ⇒ `store.session`(`tidb` ⇒ `mysql`);`memoryBackend` ⇒ 丢(从未到达引擎);`coords.tidb*` +
493
520
  * `runtime.db` ⇒ `store.db`(缺端口 ⇒ 3306),`tidbPasswordEnv` ⇒ `dbPasswordEnv`;无 host ⇒ `store.backend = local`;env 里的
494
- * `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` ⇒ 搬进 `store`。表达不了 ⇒ 拒:值写错了栏、不认识的词、新旧两形并存、
521
+ * `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` ⇒ 搬进 `store`(方言 `tidb` ⇒ `mysql`)。表达不了 ⇒ 拒:值写错了栏、不认识的词、新旧两形并存、
495
522
  * `DB_BACKEND=pg` 配 MySQL 族坐标。不是 6.x 形的行(没有任何旧键,该有的块都在)⇒ 不动 —— 它读不读得进是 `readWorkersRows` 的事。
496
523
  */
497
524
  function migrateWorkerRow(row) {
@@ -538,7 +565,7 @@ function migrateWorkerRow(row) {
538
565
  if (secretStoreKey !== undefined)
539
566
  return refuse(["secretEnv", secretStoreKey], `${secretStoreKey} is set from a host secret — the migration cannot read it; declare runtime.store by hand`);
540
567
  if (has(rawSecretEnv, "REMOTE_EXEC"))
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");
568
+ return refuse(["secretEnv", "REMOTE_EXEC"], "the execution lane is set from a host secret — the migration cannot read it; declare runtime.execution.lanes by hand");
542
569
  const needLane = !hasExecution && (enabled || laneTouched);
543
570
  const needStore = !hasStore && (enabled || storeTouched);
544
571
  // 收尾要问「原来的 env / secretEnv 有没有键」:在拷之前、浅读里问好
@@ -567,7 +594,7 @@ function migrateWorkerRow(row) {
567
594
  if (rx === undefined || rx === "") {
568
595
  if (enabled) {
569
596
  return refuse(["env", "REMOTE_EXEC"], `an enabled worker names no execution lane (env.REMOTE_EXEC is unset) — it boots with no hands and still reports tasks completed. ` +
570
- `The migration does not guess a lane: set runtime.execution.lane (${EXECUTION_LANE_WORDS.join(" | ")}) by hand, or disable the worker`);
597
+ `The migration does not guess a lane: set runtime.execution.lanes (${EXECUTION_LANE_WORDS.join(" | ")}) by hand, or disable the worker`);
571
598
  }
572
599
  for (const k of envLaneKeys)
573
600
  drop(env, "env", k); // 停用且没点名车道 ⇒ 每个车道键都是惰性的
@@ -576,7 +603,7 @@ function migrateWorkerRow(row) {
576
603
  }
577
604
  else {
578
605
  if (!isLaneWord(rx)) {
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`);
606
+ 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.lanes by hand`);
580
607
  }
581
608
  const lane = rx;
582
609
  const laneCoords = emptyDocument();
@@ -615,7 +642,7 @@ function migrateWorkerRow(row) {
615
642
  laneSecretRefs[k] = v;
616
643
  delete secretEnv[k];
617
644
  }
618
- runtime.execution = { lane, coords: laneCoords, secretRefs: laneSecretRefs };
645
+ runtime.execution = { lanes: [lane], coords: laneCoords, secretRefs: laneSecretRefs };
619
646
  }
620
647
  }
621
648
  // ── 店
@@ -709,10 +736,11 @@ function migrateWorkerRow(row) {
709
736
  }
710
737
  const memoryBackendRaw = env.MEMORY_ENGINE_BACKEND;
711
738
  if (memoryBackendRaw !== undefined) {
712
- if (!isMemoryBackendWord(memoryBackendRaw)) {
713
- return refuse(["env", "MEMORY_ENGINE_BACKEND"], `env.MEMORY_ENGINE_BACKEND ${describeWord(memoryBackendRaw)} is not a memory backend (${MEMORY_ENGINE_BACKENDS.join(" | ")}) — the engine refuses to start with it`);
739
+ const today = typeof memoryBackendRaw === "string" ? SIX_X_MEMORY_BACKEND.get(memoryBackendRaw) : undefined;
740
+ if (today === undefined) {
741
+ return refuse(["env", "MEMORY_ENGINE_BACKEND"], `env.MEMORY_ENGINE_BACKEND ${describeWord(memoryBackendRaw)} is not a 6.x memory backend (${[...SIX_X_MEMORY_BACKEND.keys()].join(" | ")}) — the engine refused to start with it`);
714
742
  }
715
- memory.backend = memoryBackendRaw;
743
+ memory.backend = today;
716
744
  delete env.MEMORY_ENGINE_BACKEND;
717
745
  }
718
746
  if (Object.keys(memory).length > 0)
@@ -725,16 +753,95 @@ function migrateWorkerRow(row) {
725
753
  delete w.env;
726
754
  if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && secretEnvHadKeys)
727
755
  delete w.secretEnv;
728
- // 改写后的行仍读不进 7.x 形 ⇒ 不写回半成品:拒,原文留盘(读口 `readWorkersRows` 扣下它)。判据与拒句是读口那一只(`workerRowVerdict`)。
729
- const verdict = workerRowVerdict(w, "migrated");
730
- if (verdict !== undefined)
731
- return refuse(verdict.field, verdict.reason);
732
- const out = captureJson(w, "ordinary"); // 交出去的是寻常对象(隔离原型的深副本只是工作副本)
733
- if (!out.ok)
734
- throw new UninspectableError(out.sentence);
735
- return { kind: "rewritten", row: out.value, dropped };
756
+ // 改写后的行还没判:终判在 workers 行步(两只迁移的改写都做完之后判一次,见 `stepWorkersRows`)。
757
+ return { kind: "rewritten", row: w, dropped };
736
758
  }
737
- function stepWorkers6x(out, receipts) {
759
+ /** SS-M12 认的晚登记车道键(16.0.0 那一批;表在 `execution-lane-index.ts`,这里只取归 SS-M12 的那几个)。 */
760
+ const SS_M12_ENV_KEYS = [...LATE_LANE_ENV_KEYS].filter(([, m]) => m.migration === "SS-M12").map(([k]) => k);
761
+ /**
762
+ * 一行的 15.x 改写(规则见登记表 SS-M12 行)。只认 7.0+ 行上的三种形 —— `runtime.execution.lane`、`runtime.store.memory.backend: "tidb"`、带 `runtime.execution` 的行
763
+ * 逃生口里的晚登记车道键 —— 逐字段改,每改一处一张收据。先浅读判要不要迁(没有这三种形的行一个字段都不动、不拷);要迁才拍快照改副本。
764
+ * 改完的行不在这里判:SS-M4 可能还要改同一行,读侧那一只(`workerRowVerdict`)在 workers 行步里两只都改完之后判一次(`stepWorkersRows`)。
765
+ */
766
+ function migrateWorkerRow15(row) {
767
+ const refuse = (field, reason) => ({ kind: "refused", field, reason });
768
+ const peek = (o, k) => (isPlainJsonObject(o) ? ownOrThrow(o, k) : undefined);
769
+ const rawRuntime = peek(row, "runtime");
770
+ const rawExecution = peek(rawRuntime, "execution");
771
+ const laneOld = has(rawExecution, "lane");
772
+ const memoryTidb = peek(peek(peek(rawRuntime, "store"), "memory"), "backend") === "tidb";
773
+ const lateKeysAt = (slot) => (isPlainJsonObject(rawExecution) ? SS_M12_ENV_KEYS.filter((k) => has(peek(row, slot), k)) : []);
774
+ const late = { env: lateKeysAt("env"), secretEnv: lateKeysAt("secretEnv") };
775
+ if (!laneOld && !memoryTidb && late.env.length + late.secretEnv.length === 0)
776
+ return { kind: "unchanged" };
777
+ if (laneOld && has(rawExecution, "lanes")) {
778
+ return refuse(["runtime", "execution", "lane"], "declares runtime.execution.lane (the 15.x single lane) AND runtime.execution.lanes (the 16.0.0 lane set) — keep one form; the migration does not pick a winner");
779
+ }
780
+ const working = captureJson(row);
781
+ if (!working.ok)
782
+ throw new UninspectableError(working.sentence);
783
+ const w = working.value;
784
+ const runtime = w.runtime;
785
+ const changes = [];
786
+ if (laneOld) {
787
+ // 换名不换位:`lanes` 落在 `lane` 原来的键序位置上,值是只含旧车道的一元表
788
+ const before = runtime.execution;
789
+ const next = emptyDocument();
790
+ for (const [k, v] of Object.entries(before))
791
+ Object.defineProperty(next, k === "lane" ? "lanes" : k, dataDescriptor(k === "lane" ? [v] : v));
792
+ runtime.execution = next;
793
+ changes.push({ appliedAs: "moved", path: ["runtime", "execution", "lane"], to: ["runtime", "execution", "lanes"] });
794
+ }
795
+ if (memoryTidb) {
796
+ runtime.store.memory.backend = "mysql";
797
+ changes.push({ appliedAs: "rewritten", path: ["runtime", "store", "memory", "backend"] });
798
+ }
799
+ const execution = runtime.execution;
800
+ if (isPlainJsonObject(execution)) {
801
+ const lanes = isJsonArray(execution.lanes) ? execution.lanes : [];
802
+ for (const slot of ["env", "secretEnv"]) {
803
+ if (late[slot].length === 0)
804
+ continue;
805
+ const rec = w[slot];
806
+ for (const key of late[slot]) {
807
+ const owner = LANE_ENV_OWNER.get(key);
808
+ if (owner === undefined)
809
+ throw new Error(`SS-M12: ${key} has no lane in the execution lane table (package invariant broken)`);
810
+ if (!lanes.includes(owner.lane)) {
811
+ delete rec[key]; // 这台 worker 不声明那条车道:引擎只在那条车道的臂里读它,从没生效
812
+ changes.push({ appliedAs: "dropped-inert", path: [slot, key] });
813
+ continue;
814
+ }
815
+ if (owner.secret)
816
+ return refuse([slot, key], `${key} is a secret of lane "${owner.lane}" — declare it by hand in runtime.execution.secretRefs.${key} (the host env-NAME holding it)`);
817
+ if (slot === "secretEnv")
818
+ return refuse([slot, key], `${key} is a non-secret coordinate of lane "${owner.lane}" but is set from a host secret — the migration cannot read the value; write it in runtime.execution.coords.${key} by hand`);
819
+ const coords = isPlainJsonObject(execution.coords) ? execution.coords : emptyDocument();
820
+ if (Object.hasOwn(coords, key))
821
+ return refuse([slot, key], `${key} is written both in env and in runtime.execution.coords — keep one (the coordinate); the migration does not pick a winner`);
822
+ const value = rec[key];
823
+ if (typeof value !== "string")
824
+ return refuse([slot, key], `env.${key} is not a string`);
825
+ coords[key] = value;
826
+ execution.coords = coords;
827
+ delete rec[key];
828
+ changes.push({ appliedAs: "moved", path: [slot, key], to: ["runtime", "execution", "coords", key] });
829
+ }
830
+ if (Object.keys(rec).length === 0)
831
+ delete w[slot]; // 被搬空的逃生口整键去掉(可选字段;留一个 `{}` 只是噪声)
832
+ }
833
+ }
834
+ // 改写后的行还没判:SS-M4 可能还要改它(6.x 的店字段),终判在 workers 行步(见 `stepWorkersRows`)。
835
+ return { kind: "rewritten", row: w, changes };
836
+ }
837
+ /**
838
+ * workers 行步:SS-M12 与 SS-M4 在**一步**里按行做 —— SS-M12 改写(7.0+ 的形)→ SS-M4 改写(6.x 的形)→ 用读侧那一只(`workerRowVerdict`)**判一次**。
839
+ * 一行两只都可能要改(7.0+ 的单值车道 + 6.x 的店字段);各判各的会互相卡死:先判的那只看见另一只的旧形还在而拒,重跑同样。
840
+ * · 形上就改不了(两形并存、值写错了栏……)⇒ 那一只的拒收据,另一只的改写不留(行原样,收据与交回的文档一致);
841
+ * · 改完读不进 ⇒ 一张拒收据,记在**改了这一行的最新那只迁移**名下(这一行要抬到的就是它的形),原文留盘(读口 `readWorkersRows` 扣下它);
842
+ * · 读得进 ⇒ 写回寻常对象(工作副本隔离原型,只是副本),收据按迁移序:SS-M12 的逐字段收据,再 SS-M4 的 `rewritten` + `dropped-inert`。
843
+ */
844
+ function stepWorkersRows(out, receipts) {
738
845
  const doc = ownOrThrow(out, "workers");
739
846
  if (!isPlainJsonObject(doc))
740
847
  return;
@@ -746,24 +853,38 @@ function stepWorkers6x(out, receipts) {
746
853
  rows.forEach((row, index) => {
747
854
  if (!isPlainJsonObject(row) || typeof ownOrThrow(row, "name") !== "string")
748
855
  return; // 认不出名字:整域读侧判
749
- const r = migrateWorkerRow(row);
750
- switch (r.kind) {
751
- case "unchanged":
752
- return;
753
- case "refused":
754
- receipts.push(refused("SS-M4", "workers", ["workers", index, ...r.field], r.reason));
755
- return;
756
- case "rewritten":
757
- next ??= [...rows];
758
- next[index] = r.row;
759
- receipts.push(receipt("SS-M4", "workers", ["workers", index], "rewritten"));
760
- for (const field of r.dropped)
761
- receipts.push(receipt("SS-M4", "workers", ["workers", index, ...field], "dropped-inert"));
762
- return;
763
- default: {
764
- const unhandled = r;
765
- throw new Error(`SS-M4: unhandled row outcome ${JSON.stringify(unhandled)}`);
766
- }
856
+ const at = (path) => ["workers", index, ...path];
857
+ const r12 = migrateWorkerRow15(row);
858
+ if (r12.kind === "refused") {
859
+ receipts.push(refused("SS-M12", "workers", at(r12.field), r12.reason));
860
+ return;
861
+ }
862
+ const r4 = migrateWorkerRow(r12.kind === "rewritten" ? r12.row : row);
863
+ if (r4.kind === "refused") {
864
+ receipts.push(refused("SS-M4", "workers", at(r4.field), r4.reason));
865
+ return;
866
+ }
867
+ if (r12.kind === "unchanged" && r4.kind === "unchanged")
868
+ return;
869
+ const final = r4.kind === "rewritten" ? r4.row : r12.row;
870
+ const verdict = workerRowVerdict(final, "migrated");
871
+ if (verdict !== undefined) {
872
+ receipts.push(refused(r12.kind === "rewritten" ? "SS-M12" : "SS-M4", "workers", at(verdict.field), verdict.reason));
873
+ return;
874
+ }
875
+ const written = captureJson(final, "ordinary"); // 交出去的是寻常对象(隔离原型的深副本只是工作副本)
876
+ if (!written.ok)
877
+ throw new UninspectableError(written.sentence);
878
+ next ??= [...rows];
879
+ next[index] = written.value;
880
+ if (r12.kind === "rewritten") {
881
+ for (const c of r12.changes)
882
+ receipts.push(c.appliedAs === "moved" ? moved("SS-M12", "workers", at(c.path), { document: "workers", path: at(c.to) }) : receipt("SS-M12", "workers", at(c.path), c.appliedAs));
883
+ }
884
+ if (r4.kind === "rewritten") {
885
+ receipts.push(receipt("SS-M4", "workers", ["workers", index], "rewritten"));
886
+ for (const field of r4.dropped)
887
+ receipts.push(receipt("SS-M4", "workers", at(field), "dropped-inert"));
767
888
  }
768
889
  });
769
890
  if (next !== undefined)
@@ -30,15 +30,22 @@ import { z } from "zod";
30
30
  import { INTEGER } from "./integer.js";
31
31
  import { buildQuotedIssue } from "./quoted-issue.js";
32
32
  import { ENV_NAME, K8S_QUANTITY, NON_SECRET_URL, TCP_PORT, containsSecretToken, hasNul } from "./types.js";
33
- import { retiredLaneWordRefusal } from "./execution-lanes.js";
33
+ import { isBlankCoordinate, retiredLaneWordRefusal } from "./execution-lanes.js";
34
34
  import { ownValueOf } from "./own-key-read.js";
35
35
  import { isPlainJsonObject } from "./plain-json-object.js";
36
36
  /** The name of this contract's document in a `config.d` directory (`config.d/remote-exec.json`) —— the one spelling: the local reader /
37
37
  * writer, the directory migration and the command line name the document by it. It is not a config domain (not in `DOMAIN_SCHEMAS`). */
38
38
  export const REMOTE_EXEC_DOCUMENT = "remote-exec";
39
- /** A non-empty exec-bound string (an image / path / shell / arg the adapter feeds to spawn()/fs). Rejects a NUL
40
- * byte uniformly — a NUL validates here but makes Node throw at spawn/boot (fail-OPEN → runtime crash). R20. */
41
- const execStr = z.string().min(1).refine((s) => !hasNul(s), buildQuotedIssue `must not contain a NUL byte`);
39
+ /** A non-blank exec-bound string (an image / path / shell / arg the adapter feeds to spawn()/fs) — THE one grammar of every string field in this file that
40
+ * is not an env-NAME / quantity / URL. Blank = empty or whitespace-only, asked of THE lane-coordinate blank judge (`isBlankCoordinate`, the one the
41
+ * write gate asks of every required coordinate): the same deployment statement ("which image", "which namespace") is refused under one reading on
42
+ * both surfaces, and an empty string and a whitespace-only one are one refusal with one sentence (no second reading of "empty"). The judge only
43
+ * judges — an accepted value is carried verbatim, never trimmed. Rejects a NUL byte uniformly — a NUL validates here but makes Node throw at
44
+ * spawn/boot (fail-OPEN → runtime crash). R20. */
45
+ const execStr = z
46
+ .string()
47
+ .refine((s) => !isBlankCoordinate(s), buildQuotedIssue `must not be empty or whitespace-only — give it a real value (an optional key can simply be left out)`)
48
+ .refine((s) => !hasNul(s), buildQuotedIssue `must not contain a NUL byte`);
42
49
  /** Container knobs of the `local-docker` lane. `network` defaults to `none` at the adapter (the safe posture for
43
50
  * untrusted code — see the dual-mode design note). */
44
51
  const DOCKER_FIELDS = {
@@ -572,7 +572,7 @@ export declare const SAFETY_MERGE_SPEC: {
572
572
  };
573
573
  readonly enforcement: "client";
574
574
  readonly fail: "open";
575
- readonly seam: "MemoryStore";
575
+ readonly seam: "MemoryEngine";
576
576
  };
577
577
  readonly autoCompaction: {
578
578
  readonly mergeShape: "replace";
@@ -430,7 +430,7 @@ export const SAFETY_MERGE_SPEC = {
430
430
  model: { mergeShape: "replace", minTrust: "session", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "RoleSpec / TaskSpec.{model,thinking}" },
431
431
  env: { mergeShape: "concat", minTrust: "project", tighten: { rule: "none" }, enforcement: "resolver", fail: "open", seam: "NodeExecutionEnv.shellEnv (later layer overrides same name)" },
432
432
  envSecret: { mergeShape: "replace", minTrust: "global", tighten: { rule: "none" }, enforcement: "resolver", fail: "closed", seam: "§7 secret-ref resolution" },
433
- memoryScopes: { mergeShape: "concat", minTrust: "global", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "MemoryStore" },
433
+ memoryScopes: { mergeShape: "concat", minTrust: "global", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "MemoryEngine" },
434
434
  autoCompaction: { mergeShape: "replace", minTrust: "session", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "compaction config" },
435
435
  ui: { mergeShape: "replace", minTrust: "local", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "shell-only (theme/keybindings/statusLine) — never touches the engine" },
436
436
  org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "web-admin → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)" },
package/dist/types.d.ts CHANGED
@@ -506,11 +506,11 @@ export type SystemsConfig = z.infer<typeof SystemsConfig>;
506
506
  export declare const SystemStatusEntry: z.ZodObject<{
507
507
  name: z.ZodString;
508
508
  phase: z.ZodEnum<{
509
+ refused: "refused";
509
510
  bootstrap: "bootstrap";
510
511
  steady: "steady";
511
512
  rotating: "rotating";
512
513
  grace: "grace";
513
- refused: "refused";
514
514
  "lkg-hold": "lkg-hold";
515
515
  }>;
516
516
  fp: z.ZodString;
@@ -1427,7 +1427,8 @@ export declare const WorkerSpec: z.ZodObject<{
1427
1427
  }, z.core.$strip>>;
1428
1428
  runtime: z.ZodObject<{
1429
1429
  execution: z.ZodOptional<z.ZodObject<{
1430
- lane: z.ZodEnum<{
1430
+ lane: z.ZodOptional<z.ZodNever>;
1431
+ lanes: z.ZodArray<z.ZodEnum<{
1431
1432
  host: "host";
1432
1433
  e2b: "e2b";
1433
1434
  k8s: "k8s";
@@ -1435,7 +1436,7 @@ export declare const WorkerSpec: z.ZodObject<{
1435
1436
  adb: "adb";
1436
1437
  "local-docker": "local-docker";
1437
1438
  device: "device";
1438
- }>;
1439
+ }>>;
1439
1440
  coords: z.ZodPrefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1440
1441
  secretRefs: z.ZodPrefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1441
1442
  }, z.core.$strip>>;
@@ -1458,8 +1459,8 @@ export declare const WorkerSpec: z.ZodObject<{
1458
1459
  }>>;
1459
1460
  backend: z.ZodDefault<z.ZodEnum<{
1460
1461
  file: "file";
1462
+ mysql: "mysql";
1461
1463
  pg: "pg";
1462
- tidb: "tidb";
1463
1464
  }>>;
1464
1465
  }, z.core.$strip>>;
1465
1466
  db: z.ZodOptional<z.ZodObject<{
@@ -1595,7 +1596,8 @@ export declare const WorkersConfig: z.ZodObject<{
1595
1596
  }, z.core.$strip>>;
1596
1597
  runtime: z.ZodObject<{
1597
1598
  execution: z.ZodOptional<z.ZodObject<{
1598
- lane: z.ZodEnum<{
1599
+ lane: z.ZodOptional<z.ZodNever>;
1600
+ lanes: z.ZodArray<z.ZodEnum<{
1599
1601
  host: "host";
1600
1602
  e2b: "e2b";
1601
1603
  k8s: "k8s";
@@ -1603,7 +1605,7 @@ export declare const WorkersConfig: z.ZodObject<{
1603
1605
  adb: "adb";
1604
1606
  "local-docker": "local-docker";
1605
1607
  device: "device";
1606
- }>;
1608
+ }>>;
1607
1609
  coords: z.ZodPrefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1608
1610
  secretRefs: z.ZodPrefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1609
1611
  }, z.core.$strip>>;
@@ -1626,8 +1628,8 @@ export declare const WorkersConfig: z.ZodObject<{
1626
1628
  }>>;
1627
1629
  backend: z.ZodDefault<z.ZodEnum<{
1628
1630
  file: "file";
1631
+ mysql: "mysql";
1629
1632
  pg: "pg";
1630
- tidb: "tidb";
1631
1633
  }>>;
1632
1634
  }, z.core.$strip>>;
1633
1635
  db: z.ZodOptional<z.ZodObject<{
@@ -1787,7 +1789,8 @@ export declare const HostStatus: z.ZodObject<{
1787
1789
  }, z.core.$strip>>;
1788
1790
  }, z.core.$strip>>;
1789
1791
  }, z.core.$strip>;
1790
- ts: z.ZodString;
1792
+ ts: z.ZodISODateTime;
1793
+ heartbeatMs: z.ZodOptional<z.ZodNumber>;
1791
1794
  }, z.core.$strip>;
1792
1795
  export type HostStatus = z.infer<typeof HostStatus>;
1793
1796
  export declare const SkillSpec: z.ZodObject<{
@@ -2673,7 +2676,8 @@ export declare const DOMAIN_SCHEMAS: {
2673
2676
  }, z.core.$strip>>;
2674
2677
  runtime: z.ZodObject<{
2675
2678
  execution: z.ZodOptional<z.ZodObject<{
2676
- lane: z.ZodEnum<{
2679
+ lane: z.ZodOptional<z.ZodNever>;
2680
+ lanes: z.ZodArray<z.ZodEnum<{
2677
2681
  host: "host";
2678
2682
  e2b: "e2b";
2679
2683
  k8s: "k8s";
@@ -2681,7 +2685,7 @@ export declare const DOMAIN_SCHEMAS: {
2681
2685
  adb: "adb";
2682
2686
  "local-docker": "local-docker";
2683
2687
  device: "device";
2684
- }>;
2688
+ }>>;
2685
2689
  coords: z.ZodPrefault<z.ZodRecord<z.ZodString, z.ZodString>>;
2686
2690
  secretRefs: z.ZodPrefault<z.ZodRecord<z.ZodString, z.ZodString>>;
2687
2691
  }, z.core.$strip>>;
@@ -2704,8 +2708,8 @@ export declare const DOMAIN_SCHEMAS: {
2704
2708
  }>>;
2705
2709
  backend: z.ZodDefault<z.ZodEnum<{
2706
2710
  file: "file";
2711
+ mysql: "mysql";
2707
2712
  pg: "pg";
2708
- tidb: "tidb";
2709
2713
  }>>;
2710
2714
  }, z.core.$strip>>;
2711
2715
  db: z.ZodOptional<z.ZodObject<{