@sema-agent/settings-schema 10.1.0 → 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 +419 -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
@@ -8,7 +8,7 @@ export declare function promptsContentDigest(payload: {
8
8
  contentDigest: string;
9
9
  packId: string;
10
10
  /** [1062]③ 顺带:逐 section 对 `text` 铸的 contentHash(与 core 1.315 校验语义同刻度=
11
- * sha256(text));元素无 string text 位=undefined。center 物化流程=先把这些写回
11
+ * sha256(text));元素无 string text 位=undefined。web-admin 物化流程=先把这些写回
12
12
  * declaration.contentHash,再对含 hash 的终形铸整包 digest(本函数对入参原样铸,两步自洽)。 */
13
13
  sectionHashes: Array<string | undefined>;
14
14
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @sema-agent/settings-schema/node — prompts 下发包的**内容摘要**([1057]①/[1062]③)。
3
3
  *
4
- * 这两只是跨仓**对账锚**:center 铸、server 透传、core 的 manifest 归因**独立复算**,三方算出同一个值才
4
+ * 这两只是跨仓**对账锚**:web-admin 铸、server 透传、core 的 manifest 归因**独立复算**,三方算出同一个值才
5
5
  * 对得上账([1050]② operator 对账位)。所以它们只有一件事要守:**一个刻度**。
6
6
  *
7
7
  * 4.0.0 从 `scheduler-store-node.ts` 搬到这里(不 breaking —— 仍挂在 `./node` 这条 subpath 上,消费方的
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { stableStringify } from './hash.js';
18
18
  import { sha256Hex } from './sha256.js';
19
+ import { PROMPT_PACK_ID_PREFIX } from './types.js';
19
20
  /**
20
21
  * 本模块**唯一**的 sha256 铸点:一段 utf8 文本 → `{ hex, anchor }`。`anchor` = 出门形 `sha256:<hex>`
21
22
  * (自描述算法,与 skills 的内容地址同形),`hex` 给需要裸 hex 的那一处(packId 取前 12 位)。
@@ -40,7 +41,7 @@ export function promptsContentDigest(payload) {
40
41
  return typeof t === 'string' ? sectionContentHash(t) : undefined;
41
42
  })
42
43
  : [];
43
- return { contentDigest: anchor, packId: `center:${hex.slice(0, 12)}`, sectionHashes };
44
+ return { contentDigest: anchor, packId: `${PROMPT_PACK_ID_PREFIX}${hex.slice(0, 12)}`, sectionHashes };
44
45
  }
45
46
  /** 逐 section 对账锚(core 1.315 `PromptTextDeclaration.contentHash` 同刻度):sha256(text)。 */
46
47
  export function sectionContentHash(text) {
package/dist/reader.d.ts CHANGED
@@ -2,9 +2,9 @@ import type { DomainConfig, DomainName, EffectiveConfig } from "./types.js";
2
2
  /**
3
3
  * The MINIMAL read surface a consumer needs to resolve config — a local file source (FileConfigStore), the
4
4
  * TOC desktop, or a test fixture implements this WITHOUT stubbing the ~32 publish/RBAC/worker-status/lifecycle
5
- * methods of sema-registry's full ConfigStore.
5
+ * methods of web-admin's full ConfigStore.
6
6
  *
7
- * sema-registry's fat `ConfigStore` should `extends ConfigReader` (every store impl already has all three
7
+ * web-admin's fat `ConfigStore` should `extends ConfigReader` (every store impl already has all three
8
8
  * methods, so the extension is satisfied automatically). The signatures here MUST match store.ts exactly
9
9
  * (incl. the `<K extends DomainName>` generic and the `| undefined` on getDomain) or that `extends` fails.
10
10
  */
@@ -17,7 +17,8 @@
17
17
  * vocabulary; `test/execution-lanes-7.0.0.test.ts` pins `provider ⊆ EXECUTION_LANE_WORDS`, at runtime and at compile time).
18
18
  * The `remote-docker` variant was DELETED (zero aliases): no engine read point ever turned that word into a running lane
19
19
  * (the engine refuses `REMOTE_EXEC=remote-docker` at boot and only warned "unwired" when this file named it). A remote
20
- * docker daemon is the `local-docker` lane pointed at it (`DOCKER_HOST` — in a WorkerSpec, `runtime.execution.coords`).
20
+ * docker daemon is the `local-docker` lane pointed at it (`DOCKER_HOST` — in a WorkerSpec, `runtime.execution.coords`); the word
21
+ * itself sits in the lane-word tombstone `RETIRED_LANE_WORDS`, so a file still naming it is refused with that way out.
21
22
  *
22
23
  * SECRET BOUNDARY (identical to the rest of the contract): every secret VALUE the consumer reads from its own
23
24
  * env (E2B_API_KEY, K8S_TOKEN, SSH_PRIVATE_KEY, MINIO_*) is carried here as an env-NAME ref (the `ENV_NAME` regex
@@ -26,208 +27,65 @@
26
27
  * ONLY; the service owns the adapter implementations.
27
28
  */
28
29
  import { z } from "zod";
29
- export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodObject<{
30
+ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<[z.ZodObject<{
30
31
  provider: z.ZodLiteral<"host">;
31
- /** Working ROOT for the agent's tools; absent = the workspace/cwd. (≡ the worker service's resolved
32
- * `workspaceBase` — the adapter runs each task under `<workdir>/ai-agent-host-<id>`.) */
33
- workdir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
34
- /** Optional writable-path allowlist bounding what the host-exec tools may touch (defence for a TOC box). */
35
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
36
- /** Shell to spawn (absent = the platform default). */
37
- shell: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
38
- /** Per-COMMAND timeout (ms) for the host lane — host is long-lived so this bounds a single exec, not the
39
- * task (mirrors e2b/k8s `timeoutMs`; = the worker service's `commandTimeoutMs`). Absent = adapter default. */
32
+ workdir: z.ZodOptional<z.ZodString>;
33
+ allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
34
+ shell: z.ZodOptional<z.ZodString>;
40
35
  commandTimeoutMs: z.ZodOptional<z.ZodNumber>;
41
- }, "strip", z.ZodTypeAny, {
42
- provider: "host";
43
- workdir?: string | undefined;
44
- allowedPaths?: string[] | undefined;
45
- shell?: string | undefined;
46
- commandTimeoutMs?: number | undefined;
47
- }, {
48
- provider: "host";
49
- workdir?: string | undefined;
50
- allowedPaths?: string[] | undefined;
51
- shell?: string | undefined;
52
- commandTimeoutMs?: number | undefined;
53
- }>, z.ZodObject<{
54
- image: z.ZodEffects<z.ZodString, string, string>;
55
- memory: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
36
+ }, z.core.$strip>, z.ZodObject<{
37
+ image: z.ZodString;
38
+ memory: z.ZodOptional<z.ZodString>;
56
39
  cpus: z.ZodOptional<z.ZodNumber>;
57
- network: z.ZodOptional<z.ZodEnum<["none", "bridge", "host"]>>;
58
- workdir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
59
- mountPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
40
+ network: z.ZodOptional<z.ZodEnum<{
41
+ none: "none";
42
+ host: "host";
43
+ bridge: "bridge";
44
+ }>>;
45
+ workdir: z.ZodOptional<z.ZodString>;
46
+ mountPath: z.ZodOptional<z.ZodString>;
60
47
  provider: z.ZodLiteral<"local-docker">;
61
- }, "strip", z.ZodTypeAny, {
62
- provider: "local-docker";
63
- image: string;
64
- memory?: string | undefined;
65
- workdir?: string | undefined;
66
- cpus?: number | undefined;
67
- network?: "none" | "host" | "bridge" | undefined;
68
- mountPath?: string | undefined;
69
- }, {
70
- provider: "local-docker";
71
- image: string;
72
- memory?: string | undefined;
73
- workdir?: string | undefined;
74
- cpus?: number | undefined;
75
- network?: "none" | "host" | "bridge" | undefined;
76
- mountPath?: string | undefined;
77
- }>, z.ZodObject<{
48
+ }, z.core.$strip>, z.ZodObject<{
78
49
  provider: z.ZodLiteral<"e2b">;
79
- /** env-NAME holding the E2B API key (service: E2B_API_KEY). NEVER the key. */
80
- apiKeyEnv: z.ZodEffects<z.ZodString, string, string>;
81
- /** E2B template id (service: E2B_TEMPLATE). */
82
- template: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
50
+ apiKeyEnv: z.ZodString;
51
+ template: z.ZodOptional<z.ZodString>;
83
52
  timeoutMs: z.ZodOptional<z.ZodNumber>;
84
- /** Idle/liveness bound for a command (service: E2B_LIVENESS_MS, the [49] hang-fix). */
85
53
  livenessMs: z.ZodOptional<z.ZodNumber>;
86
54
  allowInternetAccess: z.ZodOptional<z.ZodBoolean>;
87
- /** Which of THIS process's env-NAMEs to forward into the sandbox (service: E2B_SANDBOX_ENV, a CSV of NAMEs). */
88
- sandboxEnv: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
89
- }, "strip", z.ZodTypeAny, {
90
- provider: "e2b";
91
- apiKeyEnv: string;
92
- template?: string | undefined;
93
- timeoutMs?: number | undefined;
94
- livenessMs?: number | undefined;
95
- allowInternetAccess?: boolean | undefined;
96
- sandboxEnv?: string[] | undefined;
97
- }, {
98
- provider: "e2b";
99
- apiKeyEnv: string;
100
- template?: string | undefined;
101
- timeoutMs?: number | undefined;
102
- livenessMs?: number | undefined;
103
- allowInternetAccess?: boolean | undefined;
104
- sandboxEnv?: string[] | undefined;
105
- }>, z.ZodObject<{
55
+ sandboxEnv: z.ZodOptional<z.ZodArray<z.ZodString>>;
56
+ }, z.core.$strip>, z.ZodObject<{
106
57
  provider: z.ZodLiteral<"k8s">;
107
- /** Sandbox container image (service: K8S_SANDBOX_IMAGE). Required so a run is reproducible. */
108
- image: z.ZodEffects<z.ZodString, string, string>;
109
- /** Kube API URL (service: K8S_API_URL); absent → in-cluster service account. NO embedded credentials. */
110
- apiUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
111
- /** env-NAME holding the kube bearer token (service: K8S_TOKEN). NEVER the token. */
112
- tokenEnv: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
113
- /** env-NAME holding the base64 CA cert (service: K8S_CA_CERT_B64). NEVER the cert bytes. */
114
- caCertEnv: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
58
+ image: z.ZodString;
59
+ apiUrl: z.ZodOptional<z.ZodString>;
60
+ tokenEnv: z.ZodOptional<z.ZodString>;
61
+ caCertEnv: z.ZodOptional<z.ZodString>;
115
62
  insecureTls: z.ZodOptional<z.ZodBoolean>;
116
- namespace: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
117
- runtimeClass: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
118
- mountPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
63
+ namespace: z.ZodOptional<z.ZodString>;
64
+ runtimeClass: z.ZodOptional<z.ZodString>;
65
+ mountPath: z.ZodOptional<z.ZodString>;
119
66
  timeoutMs: z.ZodOptional<z.ZodNumber>;
120
- /** Sandbox pod resource limits (service: K8S_MEMORY / K8S_CPU; adapter defaults 2Gi / 1 cpu). */
121
67
  memory: z.ZodOptional<z.ZodString>;
122
68
  cpu: z.ZodOptional<z.ZodString>;
123
- /** Workspace-durable suspend/resume via MinIO/S3 (service: MINIO_*). Creds are env-NAME refs; the sandbox
124
- * only ever sees a presigned URL. accessKeyEnv + secretKeyEnv required to enable (mirrors the service's
125
- * "all three of endpoint/access/secret required" gate). */
126
69
  s3Snapshot: z.ZodOptional<z.ZodObject<{
127
- endpoint: z.ZodEffects<z.ZodString, string, string>;
128
- bucket: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
129
- accessKeyEnv: z.ZodEffects<z.ZodString, string, string>;
130
- secretKeyEnv: z.ZodEffects<z.ZodString, string, string>;
131
- region: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
132
- keyPrefix: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
70
+ endpoint: z.ZodString;
71
+ bucket: z.ZodOptional<z.ZodString>;
72
+ accessKeyEnv: z.ZodString;
73
+ secretKeyEnv: z.ZodString;
74
+ region: z.ZodOptional<z.ZodString>;
75
+ keyPrefix: z.ZodOptional<z.ZodString>;
133
76
  presignTtlSec: z.ZodOptional<z.ZodNumber>;
134
- }, "strip", z.ZodTypeAny, {
135
- endpoint: string;
136
- accessKeyEnv: string;
137
- secretKeyEnv: string;
138
- bucket?: string | undefined;
139
- region?: string | undefined;
140
- keyPrefix?: string | undefined;
141
- presignTtlSec?: number | undefined;
142
- }, {
143
- endpoint: string;
144
- accessKeyEnv: string;
145
- secretKeyEnv: string;
146
- bucket?: string | undefined;
147
- region?: string | undefined;
148
- keyPrefix?: string | undefined;
149
- presignTtlSec?: number | undefined;
150
- }>>;
151
- }, "strip", z.ZodTypeAny, {
152
- provider: "k8s";
153
- image: string;
154
- memory?: string | undefined;
155
- cpu?: string | undefined;
156
- mountPath?: string | undefined;
157
- timeoutMs?: number | undefined;
158
- apiUrl?: string | undefined;
159
- tokenEnv?: string | undefined;
160
- caCertEnv?: string | undefined;
161
- insecureTls?: boolean | undefined;
162
- namespace?: string | undefined;
163
- runtimeClass?: string | undefined;
164
- s3Snapshot?: {
165
- endpoint: string;
166
- accessKeyEnv: string;
167
- secretKeyEnv: string;
168
- bucket?: string | undefined;
169
- region?: string | undefined;
170
- keyPrefix?: string | undefined;
171
- presignTtlSec?: number | undefined;
172
- } | undefined;
173
- }, {
174
- provider: "k8s";
175
- image: string;
176
- memory?: string | undefined;
177
- cpu?: string | undefined;
178
- mountPath?: string | undefined;
179
- timeoutMs?: number | undefined;
180
- apiUrl?: string | undefined;
181
- tokenEnv?: string | undefined;
182
- caCertEnv?: string | undefined;
183
- insecureTls?: boolean | undefined;
184
- namespace?: string | undefined;
185
- runtimeClass?: string | undefined;
186
- s3Snapshot?: {
187
- endpoint: string;
188
- accessKeyEnv: string;
189
- secretKeyEnv: string;
190
- bucket?: string | undefined;
191
- region?: string | undefined;
192
- keyPrefix?: string | undefined;
193
- presignTtlSec?: number | undefined;
194
- } | undefined;
195
- }>, z.ZodObject<{
77
+ }, z.core.$strip>>;
78
+ }, z.core.$strip>, z.ZodObject<{
196
79
  provider: z.ZodLiteral<"ssh">;
197
- host: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
80
+ host: z.ZodString;
198
81
  port: z.ZodOptional<z.ZodNumber>;
199
- username: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
200
- /** env-NAME holding the SSH private key (service: SSH_PRIVATE_KEY). NEVER the key value. */
201
- privateKeyEnv: z.ZodEffects<z.ZodString, string, string>;
202
- mountPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
203
- }, "strip", z.ZodTypeAny, {
204
- host: string;
205
- provider: "ssh";
206
- username: string;
207
- privateKeyEnv: string;
208
- port?: number | undefined;
209
- mountPath?: string | undefined;
210
- }, {
211
- host: string;
212
- provider: "ssh";
213
- username: string;
214
- privateKeyEnv: string;
215
- port?: number | undefined;
216
- mountPath?: string | undefined;
217
- }>, z.ZodObject<{
82
+ username: z.ZodString;
83
+ privateKeyEnv: z.ZodString;
84
+ mountPath: z.ZodOptional<z.ZodString>;
85
+ }, z.core.$strip>, z.ZodObject<{
218
86
  provider: z.ZodLiteral<"adb">;
219
- serial: z.ZodEffects<z.ZodString, string, string>;
220
- adbPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
221
- mountPath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
222
- }, "strip", z.ZodTypeAny, {
223
- provider: "adb";
224
- serial: string;
225
- mountPath?: string | undefined;
226
- adbPath?: string | undefined;
227
- }, {
228
- provider: "adb";
229
- serial: string;
230
- mountPath?: string | undefined;
231
- adbPath?: string | undefined;
232
- }>]>;
87
+ serial: z.ZodString;
88
+ adbPath: z.ZodOptional<z.ZodString>;
89
+ mountPath: z.ZodOptional<z.ZodString>;
90
+ }, z.core.$strip>], "provider">;
233
91
  export type RemoteExecSpec = z.infer<typeof RemoteExecSpec>;
@@ -17,7 +17,8 @@
17
17
  * vocabulary; `test/execution-lanes-7.0.0.test.ts` pins `provider ⊆ EXECUTION_LANE_WORDS`, at runtime and at compile time).
18
18
  * The `remote-docker` variant was DELETED (zero aliases): no engine read point ever turned that word into a running lane
19
19
  * (the engine refuses `REMOTE_EXEC=remote-docker` at boot and only warned "unwired" when this file named it). A remote
20
- * docker daemon is the `local-docker` lane pointed at it (`DOCKER_HOST` — in a WorkerSpec, `runtime.execution.coords`).
20
+ * docker daemon is the `local-docker` lane pointed at it (`DOCKER_HOST` — in a WorkerSpec, `runtime.execution.coords`); the word
21
+ * itself sits in the lane-word tombstone `RETIRED_LANE_WORDS`, so a file still naming it is refused with that way out.
21
22
  *
22
23
  * SECRET BOUNDARY (identical to the rest of the contract): every secret VALUE the consumer reads from its own
23
24
  * env (E2B_API_KEY, K8S_TOKEN, SSH_PRIVATE_KEY, MINIO_*) is carried here as an env-NAME ref (the `ENV_NAME` regex
@@ -26,7 +27,9 @@
26
27
  * ONLY; the service owns the adapter implementations.
27
28
  */
28
29
  import { z } from "zod";
30
+ import { INTEGER } from "./integer.js";
29
31
  import { ENV_NAME, K8S_QUANTITY, NON_SECRET_URL, TCP_PORT, containsSecretToken, hasNul } from "./types.js";
32
+ import { retiredLaneWordRefusal } from "./execution-lanes.js";
30
33
  /** A non-empty exec-bound string (an image / path / shell / arg the adapter feeds to spawn()/fs). Rejects a NUL
31
34
  * byte uniformly — a NUL validates here but makes Node throw at spawn/boot (fail-OPEN → runtime crash). R20. */
32
35
  const execStr = z.string().min(1).refine((s) => !hasNul(s), "must not contain a NUL byte");
@@ -56,7 +59,7 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
56
59
  shell: execStr.optional(),
57
60
  /** Per-COMMAND timeout (ms) for the host lane — host is long-lived so this bounds a single exec, not the
58
61
  * task (mirrors e2b/k8s `timeoutMs`; = the worker service's `commandTimeoutMs`). Absent = adapter default. */
59
- commandTimeoutMs: z.number().int().positive().optional(),
62
+ commandTimeoutMs: INTEGER.positive().optional(),
60
63
  }),
61
64
  // ── local-docker: containers on this machine's docker (isolated; enables parallel fan-out). No secrets
62
65
  // (local docker.sock). Service implements this fast-follow after host.
@@ -68,9 +71,9 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
68
71
  apiKeyEnv: ENV_NAME,
69
72
  /** E2B template id (service: E2B_TEMPLATE). */
70
73
  template: execStr.optional(),
71
- timeoutMs: z.number().int().positive().optional(),
74
+ timeoutMs: INTEGER.positive().optional(),
72
75
  /** Idle/liveness bound for a command (service: E2B_LIVENESS_MS, the [49] hang-fix). */
73
- livenessMs: z.number().int().positive().optional(),
76
+ livenessMs: INTEGER.positive().optional(),
74
77
  allowInternetAccess: z.boolean().optional(),
75
78
  /** Which of THIS process's env-NAMEs to forward into the sandbox (service: E2B_SANDBOX_ENV, a CSV of NAMEs). */
76
79
  sandboxEnv: z.array(ENV_NAME).optional(),
@@ -90,7 +93,7 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
90
93
  namespace: execStr.optional(),
91
94
  runtimeClass: execStr.optional(),
92
95
  mountPath: execStr.optional(),
93
- timeoutMs: z.number().int().positive().optional(),
96
+ timeoutMs: INTEGER.positive().optional(),
94
97
  /** Sandbox pod resource limits (service: K8S_MEMORY / K8S_CPU; adapter defaults 2Gi / 1 cpu). */
95
98
  memory: K8S_QUANTITY.optional(),
96
99
  cpu: K8S_QUANTITY.optional(),
@@ -105,7 +108,7 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
105
108
  secretKeyEnv: ENV_NAME,
106
109
  region: execStr.optional(),
107
110
  keyPrefix: execStr.optional(),
108
- presignTtlSec: z.number().int().positive().optional(),
111
+ presignTtlSec: INTEGER.positive().optional(),
109
112
  })
110
113
  .optional(),
111
114
  }),
@@ -128,4 +131,7 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
128
131
  adbPath: execStr.optional(),
129
132
  mountPath: execStr.optional(),
130
133
  }),
131
- ]);
134
+ ], {
135
+ // A retired lane word as `provider` (no matching discriminator) gets the tombstone's way out; any other miss keeps zod's sentence.
136
+ error: (iss) => (iss.code === "invalid_union" && iss.input !== null && typeof iss.input === "object" ? retiredLaneWordRefusal(iss.input.provider) : undefined),
137
+ });
@@ -1,13 +1,17 @@
1
1
  /**
2
2
  * Per-worker roster resolution (the multi-roster design note). Turns the GLOBAL effective config + a worker name
3
3
  * into THAT worker's effective config: its `models` domain is the roster's view (model subset + role-map +
4
- * @-allowlist override), everything else stays global. The `rosters` domain is control-plane-only and is
4
+ * `atMention` override), everything else stays global. The `rosters` domain is control-plane-only and is
5
5
  * always stripped from the wire payload (no /effective consumer reads it).
6
6
  *
7
7
  * Pure (no I/O) → unit-testable. Tolerant of dangling refs (unknown worker / missing roster → global
8
- * fallback); strict referential integrity is enforced at PUT time (the [domain] route), not here.
8
+ * fallback; a roster entry naming no catalog model is dropped) — but not silent (11.0.0, S-1066 ③): whether a reference resolves
9
+ * is THE judge's call ({@link danglingRefs}, the same one the write seat refuses with), and the dangling references this
10
+ * resolution consulted come back as {@link ResolvedEffective.refIssues}. Strict refusal stays at PUT time (`refIntegrityIssues`);
11
+ * a local `config.d` never passes a write seat, which is why the read side still has to drop.
9
12
  */
10
- import type { EffectiveConfig, EffectiveWire, ModelRole, RoleTarget, RosterEntry } from "./types.js";
13
+ import type { AtMention, EffectiveConfig, EffectiveWire, ModelRole, RoleTarget, RosterEntry } from "./types.js";
14
+ import { type RefIssue } from "./cross-domain.js";
11
15
  /** primaryModel 标记喂的角色族(§3)。 */
12
16
  export declare const ROSTER_PRIMARY_ROLES: readonly ["default", "team", "synthesize"];
13
17
  /** cheapModel 标记喂的角色族(§3)。 */
@@ -16,6 +20,12 @@ export declare const ROSTER_CHEAP_ROLES: readonly ["subagent", "summarize"];
16
20
  * models.roles(合并点见 {@link resolveEffectiveForWorker})。wire 形状不变——service 零改仍兼容,
17
21
  * 收敛后直接读标记亦可(effective 的 rosters 域不上 wire,标记经此物化进 models.roles)。 */
18
22
  export declare function rosterMarkerRoles(r: Pick<RosterEntry, "primaryModel" | "cheapModel">): Partial<Record<ModelRole, RoleTarget>>;
23
+ /**
24
+ * 「`@` 能点名哪些模型」收窄到一组**可用**模型名上(纯函数;一只读法 —— roster 解析与任何别的收窄面,如按 principal 收窄,都调它)。
25
+ * `all` / `none` 原样;`only` 剔掉不在 `available` 里的名,剔空 ⇒ `{mode:"none"}`(管理员只许那几个,而这里一个都没有 = 一个都不许;
26
+ * 11.0.0 之前的数组形表达不了这一态,只好原样留着悬空名单,让消费方的成员判据兜底)。没有任何名被剔 ⇒ 返回**原对象**。
27
+ */
28
+ export declare function narrowAtMention(atMention: AtMention, available: ReadonlySet<string>): AtMention;
19
29
  export interface ResolvedEffective {
20
30
  /** The wire payload: `models` resolved for the worker, `rosters` stripped, `skills` shipped as a manifest. */
21
31
  config: EffectiveWire;
@@ -24,5 +34,10 @@ export interface ResolvedEffective {
24
34
  /** Quoted ETag. Global path = `"v<version>"` (today's behaviour). Scoped path = content hash (excludes
25
35
  * version/updatedAt) so an unrelated roster change at a new version doesn't churn this worker's cache. */
26
36
  etag: string;
37
+ /** The dangling references this resolution consulted, as THE judge ({@link danglingRefs}) sees them (11.0.0, S-1066 ③): the
38
+ * worker's own roster reference and every model reference of its roster. Each is something the resolution left out (a roster
39
+ * that does not exist ⇒ global fallback; a roster model / role / marker / @-name naming no catalog model ⇒ dropped). A caller
40
+ * logs them (or surfaces them); before 11.0.0 they vanished silently. Global pull (no worker) ⇒ empty. */
41
+ refIssues: readonly RefIssue[];
27
42
  }
28
43
  export declare function resolveEffectiveForWorker(eff: EffectiveConfig, worker: string | null | undefined): ResolvedEffective;
@@ -1,10 +1,11 @@
1
1
  import { stableHash } from "./hash.js";
2
+ import { danglingRefs } from "./cross-domain.js";
2
3
  import { toSkillsManifest } from "./skills-manifest.js";
3
4
  // ── 主/cheap 角色标记语义(0.6.3, E3)────────────────────────────────
4
5
  // service 现有 ModelRoles 口径:default/team/synthesize→主、subagent/summarize→cheap。1.8.0 起 `classifier`/`consolidate`
5
6
  // 同 `verifier` 一样**不派生**(归 cheap 会盖掉 models.tiers 档位与 core 兜底链)。`verifier` 刻意
6
7
  // 不派生(unset→core 的 default 兜底链,与 service 语义一致)。常量导出=单一语义源,service 收敛小单与
7
- // center 表单同源引用,别第二份。
8
+ // web-admin 表单同源引用,别第二份。
8
9
  /** primaryModel 标记喂的角色族(§3)。 */
9
10
  export const ROSTER_PRIMARY_ROLES = ["default", "team", "synthesize"];
10
11
  /** cheapModel 标记喂的角色族(§3)。 */
@@ -22,6 +23,19 @@ export function rosterMarkerRoles(r) {
22
23
  out[role] = { model: r.cheapModel };
23
24
  return out;
24
25
  }
26
+ /**
27
+ * 「`@` 能点名哪些模型」收窄到一组**可用**模型名上(纯函数;一只读法 —— roster 解析与任何别的收窄面,如按 principal 收窄,都调它)。
28
+ * `all` / `none` 原样;`only` 剔掉不在 `available` 里的名,剔空 ⇒ `{mode:"none"}`(管理员只许那几个,而这里一个都没有 = 一个都不许;
29
+ * 11.0.0 之前的数组形表达不了这一态,只好原样留着悬空名单,让消费方的成员判据兜底)。没有任何名被剔 ⇒ 返回**原对象**。
30
+ */
31
+ export function narrowAtMention(atMention, available) {
32
+ if (atMention.mode !== "only")
33
+ return atMention;
34
+ const kept = atMention.models.filter((n) => available.has(n));
35
+ if (kept.length === atMention.models.length)
36
+ return atMention;
37
+ return kept.length > 0 ? { mode: "only", models: kept } : { mode: "none" };
38
+ }
25
39
  /** Per-roster targeting (v2): keep items whose `rosters` is empty (= all rosters) or includes `rosterName`.
26
40
  * `rosterName: null` = the unscoped/global pull → no filtering (ship all). */
27
41
  function forRoster(items, rosterName) {
@@ -32,10 +46,7 @@ function forRoster(items, rosterName) {
32
46
  /** Shape the wire payload: swap in the resolved `models`, empty the control-plane-only `rosters` domain
33
47
  * (never shipped on the wire), ship `skills` as a manifest (content → contentHash), and filter
34
48
  * skills + mcp by the worker's roster (`rosterName: null` on the global path = no filter).
35
- * (The `access` domain was deleted outright in 0.6.0 — its strip here went with it, EXPERT-REDESIGN §9.
36
- * `runtime` rides the wire as {@link EffectiveConfig}'s `runtime` slot — the LIMIT RESIDUE only (the 0.6.0
37
- * governance 双写镜像 was withdrawn in 0.7.1; the transitional `RuntimeWire` alias deleted in #282 件3) —
38
- * built upstream in buildEffective; nothing to reshape here.) */
49
+ * (The `access` domain was deleted outright in 0.6.0 — its strip here went with it, EXPERT-REDESIGN §9.) */
39
50
  function shipped(eff, models, rosterName) {
40
51
  return {
41
52
  ...eff,
@@ -50,22 +61,23 @@ function shipped(eff, models, rosterName) {
50
61
  }
51
62
  export function resolveEffectiveForWorker(eff, worker) {
52
63
  if (!worker) {
53
- return { config: shipped(eff, eff.models, null), rosterLabel: "global", etag: `"v${eff.version}"` };
64
+ return { config: shipped(eff, eff.models, null), rosterLabel: "global", etag: `"v${eff.version}"`, refIssues: [] };
54
65
  }
55
66
  const w = eff.workers.workers.find((x) => x.name === worker);
56
67
  const roster = w?.roster ? eff.rosters.rosters.find((r) => r.name === w.roster) : undefined;
68
+ // THE judge's conclusion, narrowed to the references this resolution reads: the worker's, and its roster's.
69
+ const refIssues = danglingRefs(eff).filter((i) => (i.referrer.domain === "workers" && i.referrer.name === worker) || (roster !== undefined && i.referrer.domain === "rosters" && i.referrer.name === roster.name));
57
70
  if (!roster) {
58
- // unknown worker / worker without a roster / dangling roster ref → global fallback (diagnosable).
59
- return { config: shipped(eff, eff.models, null), rosterLabel: "global(fallback)", etag: `"v${eff.version}"` };
71
+ // unknown worker / worker without a roster / dangling roster ref → global fallback (diagnosable; the dangling ref is in refIssues).
72
+ return { config: shipped(eff, eff.models, null), rosterLabel: "global(fallback)", etag: `"v${eff.version}"`, refIssues };
60
73
  }
61
74
  const allow = roster.models.length ? new Set(roster.models) : null; // empty roster.models = full catalog
62
75
  const resolvedModels = allow ? eff.models.models.filter((m) => allow.has(m.name)) : eff.models.models;
63
- // Defence-in-depth: the shipped @-allowlist must only name models actually in this worker's
64
- // set — otherwise a roster that restricts `models` but keeps a stale allowlist ships an inconsistent config.
65
- // Validation also blocks this at PUT, but filter here so resolution is self-consistent regardless. Keep
66
- // `undefined` (no override + no global) as-is = "all enabled models are @-mentionable".
76
+ // Defence-in-depth: the shipped `atMention` must only name models actually in this worker's set — otherwise a
77
+ // roster that restricts `models` but keeps a stale `only` list ships an inconsistent config. Validation also blocks
78
+ // this at PUT, but narrow here so resolution is self-consistent regardless ({@link narrowAtMention}). A roster
79
+ // without its own `atMention` inherits the global one.
67
80
  const finalNames = new Set(resolvedModels.map((m) => m.name));
68
- const srcAllowlist = roster.atModelAllowlist ?? eff.models.atModelAllowlist;
69
81
  // Same defence for `roles` as for the @-allowlist (the MORE load-bearing one — roles drive real model
70
82
  // selection, not just @-mention). A `{model}`-form role pointing at a model NOT in this worker's set (an
71
83
  // inherited global default under a `models` subset, or a stale role after a catalog deletion — PUT does no
@@ -80,30 +92,21 @@ export function resolveEffectiveForWorker(eff, worker) {
80
92
  // after a catalog change still falls through the dangling-{model} drop below (defence-in-depth).
81
93
  const srcRoles = { ...eff.models.roles, ...rosterMarkerRoles(roster), ...(roster.roles ?? {}) };
82
94
  const roles = Object.fromEntries(Object.entries(srcRoles).filter(([, t]) => !(t && "model" in t) || finalNames.has(t.model)));
83
- // @-allowlist: drop names not in this worker's model set (consistency) — BUT if a NON-EMPTY allowlist would
84
- // filter to EMPTY (the worker exposes none of the allowlisted models), DON'T ship `[]`: empty means "ALL
85
- // @-mentionable" (contract, types.ts), which would INVERT the admin's restriction into permissive. Keep the
86
- // source instead — the consumer also gates on model membership, so nothing the worker lacks becomes
87
- // mentionable; the restriction is preserved (a dangling allowlist name simply never matches). Round-5.
88
- const filteredAllow = srcAllowlist?.filter((n) => finalNames.has(n));
89
95
  // 档位组(0.9.0):直通 + 逐档悬空-drop(照上面 roles 的 dangling-{model} drop 先例)——roster 收窄后
90
96
  // 组内绑到本 worker 模型集之外的档剔除,引擎侧「未绑档沿链降档 fail-open」接手,与 roles
91
- // drop→core fallback 同构。组名/active 选择不动(组是 center 侧抽象,per-worker 只影响绑定可见性)。
97
+ // drop→core fallback 同构。组名/active 选择不动(组是 web-admin 侧抽象,per-worker 只影响绑定可见性)。
92
98
  const tierGroups = eff.models.tierGroups.map((g) => ({
93
99
  ...g,
94
100
  tiers: Object.fromEntries(Object.entries(g.tiers).filter(([, m]) => typeof m !== "string" || finalNames.has(m))),
95
101
  }));
96
102
  const models = {
97
103
  models: resolvedModels,
98
- // 结构级 default(0.10.11):直通 + 集外悬空-drop(照下方 roles 的 dangling-{model} drop 先例)——
99
- // roster 收窄后 default 指到本 worker 模型集之外 ⇒ 剔除,消费方 fallback 链(roles/档位)接手。
100
- default: eff.models.default !== undefined && finalNames.has(eff.models.default) ? eff.models.default : undefined,
101
104
  roles,
102
- atModelAllowlist: srcAllowlist && srcAllowlist.length > 0 && filteredAllow && filteredAllow.length === 0 ? srcAllowlist : filteredAllow,
105
+ atMention: narrowAtMention(roster.atMention ?? eff.models.atMention, finalNames),
103
106
  tierGroups,
104
107
  activeTierGroup: eff.models.activeTierGroup,
105
108
  };
106
109
  const config = shipped(eff, models, roster.name);
107
110
  const { version: _v, updatedAt: _u, ...content } = config;
108
- return { config, rosterLabel: roster.name, etag: `"${stableHash(content)}"` };
111
+ return { config, rosterLabel: roster.name, etag: `"${stableHash(content)}"`, refIssues };
109
112
  }
@@ -0,0 +1,24 @@
1
+ /** 一行墓碑。族词在第三元;`replaced` 多一元「值语义怎么变了」。 */
2
+ export type RetiredEnv = readonly [old: string, to: string, kind: "renamed"] | readonly [old: string, to: string, kind: "replaced", why: string] | readonly [old: string, why: string, kind: "removed"];
3
+ export type RetiredEnvKind = RetiredEnv[2];
4
+ /** 墓碑拒启码(机读;拒句方括号里)。 */
5
+ export declare const RETIRED_ENV_REFUSAL_CODE = "config.env_key_retired";
6
+ export declare const RETIRED_ENVS: readonly RetiredEnv[];
7
+ /** 墓碑表的 wire 投影一行(目录 wire 的 `retired[]`):按族定形 —— renamed 只带 `replacement`;replaced 带 `replacement` + `reason`
8
+ * (值语义怎么变了);removed 只带 `reason`(能力为什么没了)。 */
9
+ export interface RetiredEnvWire {
10
+ readonly name: string;
11
+ readonly kind: RetiredEnvKind;
12
+ readonly replacement?: string;
13
+ readonly reason?: string;
14
+ }
15
+ /** 墓碑表 → wire 投影(纯数据)。**同一张表、同序**;族是闭集,穷举 switch(加一族不写这里 ⇒ 编译红)。 */
16
+ export declare function buildRetiredEnvWire(): RetiredEnvWire[];
17
+ /**
18
+ * 读给它的 env 记录,判有没有退役名在场 —— 拒启句的**唯一铸点**:有 ⇒ 整句(带 {@link RETIRED_ENV_REFUSAL_CODE};族序 renamed →
19
+ * replaced → removed,族内按表序;只点名键、从不回显值);没有 ⇒ `undefined`。空串 = 未设。
20
+ *
21
+ * 纯函数:不读 `process.env`(读方传自己要判的那份 env —— 引擎起服传进程 env,壳传它要交给子进程的那份,web-admin 传它要渲染的
22
+ * worker env);拒不拒(抛 / 退出码 / 400)是读方的事。
23
+ */
24
+ export declare function retiredEnvRefusal(env: Readonly<Record<string, string | undefined>>): string | undefined;