@sema-agent/server 7.131.0 → 7.132.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 (256) hide show
  1. package/MIGRATION.md +309 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +163 -54
  5. package/dist/approval-card.d.ts +3 -1
  6. package/dist/approval-card.js +2 -3
  7. package/dist/audit.d.ts +7 -10
  8. package/dist/auth-bridge.d.ts +4 -0
  9. package/dist/auth-bridge.js +10 -11
  10. package/dist/boot/deployment-mods.d.ts +31 -0
  11. package/dist/boot/deployment-mods.js +26 -0
  12. package/dist/boot/execution-env.d.ts +1 -1
  13. package/dist/boot/execution-env.js +19 -6
  14. package/dist/boot/memory-boundary.d.ts +2 -2
  15. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  16. package/dist/boot/read-deny-anchor-audit.js +36 -0
  17. package/dist/boot/resolve-spec.d.ts +1 -5
  18. package/dist/boot/resolve-spec.js +19 -47
  19. package/dist/boot/runner-deps.d.ts +19 -8
  20. package/dist/boot/runner-deps.js +14 -2
  21. package/dist/boot/session-faces.d.ts +1 -7
  22. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  23. package/dist/boot/stage-03-store-backend.js +2 -2
  24. package/dist/boot/stage-04-stores.d.ts +1 -1
  25. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  26. package/dist/boot/stage-05-execution-env.js +7 -10
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +5 -4
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  30. package/dist/boot/stage-07-capability-layer.js +3 -3
  31. package/dist/boot/stage-08-reapers.d.ts +2 -2
  32. package/dist/boot/stage-09-leader.d.ts +3 -9
  33. package/dist/boot/stage-10-http-server.d.ts +3 -9
  34. package/dist/boot/stage-10-http-server.js +6 -7
  35. package/dist/boot/stores.d.ts +1 -1
  36. package/dist/boot/stores.js +5 -6
  37. package/dist/boot/web-admin.d.ts +0 -3
  38. package/dist/boot/web-admin.js +35 -35
  39. package/dist/brain.js +17 -22
  40. package/dist/budget.js +5 -4
  41. package/dist/caller-text-echo.d.ts +20 -0
  42. package/dist/caller-text-echo.js +24 -0
  43. package/dist/capabilities/brand-identity.d.ts +26 -0
  44. package/dist/capabilities/brand-identity.js +27 -0
  45. package/dist/capabilities/center-plugins.js +1 -1
  46. package/dist/capabilities/center-prompts.d.ts +32 -59
  47. package/dist/capabilities/center-prompts.js +18 -32
  48. package/dist/capabilities/engine-capabilities.js +2 -1
  49. package/dist/capabilities/hands-lane.d.ts +9 -6
  50. package/dist/capabilities/hands-lane.js +1 -1
  51. package/dist/capabilities/mcp-probe.d.ts +14 -1
  52. package/dist/capabilities/mcp-probe.js +22 -133
  53. package/dist/capabilities/scenarios.d.ts +34 -40
  54. package/dist/capabilities/scenarios.js +16 -33
  55. package/dist/capabilities/skills.d.ts +19 -4
  56. package/dist/capabilities/skills.js +30 -10
  57. package/dist/config-catalog.js +6 -6
  58. package/dist/config-lkg.d.ts +9 -9
  59. package/dist/config-lkg.js +20 -4
  60. package/dist/config-provider.d.ts +3 -1
  61. package/dist/config-provider.js +12 -4
  62. package/dist/config-types.d.ts +7 -9
  63. package/dist/config.js +17 -4
  64. package/dist/device-ws-protocol.d.ts +1 -1
  65. package/dist/device-ws-protocol.js +1 -0
  66. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  67. package/dist/env-name-allowlist-knobs.js +5 -4
  68. package/dist/exec-bounds.d.ts +15 -3
  69. package/dist/exec-bounds.js +4 -0
  70. package/dist/execution-lane-caps.d.ts +9 -2
  71. package/dist/execution-lane-caps.js +11 -0
  72. package/dist/hooks/classic-stations.d.ts +86 -0
  73. package/dist/hooks/classic-stations.js +27 -0
  74. package/dist/hooks/hook-llm.d.ts +8 -5
  75. package/dist/hooks/hook-llm.js +78 -35
  76. package/dist/hooks/hook-runner.d.ts +232 -80
  77. package/dist/hooks/hook-runner.js +218 -312
  78. package/dist/http/admission.d.ts +8 -2
  79. package/dist/http/admission.js +101 -57
  80. package/dist/http/approval-sweeps.js +57 -16
  81. package/dist/http/checkpoint-ctx.d.ts +25 -8
  82. package/dist/http/checkpoint-ctx.js +11 -7
  83. package/dist/http/claimed-run.d.ts +13 -0
  84. package/dist/http/claimed-run.js +15 -0
  85. package/dist/http/device-placement.js +5 -10
  86. package/dist/http/dispatch.d.ts +0 -4
  87. package/dist/http/dispatch.js +5 -9
  88. package/dist/http/durable-run-leg.d.ts +2 -1
  89. package/dist/http/durable-run-leg.js +5 -3
  90. package/dist/http/health-self-report.d.ts +7 -5
  91. package/dist/http/health-self-report.js +4 -18
  92. package/dist/http/principal-gate.d.ts +23 -6
  93. package/dist/http/principal-gate.js +24 -5
  94. package/dist/http/refusal-body.d.ts +21 -0
  95. package/dist/http/refusal-body.js +4 -0
  96. package/dist/http/request-guards.d.ts +27 -0
  97. package/dist/http/request-guards.js +24 -3
  98. package/dist/http/resume-legs.d.ts +25 -0
  99. package/dist/http/resume-legs.js +43 -22
  100. package/dist/http/routes/capabilities.js +1 -1
  101. package/dist/http/routes/diagnostics.js +1 -1
  102. package/dist/http/routes/rules.js +2 -2
  103. package/dist/http/routes/runs.js +7 -7
  104. package/dist/http/routes/session-sync.js +4 -2
  105. package/dist/http/routes/sessions.js +1 -1
  106. package/dist/http/routes/shared-memory.js +2 -13
  107. package/dist/http/routes/side-query.js +2 -3
  108. package/dist/http/routes/tasks.js +19 -9
  109. package/dist/http/send.d.ts +9 -7
  110. package/dist/http/send.js +7 -2
  111. package/dist/http/server.d.ts +21 -11
  112. package/dist/http/server.js +28 -2
  113. package/dist/http/submit-ctx.d.ts +25 -0
  114. package/dist/http/submit-ctx.js +39 -0
  115. package/dist/http/wire-types.d.ts +1 -1
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/lane-home-probe.d.ts +43 -0
  119. package/dist/lane-home-probe.js +35 -0
  120. package/dist/limit-refusal.d.ts +47 -0
  121. package/dist/limit-refusal.js +12 -0
  122. package/dist/main.js +1 -1
  123. package/dist/mcp-assertion-header.d.ts +1 -9
  124. package/dist/mcp-assertion-header.js +0 -6
  125. package/dist/mcp-tool-faces.d.ts +5 -0
  126. package/dist/mcp-tool-faces.js +5 -1
  127. package/dist/memory-scope.d.ts +1 -1
  128. package/dist/model-provider.d.ts +39 -12
  129. package/dist/model-provider.js +19 -2
  130. package/dist/model-route-endpoint.d.ts +1 -1
  131. package/dist/model-route-endpoint.js +5 -5
  132. package/dist/observability/cost-taxonomy.d.ts +7 -7
  133. package/dist/observability/fail-open.d.ts +42 -6
  134. package/dist/observability/fail-open.js +42 -6
  135. package/dist/observability/metrics.js +3 -1
  136. package/dist/observability/prompt-manifest.d.ts +47 -82
  137. package/dist/observability/prompt-manifest.js +119 -17
  138. package/dist/observability/run-terminal.js +14 -6
  139. package/dist/observability/secret-env-scrub.d.ts +13 -1
  140. package/dist/observability/secret-env-scrub.js +13 -1
  141. package/dist/observability/tool-trace.d.ts +16 -21
  142. package/dist/observability/tool-trace.js +51 -42
  143. package/dist/per-task-image.d.ts +7 -1
  144. package/dist/per-task-image.js +3 -3
  145. package/dist/plain-string-record.d.ts +25 -0
  146. package/dist/plain-string-record.js +32 -0
  147. package/dist/plugins/blob-backend.d.ts +68 -13
  148. package/dist/plugins/blob-backend.js +53 -10
  149. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  150. package/dist/plugins/checkpoint-store-sql.js +39 -26
  151. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  152. package/dist/plugins/k8s-bg-scripts.js +22 -0
  153. package/dist/plugins/local-checkpoint-store.js +5 -2
  154. package/dist/plugins/local-session-store.js +1 -1
  155. package/dist/plugins/one-time-migrations.d.ts +137 -90
  156. package/dist/plugins/one-time-migrations.js +356 -207
  157. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  158. package/dist/plugins/permission-rule-store-sql.js +4 -18
  159. package/dist/plugins/pg-session-storage.d.ts +4 -5
  160. package/dist/plugins/pg-session-storage.js +7 -17
  161. package/dist/plugins/remote-env-adb.d.ts +6 -0
  162. package/dist/plugins/remote-env-adb.js +9 -4
  163. package/dist/plugins/remote-env-device.js +3 -0
  164. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  165. package/dist/plugins/remote-env-e2b.js +21 -9
  166. package/dist/plugins/remote-env-host.d.ts +9 -9
  167. package/dist/plugins/remote-env-host.js +10 -32
  168. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  169. package/dist/plugins/remote-env-k8s.js +265 -90
  170. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  171. package/dist/plugins/remote-env-local-docker.js +77 -9
  172. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  173. package/dist/plugins/remote-env-ssh.js +109 -3
  174. package/dist/plugins/remote-shell.d.ts +0 -1
  175. package/dist/plugins/remote-shell.js +1 -2
  176. package/dist/plugins/retention-store-sql.js +6 -2
  177. package/dist/plugins/schema-consistency.d.ts +54 -0
  178. package/dist/plugins/schema-consistency.js +52 -0
  179. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  180. package/dist/plugins/session-epoch-anchor.js +23 -0
  181. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  182. package/dist/plugins/shared-memory-store-sql.js +5 -16
  183. package/dist/plugins/sql-driver.d.ts +141 -0
  184. package/dist/plugins/sql-driver.js +115 -1
  185. package/dist/plugins/sql-errors.d.ts +41 -0
  186. package/dist/plugins/sql-errors.js +36 -0
  187. package/dist/plugins/store-backend.js +2 -1
  188. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  189. package/dist/plugins/tidb-session-storage.js +6 -16
  190. package/dist/plugins/tidb-session-store.js +1 -1
  191. package/dist/prompts-domain-validate.d.ts +17 -61
  192. package/dist/prompts-domain-validate.js +9 -119
  193. package/dist/request-key-closure.d.ts +2 -2
  194. package/dist/request-key-closure.js +2 -4
  195. package/dist/rule-entry-list.d.ts +12 -9
  196. package/dist/rule-entry-list.js +37 -21
  197. package/dist/run-local.js +19 -19
  198. package/dist/runs.d.ts +6 -0
  199. package/dist/runs.js +16 -0
  200. package/dist/server-secret-env.d.ts +44 -18
  201. package/dist/server-secret-env.js +49 -8
  202. package/dist/session-incarnation.d.ts +15 -5
  203. package/dist/session-incarnation.js +16 -9
  204. package/dist/session-titler.js +12 -2
  205. package/dist/spec-fields.d.ts +28 -16
  206. package/dist/spec-fields.js +46 -53
  207. package/dist/sql-engine-posture.d.ts +29 -4
  208. package/dist/sql-engine-posture.js +36 -1
  209. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  210. package/dist/stored-request-tool-axes-m19.js +30 -2
  211. package/dist/task-a2a.d.ts +9 -1
  212. package/dist/task-a2a.js +73 -47
  213. package/dist/task-mcp.d.ts +46 -58
  214. package/dist/task-mcp.js +99 -65
  215. package/dist/task-settings.d.ts +7 -21
  216. package/dist/task-settings.js +7 -36
  217. package/dist/text-bidi.d.ts +3 -46
  218. package/dist/text-bidi.js +3 -3
  219. package/dist/tool-approval.d.ts +1 -1
  220. package/dist/trace/core-keyset-guard.d.ts +24 -4
  221. package/dist/trace/engine-notice-wire.d.ts +11 -3
  222. package/dist/trace/engine-notice-wire.js +8 -1
  223. package/dist/trace/ledger-events.d.ts +3 -1
  224. package/dist/trace/ledger-sink.js +4 -1
  225. package/dist/trace/model-segment.js +1 -1
  226. package/dist/trace/project.d.ts +19 -6
  227. package/dist/trace/project.js +57 -22
  228. package/dist/trace/wire-projection-faces.d.ts +79 -1
  229. package/dist/trace/wire-projection-faces.js +22 -0
  230. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  231. package/dist/web-admin-client/candidate-gate.js +3 -3
  232. package/dist/web-admin-client/http-client.js +0 -6
  233. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  234. package/dist/web-admin-client/read-warnings.js +10 -0
  235. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  236. package/dist/web-admin-client/skills-mcp.js +9 -6
  237. package/dist/web-admin-client/types.d.ts +2 -3
  238. package/dist/workspace-cache.d.ts +35 -0
  239. package/dist/workspace-cache.js +36 -0
  240. package/dist/workspace-space.d.ts +3 -2
  241. package/dist/workspace-space.js +2 -0
  242. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  243. package/package.json +4 -4
  244. package/scripts/drain-check-local.mjs +34 -1
  245. package/scripts/drain-check.mysql.sql +30 -2
  246. package/scripts/drain-check.pg.sql +29 -2
  247. package/dist/capabilities/memory-notice.d.ts +0 -138
  248. package/dist/capabilities/memory-notice.js +0 -64
  249. package/dist/capabilities/prompt.d.ts +0 -17
  250. package/dist/capabilities/prompt.js +0 -6
  251. package/dist/mcp-content-origin.d.ts +0 -34
  252. package/dist/mcp-content-origin.js +0 -10
  253. package/dist/tool-axis-words.d.ts +0 -27
  254. package/dist/tool-axis-words.js +0 -9
  255. package/dist/wire-name-bound.d.ts +0 -14
  256. package/dist/wire-name-bound.js +0 -13
@@ -10,7 +10,12 @@
10
10
  *
11
11
  * ## 律(一句;core #1753 定,本仓只传事实、不判)
12
12
  * 本包任何进程在宿主上 spawn 的子进程:env = 引擎 `inheritedHostEnv(policy, findings)` ∪ 部署方点名放回的键(只在运维真可能要传东西的腿上有旋钮),
13
- * 剥掉的键经一个登记过的 `SecretEnvScrubSite` 留痕;git 腿再叠 git 自己的两件({@link gitSubprocessEnv})。三个继承词只在引擎一处判:
13
+ * 剥掉的键经一个登记过的 `SecretEnvScrubSite` 留痕(现场按**腿**命名);git 腿再叠 git 自己的两件({@link gitSubprocessEnv})。射程 = **每一条**宿主腿:
14
+ * src 里每一只值引 `node:child_process` 的文件都是登记腿、其中每一次起子进程都显式交组合出的 env(普查门在 `test/secret-env-scrub-host-legs.test.ts`)。
15
+ * 腿的继承词是调用点的事实,按「子进程里跑的是谁选的代码」定:模型命令(host shell,交任务自己的词)/ 仓里的钩子与凭据助手(三条 git 腿)/ 执行仓代码的
16
+ * 语言服务器 / 配方脚本 ⇒ 形状剥 `"scrub"`;请求带来的 settings hooks 命令 ⇒ 严格名单(引擎的 `[names]` 臂);部署方装在宿主上的 CLI(`docker` / `adb`,命令本身
17
+ * 在容器 / 设备里跑、那边的 env 由 `-e` 显式给)⇒ `"all"` —— 只扣本服务自有凭据,CLI 与它的凭据助手 / 插件要读的生态变量照交(形状剥会剥
18
+ * `ADB_VENDOR_KEYS` / `AWS_SECRET_ACCESS_KEY` 一类,实测)。三个继承词只在引擎一处判:
14
19
  * · `"all"` = 整份环境 **减** 本进程声明的自有凭据名;`"scrub"`(缺省)= 形状剥 ∪ 该名单;`[names]` = 严格白名单,点到名单中的名 ⇒ 引擎响亮拒
15
20
  * (`inheritEnvPolicyProblem`:构造时与 spawn 时两格都拒,拒句带名;本仓不自判 withheld、不留第二读法)。
16
21
  * · 名单 = 配置目录(`config-catalog.ts`)全部 `type:"secret"` 名 ∪ 名单型旋钮名(`env-name-allowlist-knobs.ts`,其原文可能夹着被错展开的令牌)∪ **生效配置
@@ -23,17 +28,21 @@
23
28
  * 抛同路)。能力词 `inherit_env.own_credentials_withheld` 在本进程的事实 =
24
29
  * {@link ownCredentialsWithheld}:最近一次成功声明的集合 ⊇ 当前生效配置应声明的集合(活配置经 {@link bindOwnCredentialsConfig} 在 `loadConfig()` 之后绑一次;
25
30
  * 未绑 / 未声明 ⇒ false,事实读不到应声明集合就不答 true)。
26
- * · 调用方显式传入的 env(`cfg.env` / 单次命令的 `env` / 语言服务器与沙箱的点名放回)不是继承,照旧原样叠在组合结果之上 —— 那是适配器自己的凭据通道。
27
- * 各腿只调 {@link hostSubprocessEnv} / {@link gitSubprocessEnv} —— 不许再有内联拼装、不许再有 `{ ...process.env }`、不许本仓再判任何一个继承词
28
- * (类级门在 `test/secret-env-scrub-host-legs.test.ts`)。
31
+ * · **值臂**(core 7.52.0 #1754):声明是一张表 `{ names, values }`。`values` = 本进程持有凭据的变量此刻的值(目录 secret 名 ∪ 模型条目 `apiKeyEnv`,{@link serverCredentialValueSources}),
32
+ * 逐个交给引擎的同一只判;引擎判「不是凭据值」的那一个不扣、名照扣、不拒启、点名一次。能力词 `inherit_env.own_credential_values_withheld` 的事实 = {@link ownCredentialValuesWithheld}(位真 ⇔ 值臂真扣)。
33
+ * · 调用方显式传入的 env(`cfg.env` / 单次命令的 `env` / 沙箱的点名放回)不是继承,照旧原样叠在组合结果之上 —— 那是适配器自己的凭据通道。
34
+ * 各腿只调 {@link hostSubprocessEnv} / {@link gitSubprocessEnv} —— 不许再有内联拼装、不许再有 `{ ...process.env }`、不许本仓再判任何一个继承词、不许起子进程不交 env
35
+ * (类级门与普查门在 `test/secret-env-scrub-host-legs.test.ts`)。
29
36
  *
30
37
  * ## 点名放行(S-699 / S-713,一只谓词各处用)
31
- * {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env **原样**交给子进程」的唯一实现。调用:{@link hostSubprocessEnv}(剥后放回形:
32
- * 本地语言服务器的 `LSP_ENV_ALLOW`、bake-runner 的 `BAKE_ENV_ALLOW`)、沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` / `K8S_SANDBOX_ENV`(`config.ts`)。
38
+ * {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env **原样**交给沙箱」的唯一实现。调用:沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` /
39
+ * `K8S_SANDBOX_ENV`(`config.ts`)与名单解析器的「在不在」判。宿主子进程的剥后放回(`LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`)不经它 —— 那是引擎的严格名单臂(上一段)。
33
40
  * host shell 的 `inheritEnv:[names]`(hermetic 形)7.129.0 起由引擎读(与 core `NodeExecutionEnv` 同名选项同义,点到自有凭据名即拒),不经本谓词。
34
41
  * 名单型旋钮的**名单**另由 `config.ts` 的同一只解析器 `parseEnvNameAllowlist` 判形(形错拒启 / 源里没有的名按序号 warn),本函数只管取值。
35
42
  * 「点名」在各处同义:点到的键原样带值(空串值照带),密钥形也照过(点名 = 部署方显式承担);源里没有的名不产生条目。
36
- * ⚠️ 已知边界(不在本函数修):`LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点到自有凭据名会把它放回语言服务器 / 配方脚本(剥后放回形在引擎的减法之上);解析器今天只拒名单型旋钮名。
43
+ * 剥后放回形(`LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`)的放回本身也经引擎:{@link composeHostSubprocessEnv} 把名单交引擎的严格名单臂(`inheritedHostEnv([names])`)叠在
44
+ * 继承结果之上 ⇒ 点到本服务自有凭据名 = 引擎抛(spawn 时;起服时解析器 `parseEnvNameAllowlist` 按表列 `channel` 已用同一只判 `inheritEnvPolicyProblem` 拒过),
45
+ * 点到的变量值是本服务凭据的副本 = 引擎扣下。本仓不另判。
37
46
  */
38
47
  import { type InheritEnvPolicy, type SecretEnvFinding } from "@sema-agent/core";
39
48
  import type { ServiceConfig } from "./config-types.js";
@@ -48,27 +57,44 @@ export type ModelApiKeyEnvTable = Readonly<Record<string, string>>;
48
57
  */
49
58
  export declare function serverWithheldEnvNames(modelApiKeyEnv?: ModelApiKeyEnvTable): readonly string[];
50
59
  /**
51
- * 🔴 **唯一声明点**:把本进程自有凭据名向引擎声明(core `declareWithheldEnv`;重声明即替换,从下一次 spawn 起生效)。`modelApiKeyEnv` = 这一次要并进名单的模型条目表
52
- * (起服第一步 = 空表;换代 = 候选表),名单 = 此前声明过的 ∪ 这张表的应声明集合 —— 名单只随**落地**的世代增长:落地过的名不再退出(改名 / 删条目之后它的值仍在
53
- * 本进程 env 里),没落地的候选不留痕(旧世代在 CAS 处就拒、不到这里;换装拒 ⇒ {@link withdrawServerWithheldEnv})。引擎对坏形名 / 运行时回填名(`PATH` / `HOME` /
54
- * `USER` …)响亮抛且声明保持原状 —— 本函数不接住:起服那次抛 = 拒启;换代那次抛 = 候选整拒(`applyEffective` 抛,与 stage 抛同路)。返回声明的名单(只名、不值)。
60
+ * 本进程**持有凭据值**的变量名(值臂的来源;core 7.52.0 #1754):目录 secret 名 ∪ 模型条目 `apiKeyEnv` 的值 —— 名单与值臂两臂同一张表派生,只少名单型旋钮名(它们的值是
61
+ * 一张变量名单,不是凭据)。纯函数,排序只为读数稳定。
62
+ */
63
+ export declare function serverCredentialValueSources(modelApiKeyEnv?: ModelApiKeyEnvTable): readonly string[];
64
+ /**
65
+ * 🔴 **唯一声明点**:把本进程自有凭据向引擎声明(core 7.52.0 `declareWithheldEnv({ names, values })`;重声明即整表替换,从下一次 spawn 起生效)。`modelApiKeyEnv` = 这一次要并进的
66
+ * 模型条目表(起服第一步 = 空表;换代 = 候选表)。两臂都只随**落地**的世代增长(落地过的名 / 值来源不退出:本进程 env 是 boot 期输入;没落地的候选不留痕 ——
67
+ * 换装拒 ⇒ {@link withdrawServerWithheldEnv})。
68
+ * · **名臂**:此前声明过的 ∪ 这张表的 {@link serverWithheldEnvNames}。引擎对坏形名 / 运行时回填名(`PATH` / `HOME` …)响亮抛且声明保持原状 —— 本函数不接住:
69
+ * 起服那次抛 = 拒启;换代那次抛 = 候选整拒(`applyEffective` 抛,与 stage 抛同路)。第一次交表带着名臂与**此前已收下的值**(引擎收过、env 不变 ⇒ 同值照收),
70
+ * 所以这一次抛只可能是名的问题。
71
+ * · **值臂**:{@link serverCredentialValueSources} 的每一个变量此刻在 env 里的值,逐个交给引擎**同一只**判(不另写「什么算凭据值」的第二份判据):引擎收下 ⇒ 记进值臂;
72
+ * 引擎拒(不是凭据值:空白 / 占位词 / 一个字符重复 / 不足 8 字符;或它是每个子进程都会拿到的变量的值)⇒ 这一个值不扣、名照扣、**不拒启**(与引擎自己的收值口
73
+ * `holdOwnCredentialValues` 同一姿势:不收 + 说一次),`console.warn` 点名那个变量一次(引擎的拒句从不回显值;本句也不),能力位 `inherit_env.own_credential_values_withheld`
74
+ * 因此答 false(位真 ⇔ 值臂真扣)。引擎拒一个值时声明保持上一份,所以逐个交表的中间态都是上一份的超集。
75
+ * 返回声明的名单(只名、不值)。
55
76
  */
56
77
  export declare function declareServerWithheldEnv(modelApiKeyEnv?: ModelApiKeyEnvTable): readonly string[];
57
78
  /**
58
- * 席位拒 ⇒ 撤回最近一次声明(回到它之前的集合):`applyEffective` 在 Runner 换装抛时调,紧跟着那一次 {@link declareServerWithheldEnv} —— 没落地的候选不留痕。
59
- * 退回去的集合是引擎之前接受过的(判名确定)⇒ 不抛;连撤两次是同一个结果(幂等)。
79
+ * 席位拒 ⇒ 撤回最近一次声明(两臂一起回到它之前的那一份):`applyEffective` 在 Runner 换装抛时调,紧跟着那一次 {@link declareServerWithheldEnv} —— 没落地的候选不留痕。
80
+ * 退回去的那一份是引擎之前接受过的(名确定、值来源的值随 env 不变)⇒ 不抛;连撤两次是同一个结果(幂等)。
60
81
  */
61
82
  export declare function withdrawServerWithheldEnv(): void;
62
83
  /** 把活配置交给能力位读(stage-01 / run-local 在 `loadConfig()` 之后各一次;读的是 `config.modelApiKeyEnv` 这个**属性**,commit 整表重赋值之后看到的就是新表)。 */
63
84
  export declare function bindOwnCredentialsConfig(config: Pick<ServiceConfig, "modelApiKeyEnv">): void;
64
85
  /**
65
- * 能力词 `inherit_env.own_credentials_withheld` 在本进程的事实:最近一次成功声明的集合 ⊇ 当前生效配置应声明的集合。是一次比较,不是「声明过」的旗 ——
86
+ * 能力词 `inherit_env.own_credentials_withheld` 在本进程的事实:最近一次成功声明的名单 ⊇ 当前生效配置应声明的名单。是一次比较,不是「声明过」的旗 ——
66
87
  * 活表若被谁绕过声明写了新名,这里立刻 false(位不替流程担保)。没绑活配置 / 没声明过 ⇒ false(fail-closed)。
67
88
  */
68
89
  export declare function ownCredentialsWithheld(): boolean;
69
- /** 测试缝:撤回声明(引擎侧重声明为空)、忘掉声明集合(含撤回格)与活配置;生产代码从不调它。 */
90
+ /**
91
+ * 能力词 `inherit_env.own_credential_values_withheld`(core 7.52.0)在本进程的事实:当前生效配置的每一个凭据值来源({@link serverCredentialValueSources}),只要它此刻在 env 里
92
+ * 有值,那个值都已交进值臂、被引擎收下。位真 ⇔ 值臂真扣:一个值被引擎判「扣不了」⇒ false;env 里没有值的来源没有东西可扣,不影响事实。没绑活配置 / 没声明过 ⇒ false。
93
+ */
94
+ export declare function ownCredentialValuesWithheld(): boolean;
95
+ /** 测试缝:撤回声明(引擎侧重声明为空表)、忘掉两份声明镜像、点名记账与活配置;生产代码从不调它。 */
70
96
  export declare function __resetServerWithheldEnvForTest(): void;
71
- /** 一条宿主腿要什么:现场(留痕)、继承策略(缺省 `"scrub"`;只有模型可驱动的 host shell 交非缺省词)、部署方点名放回的键。 */
97
+ /** 一条宿主腿要什么:现场(留痕)、继承策略(缺省 `"scrub"`;按头注「腿的继承词」一条由调用点定)、部署方点名放回的键。 */
72
98
  export interface HostSubprocessEnvRequest {
73
99
  readonly site: SecretEnvScrubSite;
74
100
  readonly policy?: InheritEnvPolicy | undefined;
@@ -78,8 +104,8 @@ export interface HostSubprocessEnvRequest {
78
104
  * 🔴 **宿主子进程 env 的唯一组合**:{@link composeHostSubprocessEnv} + 留痕。**没有按原样交给子进程**的名字(剥掉的)经 `site` 留痕;点名放回的键取回源值,
79
105
  * 不是剥除,不计数、不披露。源 env 恒为本进程 env(引擎只读 `process.env`,本仓没有第二个源)。
80
106
  *
81
- * 六条宿主腿都只经它(直接或经 {@link gitSubprocessEnv}):host shell(三个继承词原样交给 `policy`)、project-memory / center-plugins / leader 三条 git 腿、
82
- * 本地语言服务器、bake-runner 的每一只子进程组。`allow` 只在**有部署级出路**的现场非空(今天 = `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`,
107
+ * 九条宿主腿都只经它(直接或经 {@link gitSubprocessEnv}):host shell(三个继承词原样交给 `policy`)、project-memory / center-plugins / leader 三条 git 腿、
108
+ * 本地语言服务器、bake-runner 的每一只子进程组、local-docker 与 adb 两条 CLI 腿(`"all"`)、settings hooks 的命令腿(严格名单)。`allow` 只在**有部署级出路**的现场非空(今天 = `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`,
83
109
  * 它们的现场在 `SECRET_ENV_SCRUB_SITES` 里登记了 `remedy`,披露行因此是 warn + 放行名单 + 出路句);其余现场传 `[]`。
84
110
  * 返回 `NodeJS.ProcessEnv` 形(值可能是 `undefined` 的键本来就不会进子进程);调用方显式要交的键(`LC_ALL`、`cfg.env` …)叠在它之上。
85
111
  * 调用时机 = 每次要交 env 时现算一次(`process.env` 在本仓是 boot 期输入,各次同值;计数因此是「每组合一次」)。
@@ -7,18 +7,42 @@ const NO_MODEL_ENTRIES = Object.freeze({});
7
7
  export function serverWithheldEnvNames(modelApiKeyEnv = NO_MODEL_ENTRIES) {
8
8
  return [...new Set([...CATALOG_SECRET_ENV_NAMES, ...ENV_NAME_ALLOWLIST_KNOB_NAMES, ...Object.values(modelApiKeyEnv)])].sort();
9
9
  }
10
+ export function serverCredentialValueSources(modelApiKeyEnv = NO_MODEL_ENTRIES) {
11
+ return [...new Set([...CATALOG_SECRET_ENV_NAMES, ...Object.values(modelApiKeyEnv)])].sort();
12
+ }
10
13
  let declared;
11
14
  let beforeLast;
15
+ const valueRefusalAnnounced = new Set();
12
16
  let liveConfig;
17
+ const envValueOf = (name) => process.env[name];
18
+ const valuesOf = (sources) => [...sources].map(envValueOf).filter((v) => typeof v === "string" && v !== "");
13
19
  export function declareServerWithheldEnv(modelApiKeyEnv = NO_MODEL_ENTRIES) {
14
- const names = [...new Set([...(declared ?? []), ...serverWithheldEnvNames(modelApiKeyEnv)])].sort();
15
- declareWithheldEnv(names);
20
+ const names = [...new Set([...(declared?.names ?? []), ...serverWithheldEnvNames(modelApiKeyEnv)])].sort();
21
+ const kept = [...(declared?.valueSources ?? [])];
22
+ declareWithheldEnv({ names, values: valuesOf(kept) });
23
+ const valueSources = new Set(kept);
24
+ for (const source of serverCredentialValueSources(modelApiKeyEnv)) {
25
+ if (valueSources.has(source) || valuesOf([source]).length === 0)
26
+ continue;
27
+ try {
28
+ declareWithheldEnv({ names, values: valuesOf([...valueSources, source]) });
29
+ valueSources.add(source);
30
+ }
31
+ catch (e) {
32
+ if (!valueRefusalAnnounced.has(source)) {
33
+ valueRefusalAnnounced.add(source);
34
+ console.warn(`[sema-server] own credential ${source}: its value is not withheld by value from spawned children — the engine refused it (${e instanceof Error ? e.message : String(e)}); ` +
35
+ "the variable name is still withheld under every inheritEnv policy, but a variable holding the same string under another name is not, and GET /v1/capabilities answers inherit_env.own_credential_values_withheld: false. " +
36
+ "Give it a real credential value (at least 8 characters, not a placeholder) to withhold copies of it too.");
37
+ }
38
+ }
39
+ }
16
40
  beforeLast = declared;
17
- declared = new Set(names);
41
+ declared = { names: new Set(names), valueSources };
18
42
  return names;
19
43
  }
20
44
  export function withdrawServerWithheldEnv() {
21
- declareWithheldEnv([...(beforeLast ?? [])]);
45
+ declareWithheldEnv({ names: [...(beforeLast?.names ?? [])], values: valuesOf(beforeLast?.valueSources ?? []) });
22
46
  declared = beforeLast;
23
47
  }
24
48
  export function bindOwnCredentialsConfig(config) {
@@ -27,23 +51,40 @@ export function bindOwnCredentialsConfig(config) {
27
51
  export function ownCredentialsWithheld() {
28
52
  if (declared === undefined || liveConfig === undefined)
29
53
  return false;
30
- const own = declared;
54
+ const own = declared.names;
31
55
  return serverWithheldEnvNames(liveConfig.modelApiKeyEnv).every((name) => own.has(name));
32
56
  }
57
+ export function ownCredentialValuesWithheld() {
58
+ if (declared === undefined || liveConfig === undefined)
59
+ return false;
60
+ const own = declared.valueSources;
61
+ return serverCredentialValueSources(liveConfig.modelApiKeyEnv).every((source) => own.has(source) || valuesOf([source]).length === 0);
62
+ }
33
63
  export function __resetServerWithheldEnvForTest() {
34
- declareWithheldEnv([]);
64
+ declareWithheldEnv({ names: [], values: [] });
35
65
  declared = undefined;
36
66
  beforeLast = undefined;
37
67
  liveConfig = undefined;
68
+ valueRefusalAnnounced.clear();
38
69
  }
39
70
  export function hostSubprocessEnv(o) {
40
71
  const findings = [];
41
72
  const composed = composeHostSubprocessEnv(o, findings);
42
- recordSecretEnvScrub(o.site, findings.filter((f) => composed[f.key] !== process.env[f.key]), o.allow);
73
+ const disclosed = [];
74
+ const seen = new Set();
75
+ for (const f of findings) {
76
+ const k = `${f.key}=${f.kind}`;
77
+ if (composed[f.key] === envValueOf(f.key) || seen.has(k))
78
+ continue;
79
+ seen.add(k);
80
+ disclosed.push(f);
81
+ }
82
+ recordSecretEnvScrub(o.site, disclosed, o.allow);
43
83
  return composed;
44
84
  }
45
85
  export function composeHostSubprocessEnv(o, findings = []) {
46
- return { ...inheritedHostEnv(o.policy ?? "scrub", findings), ...allowlistedEnv(o.allow, process.env) };
86
+ const putBack = o.allow.length > 0 ? inheritedHostEnv([...o.allow], findings) : {};
87
+ return { ...inheritedHostEnv(o.policy ?? "scrub", findings), ...putBack };
47
88
  }
48
89
  export function gitSubprocessEnv(o) {
49
90
  return {
@@ -61,10 +61,19 @@ export declare class SessionIncarnationMissingError extends Error {
61
61
  constructor(sessionId: string);
62
62
  }
63
63
  /**
64
- * 行上化身那一格的**唯一解码**(三只店的 `sessionFaceOf` 与 SQL 两店 wake 带出的会话元数据都经它 —— 空值判据只写这一处)。
65
- * 不是非空字符串 ⇒ {@link SessionIncarnationMissingError}。
64
+ * 空化身那一格怎么读 —— {@link incarnationFromCell} 的参数(一只解码,两种**显式**读法;不是两只函数):
65
+ * · `refuse`(缺省,读口):一位活住客必须有令牌 ⇒ 空 / 非串 = {@link SessionIncarnationMissingError}(响亮拒,不当通配);
66
+ * · `absent`:调用方问的是「这一格里有没有一只令牌」,空 / 非串的答案是**没有**(`undefined`)—— 会话树删除时的空间键
67
+ * (没有令牌 = 没有空间,不回收缓存卷)与整段换装的沿用判据(没有令牌 = 铸一只新的)。
66
68
  */
67
- export declare function incarnationFromCell(sessionId: string, cell: unknown): string;
69
+ export type EmptyIncarnationReading = "refuse" | "absent";
70
+ /**
71
+ * 行上化身那一格的**唯一解码**(三只店的 `sessionFaceOf`、SQL 两店 wake 带出的会话元数据、留存删除的空间键、整段换装的沿用判据都经它 ——
72
+ * 「是不是一只令牌」只判这一处)。非空字符串 ⇒ 那只令牌;否则按 `empty`({@link EmptyIncarnationReading}):缺省 `refuse` ⇒
73
+ * {@link SessionIncarnationMissingError},`absent` ⇒ `undefined`。
74
+ */
75
+ export declare function incarnationFromCell(sessionId: string, cell: unknown, empty?: "refuse"): string;
76
+ export declare function incarnationFromCell(sessionId: string, cell: unknown, empty: "absent"): string | undefined;
68
77
  /** 店把一行原始列读成 {@link SessionFace} 的唯一口(三只店共用;化身那一格经 {@link incarnationFromCell})。 */
69
78
  export declare function sessionFaceFromRow(sessionId: string, owner: string | null, incarnation: unknown): SessionFace;
70
79
  /** 按 id 读住客(core `SessionStore.incarnationOf` 的形):`undefined` = 没有这只会话;读抛 = 读故障,不是答案。 */
@@ -81,9 +90,10 @@ export declare function incarnationReadOf(store: Pick<SessionStore, "incarnation
81
90
  export declare function sessionFaceReadOf(store: object | undefined): SessionFaceReader | undefined;
82
91
  /**
83
92
  * session-sync **整段换装**(replace)时化身的去留 —— 三只店共用的一句判据:换装删掉再插回的是**同一位住客**的那一行
84
- * (删除带属主门),所以行在、属主相同、化身非空 ⇒ 沿用;行不在(这是一次新建)或还是空的存量形 ⇒ 铸一只新的。
93
+ * (删除带属主门),所以行在、属主相同、那一格里有一只令牌(唯一解码 {@link incarnationFromCell} 的 `absent` 读法)⇒ 沿用;
94
+ * 行不在(这是一次新建)或还是空的存量形 ⇒ 铸一只新的。
85
95
  */
86
- export declare function carriedIncarnation(prior: {
96
+ export declare function carriedIncarnation(sessionId: string, prior: {
87
97
  owner?: unknown;
88
98
  incarnation?: unknown;
89
99
  } | undefined, owner: string | null): string;
@@ -24,10 +24,19 @@ export class SessionIncarnationMissingError extends Error {
24
24
  this.name = "SessionIncarnationMissingError";
25
25
  }
26
26
  }
27
- export function incarnationFromCell(sessionId, cell) {
28
- if (typeof cell !== "string" || cell === "")
29
- throw new SessionIncarnationMissingError(sessionId);
30
- return cell;
27
+ export function incarnationFromCell(sessionId, cell, empty = "refuse") {
28
+ if (typeof cell === "string" && cell !== "")
29
+ return cell;
30
+ switch (empty) {
31
+ case "refuse":
32
+ throw new SessionIncarnationMissingError(sessionId);
33
+ case "absent":
34
+ return undefined;
35
+ default: {
36
+ const unknownReading = empty;
37
+ throw new Error(`incarnationFromCell: no reading ${JSON.stringify(unknownReading)}`);
38
+ }
39
+ }
31
40
  }
32
41
  export function sessionFaceFromRow(sessionId, owner, incarnation) {
33
42
  return { owner, incarnation: incarnationFromCell(sessionId, incarnation) };
@@ -40,11 +49,9 @@ export function sessionFaceReadOf(store) {
40
49
  const read = store?.sessionFaceOf;
41
50
  return typeof read === "function" ? read.bind(store) : undefined;
42
51
  }
43
- export function carriedIncarnation(prior, owner) {
44
- if (prior !== undefined && (prior.owner ?? null) === owner && typeof prior.incarnation === "string" && prior.incarnation !== "") {
45
- return prior.incarnation;
46
- }
47
- return mintSessionIncarnation();
52
+ export function carriedIncarnation(sessionId, prior, owner) {
53
+ const held = prior !== undefined && (prior.owner ?? null) === owner ? incarnationFromCell(sessionId, prior.incarnation, "absent") : undefined;
54
+ return held ?? mintSessionIncarnation();
48
55
  }
49
56
  export const SESSION_META_INCARNATION_COLUMN = {
50
57
  name: "incarnation",
@@ -10,12 +10,22 @@ const FAIL_CAUSE_KEY_CHARS = 80;
10
10
  const SEEN_CAP = 10_000;
11
11
  function failOpenTagFor(code) {
12
12
  switch (code) {
13
+ case "model_unresolved":
14
+ return "server.session-titler.model-unresolved";
15
+ case "route_refused":
16
+ return "server.session-titler.route-refused";
17
+ case "credential_custody_failed":
18
+ return "server.session-titler.credential-custody-failed";
13
19
  case "redirect_refused":
14
20
  return "server.session-titler.redirect-refused";
21
+ case "bad_response":
22
+ return "server.session-titler.bad-response";
15
23
  case "no_content":
16
- case "hard_timeout":
17
- case undefined:
24
+ return "server.session-titler.no-content";
25
+ case "unreachable":
18
26
  return "server.session-titler.gateway-unreachable";
27
+ case undefined:
28
+ return "server.session-titler.unclassified";
19
29
  }
20
30
  }
21
31
  export function sanitizeTitle(raw) {
@@ -4,6 +4,7 @@
4
4
  * body WITHOUT the HTTP-layer 400 validation, so a malformed value must fall back to a safe default, never throw.
5
5
  */
6
6
  import { type AgentDefinition, type TaskSpec } from "@sema-agent/core";
7
+ import { type LimitFacts, type LimitMeasure } from "./limit-refusal.js";
7
8
  /**
8
9
  * E12 — normalize `body.suggestNextPrompts` → `TaskSpec.suggestNextPrompts`. `true` = on with core defaults
9
10
  * (count 3, cheap role); an object tunes `count` (clamped 1..10) / `role` (a free string — core validates it against
@@ -51,25 +52,38 @@ export declare const MAX_TASK_AGENTS = 32;
51
52
  * `ruleEntryListShapeOk`):引擎直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成 `B` / `a` / `s` / `h` 四条
52
53
  * (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法与名字**是引擎的读法:允侧读不出的条目(`*` / 写路径 / 域名 / 退役名 …)引擎在开跑时发
53
54
  * `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目(文法或名字层)在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
54
- * `inertToolListEntryOf`,判据 = 引擎的条目读法;同一只函数也拒请求的工具名选择器清单里的近似名 / 退役名)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 ⇒ 400)。
55
- * 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界;与请求 allow 桶、运维表同一只),
56
- * 在 {@link skillToolListRefusal} 里判;无长度帽([ref])。
55
+ * `inertToolListEntryOf`,判据 = 引擎的条目读法;同一只函数也拒请求的工具名选择器清单里的近似名 / 退役名)。
56
+ * **条数帽**是全部工具名清单的同一只判(`rule-entry-list.ts` 的 `skillToolListsOverCap`;句子与机读位 `field` 点体内坐标 `skills[1].allowedTools` /
57
+ * `agents[0].skills[2].disallowedTools`):顶层 `validateUserSkills` 与子代 {@link validateTaskAgents} 两道门都逐条先形、后帽(与 7.131.0 的拒点与判序同);
58
+ * 拒点 `resolveSpec` 两腿同经的 `inertToolListEntryOf` 先过同一只判;无长度帽([ref])。
57
59
  */
58
60
  export declare const skillToolListOk: (v: unknown) => v is readonly string[];
59
61
  /**
60
- * 一条技能(形已过两道门的字符串数组判)的两只清单在受理口的拒因 —— 都过 ⇒ `undefined`。两处受理口同调:顶层 `skills[]`
61
- * (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。一问:条数帽。退役名 / 对端段或工具段铸不出、为空 /
62
- * 读成空的条目是引擎的读法(见上):允侧交引擎通告,拒侧由 `resolveSpec` 拒(7.116.0–7.122.x 本仓在这里另拒工具段为空的协议名,core 7.48.0 #1400 起
63
- * 它是引擎判官的判词,随之退役)。
62
+ * 一条子代定义的裁决:`message` = 一句可 400 的英文(不带下标,调用方加);撞帽时另带 `limit`(`rel` = 相对这条定义的坐标 `allowTools[3]` / `name`),
63
+ * 句子是撞帽的头一句(`agent.allowTools[3] has 129 characters — at most 128 are accepted`:撞了哪一条帽、送来多少、帽多少),
64
+ * {@link validateTaskAgents} 把它铸成体内坐标的机读位(`field: "agents[0].allowTools[3]"`)。
64
65
  */
65
- export declare function skillToolListRefusal(skill: Record<string, unknown>): string | undefined;
66
+ export interface TaskAgentIssue {
67
+ readonly message: string;
68
+ readonly limit?: {
69
+ readonly rel: string;
70
+ readonly measure: LimitMeasure;
71
+ readonly count: number;
72
+ readonly cap: number;
73
+ };
74
+ }
75
+ /** 提交体 `agents` 的拒:句子(带下标)+ 撞帽时的机读位(`field` / `reason` / `count` / `cap`,`limit-refusal.ts` 一处铸)。 */
76
+ export interface TaskAgentsRefusal {
77
+ readonly message: string;
78
+ readonly facts?: LimitFacts;
79
+ }
66
80
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
67
81
  * 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
68
82
  * 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。`allowTools` / `denyTools` 本门只判形与条数(`toolList`),
69
83
  * **名字归引擎一只判**(core 7.49.0 #1623 / #1433 第二步起与根 run 的 `allowTools` / `excludeTools` 是同一只选择器,开跑前,三来源同判):core 7.51.0 #1736 起
70
84
  * 没有任何工具答应的名字(无人挂的名、大小写孪生 / 退役名 `bash` / `BASH`)**引擎不拒 run** —— 条目原样保留、经 onError(config)告知一次(带该写的拼法);
71
85
  * 其中**写错现名的**(`denyTools:["BASH"]` 想禁的照用 = 静默放行;`allowTools:["BASH"]` 想放的什么也没放 = 静默收窄)由 `resolveSpec` 前置拒:**限制侧**(`denyTools`)四类全拒(近似名 · 退役名 · 文法读不出 · `*`),
72
- * **放行侧**(`allowTools`)只拒近似名 —— 放行侧的退役名 / 内容形 / 文法读不出照 7.130.0 受理、交引擎既有通告(文法读不出的放行条目引擎在子代 prepare 整 run 拒 `config.agent.unknown_tool`,根 `allowTools` 同步腿 400 `config.tool_face_control`;`rule-entry-list.ts` `inertToolListEntryOf`:近似名提交 400 `config.agent.unknown_tool` 点名现名、续跑按方向分流;
86
+ * **放行侧**(`allowTools`)只拒近似名 —— 放行侧的内容形 / 没有后继的退役名 / 文法读不出照 7.130.0 受理、交引擎既有通告(有后继的退役名〔`bash` · `Task`〕与文法读不出的放行条目引擎在子代 prepare 整 run 拒 `config.agent.unknown_tool`,根 `allowTools` 拒 `config.tool_face_control`,同步腿两码同为 400〔`runs.ts` `PREPARE_REFUSAL_HTTP_STATUS`〕;`rule-entry-list.ts` `inertToolListEntryOf`:近似名提交 400 `config.agent.unknown_tool` 点名现名、续跑按方向分流;
73
87
  * 判官仍是引擎的条目读法,不是第二个判官);
74
88
  * **真未知名**(没有近似也没有退役对应:懒挂载的合法形)照旧只告运维、不拒。
75
89
  * 任何工具名都不可能有的拼法(`grammar`)与非字符串条目引擎照旧整 run 拒 `config.agent.unknown_tool` —— 主车道的委派工具带 extraTools 工厂,本仓在同一处
@@ -79,11 +93,11 @@ export declare function skillToolListRefusal(skill: Record<string, unknown>): st
79
93
  * 本门不做名字对照(拒侧的那一判在 `resolveSpec`,见上;本门只判形与条数)。
80
94
  * ⚠️ 键判断必须 Object.hasOwn:`in` 走原型链,JSON 里的 "toString"/"constructor"/"__proto__" 键会被
81
95
  * 误判为白名单字段,随后 SHAPES[k].ok 不是函数 ⇒ TypeError(fresh 500 / resume 砖;cross-review 复现)。 */
82
- export declare function taskAgentIssue(item: unknown): string | null;
96
+ export declare function taskAgentIssue(item: unknown): TaskAgentIssue | null;
83
97
  /** [ref] HTTP 门(fresh submit,server.ts prepareSpec):body.agents 在场必须是非空数组、每条过
84
98
  * {@link taskAgentIssue}、name 数组内唯一、条数 ≤ {@link MAX_TASK_AGENTS}。返回 400 错误串或 null。
85
99
  * RESUME 重放不过这道门 —— {@link taskAgentsFromBody} defensive(按条 DROP+warn,不砖 resume)。 */
86
- export declare function validateTaskAgents(v: unknown): string | null;
100
+ export declare function validateTaskAgents(v: unknown): TaskAgentsRefusal | null;
87
101
  /** [ref] body.agents → `TaskSpec.agents`(resolveSpec 的唯一装配点;main.ts 只 spread)。两层门:
88
102
  * - 🔒 TOB 租户门:多租户(requirePrincipal===true)暂不放行 —— [ref] 原文「TOB 面按租户 caps 门」,
89
103
  * 当前没有 per-tenant caps 面可挂,先 fail-closed 关多租(warn `task_agents_ignored`,capabilities
@@ -135,8 +149,6 @@ export declare function retainBackgroundProcessesFromBody(raw: unknown, requireP
135
149
  * DEFENSIVE(resume 重放不过 HTTP 400 门):非数组 ⇒ undefined;项非非空字符串 ⇒ 逐项 DROP;空数组 ⇒
136
150
  * undefined(不挂键=core 缺省,byte-compat)。HTTP 门另有 fail-loud 400(提交面邻居姿势)。 */
137
151
  export declare function toolNameListFromBody(raw: unknown): string[] | undefined;
138
- /** 引擎从哪一版起认 `TaskSpec.allowTools`(core 7.49.0 #1623,[ref])。 */
139
- export declare const ALLOW_TOOLS_MIN_ENGINE = "7.49.0";
140
152
  /**
141
153
  * S-1448(core 7.49.0 #1623 / [ref])—— body.allowTools → `TaskSpec.allowTools`:引擎的**可用性选择器**,原样透传。
142
154
  *
@@ -145,10 +157,10 @@ export declare const ALLOW_TOOLS_MIN_ENGINE = "7.49.0";
145
157
  * `config.tool_face_control`)、不与 allow 层 `allowedTools`(免问)混词。受理门只判 wire 形(与 `excludeTools` / `deferTools` 同一只门)。
146
158
  * 续跑重放同经此处:存量体逐字再投,坏形交引擎拒(收窄键丢了 = 放宽,所以不 DROP)。
147
159
  *
148
- * 装着的引擎不认该座(< {@link ALLOW_TOOLS_MIN_ENGINE})⇒ **响亮拒** 400 `request.field_invalid`(点名键、所需版本与装着的版本),
149
- * 不静默丢。`engineVersion` 缺省读装着的 core(`CORE_ENGINE_VERSION`);测试传假低版。
160
+ * **没有引擎版本臂**:core 精确钉在认这个座(7.49.0 起)的版本之上,「装着的引擎不认该座」在生产里不可达;「core 至少是哪一版」是依赖下限门
161
+ * 的事实(`test/core-dependency-floor.test.ts` + `test/helpers/core-floor.ts`),不是本函数运行期的比对(一个事实一只源)。
150
162
  */
151
- export declare function allowToolsSpecFragment(raw: unknown, engineVersion?: string): {
163
+ export declare function allowToolsSpecFragment(raw: unknown): {
152
164
  allowTools?: string[];
153
165
  };
154
166
  /** [ref]/[ref] — body.promptProfile → `TaskSpec.promptProfile`(core 1.328 R2 主件,CC LT(model_id)
@@ -1,9 +1,9 @@
1
1
  import { isThinkingLevel } from "@sema-agent/core";
2
2
  import { taskWallClockSec } from "./task-workflow.js";
3
3
  import { isAbsoluteHostPath } from "@sema-agent/settings-schema";
4
- import { CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
5
- import { createCodedHttpError } from "./security.js";
6
- import { allowLayerEntryCountIssue, ruleEntryListShapeOk } from "./rule-entry-list.js";
4
+ import { ruleEntryListShapeOk, skillToolListsOverCap } from "./rule-entry-list.js";
5
+ import { describeLimit, limitFacts, limitRefusal } from "./limit-refusal.js";
6
+ import { quotedCallerText } from "./caller-text-echo.js";
7
7
  export function normalizeSuggestNextPrompts(v) {
8
8
  if (v === true)
9
9
  return true;
@@ -67,20 +67,19 @@ const boundedString = (max) => (v) => typeof v === "string" && v.length <= max;
67
67
  const toolList = (v) => Array.isArray(v) &&
68
68
  v.length <= MAX_AGENT_TOOLS &&
69
69
  v.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_AGENT_TOOL_NAME_CHARS);
70
+ const charLimit = (max) => (v) => typeof v === "string" && v.length > max ? { sub: "", measure: "characters", count: v.length, cap: max } : undefined;
71
+ const toolListLimit = (v) => {
72
+ if (!Array.isArray(v))
73
+ return undefined;
74
+ if (v.length > MAX_AGENT_TOOLS)
75
+ return { sub: "", measure: "entries", count: v.length, cap: MAX_AGENT_TOOLS };
76
+ if (!v.every((x) => typeof x === "string" && x.length > 0))
77
+ return undefined;
78
+ const j = v.findIndex((x) => x.length > MAX_AGENT_TOOL_NAME_CHARS);
79
+ return j < 0 ? undefined : { sub: `[${j}]`, measure: "characters", count: v[j].length, cap: MAX_AGENT_TOOL_NAME_CHARS };
80
+ };
70
81
  const plainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
71
82
  export const skillToolListOk = ruleEntryListShapeOk;
72
- const SKILL_TOOL_LISTS = ["allowedTools", "disallowedTools"];
73
- export function skillToolListRefusal(skill) {
74
- for (const key of SKILL_TOOL_LISTS) {
75
- const list = skill[key];
76
- if (!ruleEntryListShapeOk(list))
77
- continue;
78
- const over = allowLayerEntryCountIssue(`skill.${key}`, list);
79
- if (over !== undefined)
80
- return over.message;
81
- }
82
- return undefined;
83
- }
84
83
  const skillFilesOk = (v) => Array.isArray(v) &&
85
84
  v.every((f) => plainObject(f) && typeof f.path === "string" && f.path.length > 0 && typeof f.content === "string");
86
85
  const SKILL_FIELD_SHAPES = {
@@ -132,11 +131,12 @@ const TASK_AGENT_FIELD_SHAPES = {
132
131
  name: {
133
132
  desc: `a non-empty string of at most ${MAX_AGENT_NAME_CHARS} characters`,
134
133
  ok: (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_AGENT_NAME_CHARS,
134
+ limit: charLimit(MAX_AGENT_NAME_CHARS),
135
135
  },
136
- whenToUse: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS) },
137
- whenToUseLean: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS) },
138
- allowTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
139
- denyTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
136
+ whenToUse: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS), limit: charLimit(MAX_AGENT_TEXT_CHARS) },
137
+ whenToUseLean: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS), limit: charLimit(MAX_AGENT_TEXT_CHARS) },
138
+ allowTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList, limit: toolListLimit },
139
+ denyTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList, limit: toolListLimit },
140
140
  skills: {
141
141
  desc: "an array of { name, description, content } skill objects (optional allowedTools/disallowedTools/files/baseDir; allowedTools/disallowedTools are arrays of strings; files entries need { path, content }; baseDir must be an absolute path with no \"..\" segment) — no count cap; the engine renders the listing within its own byte budget",
142
142
  ok: (v) => Array.isArray(v) && v.every(skillOk),
@@ -149,57 +149,64 @@ const TASK_AGENT_FIELD_SHAPES = {
149
149
  (plainObject(v) && typeof v.id === "string" && v.id.length > 0),
150
150
  },
151
151
  thinking: { desc: "a thinking level (off/minimal/low/medium/high/xhigh/max)", ok: (v) => isThinkingLevel(v) },
152
- systemPrompt: { desc: `a string of at most ${MAX_AGENT_SYSTEM_PROMPT_CHARS} characters`, ok: boundedString(MAX_AGENT_SYSTEM_PROMPT_CHARS) },
152
+ systemPrompt: { desc: `a string of at most ${MAX_AGENT_SYSTEM_PROMPT_CHARS} characters`, ok: boundedString(MAX_AGENT_SYSTEM_PROMPT_CHARS), limit: charLimit(MAX_AGENT_SYSTEM_PROMPT_CHARS) },
153
153
  maxTurns: {
154
154
  desc: `a positive integer of at most ${MAX_AGENT_MAX_TURNS}`,
155
155
  ok: (v) => typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= MAX_AGENT_MAX_TURNS,
156
156
  },
157
157
  memory: { desc: "a memory spec object ({ scopes?, writeScope?, enabled?, scopeContract? })", ok: memoryOk },
158
- observer: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS) },
159
- observerMessage: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS) },
158
+ observer: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS), limit: charLimit(MAX_AGENT_TEXT_CHARS) },
159
+ observerMessage: { desc: `a string of at most ${MAX_AGENT_TEXT_CHARS} characters`, ok: boundedString(MAX_AGENT_TEXT_CHARS), limit: charLimit(MAX_AGENT_TEXT_CHARS) },
160
160
  };
161
161
  export function taskAgentIssue(item) {
162
162
  if (item === null || typeof item !== "object" || Array.isArray(item))
163
- return "each agent must be an object";
163
+ return { message: "each agent must be an object" };
164
164
  const rec = item;
165
165
  for (const k of Object.keys(rec)) {
166
166
  if (k === "permissionMode")
167
- return 'agent "permissionMode" is not accepted (deliberately excluded from the per-task agent contract)';
167
+ return { message: 'agent "permissionMode" is not accepted (deliberately excluded from the per-task agent contract)' };
168
168
  if (!Object.hasOwn(TASK_AGENT_FIELD_SHAPES, k))
169
- return `unknown agent key "${k}"`;
169
+ return { message: `unknown agent key ${quotedCallerText(k)}` };
170
170
  }
171
171
  if (rec.name === undefined)
172
- return "agent.name is required";
172
+ return { message: "agent.name is required" };
173
173
  for (const [k, v] of Object.entries(rec)) {
174
174
  if (v === undefined)
175
175
  continue;
176
176
  const shape = TASK_AGENT_FIELD_SHAPES[k];
177
- if (!shape.ok(v))
178
- return `agent.${k} must be ${shape.desc}`;
177
+ if (shape.ok(v))
178
+ continue;
179
+ const hit = shape.limit?.(v);
180
+ if (hit === undefined)
181
+ return { message: `agent.${k} must be ${shape.desc}` };
182
+ const rel = `${k}${hit.sub}`;
183
+ return { message: describeLimit(`agent.${rel}`, hit.measure, hit.count, hit.cap), limit: { rel, measure: hit.measure, count: hit.count, cap: hit.cap } };
179
184
  }
180
185
  return null;
181
186
  }
182
187
  export function validateTaskAgents(v) {
183
188
  if (!Array.isArray(v) || v.length === 0)
184
- return "agents must be a non-empty array of agent definitions";
189
+ return { message: "agents must be a non-empty array of agent definitions" };
185
190
  if (v.length > MAX_TASK_AGENTS)
186
- return `at most ${MAX_TASK_AGENTS} agents per request`;
191
+ return limitRefusal("agents", "entries", v.length, MAX_TASK_AGENTS);
187
192
  const names = new Set();
188
193
  for (let i = 0; i < v.length; i++) {
189
194
  const issue = taskAgentIssue(v[i]);
190
- if (issue)
191
- return `agents[${i}]: ${issue}`;
195
+ if (issue) {
196
+ const message = `agents[${i}]: ${issue.message}`;
197
+ return issue.limit === undefined ? { message } : { message, facts: limitFacts(`agents[${i}].${issue.limit.rel}`, issue.limit.measure, issue.limit.count, issue.limit.cap) };
198
+ }
192
199
  const skills = v[i].skills;
193
200
  if (Array.isArray(skills)) {
194
201
  for (let j = 0; j < skills.length; j++) {
195
- const refusal = skillToolListRefusal(skills[j]);
196
- if (refusal !== undefined)
197
- return `agents[${i}]: skills[${j}]: ${refusal}`;
202
+ const over = skillToolListsOverCap(skills[j], `agents[${i}].skills[${j}]`, "agent");
203
+ if (over !== undefined)
204
+ return { message: over.message, ...(over.extra !== undefined ? { facts: over.extra } : {}) };
198
205
  }
199
206
  }
200
207
  const name = v[i].name;
201
208
  if (names.has(name))
202
- return `agents[${i}]: duplicate agent name "${name}"`;
209
+ return { message: `agents[${i}]: duplicate agent name ${quotedCallerText(name)}` };
203
210
  names.add(name);
204
211
  }
205
212
  return null;
@@ -224,7 +231,7 @@ export function taskAgentsFromBody(raw, requirePrincipal, warn) {
224
231
  }
225
232
  const issue = taskAgentIssue(raw[i]);
226
233
  if (issue) {
227
- warn("task_agent_dropped", { index: i, reason: issue });
234
+ warn("task_agent_dropped", { index: i, reason: issue.message });
228
235
  continue;
229
236
  }
230
237
  const rec = raw[i];
@@ -272,22 +279,8 @@ export function toolNameListFromBody(raw) {
272
279
  const names = raw.filter((v) => typeof v === "string" && v.length > 0);
273
280
  return names.length > 0 ? names : undefined;
274
281
  }
275
- export const ALLOW_TOOLS_MIN_ENGINE = "7.49.0";
276
- function engineAtLeast(version, floor) {
277
- const parse = (v) => v.split(/[-+]/, 1)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
278
- const [a, b] = [parse(version), parse(floor)];
279
- for (let i = 0; i < 3; i++)
280
- if ((a[i] ?? 0) !== (b[i] ?? 0))
281
- return (a[i] ?? 0) > (b[i] ?? 0);
282
- return true;
283
- }
284
- export function allowToolsSpecFragment(raw, engineVersion = CORE_ENGINE_VERSION) {
285
- if (raw === undefined)
286
- return {};
287
- if (!engineAtLeast(engineVersion, ALLOW_TOOLS_MIN_ENGINE)) {
288
- throw createCodedHttpError(400, "request.field_invalid", `allowTools needs engine ${ALLOW_TOOLS_MIN_ENGINE} or later; this server runs engine ${engineVersion}, which would drop the list and leave every tool available — send the request without allowTools, or upgrade the server`);
289
- }
290
- return { allowTools: raw };
282
+ export function allowToolsSpecFragment(raw) {
283
+ return raw === undefined ? {} : { allowTools: raw };
291
284
  }
292
285
  export function promptProfileFromBody(raw) {
293
286
  return raw === "simple" || raw === "classic" ? raw : undefined;