@sema-agent/settings-schema 6.4.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 (95) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/README.md +34 -2
  3. package/bin/settings-schema.mjs +22 -0
  4. package/conformance/VERSION +1 -1
  5. package/conformance/cases/c50-s784-lane-host-legal/expected.json +102 -0
  6. package/conformance/cases/c50-s784-lane-host-legal/input.json +29 -0
  7. package/conformance/cases/c50-s784-lane-host-legal/meta.json +5 -0
  8. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +104 -0
  9. package/conformance/cases/c51-s784-lane-e2b-legal/input.json +32 -0
  10. package/conformance/cases/c51-s784-lane-e2b-legal/meta.json +5 -0
  11. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +106 -0
  12. package/conformance/cases/c52-s784-lane-k8s-legal/input.json +34 -0
  13. package/conformance/cases/c52-s784-lane-k8s-legal/meta.json +5 -0
  14. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +106 -0
  15. package/conformance/cases/c53-s784-lane-ssh-legal/input.json +34 -0
  16. package/conformance/cases/c53-s784-lane-ssh-legal/meta.json +5 -0
  17. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +102 -0
  18. package/conformance/cases/c54-s784-lane-adb-legal/input.json +29 -0
  19. package/conformance/cases/c54-s784-lane-adb-legal/meta.json +5 -0
  20. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +103 -0
  21. package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +30 -0
  22. package/conformance/cases/c55-s784-lane-local-docker-legal/meta.json +5 -0
  23. package/conformance/cases/c56-s784-lane-device-legal/expected.json +109 -0
  24. package/conformance/cases/c56-s784-lane-device-legal/input.json +36 -0
  25. package/conformance/cases/c56-s784-lane-device-legal/meta.json +5 -0
  26. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +83 -0
  27. package/conformance/cases/c57-s784-g1-enabled-no-lane/input.json +23 -0
  28. package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +5 -0
  29. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +83 -0
  30. package/conformance/cases/c58-s784-g2-multitenant-host/input.json +34 -0
  31. package/conformance/cases/c58-s784-g2-multitenant-host/meta.json +5 -0
  32. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +83 -0
  33. package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +26 -0
  34. package/conformance/cases/c59-s784-g3-k8s-missing-image/meta.json +5 -0
  35. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +83 -0
  36. package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +26 -0
  37. package/conformance/cases/c60-s784-g3-e2b-missing-key/meta.json +5 -0
  38. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +83 -0
  39. package/conformance/cases/c61-s784-g3-other-lane-key/input.json +30 -0
  40. package/conformance/cases/c61-s784-g3-other-lane-key/meta.json +5 -0
  41. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +83 -0
  42. package/conformance/cases/c62-s784-g7a-session-tidb/input.json +27 -0
  43. package/conformance/cases/c62-s784-g7a-session-tidb/meta.json +5 -0
  44. package/conformance/cases/c63-s784-host-two-replicas/expected.json +83 -0
  45. package/conformance/cases/c63-s784-host-two-replicas/input.json +27 -0
  46. package/conformance/cases/c63-s784-host-two-replicas/meta.json +5 -0
  47. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +83 -0
  48. package/conformance/cases/c64-s784-multitenant-local-store/input.json +30 -0
  49. package/conformance/cases/c64-s784-multitenant-local-store/meta.json +5 -0
  50. package/conformance/cases/c65-s784-pg-without-db/expected.json +83 -0
  51. package/conformance/cases/c65-s784-pg-without-db/input.json +27 -0
  52. package/conformance/cases/c65-s784-pg-without-db/meta.json +5 -0
  53. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +83 -0
  54. package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +36 -0
  55. package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +5 -0
  56. package/conformance/cases/c67-s784-retired-field-loud/expected.json +83 -0
  57. package/conformance/cases/c67-s784-retired-field-loud/input.json +22 -0
  58. package/conformance/cases/c67-s784-retired-field-loud/meta.json +5 -0
  59. package/conformance/cases/c68-s784-device-local-store/expected.json +83 -0
  60. package/conformance/cases/c68-s784-device-local-store/input.json +26 -0
  61. package/conformance/cases/c68-s784-device-local-store/meta.json +5 -0
  62. package/conformance/cases/cli04-env-value-with-newline/expected.json +13 -2
  63. package/conformance/cases/cli04-env-value-with-newline/input.json +8 -1
  64. package/conformance/cases/cli04-env-value-with-newline/meta.json +1 -1
  65. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +49 -0
  66. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +26 -0
  67. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +4 -0
  68. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +38 -0
  69. package/conformance/migrate-workers/m02-g5-idempotent/input.json +33 -0
  70. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +5 -0
  71. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +71 -0
  72. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +42 -0
  73. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +4 -0
  74. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +52 -0
  75. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +30 -0
  76. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +4 -0
  77. package/dist/cli.d.ts +8 -0
  78. package/dist/cli.js +70 -0
  79. package/dist/execution-lane-index.d.ts +19 -0
  80. package/dist/execution-lane-index.js +23 -0
  81. package/dist/execution-lanes.d.ts +64 -0
  82. package/dist/execution-lanes.js +152 -0
  83. package/dist/file-edit.js +26 -21
  84. package/dist/hooks.d.ts +4 -4
  85. package/dist/index.d.ts +2 -0
  86. package/dist/index.js +2 -0
  87. package/dist/migrate-workers.d.ts +63 -0
  88. package/dist/migrate-workers.js +331 -0
  89. package/dist/remote-exec.d.ts +23 -64
  90. package/dist/remote-exec.js +20 -71
  91. package/dist/secret-refs.d.ts +7 -1
  92. package/dist/secret-refs.js +9 -3
  93. package/dist/types.d.ts +1356 -455
  94. package/dist/types.js +258 -63
  95. package/package.json +9 -1
@@ -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
+ }
@@ -1,26 +1,29 @@
1
1
  /**
2
2
  * `remoteExec` — the execution-substrate contract (NET-NEW; not part of DOMAIN_SCHEMAS today).
3
3
  *
4
- * Per the product owner's decision the exec layer is user-configurable & pluggable. This SCHEMA is a
5
- * FORWARD-COMPATIBLE SUPERSET: it carries both the backends the worker service already implements today AND the
6
- * TOC single-machine backends the product owner added (the service implements those in PHASES — host + e2b first,
7
- * local/remote-docker fast-follow). The schema LEADS; runtime support is phased. The discriminator is `provider`:
8
- * FLEET (implemented today, the worker service's config module):
4
+ * The exec layer is user-configurable & pluggable. This SCHEMA carries the per-machine `config.d/remote-exec.json`
5
+ * shape for the lanes a consumer can drive from that file. The discriminator is `provider`:
6
+ * FLEET:
9
7
  * - `e2b` per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
10
8
  * - `k8s` per-task Kata pod on our k3s (isolated, NOT suspendable). Self-hosted sandbox lane.
11
9
  * - `ssh` a real host over SSH (NOT isolated/suspendable). AI-orchestrated batch deploy.
12
10
  * - `adb` a real device over ADB (NOT isolated/suspendable). On-device APP testing.
13
- * TOC single-machine (product-owner scope; service implements in phases — macOS + Linux first):
11
+ * TOC single-machine (macOS + Linux):
14
12
  * - `host` run tools DIRECTLY on this machine, no container (the TOC default, like a local coding
15
- * agent). No isolation. macOS/Linux.
13
+ * agent). No isolation.
16
14
  * - `local-docker` containers on THIS machine's docker (isolation for parallel/untrusted fan-out).
17
- * - `remote-docker` a user-specified REMOTE host's docker (offload; isolated).
15
+ *
16
+ * 7.0.0: every `provider` word here is an execution lane word (`EXECUTION_LANE_WORDS`, `execution-lanes.ts` — the ONE
17
+ * vocabulary; `test/execution-lanes-7.0.0.test.ts` pins `provider ⊆ EXECUTION_LANE_WORDS`, at runtime and at compile time).
18
+ * The `remote-docker` variant was DELETED (zero aliases): no engine read point ever turned that word into a running lane
19
+ * (the engine refuses `REMOTE_EXEC=remote-docker` at boot and only warned "unwired" when this file named it). A remote
20
+ * docker daemon is the `local-docker` lane pointed at it (`DOCKER_HOST` — in a WorkerSpec, `runtime.execution.coords`).
18
21
  *
19
22
  * SECRET BOUNDARY (identical to the rest of the contract): every secret VALUE the consumer reads from its own
20
- * env (E2B_API_KEY, K8S_TOKEN, SSH_PRIVATE_KEY, MINIO_*, the remote-docker TLS/SSH creds) is carried here as an
21
- * env-NAME ref (the `ENV_NAME` regex from types.ts) — NEVER a literal value. The consumer resolves NAME → value
22
- * from its own env (service env / k8s secret / the TOC local `.env`), so this package never carries a
23
- * credential. This is SCHEMA + secret-as-env-NAME ONLY; the service owns the adapter implementations.
23
+ * env (E2B_API_KEY, K8S_TOKEN, SSH_PRIVATE_KEY, MINIO_*) is carried here as an env-NAME ref (the `ENV_NAME` regex
24
+ * from types.ts) — NEVER a literal value. The consumer resolves NAME → value from its own env (service env / k8s
25
+ * secret / the TOC local `.env`), so this package never carries a credential. This is SCHEMA + secret-as-env-NAME
26
+ * ONLY; the service owns the adapter implementations.
24
27
  */
25
28
  import { z } from "zod";
26
29
  export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodObject<{
@@ -61,7 +64,7 @@ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodO
61
64
  memory?: string | undefined;
62
65
  workdir?: string | undefined;
63
66
  cpus?: number | undefined;
64
- network?: "none" | "host" | "bridge" | undefined;
67
+ network?: "host" | "none" | "bridge" | undefined;
65
68
  mountPath?: string | undefined;
66
69
  }, {
67
70
  provider: "local-docker";
@@ -69,52 +72,8 @@ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodO
69
72
  memory?: string | undefined;
70
73
  workdir?: string | undefined;
71
74
  cpus?: number | undefined;
72
- network?: "none" | "host" | "bridge" | undefined;
73
- mountPath?: string | undefined;
74
- }>, z.ZodObject<{
75
- /** The remote docker endpoint — a DOCKER_HOST url: `ssh://user@host` or `tcp://host:2376`. A `user@` is OK
76
- * (it's the ssh target, not a secret); an embedded PASSWORD is rejected — credentials go in the env-NAME
77
- * fields below, NEVER in the URL (the secret-as-env-NAME boundary). */
78
- dockerHost: z.ZodEffects<z.ZodString, string, string>;
79
- /** env-NAMEs for a tcp+TLS endpoint (NEVER the cert/key bytes). */
80
- tlsCertEnv: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
81
- tlsKeyEnv: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
82
- tlsCaEnv: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
83
- /** env-NAME for the SSH private key when dockerHost is `ssh://…` (NEVER the key value). */
84
- sshKeyEnv: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
85
- image: z.ZodEffects<z.ZodString, string, string>;
86
- memory: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
87
- cpus: z.ZodOptional<z.ZodNumber>;
88
- network: z.ZodOptional<z.ZodEnum<["none", "bridge", "host"]>>;
89
- workdir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
90
- mountPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
91
- provider: z.ZodLiteral<"remote-docker">;
92
- }, "strip", z.ZodTypeAny, {
93
- provider: "remote-docker";
94
- image: string;
95
- dockerHost: string;
96
- memory?: string | undefined;
97
- workdir?: string | undefined;
98
- cpus?: number | undefined;
99
- network?: "none" | "host" | "bridge" | undefined;
100
- mountPath?: string | undefined;
101
- tlsCertEnv?: string | undefined;
102
- tlsKeyEnv?: string | undefined;
103
- tlsCaEnv?: string | undefined;
104
- sshKeyEnv?: string | undefined;
105
- }, {
106
- provider: "remote-docker";
107
- image: string;
108
- dockerHost: string;
109
- memory?: string | undefined;
110
- workdir?: string | undefined;
111
- cpus?: number | undefined;
112
- network?: "none" | "host" | "bridge" | undefined;
75
+ network?: "host" | "none" | "bridge" | undefined;
113
76
  mountPath?: string | undefined;
114
- tlsCertEnv?: string | undefined;
115
- tlsKeyEnv?: string | undefined;
116
- tlsCaEnv?: string | undefined;
117
- sshKeyEnv?: string | undefined;
118
77
  }>, z.ZodObject<{
119
78
  provider: z.ZodLiteral<"e2b">;
120
79
  /** env-NAME holding the E2B API key (service: E2B_API_KEY). NEVER the key. */
@@ -192,8 +151,8 @@ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodO
192
151
  }, "strip", z.ZodTypeAny, {
193
152
  provider: "k8s";
194
153
  image: string;
195
- cpu?: string | undefined;
196
154
  memory?: string | undefined;
155
+ cpu?: string | undefined;
197
156
  mountPath?: string | undefined;
198
157
  timeoutMs?: number | undefined;
199
158
  apiUrl?: string | undefined;
@@ -214,8 +173,8 @@ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodO
214
173
  }, {
215
174
  provider: "k8s";
216
175
  image: string;
217
- cpu?: string | undefined;
218
176
  memory?: string | undefined;
177
+ cpu?: string | undefined;
219
178
  mountPath?: string | undefined;
220
179
  timeoutMs?: number | undefined;
221
180
  apiUrl?: string | undefined;
@@ -242,19 +201,19 @@ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodO
242
201
  privateKeyEnv: z.ZodEffects<z.ZodString, string, string>;
243
202
  mountPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
244
203
  }, "strip", z.ZodTypeAny, {
245
- provider: "ssh";
246
204
  host: string;
205
+ provider: "ssh";
247
206
  username: string;
248
207
  privateKeyEnv: string;
249
- mountPath?: string | undefined;
250
208
  port?: number | undefined;
209
+ mountPath?: string | undefined;
251
210
  }, {
252
- provider: "ssh";
253
211
  host: string;
212
+ provider: "ssh";
254
213
  username: string;
255
214
  privateKeyEnv: string;
256
- mountPath?: string | undefined;
257
215
  port?: number | undefined;
216
+ mountPath?: string | undefined;
258
217
  }>, z.ZodObject<{
259
218
  provider: z.ZodLiteral<"adb">;
260
219
  serial: z.ZodEffects<z.ZodString, string, string>;
@@ -1,34 +1,37 @@
1
1
  /**
2
2
  * `remoteExec` — the execution-substrate contract (NET-NEW; not part of DOMAIN_SCHEMAS today).
3
3
  *
4
- * Per the product owner's decision the exec layer is user-configurable & pluggable. This SCHEMA is a
5
- * FORWARD-COMPATIBLE SUPERSET: it carries both the backends the worker service already implements today AND the
6
- * TOC single-machine backends the product owner added (the service implements those in PHASES — host + e2b first,
7
- * local/remote-docker fast-follow). The schema LEADS; runtime support is phased. The discriminator is `provider`:
8
- * FLEET (implemented today, the worker service's config module):
4
+ * The exec layer is user-configurable & pluggable. This SCHEMA carries the per-machine `config.d/remote-exec.json`
5
+ * shape for the lanes a consumer can drive from that file. The discriminator is `provider`:
6
+ * FLEET:
9
7
  * - `e2b` per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
10
8
  * - `k8s` per-task Kata pod on our k3s (isolated, NOT suspendable). Self-hosted sandbox lane.
11
9
  * - `ssh` a real host over SSH (NOT isolated/suspendable). AI-orchestrated batch deploy.
12
10
  * - `adb` a real device over ADB (NOT isolated/suspendable). On-device APP testing.
13
- * TOC single-machine (product-owner scope; service implements in phases — macOS + Linux first):
11
+ * TOC single-machine (macOS + Linux):
14
12
  * - `host` run tools DIRECTLY on this machine, no container (the TOC default, like a local coding
15
- * agent). No isolation. macOS/Linux.
13
+ * agent). No isolation.
16
14
  * - `local-docker` containers on THIS machine's docker (isolation for parallel/untrusted fan-out).
17
- * - `remote-docker` a user-specified REMOTE host's docker (offload; isolated).
15
+ *
16
+ * 7.0.0: every `provider` word here is an execution lane word (`EXECUTION_LANE_WORDS`, `execution-lanes.ts` — the ONE
17
+ * vocabulary; `test/execution-lanes-7.0.0.test.ts` pins `provider ⊆ EXECUTION_LANE_WORDS`, at runtime and at compile time).
18
+ * The `remote-docker` variant was DELETED (zero aliases): no engine read point ever turned that word into a running lane
19
+ * (the engine refuses `REMOTE_EXEC=remote-docker` at boot and only warned "unwired" when this file named it). A remote
20
+ * docker daemon is the `local-docker` lane pointed at it (`DOCKER_HOST` — in a WorkerSpec, `runtime.execution.coords`).
18
21
  *
19
22
  * SECRET BOUNDARY (identical to the rest of the contract): every secret VALUE the consumer reads from its own
20
- * env (E2B_API_KEY, K8S_TOKEN, SSH_PRIVATE_KEY, MINIO_*, the remote-docker TLS/SSH creds) is carried here as an
21
- * env-NAME ref (the `ENV_NAME` regex from types.ts) — NEVER a literal value. The consumer resolves NAME → value
22
- * from its own env (service env / k8s secret / the TOC local `.env`), so this package never carries a
23
- * credential. This is SCHEMA + secret-as-env-NAME ONLY; the service owns the adapter implementations.
23
+ * env (E2B_API_KEY, K8S_TOKEN, SSH_PRIVATE_KEY, MINIO_*) is carried here as an env-NAME ref (the `ENV_NAME` regex
24
+ * from types.ts) — NEVER a literal value. The consumer resolves NAME → value from its own env (service env / k8s
25
+ * secret / the TOC local `.env`), so this package never carries a credential. This is SCHEMA + secret-as-env-NAME
26
+ * ONLY; the service owns the adapter implementations.
24
27
  */
25
28
  import { z } from "zod";
26
- import { ENV_NAME, K8S_QUANTITY, NON_SECRET_URL, TCP_PORT, containsSecretToken, hasNul, urlQueryFragmentHasSecret } from "./types.js";
29
+ import { ENV_NAME, K8S_QUANTITY, NON_SECRET_URL, TCP_PORT, containsSecretToken, hasNul } from "./types.js";
27
30
  /** A non-empty exec-bound string (an image / path / shell / arg the adapter feeds to spawn()/fs). Rejects a NUL
28
31
  * byte uniformly — a NUL validates here but makes Node throw at spawn/boot (fail-OPEN → runtime crash). R20. */
29
32
  const execStr = z.string().min(1).refine((s) => !hasNul(s), "must not contain a NUL byte");
30
- /** Container knobs shared by the two docker lanes (local-docker / remote-docker). `network` defaults to
31
- * `none` at the adapter (the safe posture for untrusted code — see the dual-mode design note). */
33
+ /** Container knobs of the `local-docker` lane. `network` defaults to `none` at the adapter (the safe posture for
34
+ * untrusted code — see the dual-mode design note). */
32
35
  const DOCKER_FIELDS = {
33
36
  /** Sandbox image (required — a run must be reproducible). */
34
37
  image: execStr,
@@ -58,60 +61,6 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
58
61
  // ── local-docker: containers on this machine's docker (isolated; enables parallel fan-out). No secrets
59
62
  // (local docker.sock). Service implements this fast-follow after host.
60
63
  z.object({ provider: z.literal("local-docker"), ...DOCKER_FIELDS }),
61
- // ── remote-docker: a user-specified remote host's docker. Creds (TLS for tcp://, key for ssh://) are
62
- // env-NAME refs, never bytes. Service implements this fast-follow.
63
- z.object({
64
- provider: z.literal("remote-docker"),
65
- ...DOCKER_FIELDS,
66
- /** The remote docker endpoint — a DOCKER_HOST url: `ssh://user@host` or `tcp://host:2376`. A `user@` is OK
67
- * (it's the ssh target, not a secret); an embedded PASSWORD is rejected — credentials go in the env-NAME
68
- * fields below, NEVER in the URL (the secret-as-env-NAME boundary). */
69
- dockerHost: z
70
- .string()
71
- .min(1)
72
- .refine((s) => {
73
- if (hasNul(s))
74
- return false; // a NUL in DOCKER_HOST crashes the adapter at spawn — reject (R20)
75
- try {
76
- const u = new URL(s);
77
- if (u.password)
78
- return false; // a `user@` ssh target is fine; an embedded PASSWORD is not
79
- // a secret can also ride in the username (`ssh://sk-live-…@host`) or the query/fragment — scan them
80
- // (Round-4). The userinfo password is already rejected above.
81
- let user = u.username;
82
- try {
83
- user = decodeURIComponent(u.username);
84
- }
85
- catch {
86
- /* keep raw */
87
- }
88
- if (user && containsSecretToken(user))
89
- return false;
90
- return !urlQueryFragmentHasSecret(u);
91
- }
92
- catch {
93
- // unparseable (e.g. a bare socket path) → reject a `:pw@` userinfo-password pattern AND any
94
- // secret-shaped TOKEN anywhere (containsSecretToken tokenizes on URL/query delimiters incl =&, so a
95
- // mid-string `/var/run/sk-…` or `socket?token=sk-…` is caught despite the ^-anchored regex). R4/5.
96
- if (/:[^/@]+@/.test(s))
97
- return false;
98
- if (containsSecretToken(s))
99
- return false;
100
- try {
101
- return !containsSecretToken(decodeURIComponent(s)); // also catch percent-encoded secrets
102
- }
103
- catch {
104
- return true; // malformed %-escape → the raw scan above already passed
105
- }
106
- }
107
- }, "no credentials in dockerHost (password / secret-shaped user/query/fragment) — put them in the env-NAME fields (sshKeyEnv / tls*Env)"),
108
- /** env-NAMEs for a tcp+TLS endpoint (NEVER the cert/key bytes). */
109
- tlsCertEnv: ENV_NAME.optional(),
110
- tlsKeyEnv: ENV_NAME.optional(),
111
- tlsCaEnv: ENV_NAME.optional(),
112
- /** env-NAME for the SSH private key when dockerHost is `ssh://…` (NEVER the key value). */
113
- sshKeyEnv: ENV_NAME.optional(),
114
- }),
115
64
  // ── e2b: the user's E2B account (service provider "e2b"; REMOTE_EXEC=e2b + E2B_API_KEY).
116
65
  z.object({
117
66
  provider: z.literal("e2b"),
@@ -163,8 +112,8 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
163
112
  // ── ssh: a real host over SSH (service provider "ssh"; REMOTE_EXEC=ssh + SSH_HOST/SSH_USER/SSH_PRIVATE_KEY).
164
113
  z.object({
165
114
  provider: z.literal("ssh"),
166
- // host + username are persisted to config.d/remote-exec.json — scan for a secret-shaped literal, same as the
167
- // remote-docker dockerHost scans its embedded ssh user (a secret must go in privateKeyEnv, not the login). R14.
115
+ // host + username are persisted to config.d/remote-exec.json — scan for a secret-shaped literal (a secret must go
116
+ // in privateKeyEnv, not the login). R14.
168
117
  host: execStr.refine((s) => !containsSecretToken(s), "no secret literal in ssh host — credentials go in privateKeyEnv"),
169
118
  port: TCP_PORT.optional(),
170
119
  username: execStr.refine((s) => !containsSecretToken(s), "no secret literal in ssh username — credentials go in privateKeyEnv"),
@@ -22,7 +22,8 @@ export interface EnvRef {
22
22
  * (integrations domain DELETED in 0.6.0 — OA/Gitea coordinates ride worker.env/secretEnv, EXPERT-REDESIGN §9.) */
23
23
  export declare function collectEnvRefs(eff: EffectiveConfig): EnvRef[];
24
24
  /**
25
- * Names of workers that reference a given env-NAME — via any of the 4 structured `secretRefs` OR the general
25
+ * Names of workers that reference a given env-NAME — via any of the structured `secretRefs`, the execution lane's
26
+ * `runtime.execution.secretRefs` (7.0.0: where E2B_API_KEY / SSH_PRIVATE_KEY / K8S_TOKEN … live now), OR the general
26
27
  * `secretEnv` passthrough. Drives batch-rotate (POST /secrets/<name>/rotate): after a credential write, recreate
27
28
  * exactly the workers that project it. Reads the raw workers domain (the same shape collectEnvRefs would see).
28
29
  */
@@ -30,4 +31,9 @@ export declare function workersReferencingSecret(workers: {
30
31
  name: string;
31
32
  secretRefs?: Record<string, string | undefined>;
32
33
  secretEnv?: Record<string, string>;
34
+ runtime?: {
35
+ execution?: {
36
+ secretRefs?: Record<string, string>;
37
+ };
38
+ };
33
39
  }[], envName: string): string[];