@sema-agent/settings-schema 6.3.0 → 7.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 (103) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +40 -5
  3. package/bin/settings-schema.mjs +22 -0
  4. package/conformance/VERSION +1 -1
  5. package/conformance/cases/c48-s935-model-headers-transported/expected.json +115 -0
  6. package/conformance/cases/c48-s935-model-headers-transported/input.json +37 -0
  7. package/conformance/cases/c48-s935-model-headers-transported/meta.json +6 -0
  8. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +98 -0
  9. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/input.json +28 -0
  10. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/meta.json +9 -0
  11. package/conformance/cases/c50-s784-lane-host-legal/expected.json +102 -0
  12. package/conformance/cases/c50-s784-lane-host-legal/input.json +29 -0
  13. package/conformance/cases/c50-s784-lane-host-legal/meta.json +5 -0
  14. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +104 -0
  15. package/conformance/cases/c51-s784-lane-e2b-legal/input.json +32 -0
  16. package/conformance/cases/c51-s784-lane-e2b-legal/meta.json +5 -0
  17. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +106 -0
  18. package/conformance/cases/c52-s784-lane-k8s-legal/input.json +34 -0
  19. package/conformance/cases/c52-s784-lane-k8s-legal/meta.json +5 -0
  20. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +106 -0
  21. package/conformance/cases/c53-s784-lane-ssh-legal/input.json +34 -0
  22. package/conformance/cases/c53-s784-lane-ssh-legal/meta.json +5 -0
  23. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +102 -0
  24. package/conformance/cases/c54-s784-lane-adb-legal/input.json +29 -0
  25. package/conformance/cases/c54-s784-lane-adb-legal/meta.json +5 -0
  26. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +103 -0
  27. package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +30 -0
  28. package/conformance/cases/c55-s784-lane-local-docker-legal/meta.json +5 -0
  29. package/conformance/cases/c56-s784-lane-device-legal/expected.json +109 -0
  30. package/conformance/cases/c56-s784-lane-device-legal/input.json +36 -0
  31. package/conformance/cases/c56-s784-lane-device-legal/meta.json +5 -0
  32. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +83 -0
  33. package/conformance/cases/c57-s784-g1-enabled-no-lane/input.json +23 -0
  34. package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +5 -0
  35. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +83 -0
  36. package/conformance/cases/c58-s784-g2-multitenant-host/input.json +34 -0
  37. package/conformance/cases/c58-s784-g2-multitenant-host/meta.json +5 -0
  38. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +83 -0
  39. package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +26 -0
  40. package/conformance/cases/c59-s784-g3-k8s-missing-image/meta.json +5 -0
  41. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +83 -0
  42. package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +26 -0
  43. package/conformance/cases/c60-s784-g3-e2b-missing-key/meta.json +5 -0
  44. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +83 -0
  45. package/conformance/cases/c61-s784-g3-other-lane-key/input.json +30 -0
  46. package/conformance/cases/c61-s784-g3-other-lane-key/meta.json +5 -0
  47. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +83 -0
  48. package/conformance/cases/c62-s784-g7a-session-tidb/input.json +27 -0
  49. package/conformance/cases/c62-s784-g7a-session-tidb/meta.json +5 -0
  50. package/conformance/cases/c63-s784-host-two-replicas/expected.json +83 -0
  51. package/conformance/cases/c63-s784-host-two-replicas/input.json +27 -0
  52. package/conformance/cases/c63-s784-host-two-replicas/meta.json +5 -0
  53. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +83 -0
  54. package/conformance/cases/c64-s784-multitenant-local-store/input.json +30 -0
  55. package/conformance/cases/c64-s784-multitenant-local-store/meta.json +5 -0
  56. package/conformance/cases/c65-s784-pg-without-db/expected.json +83 -0
  57. package/conformance/cases/c65-s784-pg-without-db/input.json +27 -0
  58. package/conformance/cases/c65-s784-pg-without-db/meta.json +5 -0
  59. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +83 -0
  60. package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +36 -0
  61. package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +5 -0
  62. package/conformance/cases/c67-s784-retired-field-loud/expected.json +83 -0
  63. package/conformance/cases/c67-s784-retired-field-loud/input.json +22 -0
  64. package/conformance/cases/c67-s784-retired-field-loud/meta.json +5 -0
  65. package/conformance/cases/c68-s784-device-local-store/expected.json +83 -0
  66. package/conformance/cases/c68-s784-device-local-store/input.json +26 -0
  67. package/conformance/cases/c68-s784-device-local-store/meta.json +5 -0
  68. package/conformance/cases/cli04-env-value-with-newline/expected.json +13 -2
  69. package/conformance/cases/cli04-env-value-with-newline/input.json +8 -1
  70. package/conformance/cases/cli04-env-value-with-newline/meta.json +1 -1
  71. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +49 -0
  72. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +26 -0
  73. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +4 -0
  74. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +38 -0
  75. package/conformance/migrate-workers/m02-g5-idempotent/input.json +33 -0
  76. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +5 -0
  77. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +71 -0
  78. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +42 -0
  79. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +4 -0
  80. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +52 -0
  81. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +30 -0
  82. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +4 -0
  83. package/dist/cli.d.ts +8 -0
  84. package/dist/cli.js +70 -0
  85. package/dist/execution-lane-index.d.ts +19 -0
  86. package/dist/execution-lane-index.js +23 -0
  87. package/dist/execution-lanes.d.ts +64 -0
  88. package/dist/execution-lanes.js +152 -0
  89. package/dist/file-edit.js +26 -21
  90. package/dist/hash.d.ts +3 -3
  91. package/dist/hash.js +3 -3
  92. package/dist/hooks.d.ts +12 -12
  93. package/dist/index.d.ts +2 -0
  94. package/dist/index.js +2 -0
  95. package/dist/migrate-workers.d.ts +63 -0
  96. package/dist/migrate-workers.js +331 -0
  97. package/dist/remote-exec.d.ts +23 -64
  98. package/dist/remote-exec.js +20 -71
  99. package/dist/secret-refs.d.ts +7 -1
  100. package/dist/secret-refs.js +9 -3
  101. package/dist/types.d.ts +1382 -430
  102. package/dist/types.js +298 -76
  103. package/package.json +9 -1
package/dist/hooks.d.ts CHANGED
@@ -1732,7 +1732,7 @@ export declare const PostModelSwitchHookInput: z.ZodObject<{
1732
1732
  pricing: z.ZodEnum<["configured", "catalog", "default"]>;
1733
1733
  hook_event_name: z.ZodLiteral<"PostModelSwitch">;
1734
1734
  }, "strip", z.ZodTypeAny, {
1735
- source: "command" | "auto" | "resume" | "picker" | "sdk";
1735
+ source: "auto" | "command" | "resume" | "picker" | "sdk";
1736
1736
  to_model: string;
1737
1737
  session_id: string;
1738
1738
  transcript_path: string;
@@ -1749,7 +1749,7 @@ export declare const PostModelSwitchHookInput: z.ZodObject<{
1749
1749
  permission_mode?: string | undefined;
1750
1750
  agent_id?: string | undefined;
1751
1751
  }, {
1752
- source: "command" | "auto" | "resume" | "picker" | "sdk";
1752
+ source: "auto" | "command" | "resume" | "picker" | "sdk";
1753
1753
  to_model: string;
1754
1754
  session_id: string;
1755
1755
  transcript_path: string;
@@ -3173,7 +3173,7 @@ export declare const HookInput: z.ZodDiscriminatedUnion<"hook_event_name", [z.Zo
3173
3173
  pricing: z.ZodEnum<["configured", "catalog", "default"]>;
3174
3174
  hook_event_name: z.ZodLiteral<"PostModelSwitch">;
3175
3175
  }, "strip", z.ZodTypeAny, {
3176
- source: "command" | "auto" | "resume" | "picker" | "sdk";
3176
+ source: "auto" | "command" | "resume" | "picker" | "sdk";
3177
3177
  to_model: string;
3178
3178
  session_id: string;
3179
3179
  transcript_path: string;
@@ -3190,7 +3190,7 @@ export declare const HookInput: z.ZodDiscriminatedUnion<"hook_event_name", [z.Zo
3190
3190
  permission_mode?: string | undefined;
3191
3191
  agent_id?: string | undefined;
3192
3192
  }, {
3193
- source: "command" | "auto" | "resume" | "picker" | "sdk";
3193
+ source: "auto" | "command" | "resume" | "picker" | "sdk";
3194
3194
  to_model: string;
3195
3195
  session_id: string;
3196
3196
  transcript_path: string;
@@ -5545,20 +5545,20 @@ export declare const HttpHookEntry: z.ZodObject<{
5545
5545
  }, "strip", z.ZodTypeAny, {
5546
5546
  type: "http";
5547
5547
  url: string;
5548
+ headers?: Record<string, string> | undefined;
5548
5549
  if?: string | undefined;
5549
5550
  timeout?: number | undefined;
5550
5551
  statusMessage?: string | undefined;
5551
5552
  once?: boolean | undefined;
5552
- headers?: Record<string, string> | undefined;
5553
5553
  allowedEnvVars?: string[] | undefined;
5554
5554
  }, {
5555
5555
  type: "http";
5556
5556
  url: string;
5557
+ headers?: Record<string, string> | undefined;
5557
5558
  if?: string | undefined;
5558
5559
  timeout?: number | undefined;
5559
5560
  statusMessage?: string | undefined;
5560
5561
  once?: boolean | undefined;
5561
- headers?: Record<string, string> | undefined;
5562
5562
  allowedEnvVars?: string[] | undefined;
5563
5563
  }>;
5564
5564
  /** One configured hook (CC settings shape + shell superset fields; discriminated on `type`). */
@@ -5655,20 +5655,20 @@ export declare const HookEntry: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
5655
5655
  }, "strip", z.ZodTypeAny, {
5656
5656
  type: "http";
5657
5657
  url: string;
5658
+ headers?: Record<string, string> | undefined;
5658
5659
  if?: string | undefined;
5659
5660
  timeout?: number | undefined;
5660
5661
  statusMessage?: string | undefined;
5661
5662
  once?: boolean | undefined;
5662
- headers?: Record<string, string> | undefined;
5663
5663
  allowedEnvVars?: string[] | undefined;
5664
5664
  }, {
5665
5665
  type: "http";
5666
5666
  url: string;
5667
+ headers?: Record<string, string> | undefined;
5667
5668
  if?: string | undefined;
5668
5669
  timeout?: number | undefined;
5669
5670
  statusMessage?: string | undefined;
5670
5671
  once?: boolean | undefined;
5671
- headers?: Record<string, string> | undefined;
5672
5672
  allowedEnvVars?: string[] | undefined;
5673
5673
  }>]>;
5674
5674
  export type HookEntry = z.infer<typeof HookEntry>;
@@ -5773,20 +5773,20 @@ export declare const HookMatcherEntry: z.ZodObject<{
5773
5773
  }, "strip", z.ZodTypeAny, {
5774
5774
  type: "http";
5775
5775
  url: string;
5776
+ headers?: Record<string, string> | undefined;
5776
5777
  if?: string | undefined;
5777
5778
  timeout?: number | undefined;
5778
5779
  statusMessage?: string | undefined;
5779
5780
  once?: boolean | undefined;
5780
- headers?: Record<string, string> | undefined;
5781
5781
  allowedEnvVars?: string[] | undefined;
5782
5782
  }, {
5783
5783
  type: "http";
5784
5784
  url: string;
5785
+ headers?: Record<string, string> | undefined;
5785
5786
  if?: string | undefined;
5786
5787
  timeout?: number | undefined;
5787
5788
  statusMessage?: string | undefined;
5788
5789
  once?: boolean | undefined;
5789
- headers?: Record<string, string> | undefined;
5790
5790
  allowedEnvVars?: string[] | undefined;
5791
5791
  }>]>, "many">;
5792
5792
  }, "strip", z.ZodTypeAny, {
@@ -5819,11 +5819,11 @@ export declare const HookMatcherEntry: z.ZodObject<{
5819
5819
  } | {
5820
5820
  type: "http";
5821
5821
  url: string;
5822
+ headers?: Record<string, string> | undefined;
5822
5823
  if?: string | undefined;
5823
5824
  timeout?: number | undefined;
5824
5825
  statusMessage?: string | undefined;
5825
5826
  once?: boolean | undefined;
5826
- headers?: Record<string, string> | undefined;
5827
5827
  allowedEnvVars?: string[] | undefined;
5828
5828
  })[];
5829
5829
  matcher?: string | undefined;
@@ -5857,11 +5857,11 @@ export declare const HookMatcherEntry: z.ZodObject<{
5857
5857
  } | {
5858
5858
  type: "http";
5859
5859
  url: string;
5860
+ headers?: Record<string, string> | undefined;
5860
5861
  if?: string | undefined;
5861
5862
  timeout?: number | undefined;
5862
5863
  statusMessage?: string | undefined;
5863
5864
  once?: boolean | undefined;
5864
- headers?: Record<string, string> | undefined;
5865
5865
  allowedEnvVars?: string[] | undefined;
5866
5866
  })[];
5867
5867
  matcher?: string | undefined;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * the config center, the worker service, and the TOC desktop/CLI.
4
4
  */
5
5
  export * from "./types.js";
6
+ export * from "./execution-lanes.js";
6
7
  export * from "./hash.js";
7
8
  export * from "./sha256.js";
8
9
  export * from "./skills-manifest.js";
@@ -17,6 +18,7 @@ export * from "./scheduler-store.js";
17
18
  export * from "./cross-session.js";
18
19
  export * from "./hooks.js";
19
20
  export * from "./fleet.js";
21
+ export * from "./migrate-workers.js";
20
22
  export * from "./migrate.js";
21
23
  export * from "./api/auth.js";
22
24
  export * from "./api/auth-bridge.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * the config center, the worker service, and the TOC desktop/CLI.
4
4
  */
5
5
  export * from "./types.js";
6
+ export * from "./execution-lanes.js"; // 7.0.0 M-R2: the ONE lane / store vocabulary (EXECUTION_LANE_WORDS / EXECUTION_LANES / STORE_BACKENDS / SESSION_BACKENDS / MEMORY_ENGINE_BACKENDS) — pure data, browser-safe
6
7
  export * from "./hash.js";
7
8
  export * from "./sha256.js";
8
9
  export * from "./skills-manifest.js";
@@ -17,6 +18,7 @@ export * from "./scheduler-store.js"; // R7 self-wake scheduler store CONTRACT (
17
18
  export * from "./cross-session.js"; // CC-parity `settings.json` cross-session keys (crossSessionInbound / dialogExpiry) — pure/browser-safe
18
19
  export * from "./hooks.js"; // CC-parity hooks contract (31-event set + owner map + per-event input/output shapes + HooksConfig) & stop attribution (stoppedBy) — pure/browser-safe
19
20
  export * from "./fleet.js"; // scheduling/fleet contract (drain protocol · instance discovery · dispatch policy · quota lease · usage report · quota reset event) — CONTROL-PLANE-SCHEDULING-DESIGN, pure/browser-safe
21
+ export * from "./migrate-workers.js"; // 7.0.0 one-time workers-domain migration (env.REMOTE_EXEC / sessionBackend / tidb* → runtime.execution / runtime.store), pure — run it on the RAW document before parseDomain
20
22
  export * from "./migrate.js"; // 0.5→0.6 migration tools (hosts grandfathering + worker.model→catalog migration PLAN — report/draft only, E1; EXPERT-REDESIGN §2-2/§3), pure/browser-safe
21
23
  export * from "./api/auth.js"; // /api/v1/auth/* wire contract (RFC 8628 device flow + rotating refresh + logout + approve) — REGISTRY-REBUILD-DESIGN §2, M1 shipped shapes frozen, pure/browser-safe
22
24
  export * from "./api/auth-bridge.js"; // 鉴权桥 contract: worker verifies registry user JWT (RS256-only, JWKS kid-miss refresh + throttle, offline grace fail-closed) → VerifiedIdentity — design §5, the service verifier's spec
@@ -0,0 +1,63 @@
1
+ /**
2
+ * workers 域的**一次性迁移**(7.0.0,M-R2 WorkerSpec 结构块)—— 持久面唯一允许的过渡,窗口关了就整文件删除
3
+ * (连同 `types.ts` 里「删除项读到即拒」那一只预处理)。纯函数、无 I/O、不改入参;文件形的入口是
4
+ * `settings-schema migrate-workers <file>`(`bin/settings-schema.mjs`,同一只函数)。
5
+ *
6
+ * ## 谁跑、在哪
7
+ * 读前抬升:在 `parseDomain("workers", raw)` / `DOMAIN_SCHEMAS.workers.safeParse` **之前**对**原始 JSON** 跑 —— workers 是
8
+ * 「读即抛」的 gate 域,6.x 存下来的行在 7.0.0 schema 上整域读不出来(删除项响亮拒、enabled 的 worker 缺两块)。中心在它唯一的
9
+ * workers 读口跑一次,结果写回;CLI 给文件后端 / 离线用。
10
+ *
11
+ * ## 规则(每个旧键恰好一种去处)
12
+ * · **车道**:`env.REMOTE_EXEC` 是车道词 ⇒ `runtime.execution.lane`;同车道的 env 键 ⇒ `coords`,同车道的 `secretEnv` 密钥 ⇒
13
+ * `secretRefs`(都按导出表 `EXECUTION_LANES` 的栏判)。别车道的键、`REQUIRE_EXECUTION_LANE` ⇒ **dropped**(引擎只读被点名车道
14
+ * 那一臂;声明了车道就恒渲 true)—— 今天就是惰性的。**enabled 且无 `REMOTE_EXEC` ⇒ refused**:那是「无手照回 completed」的
15
+ * 病例本身,迁移不替运维猜车道。停用且无 `REMOTE_EXEC` ⇒ 不写车道,残留车道键 dropped。
16
+ * · **店**:`sessionBackend` ⇒ `store.session`(`tidb` ⇒ `mysql`,且 `store.backend` 由此为 `mysql`);`memoryBackend` ⇒ **dropped**
17
+ * (它从未到达引擎);`coords.tidbHost` 在 ⇒ `store.backend = mysql`,`coords.tidb*` + `runtime.db` ⇒ `store.db`(缺端口 ⇒ 引擎
18
+ * `MYSQL_PORT` 的缺省 3306),`secretRefs.tidbPasswordEnv` ⇒ `secretRefs.dbPasswordEnv`;没有 host ⇒ `store.backend = local`
19
+ * (设计稿 §3.4:推一次、写死),零散坐标惰性 ⇒ dropped。`env` 逃生口里的 `DB_BACKEND` / `MEMORY_ENGINE` /
20
+ * `MEMORY_ENGINE_BACKEND`(渲染器以前不渲,运维只能写在那里)⇒ 搬进 `store`,显式的 `DB_BACKEND` 优先。停用且旧形没点名任何店
21
+ * (无 host、无 `DB_BACKEND`、无 `tidb` 词)⇒ 不写店,旧字段 dropped。
22
+ * · **表达不了 ⇒ refused**:值写错了栏(密钥以字面值写在 env、坐标来自宿主密钥)、不认识的词、新旧两形并存、`DB_BACKEND=pg`
23
+ * 配 MySQL 族坐标 —— 迁移不搬值进密钥位、不读宿主密钥、不替运维挑赢家、不换库。
24
+ * · **终判 = 7.0.0 schema**:迁出来的每只 worker 都过 `WorkerSpec.safeParse`;不过 ⇒ refused,拒因是 schema 自己的 issue
25
+ * (例:多租户 + host 车道、点了车道却缺必需坐标)。迁移不复述任何一条写闸规则。
26
+ *
27
+ * refused 的 worker **不进**输出 `doc`(中心不加载它、在列表标红点名 `field`;CLI 有 refused 就一字不写),但随 `refused[].original`
28
+ * 逐字带回:`doc` ∪ `refused[].original` 覆盖输入的全部 worker,中心写回前按 `index` 放回(或另存),运维的 spec 不会随迁移消失。
29
+ * 拒因 / dropped 只点名字段,不回显值。
30
+ *
31
+ * ## 幂等
32
+ * 已迁形(没有任何旧键、该有的块都在)⇒ 原样(同一引用)返回、`changed = []`。所以再跑一次 ⇒ 零变化、`doc` 字节相等。
33
+ */
34
+ import { type ConfigPath } from "./config-fns.js";
35
+ /** 一个被丢掉的旧字段(引擎本就不读它 —— 通知一行,不是错误)。`field` 相对该 worker 条目;`index` = 它在输入 `workers` 里的下标。 */
36
+ export interface WorkersMigrationDrop {
37
+ readonly name: string;
38
+ readonly index: number;
39
+ readonly field: ConfigPath;
40
+ }
41
+ /** 一只迁不了的 worker(不进输出 `doc`)。`field` 指到要人改的那一格(相对该 worker 条目;终判失败时是 7.0.0 形里的路径),
42
+ * `reason` 只点名字段、不回显值。`original` = 输入里那一行**逐字**(同一个值,未拷贝未改写)——
43
+ * 🔴 **写回 `doc` 之前必须把每个 `refused[].original` 按 `index` 放回(或另存)**,否则这只 worker 的 spec 随写回从存储里消失,
44
+ * 运维看到的是「worker 不见了」而不是「worker 被标红」。它是调用方自己的数据,原样带回;要记日志,按调用方记那一行的规矩记。 */
45
+ export interface WorkersMigrationRefusal {
46
+ readonly name: string;
47
+ readonly index: number;
48
+ readonly field: ConfigPath;
49
+ readonly reason: string;
50
+ readonly original: unknown;
51
+ }
52
+ /** `migrateWorkersDomain` 的结果。`changed` = 被改写的 worker 名(含只丢了旧字段的)。 */
53
+ export interface WorkersDomainMigration {
54
+ readonly doc: unknown;
55
+ readonly changed: readonly string[];
56
+ readonly dropped: readonly WorkersMigrationDrop[];
57
+ readonly refused: readonly WorkersMigrationRefusal[];
58
+ }
59
+ /**
60
+ * 迁移一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回(判形是 `parseDomain` 的事,迁移不抛)。
61
+ * 规则、幂等与 refused 的处置见文件头。
62
+ */
63
+ export declare function migrateWorkersDomain(doc: unknown): WorkersDomainMigration;
@@ -0,0 +1,331 @@
1
+ /**
2
+ * workers 域的**一次性迁移**(7.0.0,M-R2 WorkerSpec 结构块)—— 持久面唯一允许的过渡,窗口关了就整文件删除
3
+ * (连同 `types.ts` 里「删除项读到即拒」那一只预处理)。纯函数、无 I/O、不改入参;文件形的入口是
4
+ * `settings-schema migrate-workers <file>`(`bin/settings-schema.mjs`,同一只函数)。
5
+ *
6
+ * ## 谁跑、在哪
7
+ * 读前抬升:在 `parseDomain("workers", raw)` / `DOMAIN_SCHEMAS.workers.safeParse` **之前**对**原始 JSON** 跑 —— workers 是
8
+ * 「读即抛」的 gate 域,6.x 存下来的行在 7.0.0 schema 上整域读不出来(删除项响亮拒、enabled 的 worker 缺两块)。中心在它唯一的
9
+ * workers 读口跑一次,结果写回;CLI 给文件后端 / 离线用。
10
+ *
11
+ * ## 规则(每个旧键恰好一种去处)
12
+ * · **车道**:`env.REMOTE_EXEC` 是车道词 ⇒ `runtime.execution.lane`;同车道的 env 键 ⇒ `coords`,同车道的 `secretEnv` 密钥 ⇒
13
+ * `secretRefs`(都按导出表 `EXECUTION_LANES` 的栏判)。别车道的键、`REQUIRE_EXECUTION_LANE` ⇒ **dropped**(引擎只读被点名车道
14
+ * 那一臂;声明了车道就恒渲 true)—— 今天就是惰性的。**enabled 且无 `REMOTE_EXEC` ⇒ refused**:那是「无手照回 completed」的
15
+ * 病例本身,迁移不替运维猜车道。停用且无 `REMOTE_EXEC` ⇒ 不写车道,残留车道键 dropped。
16
+ * · **店**:`sessionBackend` ⇒ `store.session`(`tidb` ⇒ `mysql`,且 `store.backend` 由此为 `mysql`);`memoryBackend` ⇒ **dropped**
17
+ * (它从未到达引擎);`coords.tidbHost` 在 ⇒ `store.backend = mysql`,`coords.tidb*` + `runtime.db` ⇒ `store.db`(缺端口 ⇒ 引擎
18
+ * `MYSQL_PORT` 的缺省 3306),`secretRefs.tidbPasswordEnv` ⇒ `secretRefs.dbPasswordEnv`;没有 host ⇒ `store.backend = local`
19
+ * (设计稿 §3.4:推一次、写死),零散坐标惰性 ⇒ dropped。`env` 逃生口里的 `DB_BACKEND` / `MEMORY_ENGINE` /
20
+ * `MEMORY_ENGINE_BACKEND`(渲染器以前不渲,运维只能写在那里)⇒ 搬进 `store`,显式的 `DB_BACKEND` 优先。停用且旧形没点名任何店
21
+ * (无 host、无 `DB_BACKEND`、无 `tidb` 词)⇒ 不写店,旧字段 dropped。
22
+ * · **表达不了 ⇒ refused**:值写错了栏(密钥以字面值写在 env、坐标来自宿主密钥)、不认识的词、新旧两形并存、`DB_BACKEND=pg`
23
+ * 配 MySQL 族坐标 —— 迁移不搬值进密钥位、不读宿主密钥、不替运维挑赢家、不换库。
24
+ * · **终判 = 7.0.0 schema**:迁出来的每只 worker 都过 `WorkerSpec.safeParse`;不过 ⇒ refused,拒因是 schema 自己的 issue
25
+ * (例:多租户 + host 车道、点了车道却缺必需坐标)。迁移不复述任何一条写闸规则。
26
+ *
27
+ * refused 的 worker **不进**输出 `doc`(中心不加载它、在列表标红点名 `field`;CLI 有 refused 就一字不写),但随 `refused[].original`
28
+ * 逐字带回:`doc` ∪ `refused[].original` 覆盖输入的全部 worker,中心写回前按 `index` 放回(或另存),运维的 spec 不会随迁移消失。
29
+ * 拒因 / dropped 只点名字段,不回显值。
30
+ *
31
+ * ## 幂等
32
+ * 已迁形(没有任何旧键、该有的块都在)⇒ 原样(同一引用)返回、`changed = []`。所以再跑一次 ⇒ 零变化、`doc` 字节相等。
33
+ */
34
+ import { formatConfigPath, redactSecrets } from "./config-fns.js";
35
+ import { LANE_ENV_OWNER } from "./execution-lane-index.js";
36
+ import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
37
+ import { WorkerSpec } from "./types.js";
38
+ function isRec(v) {
39
+ return v !== null && typeof v === "object" && !Array.isArray(v);
40
+ }
41
+ /** 车道块的两个特殊键(不在车道表里,但归车道块)。 */
42
+ const LANE_WORD_KEYS = ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
43
+ /** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`)。 */
44
+ const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
45
+ /** 6.x 删除项:`[容器, 键]`。 */
46
+ const SIX_X_STORE_FIELDS = [
47
+ ["runtime", "sessionBackend"],
48
+ ["runtime", "memoryBackend"],
49
+ ["runtime", "db"],
50
+ ["coords", "tidbHost"],
51
+ ["coords", "tidbPort"],
52
+ ["coords", "tidbUser"],
53
+ ["secretRefs", "tidbPasswordEnv"],
54
+ ];
55
+ /** 引擎 `MEMORY_ENGINE` 的两族词(server `src/config.ts` :3011–3012,比较前 trim + 小写)。 */
56
+ const MEMORY_ENGINE_ON_WORDS = ["on", "true", "1", "yes", "enabled"];
57
+ const MEMORY_ENGINE_OFF_WORDS = ["off", "false", "0", "no", "disabled"];
58
+ /** 引擎 `MYSQL_PORT` 的缺省(server `src/config.ts` :1818)—— 旧形没写端口时引擎连的就是它。 */
59
+ const MYSQL_DEFAULT_PORT = 3306;
60
+ function isLaneWord(v) {
61
+ return typeof v === "string" && EXECUTION_LANE_WORDS.some((w) => w === v);
62
+ }
63
+ function isStoreWord(v) {
64
+ return typeof v === "string" && STORE_BACKENDS.some((w) => w === v);
65
+ }
66
+ function isMemoryBackendWord(v) {
67
+ return typeof v === "string" && MEMORY_ENGINE_BACKENDS.some((w) => w === v);
68
+ }
69
+ /** 拒因里只点名形如词的值(短、小写);其余一律不回显(值可能是粘错位置的凭据)。 */
70
+ function describeWord(v) {
71
+ return typeof v === "string" && /^[a-z0-9][a-z0-9-]{0,31}$/.test(v) ? `"${v}"` : "(value not shown)";
72
+ }
73
+ const UNCHANGED = { kind: "unchanged" };
74
+ function migrateEntry(raw, index) {
75
+ if (!isRec(raw) || typeof raw.name !== "string")
76
+ return UNCHANGED; // 不是一只可辨认的 worker:判形是 parseDomain 的事
77
+ const name = raw.name;
78
+ const refuse = (field, reason) => ({ kind: "refused", refusal: { name, index, field, reason: redactSecrets(reason), original: raw } });
79
+ const enabled = raw.enabled !== false;
80
+ const has = (container, key) => isRec(container) && Object.hasOwn(container, key);
81
+ const laneKeysOf = (rec) => isRec(rec) ? Object.keys(rec).filter((k) => LANE_WORD_KEYS.some((w) => w === k) || LANE_ENV_OWNER.has(k)) : [];
82
+ const storeKeysOf = (rec) => (isRec(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
83
+ const envLaneKeys = laneKeysOf(raw.env);
84
+ const secretLaneKeys = laneKeysOf(raw.secretEnv);
85
+ const envStoreKeys = storeKeysOf(raw.env);
86
+ const secretStoreKeys = storeKeysOf(raw.secretEnv);
87
+ const oldStoreFields = SIX_X_STORE_FIELDS.filter(([c, k]) => has(raw[c], k));
88
+ const hasExecution = has(raw.runtime, "execution");
89
+ const hasStore = has(raw.runtime, "store");
90
+ const laneTouched = envLaneKeys.length + secretLaneKeys.length > 0;
91
+ const storeTouched = oldStoreFields.length + envStoreKeys.length + secretStoreKeys.length > 0;
92
+ if (hasExecution && laneTouched) {
93
+ 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`);
94
+ }
95
+ // 新形的店字段有两处:`runtime.store` 与 `secretRefs.dbPasswordEnv`(gpt-6 腿 F1:只看前者时,手填过新口令引用、又留着旧
96
+ // `tidbPasswordEnv` 的行会被旧引用静默覆盖)。任一在场 ∧ 还有旧店字段 ⇒ 两形并存,不挑赢家。
97
+ const newStoreFields = [...(hasStore ? ["runtime.store"] : []), ...(has(raw.secretRefs, "dbPasswordEnv") ? ["secretRefs.dbPasswordEnv"] : [])];
98
+ if (newStoreFields.length > 0 && storeTouched) {
99
+ const old = [...oldStoreFields.map(([c, k]) => `${c}.${k}`), ...envStoreKeys.map((k) => `env.${k}`), ...secretStoreKeys.map((k) => `secretEnv.${k}`)];
100
+ 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`);
101
+ }
102
+ const secretStoreKey = secretStoreKeys[0];
103
+ if (secretStoreKey !== undefined)
104
+ return refuse(["secretEnv", secretStoreKey], `${secretStoreKey} is set from a host secret — the migration cannot read it; declare runtime.store by hand`);
105
+ if (has(raw.secretEnv, "REMOTE_EXEC"))
106
+ 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");
107
+ const needLane = !hasExecution && (enabled || laneTouched);
108
+ const needStore = !hasStore && (enabled || storeTouched);
109
+ if (!needLane && !needStore)
110
+ return UNCHANGED;
111
+ const w = structuredClone(raw);
112
+ const runtime = isRec(w.runtime) ? w.runtime : {};
113
+ w.runtime = runtime;
114
+ const env = isRec(w.env) ? w.env : {};
115
+ const secretEnv = isRec(w.secretEnv) ? w.secretEnv : {};
116
+ const coords = isRec(w.coords) ? w.coords : {};
117
+ const secretRefs = isRec(w.secretRefs) ? w.secretRefs : {};
118
+ const dropped = [];
119
+ const drop = (container, slot, key) => {
120
+ delete container[key];
121
+ dropped.push({ name, index, field: [slot, key] });
122
+ };
123
+ // ── 车道 ─────────────────────────────────────────────────────────────────────────────────────────
124
+ if (needLane) {
125
+ const rx = env.REMOTE_EXEC;
126
+ if (rx === undefined || rx === "") {
127
+ if (enabled) {
128
+ 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. ` +
129
+ `The migration does not guess a lane: set runtime.execution.lane (${EXECUTION_LANE_WORDS.join(" | ")}) by hand, or disable the worker`);
130
+ }
131
+ for (const k of envLaneKeys)
132
+ drop(env, "env", k); // 停用且没点名车道 ⇒ 每个车道键都是惰性的
133
+ for (const k of secretLaneKeys)
134
+ drop(secretEnv, "secretEnv", k);
135
+ }
136
+ else {
137
+ if (!isLaneWord(rx)) {
138
+ 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`);
139
+ }
140
+ const lane = rx;
141
+ const laneCoords = {};
142
+ const laneSecretRefs = {};
143
+ for (const k of envLaneKeys) {
144
+ if (k === "REMOTE_EXEC") {
145
+ delete env[k];
146
+ continue;
147
+ }
148
+ const owner = LANE_ENV_OWNER.get(k);
149
+ if (owner === undefined || owner.lane !== lane) {
150
+ drop(env, "env", k); // REQUIRE_EXECUTION_LANE(声明了车道就恒渲 true)与别车道的键(引擎只读被点名那一臂)
151
+ continue;
152
+ }
153
+ const v = env[k];
154
+ if (owner.secret) {
155
+ return refuse(["env", k], `${k} is a secret of lane "${lane}" but env carries it as a literal value — the migration does not move a value into a secret slot: put it in the host secret source and reference it from runtime.execution.secretRefs.${k}`);
156
+ }
157
+ if (typeof v !== "string")
158
+ return refuse(["env", k], `env.${k} is not a string`);
159
+ laneCoords[k] = v;
160
+ delete env[k];
161
+ }
162
+ for (const k of secretLaneKeys) {
163
+ const owner = LANE_ENV_OWNER.get(k);
164
+ if (owner === undefined || owner.lane !== lane) {
165
+ drop(secretEnv, "secretEnv", k);
166
+ continue;
167
+ }
168
+ const v = secretEnv[k];
169
+ if (!owner.secret) {
170
+ return refuse(["secretEnv", k], `${k} is a non-secret coordinate of lane "${lane}" but is set from a host secret — the migration cannot read the value; write it in runtime.execution.coords.${k} by hand`);
171
+ }
172
+ if (typeof v !== "string")
173
+ return refuse(["secretEnv", k], `secretEnv.${k} is not a string`);
174
+ laneSecretRefs[k] = v;
175
+ delete secretEnv[k];
176
+ }
177
+ runtime.execution = { lane, coords: laneCoords, secretRefs: laneSecretRefs };
178
+ }
179
+ }
180
+ // ── 店 ───────────────────────────────────────────────────────────────────────────────────────────
181
+ if (needStore) {
182
+ if (Object.hasOwn(runtime, "memoryBackend"))
183
+ drop(runtime, "runtime", "memoryBackend"); // 从未到达引擎
184
+ const sessionRaw = runtime.sessionBackend;
185
+ const host = coords.tidbHost;
186
+ const hostSet = typeof host === "string" && host !== "";
187
+ const dbBackendRaw = env.DB_BACKEND;
188
+ const dbBackendSet = dbBackendRaw !== undefined && dbBackendRaw !== "";
189
+ const named = hostSet || dbBackendSet || sessionRaw === "tidb";
190
+ const sqlCoordFields = [
191
+ [runtime, "runtime", "db"],
192
+ [coords, "coords", "tidbHost"],
193
+ [coords, "coords", "tidbPort"],
194
+ [coords, "coords", "tidbUser"],
195
+ [secretRefs, "secretRefs", "tidbPasswordEnv"],
196
+ ];
197
+ if (!enabled && !named) {
198
+ // 停用、旧形没点名任何店 ⇒ 不替它发明一个;旧字段全是惰性的
199
+ if (Object.hasOwn(runtime, "sessionBackend"))
200
+ drop(runtime, "runtime", "sessionBackend");
201
+ for (const [c, slot, k] of sqlCoordFields)
202
+ if (Object.hasOwn(c, k))
203
+ drop(c, slot, k);
204
+ for (const k of STORE_ENV_KEYS)
205
+ if (Object.hasOwn(env, k))
206
+ drop(env, "env", k);
207
+ }
208
+ else {
209
+ let session;
210
+ if (sessionRaw === undefined)
211
+ session = "auto"; // 6.x 的缺省
212
+ else if (sessionRaw === "tidb")
213
+ session = "mysql";
214
+ else if (sessionRaw === "memory" || sessionRaw === "auto")
215
+ session = sessionRaw;
216
+ else
217
+ return refuse(["runtime", "sessionBackend"], `runtime.sessionBackend ${describeWord(sessionRaw)} is not a 6.x session backend (tidb | memory | auto)`);
218
+ delete runtime.sessionBackend;
219
+ let backend;
220
+ if (dbBackendSet) {
221
+ if (!isStoreWord(dbBackendRaw))
222
+ return refuse(["env", "DB_BACKEND"], `env.DB_BACKEND ${describeWord(dbBackendRaw)} is not a store backend (${STORE_BACKENDS.join(" | ")}) — the engine refuses to start with it`);
223
+ backend = dbBackendRaw;
224
+ }
225
+ else {
226
+ backend = hostSet || sessionRaw === "tidb" ? "mysql" : "local";
227
+ }
228
+ delete env.DB_BACKEND;
229
+ const sqlCoordsPresent = sqlCoordFields.filter(([c, , k]) => Object.hasOwn(c, k));
230
+ const store = { backend, session };
231
+ if (backend === "mysql") {
232
+ if (hostSet) {
233
+ const port = coords.tidbPort;
234
+ store.db = {
235
+ host,
236
+ port: port === undefined ? MYSQL_DEFAULT_PORT : port,
237
+ ...(Object.hasOwn(coords, "tidbUser") ? { user: coords.tidbUser } : {}),
238
+ ...(Object.hasOwn(runtime, "db") ? { database: runtime.db } : {}),
239
+ };
240
+ }
241
+ if (Object.hasOwn(secretRefs, "tidbPasswordEnv"))
242
+ secretRefs.dbPasswordEnv = secretRefs.tidbPasswordEnv;
243
+ for (const [c, , k] of sqlCoordsPresent)
244
+ delete c[k];
245
+ }
246
+ else if (backend === "pg") {
247
+ if (sqlCoordsPresent.length > 0) {
248
+ return refuse(["env", "DB_BACKEND"], `env.DB_BACKEND=pg but the SQL coordinates are the MySQL family (coords.tidb* / runtime.db / secretRefs.tidbPasswordEnv, rendered as MYSQL_*) — the migration does not move a MySQL server's coordinates onto pg; declare runtime.store by hand`);
249
+ }
250
+ }
251
+ else {
252
+ for (const [c, slot, k] of sqlCoordsPresent)
253
+ drop(c, slot, k); // 非 SQL 店:SQL 坐标在引擎里是「声明不用」的惰性件
254
+ }
255
+ const memory = {};
256
+ const engineRaw = env.MEMORY_ENGINE;
257
+ if (engineRaw !== undefined) {
258
+ const word = typeof engineRaw === "string" ? engineRaw.trim().toLowerCase() : undefined;
259
+ if (word !== undefined && MEMORY_ENGINE_ON_WORDS.includes(word))
260
+ memory.engine = "on";
261
+ else if (word !== undefined && MEMORY_ENGINE_OFF_WORDS.includes(word))
262
+ memory.engine = "off";
263
+ else if (word !== "")
264
+ return refuse(["env", "MEMORY_ENGINE"], "env.MEMORY_ENGINE is not an on/off word — the engine refuses to start with it");
265
+ delete env.MEMORY_ENGINE;
266
+ }
267
+ const memoryBackendRaw = env.MEMORY_ENGINE_BACKEND;
268
+ if (memoryBackendRaw !== undefined) {
269
+ if (!isMemoryBackendWord(memoryBackendRaw)) {
270
+ 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`);
271
+ }
272
+ memory.backend = memoryBackendRaw;
273
+ delete env.MEMORY_ENGINE_BACKEND;
274
+ }
275
+ if (Object.keys(memory).length > 0)
276
+ store.memory = memory;
277
+ runtime.store = store;
278
+ }
279
+ }
280
+ // 被搬空的 env / secretEnv 整键去掉(它们是可选字段;留一个 `{}` 只是噪声)
281
+ if (isRec(w.env) && Object.keys(env).length === 0 && isRec(raw.env) && Object.keys(raw.env).length > 0)
282
+ delete w.env;
283
+ if (isRec(w.secretEnv) && Object.keys(secretEnv).length === 0 && isRec(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
284
+ delete w.secretEnv;
285
+ // 终判 = 7.0.0 schema 本身(迁移不复述写闸规则)
286
+ const verdict = WorkerSpec.safeParse(w);
287
+ if (!verdict.success) {
288
+ const issues = verdict.error.issues;
289
+ const shown = issues.slice(0, 3).map((i) => `${formatConfigPath(i.path)}: ${i.message}`);
290
+ const more = issues.length > shown.length ? ` (+${issues.length - shown.length} more)` : "";
291
+ const first = issues[0];
292
+ return refuse(first === undefined ? [] : first.path, `after migration the worker is not a valid 7.0.0 WorkerSpec — ${shown.join("; ")}${more}`);
293
+ }
294
+ return { kind: "migrated", entry: w, dropped };
295
+ }
296
+ /**
297
+ * 迁移一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回(判形是 `parseDomain` 的事,迁移不抛)。
298
+ * 规则、幂等与 refused 的处置见文件头。
299
+ */
300
+ export function migrateWorkersDomain(doc) {
301
+ if (!isRec(doc) || !Array.isArray(doc.workers))
302
+ return { doc, changed: [], dropped: [], refused: [] };
303
+ const workers = doc.workers;
304
+ const kept = [];
305
+ const changed = [];
306
+ const dropped = [];
307
+ const refused = [];
308
+ workers.forEach((entry, index) => {
309
+ const out = migrateEntry(entry, index);
310
+ switch (out.kind) {
311
+ case "unchanged":
312
+ kept.push(entry);
313
+ break;
314
+ case "migrated":
315
+ kept.push(out.entry);
316
+ changed.push(String(out.entry.name));
317
+ dropped.push(...out.dropped);
318
+ break;
319
+ case "refused":
320
+ refused.push(out.refusal);
321
+ break;
322
+ default: {
323
+ const unhandled = out;
324
+ throw new Error(`migrateWorkersDomain: unhandled outcome ${JSON.stringify(unhandled)}`);
325
+ }
326
+ }
327
+ });
328
+ if (changed.length === 0 && refused.length === 0)
329
+ return { doc, changed: [], dropped: [], refused: [] };
330
+ return { doc: { ...doc, workers: kept }, changed, dropped, refused };
331
+ }