@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/types.js CHANGED
@@ -19,6 +19,8 @@ import { z } from "zod";
19
19
  import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
20
20
  import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
21
21
  import { carriesUserinfo, percentLayers } from "./secret-scan.js"; // package-private normalization + the userinfo judgement (not in the barrel)
22
+ import { EXECUTION_LANES, EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js"; // 7.0.0 M-R2: the lane / store vocabulary (one exported table)
23
+ import { LANE_ENV_OWNER, laneEnvSeat, laneRowOf } from "./execution-lane-index.js"; // package-private indexes derived from that table (not in the barrel)
22
24
  // COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
23
25
  // on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
24
26
  // AND as an env-NAME value (an AWS key id is a valid env-NAME charset yet is a secret VALUE, not a NAME). Case-
@@ -201,9 +203,9 @@ export function extraBodyUnsafe(v) {
201
203
  }
202
204
  return false;
203
205
  }
204
- /** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody`
205
- * and `ModelEntry.compat` share this ONE function (1.10.0; before that it was inline in `extraBody` and a second
206
- * free-form field would have copied it). The judgment is identical for every such field, so it lives in one place:
206
+ /** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody`,
207
+ * `ModelEntry.compat` and `ModelEntry.headers` share this ONE function (1.10.0; before that it was inline in `extraBody`
208
+ * and a second free-form field would have copied it). The judgment is identical for every such field, so it lives in one place:
207
209
  *
208
210
  * 1. getter-free descriptor walk ({@link extraBodyUnsafe}) over the RAW value — never invokes a getter, and any
209
211
  * reflective throw (a hostile Proxy trap could throw a secret-bearing message) collapses into a FIXED message;
@@ -234,10 +236,26 @@ function vetFreeFormJson(v, field) {
234
236
  return { ok: false, message: `${field}: its serialized form carries a secret` };
235
237
  return { ok: true, value: clone };
236
238
  }
239
+ /** The transform of every **parse-transparent free-form** key (`z.unknown().optional().transform(createFreeFormTransform(field))`:
240
+ * `ModelEntry.compat` / `ModelEntry.headers`) — ONE closure, so those keys cannot drift apart: absent stays absent (key not
241
+ * minted); anything else is the {@link vetFreeFormJson} safety judgment and nothing more (no SHAPE rule — whose job that is,
242
+ * per key, is {@link PARSE_TRANSPARENT_KEYS}). `extraBody` is not one of these: it has its own shape gate first. */
243
+ function createFreeFormTransform(field) {
244
+ return (v, ctx) => {
245
+ if (v === undefined)
246
+ return undefined;
247
+ const vetted = vetFreeFormJson(v, field);
248
+ if (!vetted.ok) {
249
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vetted.message });
250
+ return z.NEVER;
251
+ }
252
+ return vetted.value;
253
+ };
254
+ }
237
255
  /** A k8s resource quantity ("250m", "1", "0.5", "512Mi", "1Gi", "128M") — catches garbage at config-submit
238
256
  * rather than surfacing as a reconcile ApplyError at the apiserver (consistent with ENV_NAME/DURATION guards). */
239
257
  export const K8S_QUANTITY = z.string().regex(/^\d+(\.\d+)?(m|[kKMGTPE]i?)?$/, 'must be a k8s quantity like "250m", "512Mi", "1Gi"');
240
- /** A TCP port: 1–65535. ONE shared schema so every port field (workers.coords.tidbPort, ssh.port, …) bounds
258
+ /** A TCP port: 1–65535. ONE shared schema so every port field (workers.runtime.store.db.port, ssh.port, …) bounds
241
259
  * the SAME way — a port of 65536/0/-1 is config corruption, reject it at validation, not at connect time. R16. */
242
260
  export const TCP_PORT = z.number().int().positive().max(65535);
243
261
  /** Per-1M-token price ($/Mtok). Maps to the service's MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE
@@ -313,6 +331,13 @@ export const PARSE_TRANSPARENT_KEYS = [
313
331
  validatedBy: "engine-ingest",
314
332
  why: "镜像引擎的 per-model wire-compat 声明(thinkingFormat 等五键);那张词表随引擎走,本包复述一份必漂。",
315
333
  },
334
+ {
335
+ domain: "models",
336
+ path: "models[].headers",
337
+ since: "6.4.0",
338
+ validatedBy: "consumer",
339
+ why: "部署 ↔ 供应商的 per-model 附加请求头;判据(头名闭集白名单 × 读它的线路 × 值形)的唯一属主是 server 的目录条目判据(坏声明 ⇒ 该条目整条不可用,不是丢头照常),本包复述一份那张表必漂、两处各一份就是两个写者。",
340
+ },
316
341
  {
317
342
  domain: "mcp",
318
343
  path: "servers[].source",
@@ -424,16 +449,20 @@ export const ModelEntry = z.object({
424
449
  compat: z
425
450
  .unknown()
426
451
  .optional()
427
- .transform((v, ctx) => {
428
- if (v === undefined)
429
- return undefined;
430
- const vetted = vetFreeFormJson(v, "compat");
431
- if (!vetted.ok) {
432
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vetted.message });
433
- return z.NEVER;
434
- }
435
- return vetted.value;
436
- }),
452
+ .transform(createFreeFormTransform("compat")),
453
+ /** Per-model extra request headers, deployment ↔ provider (e.g. `{"anthropic-beta": "context-1m-2025-08-07"}` on an
454
+ * `anthropic-messages` entry). Real need: a beta that a shell passes as `--betas` / `ANTHROPIC_BETAS` never reaches the
455
+ * request the engine sends; the catalog entry is the deployment-level seat for it — and without this key the entry's
456
+ * declaration is stripped at parse on every read leg, so it never reaches the consumer at all.
457
+ * ABSENT = key not minted = today's behaviour, zero migration.
458
+ *
459
+ * **Parse-transparent** — who validates the shape, and why not here: {@link PARSE_TRANSPARENT_KEYS}
460
+ * (`models.models[].headers`). The ONE rejection that remains is the free-form SAFETY judgment shared with `compat` /
461
+ * `extraBody` ({@link vetFreeFormJson}). */
462
+ headers: z
463
+ .unknown()
464
+ .optional()
465
+ .transform(createFreeFormTransform("headers")),
437
466
  /** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
438
467
  * (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
439
468
  quotaWeight: z.number().finite().nonnegative().default(1),
@@ -1297,8 +1326,9 @@ export const ExecutionPolicy = z.object({
1297
1326
  * shell locks sandbox-enabled by policy; user settings can't turn it off — CC 同款语义). */
1298
1327
  required: z.boolean().default(false),
1299
1328
  /** Which execution lanes are allowed. OPEN value domain — z.string(), NEVER an enum: adding a lane
1300
- * must not be a BREAKING schema change (promised to the service). Known lanes
1301
- * today: `host` / `e2b` / `k8s` / `ssh` / `adb` / `local-docker`. Unknown strings are consumer-defined. */
1329
+ * must not be a BREAKING schema change (promised to the service). The closed set of lanes a worker can
1330
+ * declare is `EXECUTION_LANE_WORDS` (`execution-lanes.ts`, 7.0.0) — read it there, not from a copy in this
1331
+ * comment. Unknown strings are consumer-defined. */
1302
1332
  allowedLanes: z.array(z.string().min(1)).default(["host"]),
1303
1333
  /** Org 默认会话镜像目标(0.10.1,附B 两层)。ABSENT = INERT(center 未管镜像,壳解析序里本腿为空)。
1304
1334
  * 读损档随域(DOMAIN_READ_FALLBACK.execution="throw" 已覆盖:损坏绝不静默退默认放权)。 */
@@ -1500,61 +1530,181 @@ const WorkerSecretRefs = z.object({
1500
1530
  serviceAuthTokenEnv: ENV_NAME.optional(),
1501
1531
  /** → GIT_API_TOKEN (只读,读被评审 repo). */
1502
1532
  gitTokenEnv: ENV_NAME.optional(),
1503
- /** → TIDB_PASSWORD. */
1504
- tidbPasswordEnv: ENV_NAME.optional(),
1533
+ /** 7.0.0:SQL 店的口令 → `MYSQL_PASSWORD` / `PG_PASSWORD`(渲染器按 `runtime.store.backend` 选名族)。
1534
+ * 在场 ⇔ `runtime.store.backend` ∈ {mysql, pg}(WorkerSpec 层判,与 `runtime.store.db` 同一条规则)。取代 6.x 的 `tidbPasswordEnv`。 */
1535
+ dbPasswordEnv: ENV_NAME.optional(),
1505
1536
  });
1506
1537
  /**
1507
- * Env keys the structured WorkerSpec fields already produce (worker-env.ts) — the general `env`/`secretEnv`
1508
- * passthrough may NOT set these (use the dedicated field). Blocks the council's two escape-hatch risks:
1509
- * redirecting model/traffic or disabling auth (PORT, MODEL_x, SESSION_BACKEND, REQUIRE_PRINCIPAL, gateway), and
1510
- * shadowing a projected secret with a literal (MODEL_API_KEY, SERVICE_AUTH_TOKEN, …). LOG_LEVEL is intentionally
1511
- * NOT reserved (benign, overridable for debugging). Keep in sync with workerToServiceEnv.
1538
+ * 每个**保留** env 名 → WorkerSpec 里产出它的那个结构字段(唯一写位)。`env` / `secretEnv` 逃生口拒这些名,拒因指路到这里写的字段
1539
+ * (例:`K8S_SANDBOX_IMAGE` → `runtime.execution.coords.K8S_SANDBOX_IMAGE`)。{@link RESERVED_WORKER_ENV} 由本表的键**派生**,
1540
+ * 不再是第二张要「保持同步」的手写表。
1541
+ *
1542
+ * 封堵逃生口的两类风险(council):改路由 / 关鉴权(PORT、MODEL_x、REQUIRE_PRINCIPAL、gateway)与用字面量遮蔽已投射的密钥
1543
+ * (MODEL_API_KEY、SERVICE_AUTH_TOKEN …)。LOG_LEVEL 刻意不保留(无害,调试可覆盖)。
1512
1544
  *
1513
- * ⚠️ SCOPE (0.1.30, after the R22→0.1.29 detour): this ONLY blocks keys that have a DEDICATED structured field
1514
- * or secretRef — so an operator is steered to that field instead of duplicating/shadowing it via env. It does
1515
- * NOT block the per-worker EXECUTION-SUBSTRATE config an operator legitimately sets via worker.env, which the
1516
- * live fleet relies on: `REMOTE_EXEC` (direct=k8s, canary=e2b set it HERE), `K8S_*` (SANDBOX_IMAGE/NAMESPACE/
1517
- * RUNTIME_CLASS/…), `E2B_*`, `MINIO_*`, `SSH_*`, `ADB_*`. worker.env is the OPERATOR's per-worker deployment env
1518
- * (publisher-gated, projected by the orchestrator to the worker pod) — NOT untrusted-task-controlled, so there
1519
- * is no "sandbox escape" to block here. R22 wrongly added those substrate keys treating worker.env as
1520
- * task-controlled; it would have REJECTED the live direct/canary workers on deploy. Reverted in 0.1.30. (Literal
1521
- * SECRET values pasted into env are still caught separately by containsSecretToken; substrate CREDS belong in
1522
- * secretEnv as env-NAME refs.)
1545
+ * 7.0.0 增删(M-R2 结构块):
1546
+ * · **加** `REMOTE_EXEC` / `REQUIRE_EXECUTION_LANE` + 车道表({@link EXECUTION_LANES})里**全部**车道 env 名 —— 车道有了结构位
1547
+ * `runtime.execution`,逃生口再能设车道就是两把尺。这是对 0.1.30「执行基座键不保留」的**反转**:当年不保留是因为没有结构位
1548
+ * (活舰队 direct = k8s、canary = e2b 只能在 `worker.env` 里设);存量行由一次性迁移 `migrateWorkersDomain` 搬进结构块。
1549
+ * 别的域也读的名(`MINIO_*`、`LOG_LEVEL`、`REMOTE_EXEC_HOME_DIR`)不在车道表里,照旧不保留。
1550
+ * · **加** `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` —— `runtime.store` 产出它们。
1551
+ * · **删** `MEMORY_BACKEND`(引擎从不读的死名,渲染器不再产出)与 `TIDB_*` 族(引擎的墓碑名,见之拒启;结构字段不再产出它们 ——
1552
+ * 墓碑名由引擎导出的墓碑表把关,不是本表的职责)。MYSQL_* / PG_* 两族归 `runtime.store.db` + `secretRefs.dbPasswordEnv`。
1523
1553
  */
1524
- export const RESERVED_WORKER_ENV = new Set([
1525
- "PORT", "MODEL_PROVIDER", "MODEL_ID", "MODEL_REASONING", "MODEL_CHEAP_ID", "MODEL_MAX_TOKENS",
1526
- "MODEL_CONTEXT_WINDOW", "MODEL_COST_INPUT", "MODEL_COST_OUTPUT", "MODEL_COST_CACHE_READ",
1527
- "MODEL_COST_CACHE_WRITE", "MAX_PRINCIPAL_COST_USD", "SESSION_BACKEND", "MEMORY_BACKEND",
1528
- "REQUIRE_PRINCIPAL", "RATE_LIMIT_RPM", "DEFAULT_SCENARIO", "TIDB_DATABASE", "TIDB_HOST",
1529
- "TIDB_PORT", "TIDB_USER", "GIT_API_BASEURL", "MODEL_GATEWAY_BASEURL", "ANTHROPIC_BASEURL",
1530
- "MODEL_API_KEY", "ANTHROPIC_API_KEY", "SERVICE_AUTH_TOKEN", "GIT_API_TOKEN", "TIDB_PASSWORD",
1531
- // server ≥2.0 的中立后端面(kind mysql|pg|local):MYSQL_* 是首选族(TIDB_* 为其兼容别名)、PG_* 是
1532
- // 第三族——三族同为 coords 结构化字段的遮蔽面,缺任何一族=该族键可从 worker.env 静默压过结构化配置
1533
- // (2026-07-31 交叉终扫 G1 补齐;此前只列 TIDB_* 一族)。
1534
- "MYSQL_DATABASE", "MYSQL_HOST", "MYSQL_PORT", "MYSQL_USER", "MYSQL_PASSWORD",
1535
- "PG_DATABASE", "PG_HOST", "PG_PORT", "PG_USER", "PG_PASSWORD",
1536
- ]);
1537
- // Which structured WorkerSpec field owns each reserved env-NAME → so a rejection names the RIGHT field to use
1538
- // (e.g. TIDB_DATABASE → runtime.db, MODEL_ID → model.modelId), not a vague "use the structured field".
1539
- const RESERVED_ENV_FIELD = {
1554
+ const RESERVED_ENV_FIELD = Object.freeze({
1540
1555
  PORT: "(fixed at 8090 — not configurable)", MODEL_PROVIDER: "model.provider", MODEL_ID: "model.modelId",
1541
1556
  MODEL_REASONING: "model.reasoning", MODEL_CHEAP_ID: "model.cheapId", MODEL_MAX_TOKENS: "model.maxTokens",
1542
1557
  MODEL_CONTEXT_WINDOW: "model.contextWindow", MODEL_COST_INPUT: "model.cost.input", MODEL_COST_OUTPUT: "model.cost.output",
1543
1558
  MODEL_COST_CACHE_READ: "model.cost.cacheRead", MODEL_COST_CACHE_WRITE: "model.cost.cacheWrite",
1544
- MAX_PRINCIPAL_COST_USD: "runtime.maxPrincipalCostUsd", SESSION_BACKEND: "runtime.sessionBackend",
1545
- MEMORY_BACKEND: "runtime.memoryBackend", REQUIRE_PRINCIPAL: "runtime.requirePrincipal", RATE_LIMIT_RPM: "runtime.rateLimitRpm",
1546
- DEFAULT_SCENARIO: "runtime.scenario", TIDB_DATABASE: "runtime.db", TIDB_HOST: "coords.tidbHost", TIDB_PORT: "coords.tidbPort",
1547
- TIDB_USER: "coords.tidbUser", GIT_API_BASEURL: "coords.gitApiBaseUrl", MODEL_GATEWAY_BASEURL: "model.gatewayBaseUrl",
1548
- // G1 补齐的两族与 TIDB_* 同归属(coords.tidb* 字段名是历史命名,承 mysql-协议三家;PG 同面)
1549
- MYSQL_DATABASE: "runtime.db", MYSQL_HOST: "coords.tidbHost", MYSQL_PORT: "coords.tidbPort", MYSQL_USER: "coords.tidbUser", MYSQL_PASSWORD: "secretRefs.tidbPasswordEnv",
1550
- PG_DATABASE: "runtime.db", PG_HOST: "coords.tidbHost", PG_PORT: "coords.tidbPort", PG_USER: "coords.tidbUser", PG_PASSWORD: "secretRefs.tidbPasswordEnv",
1559
+ MAX_PRINCIPAL_COST_USD: "runtime.maxPrincipalCostUsd", REQUIRE_PRINCIPAL: "runtime.requirePrincipal", RATE_LIMIT_RPM: "runtime.rateLimitRpm",
1560
+ DEFAULT_SCENARIO: "runtime.scenario", GIT_API_BASEURL: "coords.gitApiBaseUrl", MODEL_GATEWAY_BASEURL: "model.gatewayBaseUrl",
1551
1561
  ANTHROPIC_BASEURL: "model.gatewayBaseUrl", MODEL_API_KEY: "secretRefs.apiKeyEnv", ANTHROPIC_API_KEY: "secretRefs.apiKeyEnv",
1552
1562
  SERVICE_AUTH_TOKEN: "secretRefs.serviceAuthTokenEnv", GIT_API_TOKEN: "secretRefs.gitTokenEnv",
1553
- TIDB_PASSWORD: "secretRefs.tidbPasswordEnv",
1554
- };
1555
- export const WorkerSpec = z.object({
1563
+ // ── runtime.store(7.0.0)
1564
+ DB_BACKEND: "runtime.store.backend", SESSION_BACKEND: "runtime.store.session",
1565
+ MEMORY_ENGINE: "runtime.store.memory.engine", MEMORY_ENGINE_BACKEND: "runtime.store.memory.backend",
1566
+ MYSQL_HOST: "runtime.store.db.host", MYSQL_PORT: "runtime.store.db.port", MYSQL_USER: "runtime.store.db.user",
1567
+ MYSQL_DATABASE: "runtime.store.db.database", MYSQL_PASSWORD: "secretRefs.dbPasswordEnv",
1568
+ PG_HOST: "runtime.store.db.host", PG_PORT: "runtime.store.db.port", PG_USER: "runtime.store.db.user",
1569
+ PG_DATABASE: "runtime.store.db.database", PG_PASSWORD: "secretRefs.dbPasswordEnv",
1570
+ // ── runtime.execution(7.0.0):车道词两键 + 车道表里每个名(派生,不手抄)
1571
+ REMOTE_EXEC: "runtime.execution.lane",
1572
+ REQUIRE_EXECUTION_LANE: "runtime.execution.lane (the renderer emits REQUIRE_EXECUTION_LANE=true for every declared lane)",
1573
+ ...Object.fromEntries([...LANE_ENV_OWNER].map(([name, owner]) => [name, laneEnvSeat(name, owner)])),
1574
+ });
1575
+ /** 结构化 WorkerSpec 字段已经产出的 env 名 —— 通用 `env` / `secretEnv` 逃生口**不许**设它们(用专属字段)。由
1576
+ * `RESERVED_ENV_FIELD`(名 → 唯一写位)的键派生;增删理由见那张表的头注。 */
1577
+ export const RESERVED_WORKER_ENV = new Set(Object.keys(RESERVED_ENV_FIELD));
1578
+ // ── 7.0.0 M-R2:`runtime.execution` / `runtime.store` 两个结构块 ─────────────────────────────────────────────
1579
+ // 词表只有一个正本(`execution-lanes.ts` 的导出表):本包写闸(`WorkerSpec.safeParse`)、中心渲染器、引擎起服三处读同一张表。
1580
+ // 拒因都在 zod issue 里(`path` 指到要改的字段、`message` 带出路)—— schema 本身是唯一判据,不另导出拒因函数。
1581
+ /** 一个非密字面值(车道坐标 / SQL 坐标):≤2048,不像密钥(与 `env` 逃生口同一条判据 —— 密钥走 env-NAME 引用)。 */
1582
+ const nonSecretLiteral = (minLength) => z
1583
+ .string()
1584
+ .min(minLength)
1585
+ .max(2048)
1586
+ .refine((s) => !containsSecretToken(s), "looks like a literal secret — a credential goes in a secretRefs field as an env-NAME, never a value here");
1587
+ /**
1588
+ * `runtime.execution` —— 这台 worker 的执行车道(渲染:`REMOTE_EXEC=<lane>` + `REQUIRE_EXECUTION_LANE=true` + coords / secretRefs 逐键)。
1589
+ * - `lane`:{@link EXECUTION_LANE_WORDS} 之一。没有「无手」词。
1590
+ * - `coords`:该车道的**非密坐标**(服务变量名 → 字面值),键 ⊆ 表[lane] 的 `requiredEnv ∪ optionalEnv`;必需键在场且非空串
1591
+ * (引擎把空串读作未设)。可选键的空串照收(例:`K8S_RUNTIME_CLASS=""` 在引擎里是「不用 Kata」)。
1592
+ * - `secretRefs`:该车道的**密钥**(服务变量名 → 宿主 env-NAME),键 ⊆ 表[lane] 的 `requiredSecretEnv ∪ optionalSecretEnv`,必需键在场。
1593
+ * 别车道的键 ⇒ 拒并点名它属于哪条车道;本车道的密钥写进 coords / 坐标写进 secretRefs ⇒ 拒并指路另一栏。
1594
+ */
1595
+ const ExecutionBlock = z
1596
+ .object({
1597
+ lane: z.enum(EXECUTION_LANE_WORDS),
1598
+ coords: z.record(ENV_NAME, nonSecretLiteral(0)).default({}),
1599
+ secretRefs: z.record(ENV_NAME, ENV_NAME).default({}),
1600
+ })
1601
+ .superRefine((x, ctx) => {
1602
+ const row = laneRowOf(x.lane);
1603
+ const issue = (slot, key, message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path: [slot, key], message });
1604
+ const owned = (secret) => (secret ? [...row.requiredSecretEnv, ...row.optionalSecretEnv] : [...row.requiredEnv, ...row.optionalEnv]);
1605
+ const judgeKey = (slot, key) => {
1606
+ const wantSecret = slot === "secretRefs";
1607
+ const owner = LANE_ENV_OWNER.get(key);
1608
+ if (owner === undefined) {
1609
+ const allowed = owned(wantSecret);
1610
+ issue(slot, key, `${key} is not ${wantSecret ? "a secret" : "a coordinate"} of any execution lane — lane "${x.lane}" takes ${slot} ${allowed.length > 0 ? allowed.join(", ") : "(none)"}`);
1611
+ return false;
1612
+ }
1613
+ if (owner.lane !== x.lane) {
1614
+ issue(slot, key, `${key} belongs to lane "${owner.lane}", not "${x.lane}" — remove it, or set runtime.execution.lane to "${owner.lane}"`);
1615
+ return false;
1616
+ }
1617
+ if (owner.secret !== wantSecret) {
1618
+ issue(slot, key, owner.secret
1619
+ ? `${key} is a secret of lane "${x.lane}" — put it in runtime.execution.secretRefs as an env-NAME (never a value in coords)`
1620
+ : `${key} is a non-secret coordinate of lane "${x.lane}" — write its value in runtime.execution.coords`);
1621
+ return false;
1622
+ }
1623
+ return true;
1624
+ };
1625
+ for (const [key, value] of Object.entries(x.coords)) {
1626
+ if (judgeKey("coords", key) && value === "" && LANE_ENV_OWNER.get(key)?.required === true) {
1627
+ issue("coords", key, `required coordinate ${key} of lane "${x.lane}" is empty — the engine reads an empty value as unset and refuses to start`);
1628
+ }
1629
+ }
1630
+ for (const key of Object.keys(x.secretRefs))
1631
+ judgeKey("secretRefs", key);
1632
+ for (const key of row.requiredEnv)
1633
+ if (!Object.hasOwn(x.coords, key))
1634
+ issue("coords", key, `lane "${x.lane}" requires coords.${key} (the engine refuses to start without it)`);
1635
+ for (const key of row.requiredSecretEnv)
1636
+ if (!Object.hasOwn(x.secretRefs, key))
1637
+ issue("secretRefs", key, `lane "${x.lane}" requires secretRefs.${key} — the host env-NAME holding it`);
1638
+ });
1639
+ /** `MEMORY_ENGINE` 的两个词(引擎目录行的 `enumValues`;引擎还认 true/false/1/0… 两族同义词,结构位只收规范词)。 */
1640
+ const MEMORY_ENGINE_SWITCH = ["on", "off"];
1641
+ /** 店词 → 是不是 SQL 店。SQL 坐标(`runtime.store.db` + `secretRefs.dbPasswordEnv`)在场 ⇔ 是。穷举表:店词加词 ⇒ 编译红。 */
1642
+ const STORE_IS_SQL = { mysql: true, pg: true, local: false, memory: false };
1643
+ /** 记忆持久面方言 → 它要求的店(`file` 不要求)。穷举表:方言加词 ⇒ 编译红。 */
1644
+ const MEMORY_BACKEND_NEEDS_STORE = { file: undefined, pg: "pg", tidb: "mysql" };
1645
+ /**
1646
+ * `runtime.store` —— 这台 worker 的持久化(取代 6.x 散落的 `sessionBackend` / `memoryBackend` / `db` / `coords.tidb*` /
1647
+ * `secretRefs.tidbPasswordEnv`)。渲染(唯一写者):`DB_BACKEND` / `SESSION_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` 恒渲;
1648
+ * `backend` 为 mysql ⇒ `MYSQL_HOST/PORT/USER/DATABASE` + `MYSQL_PASSWORD`(← `secretRefs.dbPasswordEnv`),pg ⇒ `PG_*` 同形。
1649
+ * 跨字段四条在 WorkerSpec 层判(要看 `secretRefs` 与 `requirePrincipal`)。
1650
+ */
1651
+ const StoreBlock = z.object({
1652
+ backend: z.enum(STORE_BACKENDS),
1653
+ session: z.enum(SESSION_BACKENDS).default("auto"),
1654
+ memory: z
1655
+ .object({
1656
+ engine: z.enum(MEMORY_ENGINE_SWITCH).default("on"),
1657
+ backend: z.enum(MEMORY_ENGINE_BACKENDS).default("file"),
1658
+ })
1659
+ .default({}),
1660
+ /** SQL 服务器坐标(中立,一份;名族由 `backend` 决定)。 */
1661
+ db: z
1662
+ .object({
1663
+ host: nonSecretLiteral(1),
1664
+ port: TCP_PORT,
1665
+ user: nonSecretLiteral(1),
1666
+ database: nonSecretLiteral(1),
1667
+ })
1668
+ .optional(),
1669
+ });
1670
+ /** 7.0.0 删除的字段 → 它的新位。**迁移窗口代码**:与 `migrateWorkersDomain` 同窗删除(只为未迁移的旧形读到即响亮拒,而不是被
1671
+ * zod 静默剥掉 —— 剥掉的话,一行旧数据会以「什么都没声明」的样子过闸)。 */
1672
+ const RETIRED_WORKER_FIELDS = [
1673
+ { parent: "runtime", key: "sessionBackend", now: "runtime.store.session" },
1674
+ { parent: "runtime", key: "memoryBackend", now: "runtime.store.memory — the old field never reached the engine" },
1675
+ { parent: "runtime", key: "db", now: "runtime.store.db.database" },
1676
+ { parent: "coords", key: "tidbHost", now: "runtime.store.db.host" },
1677
+ { parent: "coords", key: "tidbPort", now: "runtime.store.db.port" },
1678
+ { parent: "coords", key: "tidbUser", now: "runtime.store.db.user" },
1679
+ { parent: "secretRefs", key: "tidbPasswordEnv", now: "secretRefs.dbPasswordEnv" },
1680
+ ];
1681
+ /** 一个对象的**自有数据属性**值(不触发访问器;不是普通对象 ⇒ undefined)。 */
1682
+ function ownDataValue(obj, key) {
1683
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj))
1684
+ return undefined;
1685
+ const d = Object.getOwnPropertyDescriptor(obj, key);
1686
+ return d !== undefined && "value" in d ? d.value : undefined;
1687
+ }
1688
+ function retiredWorkerFieldIssues(raw, ctx) {
1689
+ for (const { parent, key, now } of RETIRED_WORKER_FIELDS) {
1690
+ const container = ownDataValue(raw, parent);
1691
+ if (container !== null && typeof container === "object" && !Array.isArray(container) && Object.hasOwn(container, key)) {
1692
+ ctx.addIssue({
1693
+ code: z.ZodIssueCode.custom,
1694
+ path: [parent, key],
1695
+ message: `${parent}.${key} was removed in 7.0.0 (now ${now}) — run the one-time migration before reading this document: migrateWorkersDomain(doc) ahead of parseDomain, or \`settings-schema migrate-workers <file>\``,
1696
+ });
1697
+ }
1698
+ }
1699
+ return raw;
1700
+ }
1701
+ /** 一台 worker 的名字(唯一 → 容器名 / compose project / 缺省路由键)—— **唯一**的名字判据。中心在租约 / 用量 /
1702
+ * discovery `?worker=` 等面上校验外部传来的 worker 名时读它(不抄窄正则)。7.0.0 起单独导出:`WorkerSpec` 不再是普通
1703
+ * 对象 schema(预处理 + 跨字段判据),`WorkerSpec.shape.name` 不复存在。 */
1704
+ export const WORKER_NAME = z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces");
1705
+ const WorkerSpecObject = z.object({
1556
1706
  /** 唯一 → 容器名 / compose project. */
1557
- name: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces"),
1707
+ name: WORKER_NAME,
1558
1708
  enabled: z.boolean().default(true),
1559
1709
  /** 目标宿主机 id —— 哪个 per-host reconciler 认领它. 与 name 同样严格(精确匹配路由,松校验会静默不可达). */
1560
1710
  host: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces").default("default"),
@@ -1614,17 +1764,12 @@ export const WorkerSpec = z.object({
1614
1764
  // 归宿=roster.primaryModel/cheapModel 标记(0.6.3,service 已承接)。pre-0.7 存量 JSON 里的
1615
1765
  // `model` 键被 parse 静默剥除(zod strips unknown)→ workerSpecHash 变化=一次性重建;迁移工具输入=
1616
1766
  // 冻结的 LegacyWorkerModel(migrate.ts),必须在升级 0.7 前跑完(a2 已闭环,E3)。
1617
- /** 运行时 (非 secret). 映射到 service SESSION_BACKEND/REQUIRE_PRINCIPAL/RATE_LIMIT_RPM/… */
1767
+ /** 运行时 (非 secret). 映射到 service REQUIRE_PRINCIPAL / RATE_LIMIT_RPM / … 与两个结构块(7.0.0)。 */
1618
1768
  runtime: z.object({
1619
- // default "auto" (was "tidb" — Service AI's e2e-tag response traced the minimal-spec fatal here): the old
1620
- // default injected SESSION_BACKEND=tidb into EVERY spec that didn't set it, and the service fail-fasts on
1621
- // explicit tidb without TIDB_HOST (correct Tier-2 hardening — declared persistence must not silently
1622
- // degrade). "auto" = probe: with coords → tidb, without → memory, never fatal. A minimal `runtime:{}`
1623
- // worker now boots. No churn: previously-SAVED specs have "tidb" baked in (parseDomain persists defaults),
1624
- // so only NEW minimal specs see "auto"; explicit-"tidb" loud-fail semantics are untouched.
1625
- sessionBackend: z.enum(["tidb", "memory", "auto"]).default("auto"),
1626
- memoryBackend: z.enum(["off", "memory", "tidb"]).default("off"),
1627
- db: z.string().optional(), // TIDB_DATABASE (独立库)
1769
+ /** 7.0.0 执行车道块(见 `ExecutionBlock`)。enabled 的 worker 必填(WorkerSpec 层判);停用的可缺。 */
1770
+ execution: ExecutionBlock.optional(),
1771
+ /** 7.0.0 存储块(见 `StoreBlock`)。enabled 的 worker 必填(WorkerSpec 层判);停用的可缺。 */
1772
+ store: StoreBlock.optional(),
1628
1773
  scenario: z.string().optional(), // DEFAULT_SCENARIO
1629
1774
  requirePrincipal: z.boolean().default(false),
1630
1775
  rateLimitRpm: z.number().int().nonnegative().default(0),
@@ -1635,13 +1780,10 @@ export const WorkerSpec = z.object({
1635
1780
  * default → absent never materialises → no spec-hash churn. Hash-included (read at boot → cap change recreates). */
1636
1781
  maxPrincipalCostUsd: z.number().finite().nonnegative().optional(),
1637
1782
  }),
1638
- /** 物理坐标 (非 secret). 映射到 GIT_API_BASEURL / TIDB_HOST 等. */
1783
+ /** 物理坐标 (非 secret). 映射到 GIT_API_BASEURL。7.0.0 起 SQL 坐标在 `runtime.store.db`(`tidb*` 三键删除)。 */
1639
1784
  coords: z
1640
1785
  .object({
1641
1786
  gitApiBaseUrl: NON_SECRET_URL.optional().or(z.literal("")),
1642
- tidbHost: z.string().optional(),
1643
- tidbPort: TCP_PORT.optional(),
1644
- tidbUser: z.string().optional(),
1645
1787
  })
1646
1788
  .default({}),
1647
1789
  secretRefs: WorkerSecretRefs,
@@ -1656,7 +1798,7 @@ export const WorkerSpec = z.object({
1656
1798
  .superRefine((rec, ctx) => {
1657
1799
  for (const [k, v] of Object.entries(rec)) {
1658
1800
  if (RESERVED_WORKER_ENV.has(k))
1659
- ctx.addIssue({ code: "custom", message: `env cannot set reserved key "${k}" — set it via ${RESERVED_ENV_FIELD[k] ?? "the structured field"}` });
1801
+ ctx.addIssue({ code: "custom", message: `env cannot set reserved key "${k}" — set it via ${RESERVED_ENV_FIELD[k]}` });
1660
1802
  // 0.1.32: NO name exemption — a PUBLIC key is allowed because SECRET_LITERAL_RE now only flags PRIVATE keys
1661
1803
  // (root fix), so a literal private key / token in env (ANY name, incl `*_PUBKEY`) is still rejected.
1662
1804
  if (containsSecretToken(v))
@@ -1675,7 +1817,7 @@ export const WorkerSpec = z.object({
1675
1817
  .superRefine((rec, ctx) => {
1676
1818
  for (const k of Object.keys(rec))
1677
1819
  if (RESERVED_WORKER_ENV.has(k))
1678
- ctx.addIssue({ code: "custom", message: `secretEnv cannot set reserved key "${k}" — set it via ${RESERVED_ENV_FIELD[k] ?? "secretRefs"}` });
1820
+ ctx.addIssue({ code: "custom", message: `secretEnv cannot set reserved key "${k}" — set it via ${RESERVED_ENV_FIELD[k]}` });
1679
1821
  })
1680
1822
  .optional(),
1681
1823
  /** Opaque recreate trigger (hash-INCLUDED): bump it to force the reconciler to recreate the worker — e.g. to
@@ -1712,6 +1854,86 @@ export const WorkerSpec = z.object({
1712
1854
  signature: z.string().optional(),
1713
1855
  notes: z.string().optional(),
1714
1856
  });
1857
+ /** 跨字段判据(G1 / G2 / host∧多副本 / store 四条)。每条一个 issue,`path` 指到要改的字段。 */
1858
+ function workerSpecIssues(w, ctx) {
1859
+ const issue = (path, message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path, message });
1860
+ const { execution, store, requirePrincipal } = w.runtime;
1861
+ // G1:enabled 的 worker 必须声明车道与店(停用的不渲染,可缺)。
1862
+ if (w.enabled && execution === undefined) {
1863
+ issue(["runtime", "execution", "lane"], `an enabled worker must declare its execution lane — runtime.execution.lane: ${EXECUTION_LANE_WORDS.join(" | ")}. ` +
1864
+ `Without one it would boot with no hands and still report tasks completed; there is no "no hands" lane — disable the worker instead`);
1865
+ }
1866
+ if (w.enabled && store === undefined) {
1867
+ issue(["runtime", "store", "backend"], `an enabled worker must declare its store — runtime.store.backend: ${STORE_BACKENDS.join(" | ")}`);
1868
+ }
1869
+ if (execution !== undefined) {
1870
+ // G2:多租户姿态不许跑不分租户的车道;出路按表现算(不手写第五份词表)。
1871
+ if (requirePrincipal && laneRowOf(execution.lane).tenantSeparation === "single-user-only") {
1872
+ const outlets = EXECUTION_LANES.filter((r) => r.tenantSeparation === "supported").map((r) => r.lane);
1873
+ issue(["runtime", "execution", "lane"], `runtime.requirePrincipal=true (multi-tenant posture) with the "${execution.lane}" execution lane runs every tenant's Bash on one shared target — ` +
1874
+ `this lane does not separate tenants; set runtime.execution.lane to ${outlets.join(" / ")}, or set runtime.requirePrincipal=false for a single-user worker`);
1875
+ }
1876
+ // host ∧ 多副本:副本数有两个旋钮(replicas / autoscaling.maxReplicas,二者互斥),一条规则看「最多几个副本」。
1877
+ if (execution.lane === "host") {
1878
+ const [path, replicas] = w.autoscaling !== undefined ? [["autoscaling", "maxReplicas"], w.autoscaling.maxReplicas] : [["replicas"], w.replicas ?? 1];
1879
+ if (replicas > 1) {
1880
+ issue(path, `the "host" execution lane runs tools on each replica's own filesystem — ${replicas} replicas would hold ${replicas} different workspaces behind one route, ` +
1881
+ `so a session's next turn can land where its files are not. Keep a single replica, or choose a lane other than "host"`);
1882
+ }
1883
+ }
1884
+ }
1885
+ // store 四条:① SQL 店 ⇒ db + dbPasswordEnv 在场;② 非 SQL 店 ⇒ 两者都不在场(①② 是一条规则:SQL 坐标 ⇔ SQL 店);
1886
+ // ③ memory.backend 与店相配;④ 多租户 ⇒ SQL 店。
1887
+ const sql = store !== undefined && STORE_IS_SQL[store.backend];
1888
+ // 「店真开出来了没有」(引擎 server 79de35f8 读数):SQL 池只在 SQL 店 ∧ session ≠ memory 时打开(`boot/stores.ts:85` wantDb;
1889
+ // `config.ts:1781` needsTidb —— 本包 SQL 店恒带 `db`,所以 auto 必开);任何店只在 backend ≠ memory 时存在(`plugins/store-backend.ts:927`)。
1890
+ // 读开出来的店的起服不变量,写闸前置同判(同表同词):
1891
+ const sqlPoolOpens = sql && store !== undefined && store.session !== "memory";
1892
+ // device 车道 ⇒ SQL 池必须开(引擎 `config-invariants.ts:318` `device-durable-store`:设备注册簿 / session↔设备绑定 / 审计都在 SQL 里)。
1893
+ // 与 G2 同族(gpt-6 腿 F2)。同族的 `device-durable-approval` 读的是 `DURABLE_APPROVAL`(逃生口里的 env + 姿态缺省),不在结构字段上,
1894
+ // 写闸判不了 —— 那一条只在起服拒。
1895
+ if (execution?.lane === "device" && store !== undefined && !sqlPoolOpens) {
1896
+ if (!sql) {
1897
+ issue(["runtime", "store", "backend"], `the "device" execution lane keeps its device registry, session↔device binding and audit trail in SQL — store.backend "${store.backend}" has none ` +
1898
+ `(the engine refuses to start: device-durable-store); set store.backend to mysql | pg`);
1899
+ }
1900
+ else {
1901
+ issue(["runtime", "store", "session"], `the "device" execution lane needs the SQL store actually built — store.session "memory" builds no SQL coordinates ` +
1902
+ `(the engine refuses to start: device-durable-store); set store.session to auto | mysql`);
1903
+ }
1904
+ }
1905
+ if (store !== undefined) {
1906
+ // ⑧ session mysql 要一个 durable 会话店,memory 后端一只都不建(`plugins/session-store.ts:17`「a DB session backend requires a store backend」)。
1907
+ if (store.session === "mysql" && store.backend === "memory") {
1908
+ issue(["runtime", "store", "session"], `store.session "mysql" asks for a durable session store, but store.backend "memory" builds none (the engine refuses to start: a DB session backend requires a store backend); ` +
1909
+ `set store.session to auto | memory, or store.backend to mysql | pg | local`);
1910
+ }
1911
+ // ⑨ 记忆引擎开 ∧ 方言 pg / tidb ⇒ 引擎绑 SQL 池(`boot/stores.ts:209` / `:232` / `:300`);session memory 时池不开 ⇒ 起服拒。
1912
+ if (store.memory.engine === "on" && MEMORY_BACKEND_NEEDS_STORE[store.memory.backend] !== undefined && sql && !sqlPoolOpens) {
1913
+ issue(["runtime", "store", "session"], `store.memory.backend "${store.memory.backend}" binds the SQL pool, but store.session "memory" never opens it (the engine refuses to start half-configured); ` +
1914
+ `set store.session to auto | mysql, or store.memory.backend to file, or store.memory.engine to off`);
1915
+ }
1916
+ }
1917
+ if (store !== undefined) {
1918
+ if (sql && store.db === undefined)
1919
+ issue(["runtime", "store", "db"], `store.backend "${store.backend}" needs runtime.store.db {host, port, user, database}`);
1920
+ if (!sql && store.db !== undefined)
1921
+ issue(["runtime", "store", "db"], `store.backend "${store.backend}" uses no SQL server — remove runtime.store.db, or set store.backend to mysql | pg`);
1922
+ const needs = MEMORY_BACKEND_NEEDS_STORE[store.memory.backend];
1923
+ if (needs !== undefined && store.backend !== needs) {
1924
+ issue(["runtime", "store", "memory", "backend"], `store.memory.backend "${store.memory.backend}" keeps memory in the ${needs} store — it needs store.backend "${needs}" (got "${store.backend}")`);
1925
+ }
1926
+ if (requirePrincipal && !sql) {
1927
+ issue(["runtime", "store", "backend"], `runtime.requirePrincipal=true (multi-tenant posture) needs a SQL store — store.backend "${store.backend}" has no tenant isolation; set store.backend to mysql | pg`);
1928
+ }
1929
+ }
1930
+ if (sql && w.secretRefs.dbPasswordEnv === undefined)
1931
+ issue(["secretRefs", "dbPasswordEnv"], `a SQL store (runtime.store.backend mysql | pg) needs secretRefs.dbPasswordEnv — the host env-NAME holding the database password`);
1932
+ if (!sql && w.secretRefs.dbPasswordEnv !== undefined)
1933
+ issue(["secretRefs", "dbPasswordEnv"], `secretRefs.dbPasswordEnv is set but there is no SQL store (runtime.store.backend mysql | pg) — remove it`);
1934
+ }
1935
+ /** 一台 worker 的声明。写闸 = `WorkerSpec.safeParse`(7.0.0 删除项读到即拒 + 两个结构块 + 跨字段判据)。 */
1936
+ export const WorkerSpec = z.preprocess(retiredWorkerFieldIssues, WorkerSpecObject.superRefine(workerSpecIssues));
1715
1937
  /** Effective route key for a worker = explicit routePrefix, else its name. */
1716
1938
  export function workerRouteKey(w) {
1717
1939
  return w.routePrefix ?? w.name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "6.3.0",
3
+ "version": "7.0.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -18,6 +18,9 @@
18
18
  ],
19
19
  "main": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts",
21
+ "bin": {
22
+ "settings-schema": "./bin/settings-schema.mjs"
23
+ },
21
24
  "exports": {
22
25
  ".": {
23
26
  "types": "./dist/index.d.ts",
@@ -43,6 +46,10 @@
43
46
  "types": "./dist/secret-refs.d.ts",
44
47
  "default": "./dist/secret-refs.js"
45
48
  },
49
+ "./execution-lanes": {
50
+ "types": "./dist/execution-lanes.d.ts",
51
+ "default": "./dist/execution-lanes.js"
52
+ },
46
53
  "./resolve-roster": {
47
54
  "types": "./dist/resolve-roster.d.ts",
48
55
  "default": "./dist/resolve-roster.js"
@@ -107,6 +114,7 @@
107
114
  },
108
115
  "files": [
109
116
  "dist",
117
+ "bin",
110
118
  "conformance",
111
119
  "README.md",
112
120
  "CHANGELOG.md"