@sema-agent/settings-schema 10.1.1 → 11.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 (235) hide show
  1. package/CHANGELOG.md +414 -0
  2. package/README.md +141 -65
  3. package/bin/settings-schema.mjs +13 -6
  4. package/conformance/VERSION +1 -1
  5. package/conformance/baseline.json +3 -1
  6. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
  7. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
  8. package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
  9. package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
  10. package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
  11. package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
  12. package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
  13. package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
  14. package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
  15. package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
  16. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
  17. package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
  18. package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
  19. package/conformance/cases/c13-models-field-null/expected.json +3 -1
  20. package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
  21. package/conformance/cases/c15-models-field-number/expected.json +3 -1
  22. package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
  23. package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
  24. package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
  25. package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
  26. package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
  27. package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
  28. package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
  29. package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
  30. package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
  31. package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
  32. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
  33. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
  34. package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
  35. package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
  36. package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
  37. package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
  38. package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
  39. package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
  40. package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
  41. package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
  42. package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
  43. package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
  44. package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
  45. package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
  46. package/conformance/cases/c37-skills-field-null/expected.json +6 -2
  47. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
  48. package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
  49. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
  50. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
  51. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
  52. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
  53. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
  54. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
  55. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
  56. package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
  57. package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
  58. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
  59. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
  60. package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
  61. package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
  62. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
  63. package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
  64. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
  65. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
  66. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
  67. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
  68. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
  69. package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
  70. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
  71. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
  72. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
  73. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
  74. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
  75. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
  76. package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
  77. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
  78. package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
  79. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
  80. package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
  81. package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
  82. package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
  83. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
  84. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
  85. package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
  86. package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
  87. package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
  88. package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
  89. package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
  90. package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
  91. package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
  92. package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
  93. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
  94. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
  95. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
  96. package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
  97. package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
  98. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
  99. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
  100. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
  101. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
  102. package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
  103. package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
  104. package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
  105. package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
  106. package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
  107. package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
  108. package/dist/api/auth-bridge.d.ts +51 -181
  109. package/dist/api/auth-bridge.js +58 -33
  110. package/dist/api/auth.d.ts +37 -146
  111. package/dist/api/auth.js +16 -26
  112. package/dist/api/feedback.d.ts +87 -0
  113. package/dist/api/feedback.js +66 -0
  114. package/dist/api/pull-token.d.ts +32 -0
  115. package/dist/api/pull-token.js +88 -0
  116. package/dist/api/scopes.d.ts +82 -174
  117. package/dist/api/scopes.js +23 -18
  118. package/dist/bundle.d.ts +15 -7
  119. package/dist/bundle.js +28 -27
  120. package/dist/cli.d.ts +3 -1
  121. package/dist/cli.js +132 -81
  122. package/dist/config-d-file.d.ts +14 -0
  123. package/dist/config-d-file.js +67 -0
  124. package/dist/config-fns.d.ts +87 -186
  125. package/dist/config-fns.js +207 -262
  126. package/dist/conformance/contract-adapter.d.ts +9 -2
  127. package/dist/conformance/contract-adapter.js +15 -7
  128. package/dist/conformance/corpus.d.ts +3 -0
  129. package/dist/conformance/corpus.js +5 -0
  130. package/dist/conformance/derive.d.ts +3 -0
  131. package/dist/conformance/index.d.ts +2 -2
  132. package/dist/conformance/index.js +2 -2
  133. package/dist/conformance/run.js +67 -22
  134. package/dist/conformance/types.d.ts +31 -15
  135. package/dist/conformance/types.js +5 -0
  136. package/dist/conformance/warnings.d.ts +1 -1
  137. package/dist/conformance/warnings.js +1 -4
  138. package/dist/cross-domain.d.ts +66 -22
  139. package/dist/cross-domain.js +164 -93
  140. package/dist/cross-session.d.ts +29 -11
  141. package/dist/default-model.d.ts +43 -0
  142. package/dist/default-model.js +34 -0
  143. package/dist/execution-lane-index.d.ts +4 -8
  144. package/dist/execution-lane-index.js +4 -8
  145. package/dist/execution-lanes.d.ts +31 -8
  146. package/dist/execution-lanes.js +23 -5
  147. package/dist/file-edit.js +5 -2
  148. package/dist/file-store.d.ts +7 -6
  149. package/dist/file-store.js +37 -50
  150. package/dist/fleet.d.ts +364 -762
  151. package/dist/fleet.js +277 -224
  152. package/dist/hooks.d.ts +1151 -4737
  153. package/dist/hooks.js +59 -15
  154. package/dist/index.d.ts +10 -4
  155. package/dist/index.js +13 -7
  156. package/dist/integer.d.ts +11 -0
  157. package/dist/integer.js +11 -0
  158. package/dist/issue-path.d.ts +10 -0
  159. package/dist/issue-path.js +10 -0
  160. package/dist/loopback.d.ts +26 -0
  161. package/dist/loopback.js +54 -0
  162. package/dist/migration-receipt.d.ts +42 -0
  163. package/dist/migration-receipt.js +12 -0
  164. package/dist/one-time-migrations.d.ts +56 -0
  165. package/dist/one-time-migrations.js +819 -0
  166. package/dist/prompts-digest.d.ts +1 -1
  167. package/dist/prompts-digest.js +3 -2
  168. package/dist/reader.d.ts +2 -2
  169. package/dist/remote-exec.d.ts +45 -187
  170. package/dist/remote-exec.js +13 -7
  171. package/dist/resolve-roster.d.ts +18 -3
  172. package/dist/resolve-roster.js +28 -25
  173. package/dist/retired-envs.d.ts +24 -0
  174. package/dist/retired-envs.js +114 -0
  175. package/dist/retired-form.d.ts +31 -0
  176. package/dist/retired-form.js +28 -0
  177. package/dist/safety-merge-spec.d.ts +131 -191
  178. package/dist/safety-merge-spec.js +18 -14
  179. package/dist/scheduler-lifetime.d.ts +5 -0
  180. package/dist/scheduler-lifetime.js +7 -0
  181. package/dist/scheduler-store-migration.d.ts +14 -0
  182. package/dist/scheduler-store-migration.js +14 -0
  183. package/dist/scheduler-store-node.d.ts +24 -9
  184. package/dist/scheduler-store-node.js +25 -15
  185. package/dist/scheduler-store.d.ts +47 -14
  186. package/dist/scheduler-store.js +32 -61
  187. package/dist/secret-refs.d.ts +3 -3
  188. package/dist/secret-refs.js +1 -1
  189. package/dist/secret-scan.d.ts +14 -24
  190. package/dist/secret-scan.js +45 -23
  191. package/dist/sha256-core.d.ts +16 -0
  192. package/dist/sha256-core.js +138 -0
  193. package/dist/sha256.d.ts +6 -1
  194. package/dist/sha256.js +8 -98
  195. package/dist/skills-manifest.js +1 -1
  196. package/dist/types.d.ts +1501 -13536
  197. package/dist/types.js +494 -438
  198. package/dist/url-shape.d.ts +34 -0
  199. package/dist/url-shape.js +61 -0
  200. package/dist/web-admin-credential.d.ts +96 -0
  201. package/dist/{center-credential.js → web-admin-credential.js} +46 -43
  202. package/dist/web-admin-env.d.ts +33 -0
  203. package/dist/web-admin-env.js +33 -0
  204. package/dist/worker-row-verdict.d.ts +16 -0
  205. package/dist/worker-row-verdict.js +31 -0
  206. package/dist/workers-rows.d.ts +34 -0
  207. package/dist/workers-rows.js +42 -0
  208. package/dist/zod-unwrap.d.ts +11 -0
  209. package/dist/zod-unwrap.js +25 -0
  210. package/package.json +17 -9
  211. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
  212. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
  213. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
  214. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
  215. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
  216. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
  217. package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
  218. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
  219. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
  220. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
  221. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
  222. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
  223. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
  224. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
  225. package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
  226. package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
  227. package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
  228. package/dist/center-credential.d.ts +0 -116
  229. package/dist/issue-text.d.ts +0 -10
  230. package/dist/issue-text.js +0 -8
  231. package/dist/migrate-workers.d.ts +0 -71
  232. package/dist/migrate-workers.js +0 -351
  233. package/dist/migrate.d.ts +0 -255
  234. package/dist/migrate.js +0 -542
  235. /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
package/README.md CHANGED
@@ -22,12 +22,12 @@
22
22
  > deprecate 标指向新名。
23
23
 
24
24
  Engine-agnostic, zero-dependency contract layer for the sema stack — **one source of truth** for the
25
- config domain across the center (the config-center product — today `sema-web-admin` — the fleet admin), the
25
+ config domain across web-admin (the fleet admin product, today `sema-web-admin`), the
26
26
  server (the workers), and the client (desktop/CLI) seams. No two copies of the schema.
27
27
 
28
- > 命名说明:config-center 这个**产品**曾叫 `sema-registry`,又改成 `sema-web`,现在是 `sema-web-admin`
28
+ > 命名说明:web-admin 这个**产品**曾叫 `sema-registry`,又改成 `sema-web`,现在是 `sema-web-admin`
29
29
  > (clay 令 2026-08-26 仓改名;后续仍可能再改)。本**包**名与产品名解耦,**不随产品改名而动**(1.0.0 这次
30
- > 改名是本包自身的正名,与产品名无关)—— 下文凡提「center / config-center 产品」即指该产品的当期实现。
30
+ > 改名是本包自身的正名,与产品名无关)—— 下文凡提「web-admin」即指该产品的当期实现。
31
31
 
32
32
  - **Source of truth**: `github.com/sema-agent/sema-settings-schema`, branch `main` (private org repo; renamed from `sema-registry-core` on 2026-08-26, old URLs redirect).
33
33
  仓名本步不动(只改包名)。
@@ -35,18 +35,49 @@ server (the workers), and the client (desktop/CLI) seams. No two copies of the s
35
35
  (≤`0.19.0` 发在旧名 `@sema-agent/registry-core` 下,冻结)。
36
36
 
37
37
  The package is **pure**: zero `next`/`react`/server imports, zero DB drivers. Its only runtime dependency is
38
- `zod` (a `dependency` pinned `^3.24.0`, also declared as a `peerDependency` with the same range). `npm ls --prod`
38
+ `zod` (a `dependency` pinned `^4.4.0`, also declared as a `peerDependency` with the same range). `npm ls --prod`
39
39
  therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
40
40
 
41
- > **zod instances today: two, not one** (corrected in 6.2.0 — this section used to promise a single shared instance).
42
- > The range was chosen so every host would dedup to one zod, but the hosts have since moved apart: `sema-server` and the
43
- > CLI depend on zod 4 (`^4.x`), the config-center product (`sema-web-admin`) is still on zod 3. In a zod-4 host the
44
- > package manager installs this package's own zod 3 NESTED under it — the server tree today has zod 4 at the top level
45
- > and zod 3 at `node_modules/@sema-agent/settings-schema/node_modules/zod`. There, this package's schemas and the host's
46
- > are two zod instances: cross the seam with plain data (parse with the schemas / functions this package exports), and
47
- > never mix schema objects of the two instances (`instanceof ZodError`, composing a host schema with one of ours). Only
48
- > a zod-3 host (today the config-center product) still dedups to one instance. Moving this package to zod 4 is a major
49
- > of its own (S-777); 6.2.0 does not touch the dependency or peer range.
41
+ > **One zod instance in a zod-4 host** (11.0.0). The range is `^4.4.0` for both the dependency and the peer, so a host on
42
+ > zod 4 (`sema-server`, the CLI, the client core, the SDK) dedups this package's zod with its own: one instance, and schema objects
43
+ > may cross the seam. A host still on zod 3 (until it upgrades — today the web-admin product, `sema-web-admin`) does not
44
+ > satisfy the peer range, and its package manager installs this package's zod 4 NESTED under it: two instances there, so
45
+ > cross the seam with plain data only (parse with the schemas / functions this package exports; never `instanceof ZodError`
46
+ > across it, never compose a host schema with one of ours). Before 11.0.0 the picture was the mirror image (this package on
47
+ > zod 3, nested inside every zod-4 host). The move to zod 4 did not change what any schema accepts or produces —
48
+ > the conformance corpus reads byte-identically on both — only the wording of validation errors (see CHANGELOG 11.0.0).
49
+
50
+ ## 升级指引:10.x → 11.0.0 · 持久面:一次性迁移(`./one-time-migrations`)
51
+
52
+ 11.0.0 把读路径上为旧数据留的兼容臂全部收掉:旧形只有一种去处 —— **一次性迁移**把它改写成新形(你写回),读写两面遇旧形**响亮拒**,
53
+ 拒句点名迁移 id。没迁的存量会被拒,所以**先迁再读**。
54
+
55
+ - **谁要跑**:持有存量配置的一方 —— 管理面起服时对每个 scope 的草稿、每个 principal 层(个人 models / mcp / skills / plugins)、每份冻结的
56
+ 发布快照与回滚史各跑一次;引擎对自己的 last-known-good 文件跑 `migrateEffectiveSnapshot`;本地 `config.d` 目录跑命令行。
57
+ - **怎么调**(纯函数,不读环境、不读墙钟、不改入参、不抬任何版本号 —— 版本号归你):
58
+
59
+ ```ts
60
+ import { migrateStoredDocuments, migrateEffectiveSnapshot, ONE_TIME_MIGRATIONS } from "@sema-agent/settings-schema/one-time-migrations";
61
+
62
+ // 一个 scope 的全部存储文档(域名 → 文档);缺席的域 = 从未设过。
63
+ const { docs, receipts } = migrateStoredDocuments(scopeDocs, { coverage: "scope" });
64
+ // principal 层 / 回滚史的单域行 / 任意子集:缺席的域 = 没给、不动。
65
+ migrateStoredDocuments({ models: personalModels, mcp: personalMcp }, { coverage: "partial" });
66
+ // 冻结的 effective(发布快照、引擎的 LKG):version / updatedAt 与其余信封键原样。
67
+ const { effective } = migrateEffectiveSnapshot(frozen);
68
+ ```
69
+
70
+ `docs` 里没改的文档是**同一引用**、被移除的(退役的 `runtime` 域)不在、迁出的新文档(`governance` / `limits`)在 —— 据此写回 / 删除。
71
+ `receipts` 逐条说「对哪个坐标做了什么」(`moved` / `rewritten` / `dropped-shadowed` / `dropped-inert` / `refused`;不带值)。
72
+ `refused` 的坐标原样留着,读侧照旧拒:要人改。再跑一次 = 零改动。
73
+ - **目录形**:`npx @sema-agent/settings-schema migrate <config.d> [--check]`(同一只函数;目录当一个 scope;有 refused 一字不写、退出 1;
74
+ `--check` 有改动退出 3)。
75
+ - **迁什么**(七条,`ONE_TIME_MIGRATIONS` 每行写着改写规则、删法、删后由谁接住):退役 `runtime` 域(治理三闸 → `governance`、五个限额键 → `limits`)·
76
+ hosts 超界 · 调度店 durable 键(读时改写,下一个写者落盘)· workers 6.x 行 · 模型钥匙双设 · `atModelAllowlist` → `atMention` · URL fragment。
77
+ - **什么时候删**:每条的窗 = 11.0.0 发布日 + 30 天,窗关之后的那个 major 整族删(删法与删后的响亮臂见登记表那一行);本包的到期门到点即红。
78
+ 删了之后还带旧形的存量,由墓碑照旧拒 —— 墓碑是拒,不会再被读成任何值。
79
+ - **回滚史里单独一行 `runtime`**:迁不了(没有同一 scope 的 governance / limits 可判),回滚到它会被墓碑拒 —— 不可恢复。
80
+ - **不要混跑**:迁移期间别让 10.x 与 11.x 的管理面副本同时在线(10.x 副本会照旧写出旧形,11.x 副本读它会拒)。
50
81
 
51
82
  ## 升级指引:9.0.0 → 10.0.0(BREAKING:写入校验认串中任意位置的私钥 PEM 头;读侧同套 schema ⇒ 存量私钥串的条目被丢 / 域 throw;导出面零变化)
52
83
 
@@ -58,7 +89,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
58
89
  ## 升级指引:8.3.1 → 9.0.0(BREAKING:两件,硬 breaking —— 删改直接做,零别名)
59
90
 
60
91
  **① 能力读数改三态读器**:`readCenterCapabilities(body)` 删,换 `readCapabilityReading(reply): CapabilityReading`(类型 `CapabilityReading` 同入)。输入改成心跳 **2xx 的整只答复体**(键查找进读器),
61
- 答三态:`{ kind: "declared"; capabilities }`(`capabilities` 键在场且过 schema)· `{ kind: "undeclared" }`(体是对象、键缺席 = 老中心)·
92
+ 答三态:`{ kind: "declared"; capabilities }`(`capabilities` 键在场且过 schema)· `{ kind: "undeclared" }`(体是对象、键缺席 = 老 web-admin)·
62
93
  `{ kind: "unreadable"; why: "not-an-object" | "invalid-capabilities" }`(体不是对象 / 键在场但 schema 拒)。**只有 `declared` / `undeclared` 是读数;`unreadable` 不是读数 —— 引擎保留上一次状态与钟。**
63
94
 
64
95
  - **谁需要**:引擎侧的租约客户端(server)—— 围栏这条轴上「形坏」不许等于「缺席」:以前 `capabilities` 键在场但形坏读成 `undefined` = 「没宣告」,引擎据此撤围栏。
@@ -114,11 +145,11 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
114
145
  - `EngineSignal`(+ `EngineDraining`)与 `readEngineSignal(health)` —— 引擎自铸信号的**闭形**(`ready` 必填,`draining?` / `restart?` / `generation?` / `instanceId?` /
115
146
  `leaseUntil?` / `configTargetVersion?` / `configAppliedVersion?` 可选);与既有读器走同一只受保护读口。⚠️ 8.2.0 订正:今天的心跳与 `/health` 都不整只
116
147
  承载它(心跳恒带布尔 `draining`、`/health` 就绪时不带 `ready`、世代账两键是数),`readEngineSignal` 对今天的心跳体恒 `undefined` —— 见 8.2.0 升级指引(9.0.0 起这两键在 `EngineSignal` 里就是数,不再是读不出的原因)。
117
- `undefined` = 没有这只信号,**不是就绪**;布尔 `draining` 的旧方言体读不出它(两个就绪谓词的输入仍是布尔方言,下一个 major 收)。
148
+ `undefined` = 没有这只信号,**不是就绪**;布尔 `draining` 的旧方言体读不出它(两个就绪谓词当时的输入仍是布尔方言;11.0.0 起它们只收 `EngineSignal`,信号整只在 `engineSignal` 键下)。
118
149
  - 代际 —— `generation` 是非负整数的**规范十进制串**(`"7"`,不是 `"07"`、不是数字型),判别器 `isCanonicalGeneration`、数值比较 `compareGeneration`;三处 wire 形共用这一条判据。
119
150
  - 服务租约 API 型 —— `ServingLeaseRequest`、`ServingLeaseResponse`(`granted { ttlMs }` / `denied { reason: superseded | retired }` / `unknown` 三词闭集,
120
151
  **`unknown` 不是 `denied` 的一种**,`denied` 只凭正面证据;读器按 `verdict` 键判,`denied` 的原因词读不出时仍是 `denied`,原因读成 `unknown_reason`)、`SERVING_LEASE_TTL_DEFAULT_MS` / `SERVING_LEASE_TTL_FLOOR_MS` / `SERVING_LEASE_DRIFT_MARGIN_MS`、读器 `readServingLeaseRequest` / `readServingLeaseResponse`;
121
- 中心能力位 `CenterCapabilities { servingLease?: true }` + `readCenterCapabilities`(9.0.0 起删,由 `readCapabilityReading` 取代,见 9.0.0 升级指引)—— 租约客户端只在位在场时启用,缺席 = 中心不支持租约(不续租、不围栏)。
152
+ web-admin 能力位 `CenterCapabilities { servingLease?: true }` + `readCenterCapabilities`(9.0.0 起删,由 `readCapabilityReading` 取代,见 9.0.0 升级指引)—— 租约客户端只在位在场时启用,缺席 = web-admin 不支持租约(不续租、不围栏)。
122
153
  - 排空口型 —— `DrainRequest`、`DrainReceipt`(`state: draining | already_draining`)、读器 `readDrainRequest` / `readDrainReceipt`。
123
154
  - `FLEET_GENERATION_ENV` —— 配置渲染器与引擎共用的 generation env 名常量(实例身份 `instanceId` 由引擎每进程自铸,没有 env 名);它同时进了 `RESERVED_WORKER_ENV`,`worker.env` / `secretEnv` 设不了它。
124
155
  - `clampServingLeaseTtlMs(n)` —— 配置的租约 TTL 的唯一夹取点(读不出 ⇒ 缺省 600 000;低于地板 ⇒ 60 000;无上限)。
@@ -140,8 +171,9 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
140
171
 
141
172
  ## What's in it
142
173
 
143
- - **The zod contract** (`types.ts`): `DOMAIN_SCHEMAS` (17 domains — 0.10.0 adds `projects`, the project
144
- identity ledger; 0.10.18 adds `prompts`; 0.19.0 adds `limits`, the cost/rate-limit publication slot),
174
+ - **The zod contract** (`types.ts`): `DOMAIN_SCHEMAS` (16 domains — 0.10.0 adds `projects`, the project
175
+ identity ledger; 0.10.18 adds `prompts`; 0.19.0 adds `limits`, the cost/rate-limit publication slot; 11.0.0 retires
176
+ `runtime` — its tombstone is `RETIRED_DOMAINS`, see "One-time migrations" below),
145
177
  `EffectiveConfig`, `EffectiveWire`,
146
178
  `ENV_NAME`/`K8S_QUANTITY` boundaries, every domain's entry/config type.
147
179
  - **Pure config fns** (`config-fns.ts`): `buildEffective` (deterministic — `updatedAt` is a required param),
@@ -150,24 +182,28 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
150
182
  shapes (`User`, `PublishedSnapshot`, …).
151
183
  - **Roster resolution** (`resolve-roster.ts`): `resolveEffectiveForWorker` → the per-worker wire payload.
152
184
  - **Ref integrity** (`cross-domain.ts`): `refIntegrityIssues` + `createSiblingResolver` (the pure half — fleet
153
- auto-placement stays in the config-center 产品). The ref-checked domains are a RUNTIME closed set —
185
+ auto-placement stays in the web-admin 产品). The ref-checked domains are a RUNTIME closed set —
154
186
  `REF_CHECKED_DOMAINS` + `isRefCheckedDomain` (`RefCheckedDomain` is derived from the value) — so a consumer
155
187
  asks the contract instead of hand-copying the list; every `RefIssue.path` starts with the domain key.
156
188
  - **Hashing** (`hash.ts`): `stableHash` (FNV-1a 64) for ETags / version detection.
157
189
  - **Skills manifest** (`skills-manifest.ts`): content-addressing for the lazy-pull skill bodies.
158
190
  - **Secret inventory** (`secret-refs.ts`): `collectEnvRefs` — every referenced env-NAME.
159
- - **The narrow reader** (`reader.ts`): `ConfigReader` (3 methods) — the config-center 产品's fat `ConfigStore`
191
+ - **The narrow reader** (`reader.ts`): `ConfigReader` (3 methods) — the web-admin 产品's fat `ConfigStore`
160
192
  `extends` it; a local file source / test fixture implements it without stubbing ~32 methods.
161
193
  - **`remoteExec` contract** (`remote-exec.ts`): the per-machine `config.d/remote-exec.json` execution-substrate
162
194
  schema (`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`), discriminated on `provider`; every secret an
163
195
  env-NAME ref. Every `provider` word is an execution-lane word (`EXECUTION_LANE_WORDS`, pinned at runtime and at
164
196
  compile time). **7.0.0** deleted the `remote-docker` variant: no engine read point ever ran it; a remote docker
165
- daemon is the `local-docker` lane pointed at it (`DOCKER_HOST`).
197
+ daemon is the `local-docker` lane pointed at it (`DOCKER_HOST`). **11.0.0**: retired lane VALUES have a tombstone,
198
+ `RETIRED_LANE_WORDS` (`[old, to, how]`, permanent, one hop); a worker spec's `runtime.execution.lane` or this file's
199
+ `provider` naming one is refused with the way out (`retiredLaneWordRefusal(word)` is the one sentence; the issue code
200
+ stays `invalid_value` / `invalid_union`).
166
201
  - **WorkerSpec execution lane + store (7.0.0, M-R2)** (`execution-lanes.ts`, also subpath `./execution-lanes`, + `types.ts`): the lane / store
167
202
  vocabulary has ONE source — the exported data `EXECUTION_LANE_WORDS`, `EXECUTION_LANES` (per lane:
168
- `tenantSeparation` + the env names it reads, in four columns `requiredEnv` / `requiredSecretEnv` / `optionalEnv` /
203
+ `tenantSeparation` — and, on a `supported` lane only, `tenantSeparationBasis`: `isolation` | `principal-binding`, a
204
+ discriminated union (11.0.0), so a rule reads WHY a lane separates tenants instead of naming lanes — + the env names it reads, in four columns `requiredEnv` / `requiredSecretEnv` / `optionalEnv` /
169
205
  `optionalSecretEnv`, copied from the engine with per-row coordinates), `STORE_BACKENDS`, `SESSION_BACKENDS`,
170
- `MEMORY_ENGINE_BACKENDS`. The center's write gate (this package's `WorkerSpec.safeParse`), the center's env
206
+ `MEMORY_ENGINE_BACKENDS`. Web-admin's write gate (this package's `WorkerSpec.safeParse`), web-admin's env
171
207
  renderer and the engine's boot checks all read that one table, so they cannot drift. A worker declares
172
208
  `runtime.execution = { lane, coords, secretRefs }` (non-secret coordinates as literals, secrets as host env-NAMEs,
173
209
  keys ⊆ that lane's columns, required ones present and non-empty) and `runtime.store = { backend, session, memory,
@@ -244,40 +280,68 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
244
280
  and the pure client helpers `normalizeUserCode` / `nextPollInterval`. Shipped M1 shapes frozen.
245
281
  OpenAPI mirror: `docs/openapi-auth.yaml` (auth face only).
246
282
  - **Auth bridge** (`api/auth-bridge.ts`, subpath `./api/auth-bridge`): the 鉴权桥 contract —
247
- how a worker verifies a registry-minted user JWT: RS256-only, JWKS from `/api/auth/sso/jwks` with
248
- kid-miss forced refresh + ≥60s throttle, exp/nbf clock skew (60s), 24h offline grace then fail-closed;
283
+ how a worker verifies a web-admin-minted user JWT: RS256-only, JWKS from `/api/auth/sso/jwks` with
284
+ kid-miss forced refresh + ≥60s throttle, exp/nbf/iat clock skew (60s), 24h offline grace then fail-closed;
249
285
  `VerifierConfig` / `VerifiedIdentity` / `AuthBridgeErrorCode` + pure decision fns
250
286
  (`shouldForceJwksRefresh`, `jwksWithinOfflineGrace`, `classifyTimeClaims`, `toVerifiedIdentity`).
251
287
  The normative rules R1–R10 in its header are the service verifier's implementation spec.
252
- `VerifiedIdentity` carries an optional `scope` (the token's active tenant scope — see `api/scopes`).
253
- - **Center credential file** (`center-credential.ts`, main barrel; S-698): the file behind
254
- `SEMA_REGISTRY_TOKEN_FILE` is one of two KINDS — a bare line (one static token — the same kind of file as today:
288
+ `VerifiedIdentity` carries an optional `scope` (the token's active tenant scope — see `api/scopes`) and an optional `iat`
289
+ (issued-at, integer seconds, verbatim — absent claim ⇒ no key; not a non-negative integer ⇒ the token is refused).
290
+ - **web-admin credential file + connection env names** (`web-admin-credential.ts`, main barrel; S-698): the file behind
291
+ `SEMA_WEB_ADMIN_TOKEN_FILE` is one of two KINDS — a bare line (one static token — the same kind of file as today:
255
292
  its first line, printable ASCII with no control / non-ASCII characters; user JWT vs worker pull token stays the reader's `looksLikeJwt` call) or a
256
- `CenterCredentialEnvelope` (strict JSON: `kind: "user-session"` = `CENTER_CREDENTIAL_ENVELOPE_KIND`, a JWT
293
+ `WebAdminCredentialEnvelope` (strict JSON: `kind: "user-session"` = `WEB_ADMIN_CREDENTIAL_ENVELOPE_KIND`, a JWT
257
294
  `accessToken` and a `refreshToken` under the same token law as the bare line, RFC 3339
258
- `expiresAt` with seconds and a `Z` / `±hh:mm` offset in range, `issuer` = the issuing center's base URL judged
295
+ `expiresAt` with seconds and a `Z` / `±hh:mm` offset in range, `issuer` = the issuing web-admin's base URL judged
259
296
  by the same `NON_SECRET_URL` the `remoteExec` endpoints use — same-origin is the reader's comparison).
260
- `parseCenterCredentialFile(text)` is the ONE discrimination law — trimmed text starting with `{` is read ONLY
297
+ `parseWebAdminCredentialFile(text)` is the ONE discrimination law — trimmed text starting with `{` is read ONLY
261
298
  as an envelope (a failure is `unreadable`, never a fallback to a bare line); anything else is a bare line,
262
299
  and a first line that is not a token is `unreadable` too (never sent as a bearer); empty is `unreadable`. The `reason` names envelope fields / zod issue codes / an unknown-key COUNT, never file
263
300
  content. Pure: reading, atomically writing and renewing the file are the consumers' job. S-742 lifts the
264
- DECLARATION face here too: `CENTER_CREDENTIAL_ENV` (`{ token: "SEMA_REGISTRY_TOKEN", tokenFile:
265
- "SEMA_REGISTRY_TOKEN_FILE" }` — one spelling for the center rendering worker env, the shell writing its local
266
- engine's env, and the server reading it) and `judgeCenterCredentialDeclaration(env)` → `env` / `file` /
267
- `absent`, the one pick-exactly-one rule: an empty string is unset, BOTH set (non-empty) throws (the refusal
301
+ DECLARATION face here too: `WEB_ADMIN_ENV` — every env name an engine reads to reach web-admin, one table
302
+ (`url` / `token` / `tokenFile` / `worker` / `liveUrl` / `webUrl` / `dryRun` / `adminToken` / `assertionToken` /
303
+ `assertionTokenFile`, values `SEMA_WEB_ADMIN_*`; one spelling for web-admin rendering worker env, the shell writing its
304
+ local engine's env, and the server reading it) and `judgeWebAdminCredentialDeclaration(env, credential)` — `credential` ∈
305
+ `WEB_ADMIN_CREDENTIALS` (`access` reads `token` / `tokenFile`, `assertion` reads `assertionToken` / `assertionTokenFile`;
306
+ no default) → `env` / `file` / `absent`, the one pick-exactly-one rule for every credential: an empty string is unset, BOTH set (non-empty) throws (the refusal
268
307
  names the two keys, never a value), the file path comes back verbatim (resolving a relative one against a cwd
269
308
  is the reader's job).
309
+ - **Retired env names** (`retired-envs.ts`, main barrel): `RETIRED_ENVS` — the ONE tombstone table (literal tuples
310
+ `[old, to, "renamed"]` / `[old, to, "replaced", why]` / `[old, why, "removed"]`, a closed set of three families,
311
+ frozen). A tombstone is permanent: the old name present ⇒ refuse to start, whether or not the replacement is also
312
+ set (an empty string is unset); a chain is one hop (every row points at a current name). `retiredEnvRefusal(env)` is
313
+ the one refusal sentence (code `RETIRED_ENV_REFUSAL_CODE` = `config.env_key_retired`; names keys, never values;
314
+ pure — pass the env you are judging); `buildRetiredEnvWire()` is the catalog projection (`retired[]`). The engine's
315
+ boot, the shell starting a local engine and web-admin rendering worker env all judge this one table.
270
316
  - **Multi-tenant scopes** (`api/scopes.ts`, subpath `./api/scopes`): the scope contract — the `Scope`
271
317
  entity (`{id,name,createdAt}`, reserved default `"global"` always exists), per-scope `ScopeMember`
272
- rows (`{scopeId,principal,role}` on the existing viewer<editor<publisher<admin ladder), the optional
273
- JWT `scope` claim (absent = `"global"`, fully backward compatible; refresh tokens are never
274
- scope-bound), and the wire shapes for `GET/POST /api/v1/scopes`, `POST /api/v1/auth/scope`
318
+ rows (`{scopeId,principal,role}` on the existing viewer<editor<publisher<admin ladder), the JWT
319
+ `scope` claim (every minted access token carries it; 11.0.0: a token without it is refused, not read as `"global"`;
320
+ refresh tokens are never scope-bound), and the wire shapes for `GET/POST /api/v1/scopes`, `POST /api/v1/auth/scope`
275
321
  (scope-switch mint, OAuth-style errors) and `PUT/DELETE /api/v1/scopes/{id}/members`. The DECISION
276
- SEMANTICS live here as pure functions the registry imports (never re-implements):
322
+ SEMANTICS live here as pure functions web-admin imports (never re-implements):
277
323
  `resolveTokenScope` (mint-time scope binding: 1 membership → it; several incl. global → global;
278
324
  several without global → lexicographically first; none → `{scope:"global", role:instanceRole}`),
279
325
  `canSwitchScope` (member or instance admin), `effectiveScopeRole` (instance admin is `"admin"` in
280
- every scope) and `tokenScopeOf` (missing claim = `"global"`). OpenAPI mirror: `docs/openapi-scopes.yaml`.
326
+ every scope) and `readTokenScope` (a set claim ⇒ `{ scope }`; a missing / empty / non-string claim ⇒ `{ refused:
327
+ "scope_claim_missing" }` — 11.0.0: no longer read as `"global"`). OpenAPI mirror: `docs/openapi-scopes.yaml`.
328
+ - **Scope-bound tokens** (`api/pull-token.ts`, subpath `./api/pull-token`): the FORM and minting rule of the per-worker pull
329
+ token web-admin mints for every worker (`WEB_ADMIN_ENV.token`) — one rule, token families are data
330
+ (`ScopedTokenFamily = { v1Prefix, v2Prefix, macTag }`; `WORKER_PULL_TOKEN` = `wpt_` / `wpt2_` / `worker-pull`). Default scope ⇒
331
+ v1 `<v1Prefix><worker>_<mac>`, any other scope ⇒ v2 `<v2Prefix><scope>_<worker>_<mac>`, `mac` = HMAC-SHA256(shared key,
332
+ `<macTag>:v1:<worker>` / `<macTag>:v2:<scope>:<worker>`) in lowercase hex (pure-JS HMAC, byte-identical to `node:crypto`).
333
+ `mintScopedToken` throws instead of minting a token that could never verify (empty key, a scope that is not a scope id, a
334
+ worker name that is empty or carries the `_` frame separator); `readScopedTokenBinding` reads what a token CLAIMS;
335
+ `verifyScopedToken` = claim → re-mint → constant-time compare (a v2 token claiming the default scope never verifies).
336
+ The engine only carries the token as an opaque bearer.
337
+ - **Principal length** (`api/auth-bridge.ts`): `PRINCIPAL_MAX_LENGTH` = 190 — the engine's principal gate refuses a longer
338
+ one; validate / mint principals against this value.
339
+ - **/bug reports** (`api/feedback.ts`, subpath `./api/feedback`): `FeedbackSubmitRequest` (the POST body the shell sends to
340
+ `FEEDBACK_V1_PATH`; `description` trimmed, non-empty, ≤ `FEEDBACK_DESCRIPTION_MAX_CHARS`; `transcript` serialized ≤
341
+ `FEEDBACK_TRANSCRIPT_MAX_JSON_CHARS`; `principal` is never a body field — it is the token subject), `FeedbackWire` (one
342
+ report row, server-stamped `createdAt`) and `FeedbackListResponse`.
343
+ - **Content digest form** (`sha256.ts`): `Sha256Digest` / `SHA256_DIGEST_RE` — the one `sha256:<64 lowercase hex>` spelling every
344
+ digest-bearing seat in this package uses.
281
345
  - **Local file source** (`file-store.ts`, `file-edit.ts`): `FileConfigStore` reads `config.d/*.json` →
282
346
  `EffectiveConfig`; `validateDomain` / `writeDomainFile` / `listRequiredEnvNames` are the TOC's
283
347
  read/write/validate helpers. `getEffective({tolerant:true})` (0.10.12) isolates a malformed
@@ -288,7 +352,8 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
288
352
  and any top-level key a domain schema strips are both NAMED through `onWarning`
289
353
  (`unread-config-file` / `unknown-keys-dropped`) instead of vanishing silently (**6.3.0**: every warning coordinate —
290
354
  unknown keys included — leaves through `redactSecrets`, so a secret-shaped key segment reads `[redacted-secret]`;
291
- `droppedKeys` / `carriedUnknownKeys` return the same outgoing form). **2.0.0** adds per-ENTRY
355
+ the unknown-key scans behind those warnings are module-private since 11.0.0 — `parseDomainLoud` is the one public
356
+ primitive). **2.0.0** adds per-ENTRY
292
357
  tolerance on the same read path: one malformed catalog entry is dropped on its own (named through
293
358
  `onWarning` as `entry-dropped`) and its siblings survive, instead of the whole domain falling to
294
359
  its schema default. It is the SAME `DOMAIN_READ_FALLBACK` table that decides where this applies — gate
@@ -340,15 +405,15 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
340
405
  (forward-compatible: unknown fields survive a round-trip, malformed records are dropped one by one).
341
406
  - **Fleet contract** (`fleet.ts`, subpath `./fleet`): drain protocol, instance discovery, dispatch policy,
342
407
  quota lease, usage report, quota-reset event. **S-741** adds the restart signal, defined ONCE:
343
- `RestartSignal` (`{ reasons: string[] (≥1, non-empty words), version: number (the config-center version that
408
+ `RestartSignal` (`{ reasons: string[] (≥1, non-empty words), version: number (the web-admin config version that
344
409
  differs from boot — finite, judged exactly as `readEffectiveWire` judges a config version), since: number (epoch
345
410
  ms, non-negative integer) }` — exactly what the server already ships on /health). The heartbeat carries it as
346
- `InstanceAnnouncement.restart?` (so `InstanceRegistration` carries it too); on /health, `InstanceHealth` is
347
- UNCHANGED (passthrough — `restart` and the redundant top-level `restartRequired` ride untyped, so today's lenient
348
- read stays lenient) and `readRestartSignal(health)` reads `health.restart` against the one shape (`undefined` on
349
- any mismatch, never throws). Presence is the signal (present ⇔ restart required; no `required` boolean inside).
350
- A heartbeat re-announces the full row, so one without `restart` means "no longer required". Typing
351
- `InstanceHealth.restart` strictly is left for the next major. **S-768** adds the model-refusals signal the same
411
+ `InstanceAnnouncement.restart?` (so `InstanceRegistration` carries it too); on /health it rode untyped
412
+ (passthrough, next to the redundant top-level `restartRequired`). Presence is the signal (present ⇔ restart required;
413
+ no `required` boolean inside). A heartbeat re-announces the full row, so one without it means "no longer required".
414
+ Since 11.0.0 it is engine state and is read in ONE place on both exits — inside `EngineSignal` under `engineSignal`
415
+ (`readEngineSignal(body)?.restart`); the flat `restart` both bodies still carry is the pre-11 dialect, read by nothing,
416
+ and the flat reader `readRestartSignal` is gone. **S-768** adds the model-refusals signal the same
352
417
  way (one mint point, two exits, ONE reader): the wire face `ModelRefusalsWire` (`{ entries: { <catalog entry name>:
353
418
  <code> }, plane?: <code> }` with codes as open strings — name → code is the whole wire, no upstream id / provider /
354
419
  field keys / prose) rides the heartbeat as `InstanceAnnouncement.modelRefusals?` and /health (`InstanceHealth` again
@@ -364,30 +429,40 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
364
429
  rule the dropped-entry identity on read warnings already uses: the `DISCLOSABLE_IDENTITY_RE` allowlist ∧ not
365
430
  secret-shaped, 6.3.0). **8.1.0** (additive) puts the engine's own state in one closed shape: `RestartSignal` gains the
366
431
  optional `withheld` ("the engine says this beat must not be handed to a new generation"; absent ≡ false);
367
- `EngineSignal` (`{ ready, draining?: EngineDraining, restart?, generation?, instanceId?, leaseUntil?,
368
- configTargetVersion?, configAppliedVersion? }`) is the closed form of the engine-minted subset of the /health
369
- self-description (`InstanceHealth` stays the open record; neither exit carries it whole yet — see its doc comment) and
370
- `readEngineSignal(health)` reads it through the same guarded reader as `readRestartSignal` / `readModelRefusals` —
371
- `undefined` means "no such signal", never "ready", and a body carrying the boolean `draining` of the 8.0 dialect is not
372
- one. The serving-lease wire (`ServingLeaseRequest` / `ServingLeaseResponse`: `granted { ttlMs }` | `denied { reason:
432
+ `EngineSignal` (`{ ready, draining?: EngineDraining, restart?, generation?, leaseState?, leaseUntil?,
433
+ configTargetVersion?, configAppliedVersion? }`; 11.0.0 took `instanceId` out — the instance's identity is the body's top
434
+ level) is the closed form of what the engine mints about itself. **11.0.0**: it rides
435
+ BOTH exits (`/health` and the heartbeat) WHOLE under the one key `ENGINE_SIGNAL_KEY` (`"engineSignal"`) — nested, because the
436
+ boolean `draining` dialect is still sent in its contraction window and one key cannot hold a boolean and an object;
437
+ `readEngineSignal(body)` reads that key only (same guarded reader as `readModelRefusals`; `undefined` =
438
+ "no such signal", never "ready"), both shapes type it (`InstanceAnnouncement.engineSignal` / `InstanceHealth.engineSignal`, a
439
+ malformed one is absent),
440
+ and the judges read nothing else: `judgeReadiness(signal)` / `judgeRoutability(row, nowMs)` → `{ routable: true }` |
441
+ `{ routable: false, code, why }` (closed set `NOT_ROUTABLE_CODES`: no-engine-signal / stale / observer-not-ready / draining /
442
+ not-ready), with `isReadyForTraffic` / `isRoutable` / `routableInstances` as their boolean forms. A row or body without the
443
+ signal — an engine that predates it — is `no-engine-signal`, never routable. One fact, one place: engine state only in the
444
+ signal, the row's identity (`instanceId`, `worker`, `address`, `version`, `heartbeatMs`) only at the top level; the flat state
445
+ keys both bodies still carry (`ready`, the boolean `draining` / `drainingSince`, `restart`, `generation`, `leaseState`,
446
+ `leaseUntil`) are the pre-11 dialect — shape unchanged, `@deprecated`, read by nothing here; their removal is registered in
447
+ `ONE_TIME_MIGRATIONS`. The serving-lease wire (`ServingLeaseRequest` / `ServingLeaseResponse`: `granted { ttlMs }` | `denied { reason:
373
448
  superseded | retired }` | `unknown` — a closed three-word set in which `unknown` is NOT a kind of `denied`; plus
374
449
  `SERVING_LEASE_TTL_DEFAULT_MS` 600 000, `SERVING_LEASE_TTL_FLOOR_MS` 60 000 and `SERVING_LEASE_DRIFT_MARGIN_MS`, derived
375
- from `DEFAULT_HEARTBEAT_MS`; `denied` rests on positive evidence only), the center capability bit `CenterCapabilities`
376
- (`servingLease?: true` — absent means the center has no lease, so the engine neither renews nor is fenced), the drain
450
+ from `DEFAULT_HEARTBEAT_MS`; `denied` rests on positive evidence only), the web-admin capability bit `WebAdminCapabilities`
451
+ (`servingLease?: true` — absent means web-admin has no lease, so the engine neither renews nor is fenced), the drain
377
452
  endpoint's `DrainRequest` / `DrainReceipt`, one reader per wire shape, and the env name `FLEET_GENERATION_ENV` are defined
378
453
  here once for the renderer and the engine (an instance's id is minted by the engine per process — it has no env name).
379
454
  **8.2.0** (additive) adds the engine's lease vocabulary `LEASE_STATES` (`none | granted | expired | denied`) as
380
455
  `EngineSignal.leaseState?`, keeps `generation` / `leaseState` / `leaseUntil` on the heartbeat (`InstanceAnnouncement` no longer
381
- strips them; a malformed one drops to absent, the heartbeat still lands), spells the center's fleet endpoints once —
456
+ strips them; a malformed one drops to absent, the heartbeat still lands), spells web-admin's fleet endpoints once —
382
457
  `FLEET_PATHS` (heartbeat, usage, quota leases, serving lease `/api/fleet/live/serving-lease`, host planner
383
458
  `/api/fleet/live/host-planner/{host}`) and the liveness group `FLEET_LIVE_PATHS` (heartbeat, serving lease, host planner), also
384
459
  shipped as `@sema-agent/settings-schema/fleet-paths.json` for tooling that cannot import TypeScript — and names the one carrier of
385
- the center's capability bits: `HEARTBEAT_REPLY_CAPABILITIES_KEY` (`"capabilities"`) on the heartbeat's 2xx reply.
460
+ web-admin's capability bits: `HEARTBEAT_REPLY_CAPABILITIES_KEY` (`"capabilities"`) on the heartbeat's 2xx reply.
386
461
  **8.3.0** (additive) adds the env name `FLEET_SCOPE_ENV` (`"FLEET_SCOPE"`) — the tenant scope a fleet member belongs to, set by the
387
462
  config renderer and read by the engine as its own identity's scope (absent ⇒ `GLOBAL_SCOPE`); a reserved worker env name.
388
463
  **9.0.0** (BREAKING) replaces `readCenterCapabilities` with the three-state reader `readCapabilityReading(reply)` (`declared` |
389
464
  `undeclared` | `unreadable` — a malformed declaration is not an absent one) and types `EngineSignal`'s two config-version keys as numbers.
390
- - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
465
+ - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE web-admin of keys this package
391
466
  carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side or
392
467
  a consumer's enforcement point). Today: `models[].compat` (1.10.0), `models[].headers` (6.4.0; the header-name
393
468
  allowlist × lane is sema-server's, not restated here), `mcp.servers[].source` (1.7.2), `mcp.servers[].toolFaces`
@@ -402,9 +477,10 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
402
477
  ## Cross-repo conformance gate (`./conformance`, 3.0.0)
403
478
 
404
479
  契约仓自带**金样语料 + 比对器**,随包发(`conformance/cases/**`)。每个消费方把**自己的生产入口**塞给
405
- kit 跑一遍 ⇒ 同一份 settings 语料在配置中心、worker、管理面、契约仓自身读出的 effective 逐字节一致、
480
+ kit 跑一遍 ⇒ 同一份 settings 语料在 web-admin、worker、管理面、契约仓自身读出的 effective 逐字节一致、
406
481
  告警多重集一致;写入席对同一份文档收不收也一致。**四条腿**(8.0.0):`domains`(各域原始文档 → effective)、
407
- `strictWrite`(同一份文档 → 严写受理;契约仓 = `parseDomain`,任一份不受理 ⇒ `rejected{validate}`,读腿抢救处严写拒)、
482
+ `strictWrite`(同一份文档 → 严写受理;契约仓 = `parseDomainLoud` + 跨域引用完整性,任一份不受理 ⇒ `rejected{validate}`,悬空引用 ⇒
483
+ `rejected{validate, ref-integrity}` 带 `refIssues` 坐标,读腿抢救处严写拒;受理时的剥键 / 承运告警进告警面)、
408
484
  `wire`、`localRoot`。
409
485
 
410
486
  ```ts
@@ -465,15 +541,15 @@ defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
465
541
  codex 复审持「严格 semver 应升 major」异议,原样留档候裁)。
466
542
  - **文案 / 注释 / 测试 / bug 修复 = patch**:不改任何 wire 形、类型面、默认值与校验结果。行为面的
467
543
  「修 bug」若改变了合法输入的解析结果,按上两档判,不按 patch。
468
- - 契约破坏面动之前先盘 center / server / CLI 三方消费点;commit message 的 `feat!`/`BREAKING` 标记照旧,
544
+ - 契约破坏面动之前先盘 web-admin / server / CLI 三方消费点;commit message 的 `feat!`/`BREAKING` 标记照旧,
469
545
  但标记不豁免版号义务。每版 CHANGELOG 一节,git tag `v<version>` 同名。
470
546
  - **持久数据面的破坏性变更(例:7.0.0)**:类型 / API 面直接删改、零别名(消费方 tsc 红即通知);**存下来的数据**不会自己红,
471
547
  所以同版配三件:读到旧形即**响亮拒**(拒因指路迁移命令,不是被 zod 静默剥掉)、一只**一次性迁移**(纯函数、幂等、
472
- 表达不了就 refused 点名,不替运维猜)、以及一个**写明的删除窗**(7.0.0 的 `migrateWorkersDomain` 在中心接上迁移后的下一版删除)——
548
+ 表达不了就 refused 点名,不替运维猜)、以及一个**写明的删除窗**(7.0.0 的 `migrateWorkersDomain` 在 web-admin 接上迁移后的下一版删除)——
473
549
  绝不长成长命双读。
474
550
  - **历史注记(0.x 时代)**:0.x 下 `^0.7.0` 这类 range 只自动收 patch,故当年规矩是「破坏性必 bump
475
551
  minor,patch 只装 additive/修 bug/文档」。0.7.1(2026-07-10)曾携带两件预协调 BREAKING(撤 governance
476
- effective 双写镜像、撤 QuotaLease 0.5 `tokens` 别名 shim),名义上违反该条;当时下游(center/service
552
+ effective 双写镜像、撤 QuotaLease 0.5 `tokens` 别名 shim),名义上违反该条;当时下游(web-admin/service
477
553
  1.150.0)均已显式迁移、实际破坏面为零,PM 拍板不重发 0.8.0(纯扰动)。该例外随 1.x 政策一并归档。
478
554
  - 发布(单腿 npmjs,内网腿已废止):`npm run publish:npm`;发后必须 curl 直打 npmjs 复验 shasum。
479
555
 
@@ -482,4 +558,4 @@ defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
482
558
  - 真源 = `sema-agent/sema-settings-schema` org 仓 `main`(2026-08-26 由 sema-registry-core 改名),所有变更直接落 org main(或 PR 进 main);
483
559
  不再向任何个人位/镜像位 push。
484
560
  - 提交门 = `npm run build` + `npx vitest run`;改域/契约后必跑 `npm run e2e`(examples/toc-e2e.mjs)。
485
- - 契约破坏面动之前先盘 center / server / CLI 三方消费点;破坏性变更必 bump major(见上节)。
561
+ - 契约破坏面动之前先盘 web-admin / server / CLI 三方消费点;破坏性变更必 bump major(见上节)。
@@ -1,20 +1,27 @@
1
1
  #!/usr/bin/env node
2
- // `settings-schema` — this package's command line (7.0.0: `migrate-workers <workers.json> [--check]`).
2
+ // `settings-schema` — this package's command line (11.0.0: `migrate <config.d directory> [--check]`, the one-time migrations).
3
3
  // The ONLY place the package writes files with node:fs; the logic is `runSettingsSchemaCli` (dist/cli.js), which never
4
4
  // touches the filesystem itself — this entry injects real file I/O into it.
5
- import { readFile, rename, stat, writeFile } from "node:fs/promises";
5
+ import { readdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { runSettingsSchemaCli } from "../dist/cli.js";
7
7
 
8
8
  const io = {
9
+ readDir: (path) => readdir(path),
9
10
  readFile: (path) => readFile(path, "utf8"),
10
- // atomic: write a sibling temp file (same mode as the original), then rename over it — a half-written workers
11
- // document is worse than the old one.
11
+ // atomic: write a sibling temp file (same mode as the original when there is one), then rename over it — a half-written
12
+ // domain document is worse than the old one.
12
13
  writeFileAtomic: async (path, text) => {
13
- const { mode } = await stat(path);
14
+ let mode = 0o600;
15
+ try {
16
+ mode = (await stat(path)).mode & 0o777;
17
+ } catch (e) {
18
+ if (e?.code !== "ENOENT") throw e;
19
+ }
14
20
  const tmp = `${path}.migrate-${process.pid}.tmp`;
15
- await writeFile(tmp, text, { encoding: "utf8", mode: mode & 0o777 });
21
+ await writeFile(tmp, text, { encoding: "utf8", mode });
16
22
  await rename(tmp, path);
17
23
  },
24
+ removeFile: (path) => unlink(path),
18
25
  stdout: (text) => process.stdout.write(text),
19
26
  stderr: (text) => process.stderr.write(text),
20
27
  };
@@ -1 +1 @@
1
- 10.1.1
1
+ 11.0.0
@@ -23,6 +23,9 @@
23
23
  "servers": []
24
24
  },
25
25
  "models": {
26
+ "atMention": {
27
+ "mode": "all"
28
+ },
26
29
  "models": [],
27
30
  "roles": {},
28
31
  "tierGroups": []
@@ -39,7 +42,6 @@
39
42
  "rosters": {
40
43
  "rosters": []
41
44
  },
42
- "runtime": {},
43
45
  "scenarios": {
44
46
  "scenarios": []
45
47
  },
@@ -26,6 +26,9 @@
26
26
  "servers": []
27
27
  },
28
28
  "models": {
29
+ "atMention": {
30
+ "mode": "all"
31
+ },
29
32
  "models": [
30
33
  {
31
34
  "api": "openai-completions",
@@ -55,7 +58,6 @@
55
58
  "rosters": {
56
59
  "rosters": []
57
60
  },
58
- "runtime": {},
59
61
  "scenarios": {
60
62
  "scenarios": []
61
63
  },
@@ -34,6 +34,9 @@
34
34
  "servers": []
35
35
  },
36
36
  "models": {
37
+ "atMention": {
38
+ "mode": "all"
39
+ },
37
40
  "models": [],
38
41
  "roles": {},
39
42
  "tierGroups": []
@@ -50,7 +53,6 @@
50
53
  "rosters": {
51
54
  "rosters": []
52
55
  },
53
- "runtime": {},
54
56
  "scenarios": {
55
57
  "scenarios": []
56
58
  },
@@ -67,10 +69,15 @@
67
69
  "warnings": [
68
70
  {
69
71
  "domain": "hosts",
70
- "droppedNames": [
71
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx…"
72
+ "identity": {
73
+ "name": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
74
+ },
75
+ "kind": "entry-dropped",
76
+ "path": [
77
+ "hosts",
78
+ 0
72
79
  ],
73
- "kind": "hosts-grandfathered"
80
+ "reasonCode": "entry-schema-invalid"
74
81
  },
75
82
  {
76
83
  "domain": "hosts",
@@ -93,7 +100,7 @@
93
100
  "strictWrite": {
94
101
  "outcome": "rejected",
95
102
  "stage": "validate",
96
- "why": "拒:读腿对 hosts 先做存量夹紧(超长名剔除,hosts-grandfathered)再逐条抢救(entry-dropped);两样都只活在读路径,严写按现行 schema 判:名长 > 63 与名字形不合法都拒。"
103
+ "why": "拒:严写按现行 schema 判 —— 名长 > 63 与名字形不合法都拒;读腿逐条抢救(entry-dropped)。读写同一只 schema(11.0.0 起读侧不再夹紧)。"
97
104
  },
98
105
  "wire": {
99
106
  "derive": true
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contributor": "contract",
3
- "claim": "读路径上有两个压缩点(hosts 的按名夹紧 + 逐条抢救)。两层叠加时,内层铸的 entry-dropped 坐标要过外层的位移表一次才是原文档坐标(S-323b)。少回写一层,告警就指向第三条别人的记录。",
3
+ "claim": "读路径上只剩一个压缩点(逐条抢救;11.0.0 起 hosts 的读侧夹紧臂退役,超长名那一条也由它剔)。超长名与形不合法的条目都作 entry-dropped 报原文档坐标;同文档里的拼错键回写到原下标。少回写一次,告警就指向别人的记录。",
4
4
  "firstRedOn": [
5
5
  "settings-schema@1.0.0",
6
6
  "settings-schema@1.10.0",
@@ -26,6 +26,9 @@
26
26
  "servers": []
27
27
  },
28
28
  "models": {
29
+ "atMention": {
30
+ "mode": "all"
31
+ },
29
32
  "models": [
30
33
  {
31
34
  "api": "openai-completions",
@@ -67,7 +70,6 @@
67
70
  "rosters": {
68
71
  "rosters": []
69
72
  },
70
- "runtime": {},
71
73
  "scenarios": {
72
74
  "scenarios": []
73
75
  },
@@ -106,6 +108,9 @@
106
108
  "outcome": "effective",
107
109
  "effective": {
108
110
  "models": {
111
+ "atMention": {
112
+ "mode": "all"
113
+ },
109
114
  "models": [
110
115
  {
111
116
  "api": "openai-completions",
@@ -136,7 +141,22 @@
136
141
  "tierGroups": []
137
142
  }
138
143
  },
139
- "warnings": [],
144
+ "warnings": [
145
+ {
146
+ "domain": "models",
147
+ "keys": [
148
+ [
149
+ "models",
150
+ 1,
151
+ "k"
152
+ ],
153
+ [
154
+ "models.1.k"
155
+ ]
156
+ ],
157
+ "kind": "unknown-keys-dropped"
158
+ }
159
+ ],
140
160
  "metadata": {
141
161
  "version": 7,
142
162
  "updatedAt": "2026-09-16T00:00:00.000Z"
@@ -26,6 +26,9 @@
26
26
  "servers": []
27
27
  },
28
28
  "models": {
29
+ "atMention": {
30
+ "mode": "all"
31
+ },
29
32
  "models": [],
30
33
  "roles": {},
31
34
  "tierGroups": []
@@ -42,7 +45,6 @@
42
45
  "rosters": {
43
46
  "rosters": []
44
47
  },
45
- "runtime": {},
46
48
  "scenarios": {
47
49
  "scenarios": []
48
50
  },